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");
100 case XmlNodeType.Attribute:
101 throw new XmlException ("Attribute node is impossible here");
102 case XmlNodeType.EndElement:
103 throw new XmlException ("EndElement node is impossible here");
104 case XmlNodeType.EndEntity:
105 throw new XmlException ("EndEntity node is impossible here");
106 case XmlNodeType.DocumentType:
107 case XmlNodeType.Entity:
108 case XmlNodeType.Notation:
109 case XmlNodeType.XmlDeclaration:
115 private void WriteDocumentNode (XmlNode node)
117 state = XmlCanonicalizerState.BeforeDocElement;
118 for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling)
123 // If the element is not in the node-set, then the result is obtained
124 // by processing the namespace axis, then the attribute axis, then
125 // processing the child nodes of the element that are in the node-set
126 // (in document order). If the element is inthe node-set, then the result
127 // is an open angle bracket (<), the element QName, the result of
128 // processing the namespace axis, the result of processing the attribute
129 // axis, a close angle bracket (>), the result of processing the child
130 // nodes of the element that are in the node-set (in document order), an
131 // open angle bracket, a forward slash (/), the element QName, and a close
133 private void WriteElementNode (XmlNode node, bool visible)
135 // Console.WriteLine ("Debug: element node");
137 // remember current state
138 int savedPrevVisibleNamespacesStart = prevVisibleNamespacesStart;
139 int savedPrevVisibleNamespacesEnd = prevVisibleNamespacesEnd;
140 int savedVisibleNamespacesSize = visibleNamespaces.Count;
141 XmlCanonicalizerState s = state;
142 if (visible && state == XmlCanonicalizerState.BeforeDocElement)
143 state = XmlCanonicalizerState.InsideDocElement;
148 res.Append (node.Name);
151 // this is odd but you can select namespaces
152 // and attributes even if node itself is not visible
153 WriteNamespacesAxis (node, visible);
154 WriteAttributesAxis (node);
160 for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling)
166 res.Append (node.Name);
171 if (visible && s == XmlCanonicalizerState.BeforeDocElement)
172 state = XmlCanonicalizerState.AfterDocElement;
173 prevVisibleNamespacesStart = savedPrevVisibleNamespacesStart;
174 prevVisibleNamespacesEnd = savedPrevVisibleNamespacesEnd;
175 if (visibleNamespaces.Count > savedVisibleNamespacesSize) {
176 visibleNamespaces.RemoveRange (savedVisibleNamespacesSize,
177 visibleNamespaces.Count - savedVisibleNamespacesSize);
182 // Consider a list L containing only namespace nodes in the
183 // axis and in the node-set in lexicographic order (ascending). To begin
184 // processing L, if the first node is not the default namespace node (a node
185 // with no namespace URI and no local name), then generate a space followed
186 // by xmlns="" if and only if the following conditions are met:
187 // - the element E that owns the axis is in the node-set
188 // - The nearest ancestor element of E in the node-set has a default
189 // namespace node in the node-set (default namespace nodes always
190 // have non-empty values in XPath)
191 // The latter condition eliminates unnecessary occurrences of xmlns="" in
192 // the canonical form since an element only receives an xmlns="" if its
193 // default namespace is empty and if it has an immediate parent in the
194 // canonical form that has a non-empty default namespace. To finish
195 // processing L, simply process every namespace node in L, except omit
196 // namespace node with local name xml, which defines the xml prefix,
197 // if its string value is http://www.w3.org/XML/1998/namespace.
198 private void WriteNamespacesAxis (XmlNode node, bool visible)
200 // Console.WriteLine ("Debug: namespaces");
202 XmlDocument doc = node.OwnerDocument;
203 bool has_empty_namespace = false;
204 ArrayList list = new ArrayList ();
205 for (XmlNode cur = node; cur != null && cur != doc; cur = cur.ParentNode) {
206 foreach (XmlNode attribute in cur.Attributes) {
207 if (!IsNamespaceNode (attribute))
210 // get namespace prefix
211 string prefix = string.Empty;
212 if (attribute.Prefix == "xmlns")
213 prefix = attribute.LocalName;
215 // check if it is "xml" namespace
216 if (prefix == "xml" && attribute.Value == "http://www.w3.org/XML/1998/namespace")
219 // make sure that this is an active namespace
221 string ns = node.GetNamespaceOfPrefix (prefix);
222 if (ns != attribute.Value)
225 // check that it is selected with XPath
226 if (!IsNodeVisible (attribute))
229 // check that we have not rendered it yet
230 bool rendered = IsNamespaceRendered (prefix, attribute.Value);
232 // add to the visible namespaces stack
234 visibleNamespaces.Add (attribute);
237 list.Add (attribute);
239 if (prefix == string.Empty)
240 has_empty_namespace = true;
244 // add empty namespace if needed
245 if (visible && !has_empty_namespace && !IsNamespaceRendered (string.Empty, string.Empty))
246 res.Append (" xmlns=\"\"");
248 list.Sort (new XmlDsigC14NTransformNamespacesComparer ());
249 foreach (object obj in list) {
250 XmlNode attribute = (obj as XmlNode);
251 if (attribute != null) {
253 res.Append (attribute.Name);
255 res.Append (attribute.Value);
260 // move the rendered namespaces stack
262 prevVisibleNamespacesStart = prevVisibleNamespacesEnd;
263 prevVisibleNamespacesEnd = visibleNamespaces.Count;
268 // In lexicographic order (ascending), process each node that
269 // is in the element's attribute axis and in the node-set.
271 // The processing of an element node E MUST be modified slightly
272 // when an XPath node-set is given as input and the element's
273 // parent is omitted from the node-set.
274 private void WriteAttributesAxis (XmlNode node)
276 // Console.WriteLine ("Debug: attributes");
278 ArrayList list = new ArrayList ();
279 foreach (XmlNode attribute in node.Attributes) {
280 if (!IsNamespaceNode (attribute) && IsNodeVisible (attribute))
281 list.Add (attribute);
284 // Add attributes from "xml" namespace for "inclusive" c14n only:
286 // The method for processing the attribute axis of an element E
287 // in the node-set is enhanced. All element nodes along E's
288 // ancestor axis are examined for nearest occurrences of
289 // attributes in the xml namespace, such as xml:lang and
290 // xml:space (whether or not they are in the node-set).
291 // From this list of attributes, remove any that are in E's
292 // attribute axis (whether or not they are in the node-set).
293 // Then, lexicographically merge this attribute list with the
294 // nodes of E's attribute axis that are in the node-set. The
295 // result of visiting the attribute axis is computed by
296 // processing the attribute nodes in this merged attribute list.
297 if (!exclusive && node.ParentNode != null && !IsNodeVisible (node.ParentNode.ParentNode)) {
298 // if we have whole document then the node.ParentNode.ParentNode
300 for (XmlNode cur = node.ParentNode; cur != null; cur = cur.ParentNode) {
301 foreach (XmlNode attribute in cur.Attributes) {
302 // we are looking for "xml:*" attributes
303 if (attribute.Prefix != "xml")
306 // exclude ones that are in the node's attributes axis
307 if (node.Attributes.GetNamedItem (attribute.LocalName, attribute.NamespaceURI) != null)
310 // finally check that we don't have the same attribute in our list
312 foreach (object obj in list) {
313 XmlNode n = (obj as XmlNode);
314 if (n.Prefix == "xml" && n.LocalName == attribute.LocalName) {
323 // now we can add this attribute to our list
324 list.Add (attribute);
329 // sort namespaces and write results
330 list.Sort (new XmlDsigC14NTransformAttributesComparer ());
331 foreach (object obj in list) {
332 XmlNode attribute = (obj as XmlNode);
333 if (attribute != null) {
335 res.Append (attribute.Name);
337 res.Append (NormalizeString (attribute.Value, XmlNodeType.Attribute));
344 // the string value, except all ampersands are replaced
345 // by &, all open angle brackets (<) are replaced by <, all closing
346 // angle brackets (>) are replaced by >, and all #xD characters are
347 // replaced by 
.
348 private void WriteTextNode (XmlNode node, bool visible)
350 // Console.WriteLine ("Debug: text node");
352 res.Append (NormalizeString (node.Value, XmlNodeType.Text));
356 // Nothing if generating canonical XML without comments. For
357 // canonical XML with comments, generate the opening comment
358 // symbol (<!--), the string value of the node, and the
359 // closing comment symbol (-->). Also, a trailing #xA is rendered
360 // after the closing comment symbol for comment children of the
361 // root node with a lesser document order than the document
362 // element, and a leading #xA is rendered before the opening
363 // comment symbol of comment children of the root node with a
364 // greater document order than the document element. (Comment
365 // children of the root node represent comments outside of the
366 // top-level document element and outside of the document type
368 private void WriteCommentNode (XmlNode node, bool visible)
370 // Console.WriteLine ("Debug: comment node");
371 if (visible && comments) {
372 if (state == XmlCanonicalizerState.AfterDocElement)
373 res.Append ("\x0A<!--");
377 res.Append (NormalizeString (node.Value, XmlNodeType.Comment));
379 if (state == XmlCanonicalizerState.BeforeDocElement)
380 res.Append ("-->\x0A");
386 // Processing Instruction (PI) Nodes-
387 // The opening PI symbol (<?), the PI target name of the node,
388 // a leading space and the string value if it is not empty, and
389 // the closing PI symbol (?>). If the string value is empty,
390 // then the leading space is not added. Also, a trailing #xA is
391 // rendered after the closing PI symbol for PI children of the
392 // root node with a lesser document order than the document
393 // element, and a leading #xA is rendered before the opening PI
394 // symbol of PI children of the root node with a greater document
395 // order than the document element.
396 private void WriteProcessingInstructionNode (XmlNode node, bool visible)
398 // Console.WriteLine ("Debug: PI node");
401 if (state == XmlCanonicalizerState.AfterDocElement)
402 res.Append ("\x0A<?");
406 res.Append (node.Name);
407 if (node.Value.Length > 0) {
409 res.Append (NormalizeString (node.Value, XmlNodeType.ProcessingInstruction));
412 if (state == XmlCanonicalizerState.BeforeDocElement)
413 res.Append ("?>\x0A");
419 private bool IsNodeVisible (XmlNode node)
421 // if node list is empty then we process whole document
425 // walk thru the list
426 foreach (XmlNode xn in xnl) {
427 if (node.Equals (xn))
434 private bool IsNamespaceRendered (string prefix, string uri)
436 // if the default namespace xmlns="" is not re-defined yet
437 // then we do not want to print it out
438 bool IsEmptyNs = prefix == string.Empty && uri == string.Empty;
439 int start = (IsEmptyNs) ? 0 : prevVisibleNamespacesStart;
440 for (int i = visibleNamespaces.Count - 1; i >= start; i--) {
441 XmlNode node = (visibleNamespaces[i] as XmlNode);
443 // get namespace prefix
444 string p = string.Empty;
445 if (node.Prefix == "xmlns")
448 return node.Value == uri;
455 private bool IsNamespaceNode (XmlNode node)
457 if (node == null || node.NodeType != XmlNodeType.Attribute)
459 return node.NamespaceURI == "http://www.w3.org/2000/xmlns/";
462 private string NormalizeString (string input, XmlNodeType type)
464 StringBuilder sb = new StringBuilder ();
465 for (int i = 0; i < input.Length; i++) {
467 if (ch == '<' && (type == XmlNodeType.Attribute || type == XmlNodeType.Text))
469 else if (ch == '>' && type == XmlNodeType.Text)
471 else if (ch == '&' && (type == XmlNodeType.Attribute || type == XmlNodeType.Text))
473 else if (ch == '\"' && type == XmlNodeType.Attribute)
474 sb.Append (""");
475 else if (ch == '\x09' && type == XmlNodeType.Attribute)
477 else if (ch == '\x0A' && type == XmlNodeType.Attribute)
479 else if (ch == '\x0D' && (type == XmlNodeType.Attribute ||
480 type == XmlNodeType.Text ||
481 type == XmlNodeType.Comment ||
482 type == XmlNodeType.ProcessingInstruction))
488 return sb.ToString ();
492 internal class XmlDsigC14NTransformAttributesComparer : IComparer
494 public int Compare (object x, object y)
496 XmlNode n1 = (x as XmlNode);
497 XmlNode n2 = (y as XmlNode);
506 else if (n1.Prefix == n2.Prefix)
507 return string.Compare (n1.LocalName, n2.LocalName);
509 // Attributes in the default namespace are first
510 // because the default namespace is not applied to
511 // unqualified attributes
512 if (n1.Prefix == string.Empty)
514 else if (n2.Prefix == string.Empty)
517 int ret = string.Compare (n1.NamespaceURI, n2.NamespaceURI);
519 ret = string.Compare (n1.LocalName, n2.LocalName);
524 internal class XmlDsigC14NTransformNamespacesComparer : IComparer
526 public int Compare (object x, object y)
528 XmlNode n1 = (x as XmlNode);
529 XmlNode n2 = (y as XmlNode);
538 else if (n1.Prefix == string.Empty)
540 else if (n2.Prefix == string.Empty)
543 return string.Compare (n1.LocalName, n2.LocalName);