2 // XmlCanonicalizer.cs - C14N implementation for XML Signature
3 // http://www.w3.org/TR/xml-c14n
6 // Aleksey Sanin (aleksey@aleksey.com)
8 // (C) 2003 Aleksey Sanin (aleksey@aleksey.com)
11 using System.Collections;
18 internal class XmlCanonicalizer {
20 private enum XmlCanonicalizerState
28 private bool comments;
29 private bool exclusive;
32 private XmlNodeList xnl;
33 private StringBuilder res;
35 // namespaces rendering stack
36 private XmlCanonicalizerState state;
37 private ArrayList visibleNamespaces;
38 private int prevVisibleNamespacesStart;
39 private int prevVisibleNamespacesEnd;
41 public XmlCanonicalizer (bool withComments, bool excC14N)
43 res = new StringBuilder ();
44 comments = withComments;
46 state = XmlCanonicalizerState.BeforeDocElement;
47 visibleNamespaces = new ArrayList ();
48 prevVisibleNamespacesStart = 0;
49 prevVisibleNamespacesEnd = 0;
52 public Stream Canonicalize (XmlDocument doc)
54 WriteDocumentNode (doc);
56 UTF8Encoding utf8 = new UTF8Encoding ();
57 byte[] data = utf8.GetBytes (res.ToString ());
58 return new MemoryStream (data);
61 public Stream Canonicalize (XmlNodeList nodes)
64 if (nodes == null || nodes.Count < 1)
66 return Canonicalize (nodes[0].OwnerDocument);
69 private void WriteNode (XmlNode node)
71 // Console.WriteLine ("C14N Debug: node=" + node.Name);
73 bool visible = IsNodeVisible (node);
74 switch (node.NodeType) {
75 case XmlNodeType.Document:
76 case XmlNodeType.DocumentFragment:
77 WriteDocumentNode (node);
79 case XmlNodeType.Element:
80 WriteElementNode (node, visible);
82 case XmlNodeType.CDATA:
83 case XmlNodeType.SignificantWhitespace:
84 case XmlNodeType.Text:
85 // CDATA sections are processed as text nodes
86 WriteTextNode (node, visible);
88 case XmlNodeType.Whitespace:
89 if (state == XmlCanonicalizerState.InsideDocElement)
90 WriteTextNode (node, visible);
92 case XmlNodeType.Comment:
93 WriteCommentNode (node, visible);
95 case XmlNodeType.ProcessingInstruction:
96 WriteProcessingInstructionNode (node, visible);
98 case XmlNodeType.EntityReference:
99 // throw new XmlException ("Entity references should be resolved by parser", null);
100 for (int i = 0; i < node.ChildNodes.Count; i++)
101 WriteNode (node.ChildNodes [i]);
103 case XmlNodeType.Attribute:
104 throw new XmlException ("Attribute node is impossible here", null);
105 case XmlNodeType.EndElement:
106 throw new XmlException ("EndElement node is impossible here", null);
107 case XmlNodeType.EndEntity:
108 throw new XmlException ("EndEntity node is impossible here", null);
109 case XmlNodeType.DocumentType:
110 case XmlNodeType.Entity:
111 case XmlNodeType.Notation:
112 case XmlNodeType.XmlDeclaration:
118 private void WriteDocumentNode (XmlNode node)
120 state = XmlCanonicalizerState.BeforeDocElement;
121 for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling)
126 // If the element is not in the node-set, then the result is obtained
127 // by processing the namespace axis, then the attribute axis, then
128 // processing the child nodes of the element that are in the node-set
129 // (in document order). If the element is inthe node-set, then the result
130 // is an open angle bracket (<), the element QName, the result of
131 // processing the namespace axis, the result of processing the attribute
132 // axis, a close angle bracket (>), the result of processing the child
133 // nodes of the element that are in the node-set (in document order), an
134 // open angle bracket, a forward slash (/), the element QName, and a close
136 private void WriteElementNode (XmlNode node, bool visible)
138 // Console.WriteLine ("Debug: element node");
140 // remember current state
141 int savedPrevVisibleNamespacesStart = prevVisibleNamespacesStart;
142 int savedPrevVisibleNamespacesEnd = prevVisibleNamespacesEnd;
143 int savedVisibleNamespacesSize = visibleNamespaces.Count;
144 XmlCanonicalizerState s = state;
145 if (visible && state == XmlCanonicalizerState.BeforeDocElement)
146 state = XmlCanonicalizerState.InsideDocElement;
151 res.Append (node.Name);
154 // this is odd but you can select namespaces
155 // and attributes even if node itself is not visible
156 WriteNamespacesAxis (node, visible);
157 WriteAttributesAxis (node);
163 for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling)
169 res.Append (node.Name);
174 if (visible && s == XmlCanonicalizerState.BeforeDocElement)
175 state = XmlCanonicalizerState.AfterDocElement;
176 prevVisibleNamespacesStart = savedPrevVisibleNamespacesStart;
177 prevVisibleNamespacesEnd = savedPrevVisibleNamespacesEnd;
178 if (visibleNamespaces.Count > savedVisibleNamespacesSize) {
179 visibleNamespaces.RemoveRange (savedVisibleNamespacesSize,
180 visibleNamespaces.Count - savedVisibleNamespacesSize);
185 // Consider a list L containing only namespace nodes in the
186 // axis and in the node-set in lexicographic order (ascending). To begin
187 // processing L, if the first node is not the default namespace node (a node
188 // with no namespace URI and no local name), then generate a space followed
189 // by xmlns="" if and only if the following conditions are met:
190 // - the element E that owns the axis is in the node-set
191 // - The nearest ancestor element of E in the node-set has a default
192 // namespace node in the node-set (default namespace nodes always
193 // have non-empty values in XPath)
194 // The latter condition eliminates unnecessary occurrences of xmlns="" in
195 // the canonical form since an element only receives an xmlns="" if its
196 // default namespace is empty and if it has an immediate parent in the
197 // canonical form that has a non-empty default namespace. To finish
198 // processing L, simply process every namespace node in L, except omit
199 // namespace node with local name xml, which defines the xml prefix,
200 // if its string value is http://www.w3.org/XML/1998/namespace.
201 private void WriteNamespacesAxis (XmlNode node, bool visible)
203 // Console.WriteLine ("Debug: namespaces");
205 XmlDocument doc = node.OwnerDocument;
206 bool has_empty_namespace = false;
207 ArrayList list = new ArrayList ();
208 for (XmlNode cur = node; cur != null && cur != doc; cur = cur.ParentNode) {
209 foreach (XmlNode attribute in cur.Attributes) {
210 if (!IsNamespaceNode (attribute))
213 // get namespace prefix
214 string prefix = string.Empty;
215 if (attribute.Prefix == "xmlns")
216 prefix = attribute.LocalName;
218 // check if it is "xml" namespace
219 if (prefix == "xml" && attribute.Value == "http://www.w3.org/XML/1998/namespace")
222 // make sure that this is an active namespace
224 string ns = node.GetNamespaceOfPrefix (prefix);
225 if (ns != attribute.Value)
228 // check that it is selected with XPath
229 if (!IsNodeVisible (attribute))
232 // check that we have not rendered it yet
233 bool rendered = IsNamespaceRendered (prefix, attribute.Value);
235 // add to the visible namespaces stack
237 visibleNamespaces.Add (attribute);
240 list.Add (attribute);
242 if (prefix == string.Empty)
243 has_empty_namespace = true;
247 // add empty namespace if needed
248 if (visible && !has_empty_namespace && !IsNamespaceRendered (string.Empty, string.Empty))
249 res.Append (" xmlns=\"\"");
251 list.Sort (new XmlDsigC14NTransformNamespacesComparer ());
252 foreach (object obj in list) {
253 XmlNode attribute = (obj as XmlNode);
254 if (attribute != null) {
256 res.Append (attribute.Name);
258 res.Append (attribute.Value);
263 // move the rendered namespaces stack
265 prevVisibleNamespacesStart = prevVisibleNamespacesEnd;
266 prevVisibleNamespacesEnd = visibleNamespaces.Count;
271 // In lexicographic order (ascending), process each node that
272 // is in the element's attribute axis and in the node-set.
274 // The processing of an element node E MUST be modified slightly
275 // when an XPath node-set is given as input and the element's
276 // parent is omitted from the node-set.
277 private void WriteAttributesAxis (XmlNode node)
279 // Console.WriteLine ("Debug: attributes");
281 ArrayList list = new ArrayList ();
282 foreach (XmlNode attribute in node.Attributes) {
283 if (!IsNamespaceNode (attribute) && IsNodeVisible (attribute))
284 list.Add (attribute);
287 // Add attributes from "xml" namespace for "inclusive" c14n only:
289 // The method for processing the attribute axis of an element E
290 // in the node-set is enhanced. All element nodes along E's
291 // ancestor axis are examined for nearest occurrences of
292 // attributes in the xml namespace, such as xml:lang and
293 // xml:space (whether or not they are in the node-set).
294 // From this list of attributes, remove any that are in E's
295 // attribute axis (whether or not they are in the node-set).
296 // Then, lexicographically merge this attribute list with the
297 // nodes of E's attribute axis that are in the node-set. The
298 // result of visiting the attribute axis is computed by
299 // processing the attribute nodes in this merged attribute list.
300 if (!exclusive && node.ParentNode != null && !IsNodeVisible (node.ParentNode.ParentNode)) {
301 // if we have whole document then the node.ParentNode.ParentNode
303 for (XmlNode cur = node.ParentNode; cur != null; cur = cur.ParentNode) {
304 foreach (XmlNode attribute in cur.Attributes) {
305 // we are looking for "xml:*" attributes
306 if (attribute.Prefix != "xml")
309 // exclude ones that are in the node's attributes axis
310 if (node.Attributes.GetNamedItem (attribute.LocalName, attribute.NamespaceURI) != null)
313 // finally check that we don't have the same attribute in our list
315 foreach (object obj in list) {
316 XmlNode n = (obj as XmlNode);
317 if (n.Prefix == "xml" && n.LocalName == attribute.LocalName) {
326 // now we can add this attribute to our list
327 list.Add (attribute);
332 // sort namespaces and write results
333 list.Sort (new XmlDsigC14NTransformAttributesComparer ());
334 foreach (object obj in list) {
335 XmlNode attribute = (obj as XmlNode);
336 if (attribute != null) {
338 res.Append (attribute.Name);
340 res.Append (NormalizeString (attribute.Value, XmlNodeType.Attribute));
347 // the string value, except all ampersands are replaced
348 // by &, all open angle brackets (<) are replaced by <, all closing
349 // angle brackets (>) are replaced by >, and all #xD characters are
350 // replaced by 
.
351 private void WriteTextNode (XmlNode node, bool visible)
353 // Console.WriteLine ("Debug: text node");
355 res.Append (NormalizeString (node.Value, XmlNodeType.Text));
359 // Nothing if generating canonical XML without comments. For
360 // canonical XML with comments, generate the opening comment
361 // symbol (<!--), the string value of the node, and the
362 // closing comment symbol (-->). Also, a trailing #xA is rendered
363 // after the closing comment symbol for comment children of the
364 // root node with a lesser document order than the document
365 // element, and a leading #xA is rendered before the opening
366 // comment symbol of comment children of the root node with a
367 // greater document order than the document element. (Comment
368 // children of the root node represent comments outside of the
369 // top-level document element and outside of the document type
371 private void WriteCommentNode (XmlNode node, bool visible)
373 // Console.WriteLine ("Debug: comment node");
374 if (visible && comments) {
375 if (state == XmlCanonicalizerState.AfterDocElement)
376 res.Append ("\x0A<!--");
380 res.Append (NormalizeString (node.Value, XmlNodeType.Comment));
382 if (state == XmlCanonicalizerState.BeforeDocElement)
383 res.Append ("-->\x0A");
389 // Processing Instruction (PI) Nodes-
390 // The opening PI symbol (<?), the PI target name of the node,
391 // a leading space and the string value if it is not empty, and
392 // the closing PI symbol (?>). If the string value is empty,
393 // then the leading space is not added. Also, a trailing #xA is
394 // rendered after the closing PI symbol for PI children of the
395 // root node with a lesser document order than the document
396 // element, and a leading #xA is rendered before the opening PI
397 // symbol of PI children of the root node with a greater document
398 // order than the document element.
399 private void WriteProcessingInstructionNode (XmlNode node, bool visible)
401 // Console.WriteLine ("Debug: PI node");
404 if (state == XmlCanonicalizerState.AfterDocElement)
405 res.Append ("\x0A<?");
409 res.Append (node.Name);
410 if (node.Value.Length > 0) {
412 res.Append (NormalizeString (node.Value, XmlNodeType.ProcessingInstruction));
415 if (state == XmlCanonicalizerState.BeforeDocElement)
416 res.Append ("?>\x0A");
422 private bool IsNodeVisible (XmlNode node)
424 // if node list is empty then we process whole document
428 // walk thru the list
429 foreach (XmlNode xn in xnl) {
430 if (node.Equals (xn))
437 private bool IsNamespaceRendered (string prefix, string uri)
439 // if the default namespace xmlns="" is not re-defined yet
440 // then we do not want to print it out
441 bool IsEmptyNs = prefix == string.Empty && uri == string.Empty;
442 int start = (IsEmptyNs) ? 0 : prevVisibleNamespacesStart;
443 for (int i = visibleNamespaces.Count - 1; i >= start; i--) {
444 XmlNode node = (visibleNamespaces[i] as XmlNode);
446 // get namespace prefix
447 string p = string.Empty;
448 if (node.Prefix == "xmlns")
451 return node.Value == uri;
458 private bool IsNamespaceNode (XmlNode node)
460 if (node == null || node.NodeType != XmlNodeType.Attribute)
462 return node.NamespaceURI == "http://www.w3.org/2000/xmlns/";
465 private string NormalizeString (string input, XmlNodeType type)
467 StringBuilder sb = new StringBuilder ();
468 for (int i = 0; i < input.Length; i++) {
470 if (ch == '<' && (type == XmlNodeType.Attribute || type == XmlNodeType.Text))
472 else if (ch == '>' && type == XmlNodeType.Text)
474 else if (ch == '&' && (type == XmlNodeType.Attribute || type == XmlNodeType.Text))
476 else if (ch == '\"' && type == XmlNodeType.Attribute)
477 sb.Append (""");
478 else if (ch == '\x09' && type == XmlNodeType.Attribute)
480 else if (ch == '\x0A' && type == XmlNodeType.Attribute)
482 else if (ch == '\x0D' && (type == XmlNodeType.Attribute ||
483 type == XmlNodeType.Text ||
484 type == XmlNodeType.Comment ||
485 type == XmlNodeType.ProcessingInstruction))
491 return sb.ToString ();
495 internal class XmlDsigC14NTransformAttributesComparer : IComparer
497 public int Compare (object x, object y)
499 XmlNode n1 = (x as XmlNode);
500 XmlNode n2 = (y as XmlNode);
509 else if (n1.Prefix == n2.Prefix)
510 return string.Compare (n1.LocalName, n2.LocalName);
512 // Attributes in the default namespace are first
513 // because the default namespace is not applied to
514 // unqualified attributes
515 if (n1.Prefix == string.Empty)
517 else if (n2.Prefix == string.Empty)
520 int ret = string.Compare (n1.NamespaceURI, n2.NamespaceURI);
522 ret = string.Compare (n1.LocalName, n2.LocalName);
527 internal class XmlDsigC14NTransformNamespacesComparer : IComparer
529 public int Compare (object x, object y)
531 XmlNode n1 = (x as XmlNode);
532 XmlNode n2 = (y as XmlNode);
541 else if (n1.Prefix == string.Empty)
543 else if (n2.Prefix == string.Empty)
546 return string.Compare (n1.LocalName, n2.LocalName);