1
/* Copyright 2002-2005 Elliotte Rusty Harold
3
This library is free software; you can redistribute it and/or modify
4
it under the terms of version 2.1 of the GNU Lesser General Public
5
License as published by the Free Software Foundation.
7
This library is distributed in the hope that it will be useful,
8
but WITHOUT ANY WARRANTY; without even the implied warranty of
9
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
GNU Lesser General Public License for more details.
12
You should have received a copy of the GNU Lesser General Public
13
License along with this library; if not, write to the
14
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
15
Boston, MA 02111-1307 USA
17
You can contact Elliotte Rusty Harold by sending e-mail to
18
elharo@metalab.unc.edu. Please include the word "XOM" in the
19
subject line. The XOM home page is located at http://www.xom.nu/
22
package nu.xom.canonical;
24
import java.io.IOException;
25
import java.io.OutputStream;
26
import java.util.ArrayList;
27
import java.util.Arrays;
28
import java.util.Comparator;
29
import java.util.Iterator;
30
import java.util.List;
32
import java.util.SortedMap;
33
import java.util.StringTokenizer;
34
import java.util.TreeMap;
35
import java.util.Map.Entry;
37
import org.xml.sax.helpers.NamespaceSupport;
39
import nu.xom.Attribute;
40
import nu.xom.Comment;
41
import nu.xom.DocType;
42
import nu.xom.Document;
43
import nu.xom.Element;
44
import nu.xom.Namespace;
47
import nu.xom.ParentNode;
48
import nu.xom.ProcessingInstruction;
49
import nu.xom.Serializer;
51
import nu.xom.XPathContext;
55
* Writes XML in the format specified by <a target="_top"
56
* href="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">Canonical
57
* XML Version 1.0</a> or <a target="_top"
58
* href="http://www.w3.org/TR/2002/REC-xml-exc-c14n-20020718/">Exclusive
59
* XML Canonicalization Version 1.0</a>.
62
* @author Elliotte Rusty Harold
66
public class Canonicalizer {
68
private boolean withComments;
69
private boolean exclusive = false;
70
private CanonicalXMLSerializer serializer;
71
private List inclusiveNamespacePrefixes = new ArrayList();
73
private static Comparator comparator = new AttributeComparator();
76
public final static String CANONICAL_XML =
77
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
78
public final static String CANONICAL_XML_WITH_COMMENTS =
79
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments";
80
public final static String EXCLUSIVE_XML_CANONICALIZATION =
81
"http://www.w3.org/2001/10/xml-exc-c14n#";
82
public final static String EXCLUSIVE_XML_CANONICALIZATION_WITH_COMMENTS =
83
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments";
86
private static class AttributeComparator implements Comparator {
88
public int compare(Object o1, Object o2) {
89
Attribute a1 = (Attribute) o1;
90
Attribute a2 = (Attribute) o2;
92
String namespace1 = a1.getNamespaceURI();
93
String namespace2 = a2.getNamespaceURI();
94
if (namespace1.equals(namespace2)) {
95
return a1.getLocalName().compareTo(a2.getLocalName());
97
else if (namespace1.equals("")) {
100
else if (namespace2.equals("")) {
103
else { // compare namespace URIs
104
return namespace1.compareTo(namespace2);
114
* Creates a <code>Canonicalizer</code> that outputs a
115
* canonical XML document with comments.
118
* @param out the output stream the document
121
public Canonicalizer(OutputStream out) {
122
this(out, true, false);
128
* Creates a <code>Canonicalizer</code> that outputs a
129
* canonical XML document with or without comments.
132
* @param out the output stream the document
134
* @param withComments true if comments should be included
135
* in the output, false otherwise
137
public Canonicalizer(
138
OutputStream out, boolean withComments) {
139
this(out, withComments, false);
145
* Creates a <code>Canonicalizer</code> that outputs a
146
* canonical XML document with or without comments,
147
* using either the original or the exclusive canonicalization
151
* @param out the output stream the document
153
* @param withComments true if comments should be included
154
* in the output, false otherwise
155
* @param exclusive true if exclusive XML canonicalization
156
* should be performed, false if regular XML canonicalization
157
* should be performed
159
private Canonicalizer(
160
OutputStream out, boolean withComments, boolean exclusive) {
162
this.serializer = new CanonicalXMLSerializer(out);
163
serializer.setLineSeparator("\n");
164
this.withComments = withComments;
165
this.exclusive = exclusive;
172
* Creates a <code>Canonicalizer</code> that outputs a
173
* canonical XML document using the specified algorithm.
174
* Currently, four algorithms are defined and supported:
178
* <li>Canonical XML without comments:
179
* <code>http://www.w3.org/TR/2001/REC-xml-c14n-20010315</code></li>
180
* <li>Canonical XML with comments:
181
* <code>http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments</code></li>
182
* <li>Exclusive XML canonicalization without comments:
183
* <code>http://www.w3.org/2001/10/xml-exc-c14n#</code></li>
184
* <li>Exclusive XML canonicalization with comments:
185
* <code>http://www.w3.org/2001/10/xml-exc-c14n#WithComments</code></li>
188
* @param out the output stream the document
190
* @param algorithm the URI for the canonicalization algorithm
192
* @throws CanonicalizationException if the algorithm is
194
* @throws NullPointerException if the algorithm is null
197
public Canonicalizer(
198
OutputStream out, String algorithm) {
200
if (algorithm == null) {
201
throw new NullPointerException("Null algorithm");
203
this.serializer = new CanonicalXMLSerializer(out);
204
serializer.setLineSeparator("\n");
205
if (algorithm.equals(CANONICAL_XML)) {
206
this.withComments = false;
207
this.exclusive = false;
209
else if (algorithm.equals(CANONICAL_XML_WITH_COMMENTS)) {
210
this.withComments = true;
211
this.exclusive = false;
213
else if (algorithm.equals(EXCLUSIVE_XML_CANONICALIZATION)) {
214
this.withComments = false;
215
this.exclusive = true;
217
else if (algorithm.equals(EXCLUSIVE_XML_CANONICALIZATION_WITH_COMMENTS)) {
218
this.withComments = true;
219
this.exclusive = true;
222
throw new CanonicalizationException(
223
"Unsupported canonicalization algorithm: " + algorithm);
229
private class CanonicalXMLSerializer extends Serializer {
231
// If nodes is null we're canonicalizing all nodes;
232
// the entire document; this is somewhat easier than when
233
// canonicalizing only a document subset embedded in nodes
235
private NamespaceSupport inScope;
239
* Creates a <code>Serializer</code> that outputs a
240
* canonical XML document with or without comments.
243
* @param out the <code>OutputStream</code> the document
245
* @param withComments true if comments should be included
246
* in the output, false otherwise
248
CanonicalXMLSerializer(OutputStream out) {
250
setLineSeparator("\n");
256
* Serializes a document onto the output
257
* stream using the canonical XML algorithm.
260
* @param doc the <code>Document</code> to serialize
262
* @throws IOException if the underlying <code>OutputStream</code>
263
* encounters an I/O error
265
public final void write(Document doc) throws IOException {
267
inScope = new NamespaceSupport();
270
Node child = doc.getChild(position);
271
if (nodes == null || child instanceof Element || nodes.contains(child)) {
273
if (child instanceof ProcessingInstruction) breakLine();
274
else if (child instanceof Comment && withComments) {
279
if (child instanceof Element) break;
282
for (int i = position; i < doc.getChildCount(); i++) {
283
Node child = doc.getChild(i);
284
if (nodes == null || child instanceof Element || nodes.contains(child)) {
285
if (child instanceof ProcessingInstruction) breakLine();
286
else if (child instanceof Comment && withComments) {
300
* Serializes an element onto the output stream using the canonical
301
* XML algorithm. The result is guaranteed to be well-formed.
302
* If <code>element</code> does not have a parent element, it will
303
* also be namespace well-formed.
306
* @param element the <code>Element</code> to serialize
308
* @throws IOException if the underlying <code>OutputStream</code>
309
* encounters an I/O error
311
protected final void write(Element element)
314
// treat empty elements differently to avoid an
316
if (element.getChildCount() == 0) {
317
writeStartTag(element, false);
318
writeEndTag(element);
321
Node current = element;
324
int[] indexes = new int[10];
328
if (!end && current.getChildCount() > 0) {
329
writeStartTag((Element) current, false);
330
current = current.getChild(0);
333
indexes = grow(indexes, top);
338
writeEndTag((Element) current);
339
if (current == element) break;
345
ParentNode parent = current.getParent();
346
if (parent.getChildCount() - 1 == index) {
349
if (current != element) {
350
index = indexes[top];
356
indexes[top] = index;
357
current = parent.getChild(index);
366
private int[] grow(int[] indexes, int top) {
368
if (top < indexes.length) return indexes;
369
int[] result = new int[indexes.length*2];
370
System.arraycopy(indexes, 0, result, 0, indexes.length);
376
protected void writeStartTag(Element element, boolean isEmpty)
379
boolean writeElement = nodes == null || nodes.contains(element);
381
inScope.pushContext();
383
writeRaw(element.getQualifiedName());
386
SortedMap map = new TreeMap();
388
ParentNode parent = element.getParent();
389
Element parentElement = null;
390
if (parent instanceof Element) {
391
parentElement = (Element) parent;
394
i < element.getNamespaceDeclarationCount();
396
String prefix = element.getNamespacePrefix(i);
397
String uri = element.getNamespaceURI(prefix);
399
if (uri.equals(inScope.getURI(prefix))) {
402
else if (exclusive) {
403
if (needToDeclareNamespace(element, prefix, uri)) {
404
map.put(prefix, uri);
407
else if (uri.equals("")) {
408
// no need to say xmlns=""
409
if (parentElement == null) continue;
410
if ("".equals(parentElement.getNamespaceURI(""))) {
413
map.put(prefix, uri);
416
map.put(prefix, uri);
421
writeNamespaceDeclarations(map);
425
int position = indexOf(element);
426
// do we need to undeclare a default namespace?
427
// You know, should I instead create an output tree and then just
428
// canonicalize that? probably not
429
if (position != -1 && "".equals(element.getNamespaceURI())) {
430
ParentNode parent = element.getParent();
431
// Here we have to check for the nearest default on parents in the
432
// output tree, not the input tree
433
while (parent instanceof Element
434
&& !(nodes.contains(parent))) {
435
parent = parent.getParent();
437
if (parent instanceof Element) {
438
String uri = ((Element) parent).getNamespaceURI("");
439
if (! "".equals(uri)) {
445
for (int i = position+1; i < nodes.size(); i++) {
446
Node next = nodes.get(i);
447
if ( !(next instanceof Namespace) ) break;
448
Namespace namespace = (Namespace) next;
449
String prefix = namespace.getPrefix();
450
String uri = namespace.getValue();
452
if (uri.equals(inScope.getURI(prefix))) {
455
else if (exclusive) {
456
if (needToDeclareNamespace(element, prefix, uri)) {
457
map.put(prefix, uri);
461
map.put(prefix, uri);
466
writeNamespaceDeclarations(map);
470
Attribute[] sorted = sortAttributes(element);
471
for (int i = 0; i < sorted.length; i++) {
472
if (nodes == null || nodes.contains(sorted[i])
473
|| (sorted[i].getNamespaceURI().equals(Namespace.XML_NAMESPACE)
474
&& sorted[i].getParent() != element)) {
486
private void writeNamespaceDeclarations(SortedMap map) throws IOException {
488
Iterator prefixes = map.entrySet().iterator();
489
while (prefixes.hasNext()) {
490
Map.Entry entry = (Entry) prefixes.next();
491
String prefix = (String) entry.getKey();
492
String uri = (String) entry.getValue();
494
writeNamespaceDeclaration(prefix, uri);
495
inScope.declarePrefix(prefix, uri);
501
private boolean needToDeclareNamespace(
502
Element parent, String prefix, String uri) {
504
boolean match = visiblyUtilized(parent, prefix, uri);
506
if (match || inclusiveNamespacePrefixes.contains(prefix)) {
507
return noOutputAncestorUsesPrefix(parent, prefix, uri);
515
private boolean visiblyUtilized(Element element, String prefix, String uri) {
517
boolean match = false;
518
String pfx = element.getNamespacePrefix();
519
String local = element.getNamespaceURI();
520
if (prefix.equals(pfx) && local.equals(uri)) {
524
for (int i = 0; i < element.getAttributeCount(); i++) {
525
Attribute attribute = element.getAttribute(i);
526
if (nodes == null || nodes.contains(attribute)) {
527
pfx = attribute.getNamespacePrefix();
528
if (prefix.equals(pfx)) {
539
private boolean noOutputAncestorUsesPrefix(Element original, String prefix, String uri) {
541
ParentNode parent = original.getParent();
542
if (parent instanceof Document && "".equals(uri)) {
546
while (parent != null && !(parent instanceof Document)) {
547
if (nodes == null || nodes.contains(parent)) {
548
Element element = (Element) parent;
549
String pfx = element.getNamespacePrefix();
550
if (pfx.equals(prefix)) {
551
String newURI = element.getNamespaceURI(prefix);
552
return ! newURI.equals(uri);
555
for (int i = 0; i < element.getAttributeCount(); i++) {
556
Attribute attribute = element.getAttribute(i);
557
String current = attribute.getNamespacePrefix();
558
if (current.equals(prefix)) {
559
String newURI = element.getNamespaceURI(prefix);
560
return ! newURI.equals(uri);
564
parent = parent.getParent();
571
// ???? move into Nodes?
572
private int indexOf(Element element) {
573
for (int i = 0; i < nodes.size(); i++) {
574
if (nodes.get(i) == element) return i;
580
protected void write(Attribute attribute) throws IOException {
583
writeRaw(attribute.getQualifiedName());
585
writeRaw(prepareAttributeValue(attribute));
591
protected void writeEndTag(Element element) throws IOException {
593
if (nodes == null || nodes.contains(element)) {
595
writeRaw(element.getQualifiedName());
597
inScope.popContext();
602
private final XPathContext xmlcontext = new XPathContext("xml", Namespace.XML_NAMESPACE);
604
private Attribute[] sortAttributes(Element element) {
606
Map nearest = new TreeMap();
607
// add in any inherited xml: attributes
608
if (!exclusive && nodes != null && nodes.contains(element)
609
&& ! nodes.contains(element.getParent())) {
610
// grab all xml: attributes
611
Nodes attributes = element.query("ancestor::*/@xml:*", xmlcontext);
612
if (attributes.size() != 0) {
613
// It's important to count backwards here because
614
// XPath returns all nodes in document order, which
615
// is top-down. To get the nearest we need to go
616
// bottom up instead.
617
for (int i = attributes.size()-1; i >= 0; i--) {
618
Attribute a = (Attribute) attributes.get(i);
619
String name = a.getLocalName();
620
if (element.getAttribute(name, Namespace.XML_NAMESPACE) != null) {
621
// this element already has that attribute
624
if (! nearest.containsKey(name)) {
625
Element parent = (Element) a.getParent();
626
if (! nodes.contains(parent)) {
627
nearest.put(name, a);
630
nearest.put(name, null);
636
// remove null values
637
Iterator iterator = nearest.values().iterator();
638
while (iterator.hasNext()) {
639
if (iterator.next() == null) iterator.remove();
644
int localCount = element.getAttributeCount();
646
= new Attribute[localCount + nearest.size()];
647
for (int i = 0; i < localCount; i++) {
648
result[i] = element.getAttribute(i);
651
Iterator iterator = nearest.values().iterator();
652
for (int j = localCount; j < result.length; j++) {
653
result[j] = (Attribute) iterator.next();
656
Arrays.sort(result, comparator);
663
private String prepareAttributeValue(Attribute attribute) {
665
String value = attribute.getValue();
666
StringBuffer result = new StringBuffer(value.length());
668
if (attribute.getType().equals(Attribute.Type.CDATA)
669
|| attribute.getType().equals(Attribute.Type.UNDECLARED)) {
670
char[] data = value.toCharArray();
671
for (int i = 0; i < data.length; i++) {
674
result.append("	");
676
else if (c == '\n') {
677
result.append("
");
679
else if (c == '\r') {
680
result.append("
");
682
else if (c == '\"') {
683
result.append(""");
686
result.append("&");
689
result.append("<");
697
// According to the spec, "Whitespace character references
698
// other than   are not affected by attribute value
699
// normalization. For parsed documents, the parser will
700
// still replace these with the actual character. I am
701
// going to assume that if one is found here, that the
702
// user meant to put it there; and so we will escape it
703
// with a character reference
704
char[] data = value.toCharArray();
705
boolean seenFirstNonSpace = false;
706
for (int i = 0; i < data.length; i++) {
707
if (data[i] == ' ') {
708
if (i != data.length-1 && data[i+1] != ' ' && seenFirstNonSpace) {
709
result.append(data[i]);
713
seenFirstNonSpace = true;
714
if (data[i] == '\t') {
715
result.append("	");
717
else if (data[i] == '\n') {
718
result.append("
");
720
else if (data[i] == '\r') {
721
result.append("
");
723
else if (data[i] == '\"') {
724
result.append(""");
726
else if (data[i] == '&') {
727
result.append("&");
729
else if (data[i] == '<') {
730
result.append("<");
733
result.append(data[i]);
738
return result.toString();
745
* Serializes a <code>Text</code> object
746
* onto the output stream using the UTF-8 encoding.
747
* The reserved characters <, >, and &
748
* are escaped using the standard entity references such as
749
* <code>&lt;</code>, <code>&gt;</code>,
750
* and <code>&amp;</code>.
753
* @param text the <code>Text</code> to serialize
755
* @throws IOException if the underlying <code>OutputStream</code>
756
* encounters an I/O error
758
protected final void write(Text text) throws IOException {
760
if (nodes == null || nodes.contains(text)) {
761
String input = text.getValue();
762
StringBuffer result = new StringBuffer(input.length());
763
for (int i = 0; i < input.length(); i++) {
764
char c = input.charAt(i);
766
result.append("
");
769
result.append("&");
772
result.append("<");
775
result.append(">");
781
writeRaw(result.toString());
789
* Serializes a <code>Comment</code> object
790
* onto the output stream if and only if this
791
* serializer is configured to produce canonical XML
795
* @param comment the <code>Comment</code> to serialize
797
* @throws IOException if the underlying <code>OutputStream</code>
798
* encounters an I/O error
800
protected final void write(Comment comment)
802
if (withComments && (nodes == null || nodes.contains(comment))) {
803
super.write(comment);
808
protected final void write(ProcessingInstruction pi)
810
if (nodes == null || nodes.contains(pi)) {
818
* Does nothing because canonical XML does not include
819
* document type declarations.
822
* @param doctype the document type declaration to serialize
824
protected final void write(DocType doctype) {
825
// DocType is not serialized in canonical XML
829
public void write(Node node) throws IOException {
831
if (node instanceof Document) {
832
write((Document) node);
834
else if (node instanceof Attribute) {
835
write((Attribute) node);
837
else if (node instanceof Namespace) {
838
write((Namespace) node);
847
private void write(Namespace namespace) throws IOException {
849
String prefix = namespace.getPrefix();
850
String uri = namespace.getValue();
852
if (!"".equals(prefix)) {
857
writeAttributeValue(uri);
867
* Serializes a node onto the output stream using the specified
868
* canonicalization algorithm. If the node is a document or an
869
* element, then the node's entire subtree is written out.
872
* @param node the node to canonicalize
874
* @throws IOException if the underlying <code>OutputStream</code>
875
* encounters an I/O error
877
public final void write(Node node) throws IOException {
880
// http://lists.ibiblio.org/pipermail/xom-interest/2005-October/002656.html
881
if (node instanceof Element) {
882
Document doc = node.getDocument();
883
Element pseudoRoot = null;
885
pseudoRoot = new Element("pseudo");
886
doc = new Document(pseudoRoot);
887
ParentNode root = (ParentNode) node;
888
while (root.getParent() != null) root = root.getParent();
889
pseudoRoot.appendChild(root);
892
write(node.query(".//. | .//@* | .//namespace::*"));
895
if (pseudoRoot != null) pseudoRoot.removeChild(0);
899
serializer.nodes = null;
900
serializer.write(node);
909
* Serializes a document subset onto the output stream using the
910
* canonical XML algorithm. All nodes in the list must come from
911
* same document. Furthermore, they must come from a document.
912
* They cannot be detached. The nodes need not be sorted. This
913
* method will sort them into the appropriate order for
918
* In most common use cases, these nodes will be the result of
919
* evaluating an XPath expression. For example,
922
* <pre><code> Canonicalizer canonicalizer
923
* = new Canonicalizer(System.out, Canonicalizer.CANONICAL_XML);
924
* Nodes result = doc.query("//. | //@* | //namespace::*");
925
* canonicalizer.write(result);
929
* Children are not output unless the subset also includes them.
930
* Including an element in the subset does not automatically
931
* select all the element's children, attributes, and namespaces.
932
* Furthermore, not selecting an element does not imply that its
933
* children, namespaces, attributes will not be output.
936
* @param documentSubset the nodes to serialize
938
* @throws IOException if the underlying <code>OutputStream</code>
939
* encounters an I/O error
940
* @throws CanonicalizationException if the nodes come from more
941
* than one document; or if a detached node is in the list
943
public final void write(Nodes documentSubset) throws IOException {
945
if (documentSubset.size() > 0) {
946
Document doc = documentSubset.get(0).getDocument();
948
throw new CanonicalizationException(
949
"Canonicalization is not defined for detached nodes");
951
Nodes result = sort(documentSubset);
952
serializer.nodes = result;
953
serializer.write(doc);
962
* Specifies the prefixes that will be output as specified in
963
* regular canonical XML, even when doing exclusive
964
* XML canonicalization.
967
* @param inclusiveNamespacePrefixes a whitespace separated list
968
* of namespace prefixes that will always be included in the
969
* output, even in exclusive canonicalization
971
public final void setInclusiveNamespacePrefixList(String inclusiveNamespacePrefixes)
974
this.inclusiveNamespacePrefixes.clear();
975
if (this.exclusive && inclusiveNamespacePrefixes != null) {
976
StringTokenizer tokenizer = new StringTokenizer(
977
inclusiveNamespacePrefixes, " \t\r\n", false);
978
while (tokenizer.hasMoreTokens()) {
979
this.inclusiveNamespacePrefixes.add(tokenizer.nextToken());
986
// XXX remove recursion
987
// recursively descend through document; in document
988
// order, and add results as they are found
989
private Nodes sort(Nodes in) {
991
Node root = in.get(0).getDocument();
993
Nodes out = new Nodes();
994
List list = new ArrayList(in.size());
995
List namespaces = new ArrayList();
996
for (int i = 0; i < in.size(); i++) {
997
Node node = in.get(i);
999
if (node instanceof Namespace) namespaces.add(node);
1001
sort(list, namespaces, out, (ParentNode) root);
1002
if (! list.isEmpty() ) {
1003
// Are these just duplicates; or is there really a node
1004
// from a different document?
1005
Iterator iterator = list.iterator();
1006
while (iterator.hasNext()) {
1007
Node next = (Node) iterator.next();
1008
if (root != next.getDocument()) {
1009
throw new CanonicalizationException(
1010
"Cannot canonicalize subsets that contain nodes from more than one document");
1017
return new Nodes(in.get(0));
1023
private static void sort(List in, List namespaces, Nodes out, ParentNode parent) {
1025
if (in.isEmpty()) return;
1026
if (in.contains(parent)) {
1029
// I'm fairly sure this next line is unreachable, but just
1030
// in case it isn't I'll leave this comment here.
1031
// if (in.isEmpty()) return;
1034
int childCount = parent.getChildCount();
1035
for (int i = 0; i < childCount; i++) {
1036
Node child = parent.getChild(i);
1037
if (child instanceof Element) {
1038
Element element = (Element) child;
1039
if (in.contains(element)) {
1040
out.append(element);
1043
// attach namespaces
1044
if (!namespaces.isEmpty()) {
1045
Iterator iterator = in.iterator();
1046
while (iterator.hasNext()) {
1047
Object o = iterator.next();
1048
if (o instanceof Namespace) {
1049
Namespace n = (Namespace) o;
1050
if (element == n.getParent()) {
1058
// attach attributes
1059
for (int a = 0; a < element.getAttributeCount(); a++) {
1060
Attribute att = element.getAttribute(a);
1061
if (in.contains(att)) {
1064
if (in.isEmpty()) return;
1067
sort(in, namespaces, out, element);
1070
if (in.contains(child)) {
1073
if (in.isEmpty()) return;
b'\\ No newline at end of file'