New test.
[mono.git] / mcs / class / System.Security / Mono.Xml / XmlCanonicalizer.cs
1 //
2 // XmlCanonicalizer.cs - C14N implementation for XML Signature
3 // http://www.w3.org/TR/xml-c14n
4 //
5 // Author:
6 //      Aleksey Sanin (aleksey@aleksey.com)
7 //
8 // (C) 2003 Aleksey Sanin (aleksey@aleksey.com)
9 //
10
11 //
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
19 // 
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
22 // 
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 //
31 using System;
32 using System.Collections;
33 using System.IO;
34 using System.Text;
35 using System.Xml;
36
37 namespace Mono.Xml { 
38
39         internal class XmlCanonicalizer {
40
41                 private enum XmlCanonicalizerState
42                 {
43                         BeforeDocElement,
44                         InsideDocElement,
45                         AfterDocElement
46                 }
47                 
48                 // c14n parameters
49                 private bool comments;
50                 private bool exclusive;
51
52                 // input/output
53                 private XmlNodeList xnl;
54                 private StringBuilder res;
55                 
56                 // namespaces rendering stack
57                 private XmlCanonicalizerState state;
58                 private ArrayList visibleNamespaces;
59                 private int prevVisibleNamespacesStart;
60                 private int prevVisibleNamespacesEnd;
61
62                 public XmlCanonicalizer (bool withComments, bool excC14N)
63                 {           
64                         res = new StringBuilder ();
65                         comments = withComments;
66                         exclusive = excC14N;
67                         state = XmlCanonicalizerState.BeforeDocElement;
68                         visibleNamespaces = new ArrayList ();
69                         prevVisibleNamespacesStart = 0;
70                         prevVisibleNamespacesEnd = 0;
71                 }
72                 
73                 public Stream Canonicalize (XmlDocument doc)
74                 {
75                         WriteDocumentNode (doc);
76                         
77                         UTF8Encoding utf8 = new UTF8Encoding ();
78                         byte[] data = utf8.GetBytes (res.ToString ());
79                         return new MemoryStream (data);
80                 }
81                 
82                 public Stream Canonicalize (XmlNodeList nodes)
83                 {
84                         xnl = nodes;
85                         if (nodes == null || nodes.Count < 1)
86                                 return new MemoryStream ();
87                         return Canonicalize (nodes[0].OwnerDocument);
88                 }               
89
90                 private void WriteNode (XmlNode node)
91                 {
92                         // Console.WriteLine ("C14N Debug: node=" + node.Name);
93
94                         bool visible = IsNodeVisible (node);
95                         switch (node.NodeType) {
96                         case XmlNodeType.Document:
97                         case XmlNodeType.DocumentFragment:
98                                 WriteDocumentNode (node);
99                                 break;
100                         case XmlNodeType.Element:
101                                 WriteElementNode (node, visible);
102                                 break;
103                         case XmlNodeType.CDATA:
104                         case XmlNodeType.SignificantWhitespace:
105                         case XmlNodeType.Text:
106                                 // CDATA sections are processed as text nodes
107                                 WriteTextNode (node, visible);
108                                 break;
109                         case XmlNodeType.Whitespace:
110                                 if (state == XmlCanonicalizerState.InsideDocElement)
111                                         WriteTextNode (node, visible);
112                                 break;
113                         case XmlNodeType.Comment:
114                                 WriteCommentNode (node, visible);
115                                 break;
116                         case XmlNodeType.ProcessingInstruction:
117                                 WriteProcessingInstructionNode (node, visible);
118                                 break;
119                         case XmlNodeType.EntityReference:
120                                 for (int i = 0; i < node.ChildNodes.Count; i++)
121                                         WriteNode (node.ChildNodes [i]);
122                                 break;
123                         case XmlNodeType.Attribute:
124                                 throw new XmlException ("Attribute node is impossible here", null);
125                         case XmlNodeType.EndElement:
126                                 throw new XmlException ("EndElement node is impossible here", null);
127                         case XmlNodeType.EndEntity:
128                                 throw new XmlException ("EndEntity node is impossible here", null);
129                         case XmlNodeType.DocumentType:
130                         case XmlNodeType.Entity:
131                         case XmlNodeType.Notation:
132                         case XmlNodeType.XmlDeclaration:
133                                 // just do nothing
134                                 break;
135                         }
136                 }
137
138                 private void WriteDocumentNode (XmlNode node)
139                 {
140                         state = XmlCanonicalizerState.BeforeDocElement;
141                         for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling)
142                                 WriteNode (child);
143                 }
144                 
145                 // Element Nodes
146                 // If the element is not in the node-set, then the result is obtained 
147                 // by processing the namespace axis, then the attribute axis, then 
148                 // processing the child nodes of the element that are in the node-set 
149                 // (in document order). If the element is inthe node-set, then the result 
150                 // is an open angle bracket (<), the element QName, the result of 
151                 // processing the namespace axis, the result of processing the attribute 
152                 // axis, a close angle bracket (>), the result of processing the child 
153                 // nodes of the element that are in the node-set (in document order), an 
154                 // open angle bracket, a forward slash (/), the element QName, and a close 
155                 // angle bracket.
156                 private void WriteElementNode (XmlNode node, bool visible)
157                 {
158                         // Console.WriteLine ("Debug: element node");
159                     
160                         // remember current state 
161                         int savedPrevVisibleNamespacesStart = prevVisibleNamespacesStart;
162                         int savedPrevVisibleNamespacesEnd = prevVisibleNamespacesEnd;
163                         int savedVisibleNamespacesSize = visibleNamespaces.Count;
164                         XmlCanonicalizerState s = state;
165                         if (visible && state == XmlCanonicalizerState.BeforeDocElement)
166                                 state = XmlCanonicalizerState.InsideDocElement;
167                     
168                         // write start tag
169                         if (visible) {
170                                 res.Append ("<");
171                                 res.Append (node.Name);
172                         }
173                     
174                         // this is odd but you can select namespaces
175                         // and attributes even if node itself is not visible
176                         WriteNamespacesAxis (node, visible);
177                         WriteAttributesAxis (node);                     
178         
179                         if (visible)
180                                 res.Append (">");
181
182                         // write children
183                         for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling)
184                                 WriteNode (child);
185                                     
186                         // write end tag            
187                         if (visible) {
188                                 res.Append ("</");
189                                 res.Append (node.Name);
190                                 res.Append (">");
191                         }
192                     
193                         // restore state
194                         if (visible && s == XmlCanonicalizerState.BeforeDocElement)
195                                 state = XmlCanonicalizerState.AfterDocElement;
196                         prevVisibleNamespacesStart = savedPrevVisibleNamespacesStart;
197                         prevVisibleNamespacesEnd = savedPrevVisibleNamespacesEnd;
198                         if (visibleNamespaces.Count > savedVisibleNamespacesSize) {
199                                 visibleNamespaces.RemoveRange (savedVisibleNamespacesSize, 
200                                         visibleNamespaces.Count - savedVisibleNamespacesSize);
201                         }
202                 }
203
204                 // Namespace Axis
205                 // Consider a list L containing only namespace nodes in the 
206                 // axis and in the node-set in lexicographic order (ascending). To begin 
207                 // processing L, if the first node is not the default namespace node (a node 
208                 // with no namespace URI and no local name), then generate a space followed 
209                 // by xmlns="" if and only if the following conditions are met:
210                 //    - the element E that owns the axis is in the node-set
211                 //    - The nearest ancestor element of E in the node-set has a default 
212                 //          namespace node in the node-set (default namespace nodes always 
213                 //      have non-empty values in XPath)
214                 // The latter condition eliminates unnecessary occurrences of xmlns="" in 
215                 // the canonical form since an element only receives an xmlns="" if its 
216                 // default namespace is empty and if it has an immediate parent in the 
217                 // canonical form that has a non-empty default namespace. To finish 
218                 // processing  L, simply process every namespace node in L, except omit 
219                 // namespace node with local name xml, which defines the xml prefix, 
220                 // if its string value is http://www.w3.org/XML/1998/namespace.
221                 private void WriteNamespacesAxis (XmlNode node, bool visible)
222                 {
223                         // Console.WriteLine ("Debug: namespaces");
224
225                         XmlDocument doc = node.OwnerDocument;    
226                         bool has_empty_namespace = false;
227                         ArrayList list = new ArrayList ();
228                         for (XmlNode cur = node; cur != null && cur != doc; cur = cur.ParentNode) {
229                                 foreach (XmlNode attribute in cur.Attributes) {         
230                                         if (!IsNamespaceNode (attribute)) 
231                                                 continue;
232                                 
233                                         // get namespace prefix
234                                         string prefix = string.Empty;
235                                         if (attribute.Prefix == "xmlns") 
236                                                 prefix = attribute.LocalName;
237                             
238                                         // check if it is "xml" namespace                           
239                                         if (prefix == "xml" && attribute.Value == "http://www.w3.org/XML/1998/namespace")
240                                                 continue;
241                             
242                                         // make sure that this is an active namespace
243                                         // for our node
244                                         string ns = node.GetNamespaceOfPrefix (prefix);
245                                         if (ns != attribute.Value) 
246                                                 continue;
247                             
248                                         // check that it is selected with XPath
249                                         if (!IsNodeVisible (attribute)) 
250                                                 continue;
251
252                                         // check that we have not rendered it yet
253                                         bool rendered = IsNamespaceRendered (prefix, attribute.Value);
254
255                                         // add to the visible namespaces stack
256                                         if (visible)
257                                                 visibleNamespaces.Add (attribute);                            
258                             
259                                         if (!rendered)
260                                                 list.Add (attribute);
261                                     
262                                         if (prefix == string.Empty)
263                                                 has_empty_namespace = true;
264                                 }
265                         }
266
267                         // add empty namespace if needed                    
268                         if (visible && !has_empty_namespace && !IsNamespaceRendered (string.Empty, string.Empty)) 
269                                 res.Append (" xmlns=\"\"");
270                     
271                         list.Sort (new XmlDsigC14NTransformNamespacesComparer ());
272                         foreach (object obj in list) {
273                                 XmlNode attribute = (obj as XmlNode);
274                                 if (attribute != null) {
275                                         res.Append (" ");
276                                         res.Append (attribute.Name);
277                                         res.Append ("=\"");
278                                         res.Append (attribute.Value);
279                                         res.Append ("\"");
280                                 }
281                         }
282                     
283                         // move the rendered namespaces stack
284                         if (visible) {
285                                 prevVisibleNamespacesStart = prevVisibleNamespacesEnd;
286                                 prevVisibleNamespacesEnd = visibleNamespaces.Count;     
287                         }
288                 }
289                 
290                 // Attribute Axis 
291                 // In lexicographic order (ascending), process each node that 
292                 // is in the element's attribute axis and in the node-set.
293                 // 
294                 // The processing of an element node E MUST be modified slightly 
295                 // when an XPath node-set is given as input and the element's 
296                 // parent is omitted from the node-set.
297                 private void WriteAttributesAxis (XmlNode node)
298                 {
299                         // Console.WriteLine ("Debug: attributes");
300                 
301                         ArrayList list = new ArrayList ();
302                         foreach (XmlNode attribute in node.Attributes) {        
303                                 if (!IsNamespaceNode (attribute) && IsNodeVisible (attribute))
304                                         list.Add (attribute);
305                         }
306                      
307                         // Add attributes from "xml" namespace for "inclusive" c14n only:
308                         //
309                         // The method for processing the attribute axis of an element E 
310                         // in the node-set is enhanced. All element nodes along E's 
311                         // ancestor axis are examined for nearest occurrences of 
312                         // attributes in the xml namespace, such as xml:lang and 
313                         // xml:space (whether or not they are in the node-set). 
314                         // From this list of attributes, remove any that are in E's 
315                         // attribute axis (whether or not they are in the node-set). 
316                         // Then, lexicographically merge this attribute list with the 
317                         // nodes of E's attribute axis that are in the node-set. The 
318                         // result of visiting the attribute axis is computed by 
319                         // processing the attribute nodes in this merged attribute list.
320                         if (!exclusive && node.ParentNode != null && node.ParentNode.ParentNode != null && !IsNodeVisible (node.ParentNode.ParentNode)) {
321                                 // if we have whole document then the node.ParentNode.ParentNode
322                                 // is always visible
323                                 for (XmlNode cur = node.ParentNode; cur != null; cur = cur.ParentNode) {
324                                         if (cur.Attributes == null)
325                                                 continue;
326                                         foreach (XmlNode attribute in cur.Attributes) {
327                                                 // we are looking for "xml:*" attributes
328                                                 if (attribute.Prefix != "xml")
329                                                         continue;
330                                 
331                                                 // exclude ones that are in the node's attributes axis
332                                                 if (node.Attributes.GetNamedItem (attribute.LocalName, attribute.NamespaceURI) != null)
333                                                         continue;
334                                 
335                                                 // finally check that we don't have the same attribute in our list
336                                                 bool found = false;
337                                                 foreach (object obj in list) {
338                                                         XmlNode n = (obj as XmlNode);
339                                                         if (n.Prefix == "xml" && n.LocalName == attribute.LocalName) {
340                                                                 found = true;
341                                                                 break;
342                                                         }
343                                                 }
344                                 
345                                                 if (found) 
346                                                         continue;
347                                 
348                                                 // now we can add this attribute to our list
349                                                 list.Add (attribute);
350                                         }
351                                 }               
352                         }
353                         
354                         // sort namespaces and write results        
355                         list.Sort (new XmlDsigC14NTransformAttributesComparer ());
356                         foreach (object obj in list) {
357                                 XmlNode attribute = (obj as XmlNode);
358                                 if (attribute != null) {
359                                         res.Append (" ");
360                                         res.Append (attribute.Name);
361                                         res.Append ("=\"");
362                                         res.Append (NormalizeString (attribute.Value, XmlNodeType.Attribute));
363                                         res.Append ("\"");
364                                 }
365                         }
366                 }
367
368                 // Text Nodes
369                 // the string value, except all ampersands are replaced 
370                 // by &amp;, all open angle brackets (<) are replaced by &lt;, all closing 
371                 // angle brackets (>) are replaced by &gt;, and all #xD characters are 
372                 // replaced by &#xD;.
373                 private void WriteTextNode (XmlNode node, bool visible)
374                 {
375                         // Console.WriteLine ("Debug: text node");
376                         if (visible)
377                                 res.Append (NormalizeString (node.Value, node.NodeType));
378 //                              res.Append (NormalizeString (node.Value, XmlNodeType.Text));
379                 }               
380
381                 // Comment Nodes
382                 // Nothing if generating canonical XML without comments. For 
383                 // canonical XML with comments, generate the opening comment 
384                 // symbol (<!--), the string value of the node, and the 
385                 // closing comment symbol (-->). Also, a trailing #xA is rendered 
386                 // after the closing comment symbol for comment children of the 
387                 // root node with a lesser document order than the document 
388                 // element, and a leading #xA is rendered before the opening 
389                 // comment symbol of comment children of the root node with a 
390                 // greater document order than the document element. (Comment 
391                 // children of the root node represent comments outside of the 
392                 // top-level document element and outside of the document type 
393                 // declaration).
394                 private void WriteCommentNode (XmlNode node, bool visible)
395                 {
396                         // Console.WriteLine ("Debug: comment node");
397                         if (visible && comments) {
398                             if (state == XmlCanonicalizerState.AfterDocElement)
399                                     res.Append ("\x0A<!--");
400                             else
401                                     res.Append ("<!--");
402                         
403                             res.Append (NormalizeString (node.Value, XmlNodeType.Comment));
404                             
405                             if (state == XmlCanonicalizerState.BeforeDocElement)
406                                     res.Append ("-->\x0A");
407                             else
408                                     res.Append ("-->");
409                         }
410                 }
411                 
412                 // Processing Instruction (PI) Nodes- 
413                 // The opening PI symbol (<?), the PI target name of the node, 
414                 // a leading space and the string value if it is not empty, and 
415                 // the closing PI symbol (?>). If the string value is empty, 
416                 // then the leading space is not added. Also, a trailing #xA is 
417                 // rendered after the closing PI symbol for PI children of the 
418                 // root node with a lesser document order than the document 
419                 // element, and a leading #xA is rendered before the opening PI 
420                 // symbol of PI children of the root node with a greater document 
421                 // order than the document element.
422                 private void WriteProcessingInstructionNode (XmlNode node, bool visible)
423                 {
424                         // Console.WriteLine ("Debug: PI node");
425
426                         if (visible) {
427                                 if (state == XmlCanonicalizerState.AfterDocElement)
428                                         res.Append ("\x0A<?");
429                                 else
430                                         res.Append ("<?");
431                         
432                                 res.Append (node.Name);
433                                 if (node.Value.Length > 0) {
434                                         res.Append (" ");
435                                         res.Append (NormalizeString (node.Value, XmlNodeType.ProcessingInstruction));
436                                 }
437                         
438                                 if (state == XmlCanonicalizerState.BeforeDocElement)
439                                         res.Append ("?>\x0A");
440                                 else
441                                         res.Append ("?>");
442                         }
443                 }
444                 
445                 private bool IsNodeVisible (XmlNode node)
446                 {
447                         // if node list is empty then we process whole document
448                         if (xnl == null) 
449                                 return true;
450                     
451                         // walk thru the list
452                         foreach (XmlNode xn in xnl) {
453                                 if (node.Equals (xn)) 
454                                         return true;
455                         }
456                     
457                         return false;
458                 }
459
460                 private bool IsNamespaceRendered (string prefix, string uri)
461                 {
462                         // if the default namespace xmlns="" is not re-defined yet
463                         // then we do not want to print it out
464                         bool IsEmptyNs = prefix == string.Empty && uri == string.Empty;
465                         int start = (IsEmptyNs) ? 0 : prevVisibleNamespacesStart;
466                         for (int i = visibleNamespaces.Count - 1; i >= start; i--) {
467                                 XmlNode node = (visibleNamespaces[i] as XmlNode);
468                                 if (node != null) {
469                                         // get namespace prefix
470                                         string p = string.Empty;
471                                         if (node.Prefix == "xmlns") 
472                                                 p = node.LocalName;
473                                         if (p == prefix)
474                                                 return node.Value == uri;
475                                 }
476                         }
477                     
478                         return IsEmptyNs;
479                 }
480                 
481                 private bool IsNamespaceNode (XmlNode node)
482                 {
483                         if (node == null || node.NodeType != XmlNodeType.Attribute) 
484                                 return false;
485                         return node.NamespaceURI == "http://www.w3.org/2000/xmlns/";
486                 }
487     
488                 private bool IsTextNode (XmlNodeType type)
489                 {
490                         switch (type) {
491                         case XmlNodeType.Text:
492                         case XmlNodeType.CDATA:
493                         case XmlNodeType.SignificantWhitespace:
494                         case XmlNodeType.Whitespace:
495                                 return true;
496                         }
497                         return false;
498                 }
499
500                 private string NormalizeString (string input, XmlNodeType type)
501                 {
502                         StringBuilder sb = new StringBuilder ();
503                         for (int i = 0; i < input.Length; i++) {
504                                 char ch = input[i];
505                                 if (ch == '<' && (type == XmlNodeType.Attribute || IsTextNode (type)))
506                                         sb.Append ("&lt;");
507                                 else if (ch == '>' && IsTextNode (type))
508                                         sb.Append ("&gt;");
509                                 else if (ch == '&' && (type == XmlNodeType.Attribute || IsTextNode (type)))
510                                         sb.Append ("&amp;");
511                                 else if (ch == '\"' && type == XmlNodeType.Attribute)
512                                         sb.Append ("&quot;");
513                                 else if (ch == '\x09' && type == XmlNodeType.Attribute)
514                                         sb.Append ("&#x9;");
515                                 else if (ch == '\x0A' && type == XmlNodeType.Attribute)
516                                         sb.Append ("&#xA;");
517                                 else if (ch == '\x0D')
518                                         sb.Append ("&#xD;");
519                                 else
520                                         sb.Append (ch);
521                         }
522                     
523                         return sb.ToString ();
524                 }
525         }
526     
527         internal class XmlDsigC14NTransformAttributesComparer : IComparer
528         {
529                 public int Compare (object x, object y)
530                 {
531                         XmlNode n1 = (x as XmlNode);
532                         XmlNode n2 = (y as XmlNode);
533                 
534                         // simple cases
535                         if (n1 == n2) 
536                                 return 0;
537                         else if (n1 == null) 
538                                 return -1;
539                         else if (n2 == null) 
540                                 return 1;
541                         else if (n1.Prefix == n2.Prefix) 
542                                 return string.Compare (n1.LocalName, n2.LocalName);
543         
544                         // Attributes in the default namespace are first
545                         // because the default namespace is not applied to
546                         // unqualified attributes
547                         if (n1.Prefix == string.Empty) 
548                                 return -1;
549                         else if (n2.Prefix == string.Empty) 
550                                 return 1;
551                     
552                         int ret = string.Compare (n1.NamespaceURI, n2.NamespaceURI);
553                         if (ret == 0)
554                                 ret = string.Compare (n1.LocalName, n2.LocalName);
555                         return ret;
556                 }
557         }
558
559         internal class XmlDsigC14NTransformNamespacesComparer : IComparer
560         {
561                 public int Compare (object x, object y)
562                 {
563                         XmlNode n1 = (x as XmlNode);
564                         XmlNode n2 = (y as XmlNode);
565                 
566                         // simple cases
567                         if (n1 == n2) 
568                                 return 0;
569                         else if (n1 == null) 
570                                 return -1;
571                         else if (n2 == null) 
572                                 return 1;
573                         else if (n1.Prefix == string.Empty) 
574                                 return -1;
575                         else if (n2.Prefix == string.Empty) 
576                                 return 1;
577                     
578                         return string.Compare (n1.LocalName, n2.LocalName);
579                 }
580         }
581 }
582