Initial commit
[mono.git] / mcs / class / referencesource / System.Xml / System / Xml / ValidateNames.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="ValidateNames.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 // <owner current="true" primary="true">[....]</owner>
6 //------------------------------------------------------------------------------
7
8 using System;
9 #if !SILVERLIGHT
10 using System.Xml.XPath;
11 #endif
12 using System.Diagnostics;
13 using System.Globalization;
14
15 #if SILVERLIGHT_XPATH
16 namespace System.Xml.XPath {
17 #else
18 namespace System.Xml {
19 #endif
20
21     /// <summary>
22     /// Contains various static functions and methods for parsing and validating:
23     ///     NCName (not namespace-aware, no colons allowed)
24     ///     QName (prefix:local-name)
25     /// </summary>
26     internal static class ValidateNames {
27
28         internal enum Flags {
29             NCNames = 0x1,              // Validate that each non-empty prefix and localName is a valid NCName
30             CheckLocalName = 0x2,       // Validate the local-name
31             CheckPrefixMapping = 0x4,   // Validate the prefix --> namespace mapping
32             All = 0x7,
33             AllExceptNCNames = 0x6,
34             AllExceptPrefixMapping = 0x3,
35         };
36
37         static XmlCharType xmlCharType = XmlCharType.Instance;
38
39 #if !SILVERLIGHT
40         //-----------------------------------------------
41         // Nmtoken parsing
42         //-----------------------------------------------
43         /// <summary>
44         /// Attempts to parse the input string as an Nmtoken (see the XML spec production [7] && XML Namespaces spec).
45         /// Quits parsing when an invalid Nmtoken char is reached or the end of string is reached.
46         /// Returns the number of valid Nmtoken chars that were parsed.
47         /// </summary>
48         internal static unsafe int ParseNmtoken(string s, int offset) {
49             Debug.Assert(s != null && offset <= s.Length);
50
51             // Keep parsing until the end of string or an invalid NCName character is reached
52             int i = offset;
53             while (i < s.Length) {
54                 if ((xmlCharType.charProperties[s[i]] & XmlCharType.fNCNameSC) != 0) { // if (xmlCharType.IsNCNameSingleChar(s[i])) {
55                     i++;
56                 }
57 #if XML10_FIFTH_EDITION
58                 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
59                     i += 2;
60                 }
61 #endif
62                 else {
63                     break;
64                 }
65             }
66
67             return i - offset;
68         }
69 #endif
70
71         //-----------------------------------------------
72         // Nmtoken parsing (no XML namespaces support)
73         //-----------------------------------------------
74         /// <summary>
75         /// Attempts to parse the input string as an Nmtoken (see the XML spec production [7]) without taking 
76         /// into account the XML Namespaces spec. What it means is that the ':' character is allowed at any 
77         /// position and any number of times in the token.
78         /// Quits parsing when an invalid Nmtoken char is reached or the end of string is reached.
79         /// Returns the number of valid Nmtoken chars that were parsed.
80         /// </summary>
81 #if SILVERLIGHT && !SILVERLIGHT_DISABLE_SECURITY && XMLCHARTYPE_USE_RESOURCE
82         [System.Security.SecuritySafeCritical]
83 #endif
84         internal static unsafe int ParseNmtokenNoNamespaces(string s, int offset) {
85
86             Debug.Assert(s != null && offset <= s.Length);
87
88             // Keep parsing until the end of string or an invalid Name character is reached
89             int i = offset;
90             while (i < s.Length) {
91                 if ((xmlCharType.charProperties[s[i]] & XmlCharType.fNCNameSC) != 0 || s[i] == ':') { // if (xmlCharType.IsNameSingleChar(s[i])) {
92                     i++;
93                 }
94 #if XML10_FIFTH_EDITION
95                 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
96                     i += 2;
97                 }
98 #endif
99                 else {
100                     break;
101                 }
102             }
103
104             return i - offset;
105         }
106
107         // helper methods
108         internal static bool IsNmtokenNoNamespaces(string s) {
109             int endPos = ParseNmtokenNoNamespaces(s, 0);
110             return endPos > 0 && endPos == s.Length;
111         }
112
113         //-----------------------------------------------
114         // Name parsing (no XML namespaces support)
115         //-----------------------------------------------
116         /// <summary>
117         /// Attempts to parse the input string as a Name without taking into account the XML Namespaces spec.
118         /// What it means is that the ':' character does not delimiter prefix and local name, but it is a regular
119         /// name character, which is allowed to appear at any position and any number of times in the name.
120         /// Quits parsing when an invalid Name char is reached or the end of string is reached.
121         /// Returns the number of valid Name chars that were parsed.
122         /// </summary>
123 #if SILVERLIGHT && !SILVERLIGHT_DISABLE_SECURITY && XMLCHARTYPE_USE_RESOURCE
124         [System.Security.SecuritySafeCritical]
125 #endif
126         internal static unsafe int ParseNameNoNamespaces(string s, int offset) {
127
128             Debug.Assert(s != null && offset <= s.Length);
129
130             // Quit if the first character is not a valid NCName starting character
131             int i = offset;
132             if (i < s.Length) {
133                 if ((xmlCharType.charProperties[s[i]] & XmlCharType.fNCStartNameSC) != 0 || s[i] == ':') { // xmlCharType.IsStartNCNameSingleChar(s[i])) {
134                     i++;
135                 }
136 #if XML10_FIFTH_EDITION
137                 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
138                     i += 2;
139                 }
140 #endif
141                 else {
142                     return 0; // no valid StartNCName char
143                 }
144
145                 // Keep parsing until the end of string or an invalid NCName character is reached
146                 while (i < s.Length) {
147                     if ((xmlCharType.charProperties[s[i]] & XmlCharType.fNCNameSC) != 0 || s[i] == ':') { // if (xmlCharType.IsNCNameSingleChar(s[i]))
148                         i++;
149                     }
150 #if XML10_FIFTH_EDITION
151                     else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
152                         i += 2;
153                     }
154 #endif
155                     else {
156                         break;
157                     }
158                 }
159             }
160
161             return i - offset;
162         }
163
164         // helper methods
165         internal static bool IsNameNoNamespaces(string s) {
166             int endPos = ParseNameNoNamespaces(s, 0);
167             return endPos > 0 && endPos == s.Length;
168         }
169
170         //-----------------------------------------------
171         // NCName parsing
172         //-----------------------------------------------
173
174         /// <summary>
175         /// Attempts to parse the input string as an NCName (see the XML Namespace spec).
176         /// Quits parsing when an invalid NCName char is reached or the end of string is reached.
177         /// Returns the number of valid NCName chars that were parsed.
178         /// </summary>
179 #if SILVERLIGHT && !SILVERLIGHT_DISABLE_SECURITY && XMLCHARTYPE_USE_RESOURCE
180         [System.Security.SecuritySafeCritical]
181 #endif
182         internal static unsafe int ParseNCName(string s, int offset) {
183
184             Debug.Assert(s != null && offset <= s.Length);
185
186             // Quit if the first character is not a valid NCName starting character
187             int i = offset;
188             if (i < s.Length) {
189                 if ((xmlCharType.charProperties[s[i]] & XmlCharType.fNCStartNameSC) != 0) { // xmlCharType.IsStartNCNameSingleChar(s[i])) {
190                     i++;
191                 }
192 #if XML10_FIFTH_EDITION
193                 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
194                     i += 2;
195                 }
196 #endif
197                 else {
198                     return 0; // no valid StartNCName char
199                 }
200
201                 // Keep parsing until the end of string or an invalid NCName character is reached
202                 while (i < s.Length) {
203                     if ((xmlCharType.charProperties[s[i]] & XmlCharType.fNCNameSC) != 0) { // if (xmlCharType.IsNCNameSingleChar(s[i]))
204                         i++;
205                     }
206 #if XML10_FIFTH_EDITION
207                     else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
208                         i += 2;
209                     }
210 #endif
211                     else {
212                         break;
213                     }
214                 }
215             }
216
217             return i - offset;
218         }
219
220         internal static int ParseNCName(string s) {
221             return ParseNCName(s, 0);
222         }
223
224         /// <summary>
225         /// Calls parseName and throws exception if the resulting name is not a valid NCName.
226         /// Returns the input string if there is no error.
227         /// </summary>
228         internal static string ParseNCNameThrow(string s) {
229             // throwOnError = true
230             ParseNCNameInternal(s, true);
231             return s;
232         }
233
234         /// <summary>
235         /// Calls parseName and returns false or throws exception if the resulting name is not
236         /// a valid NCName.  Returns the input string if there is no error.
237         /// </summary>
238         private static bool ParseNCNameInternal(string s, bool throwOnError) {
239             int len = ParseNCName(s, 0);
240
241             if (len == 0 || len != s.Length) {
242                 // If the string is not a valid NCName, then throw or return false
243                 if (throwOnError) ThrowInvalidName(s, 0, len);
244                 return false;
245             }
246
247             return true;
248         }
249
250         //-----------------------------------------------
251         // QName parsing
252         //-----------------------------------------------
253
254         /// <summary>
255         /// Attempts to parse the input string as a QName (see the XML Namespace spec).
256         /// Quits parsing when an invalid QName char is reached or the end of string is reached.
257         /// Returns the number of valid QName chars that were parsed.
258         /// Sets colonOffset to the offset of a colon character if it exists, or 0 otherwise.
259         /// </summary>
260         internal static int ParseQName(string s, int offset, out int colonOffset) {
261             int len, lenLocal;
262
263             // Assume no colon
264             colonOffset = 0;
265
266             // Parse NCName (may be prefix, may be local name)
267             len = ParseNCName(s, offset);
268             if (len != 0) {
269
270                 // Non-empty NCName, so look for colon if there are any characters left
271                 offset += len;
272                 if (offset < s.Length && s[offset] == ':') {
273
274                     // First NCName was prefix, so look for local name part
275                     lenLocal = ParseNCName(s, offset + 1);
276                     if (lenLocal != 0) {
277                         // Local name part found, so increase total QName length (add 1 for colon)
278                         colonOffset = offset;
279                         len += lenLocal + 1;
280                     }
281                 }
282             }
283
284             return len;
285         }
286
287         /// <summary>
288         /// Calls parseQName and throws exception if the resulting name is not a valid QName.
289         /// Returns the prefix and local name parts.
290         /// </summary>
291         internal static void ParseQNameThrow(string s, out string prefix, out string localName) {
292             int colonOffset;
293             int len = ParseQName(s, 0, out colonOffset);
294
295             if (len == 0 || len != s.Length) {
296                 // If the string is not a valid QName, then throw
297                 ThrowInvalidName(s, 0, len);
298             }
299
300             if (colonOffset != 0) {
301                 prefix = s.Substring(0, colonOffset);
302                 localName = s.Substring(colonOffset + 1);
303             }
304             else {
305                 prefix = "";
306                 localName = s;
307             }
308         }
309
310 #if !SILVERLIGHT
311         /// <summary>
312         /// Parses the input string as a NameTest (see the XPath spec), returning the prefix and
313         /// local name parts.  Throws an exception if the given string is not a valid NameTest.
314         /// If the NameTest contains a star, null values for localName (case NCName':*'), or for
315         /// both localName and prefix (case '*') are returned.
316         /// </summary>
317         internal static void ParseNameTestThrow(string s, out string prefix, out string localName) {
318             int len, lenLocal, offset;
319
320             if (s.Length != 0 && s[0] == '*') {
321                 // '*' as a NameTest
322                 prefix = localName = null;
323                 len = 1;
324             }
325             else {
326                 // Parse NCName (may be prefix, may be local name)
327                 len = ParseNCName(s, 0);
328                 if (len != 0) {
329
330                     // Non-empty NCName, so look for colon if there are any characters left
331                     localName = s.Substring(0, len);
332                     if (len < s.Length && s[len] == ':') {
333
334                         // First NCName was prefix, so look for local name part
335                         prefix = localName;
336                         offset = len + 1;
337                         if (offset < s.Length && s[offset] == '*') {
338                             // '*' as a local name part, add 2 to len for colon and star
339                             localName = null;
340                             len += 2;
341                         }
342                         else {
343                             lenLocal = ParseNCName(s, offset);
344                             if (lenLocal != 0) {
345                                 // Local name part found, so increase total NameTest length
346                                 localName = s.Substring(offset, lenLocal);
347                                 len += lenLocal + 1;
348                             }
349                         }
350                     }
351                     else {
352                         prefix = string.Empty;
353                     }
354                 }
355                 else {
356                     // Make the compiler happy
357                     prefix = localName = null;
358                 }
359             }
360
361             if (len == 0 || len != s.Length) {
362                 // If the string is not a valid NameTest, then throw
363                 ThrowInvalidName(s, 0, len);
364             }
365         }
366 #endif
367
368         /// <summary>
369         /// Throws an invalid name exception.
370         /// </summary>
371         /// <param name="s">String that was parsed.</param>
372         /// <param name="offsetStartChar">Offset in string where parsing began.</param>
373         /// <param name="offsetBadChar">Offset in string where parsing failed.</param>
374         internal static void ThrowInvalidName(string s, int offsetStartChar, int offsetBadChar) {
375             // If the name is empty, throw an exception
376             if (offsetStartChar >= s.Length)
377 #if !SILVERLIGHT_XPATH
378                 throw new XmlException(Res.Xml_EmptyName, string.Empty);
379 #else
380                 throw new XmlException(Res.GetString(Res.Xml_EmptyName, string.Empty));
381 #endif
382
383             Debug.Assert(offsetBadChar < s.Length);
384
385             if (xmlCharType.IsNCNameSingleChar(s[offsetBadChar]) && !XmlCharType.Instance.IsStartNCNameSingleChar(s[offsetBadChar])) {
386                 // The error character is a valid name character, but is not a valid start name character
387 #if !SILVERLIGHT_XPATH
388                 throw new XmlException(Res.Xml_BadStartNameChar, XmlException.BuildCharExceptionArgs(s, offsetBadChar));
389 #else
390                 throw new XmlException(Res.GetString(Res.Xml_BadStartNameChar, XmlExceptionHelper.BuildCharExceptionArgs(s, offsetBadChar)));
391 #endif
392             }
393             else {
394                 // The error character is an invalid name character
395 #if !SILVERLIGHT_XPATH
396                 throw new XmlException(Res.Xml_BadNameChar, XmlException.BuildCharExceptionArgs(s, offsetBadChar));
397 #else
398                 throw new XmlException(Res.GetString(Res.Xml_BadNameChar, XmlExceptionHelper.BuildCharExceptionArgs(s, offsetBadChar)));
399 #endif
400             }
401         }
402
403 #if !SILVERLIGHT
404         internal static Exception GetInvalidNameException(string s, int offsetStartChar, int offsetBadChar) {
405             // If the name is empty, throw an exception
406             if (offsetStartChar >= s.Length)
407                 return new XmlException(Res.Xml_EmptyName, string.Empty);
408
409             Debug.Assert(offsetBadChar < s.Length);
410
411             if (xmlCharType.IsNCNameSingleChar(s[offsetBadChar]) && !xmlCharType.IsStartNCNameSingleChar(s[offsetBadChar])) {
412                 // The error character is a valid name character, but is not a valid start name character
413                 return new XmlException(Res.Xml_BadStartNameChar, XmlException.BuildCharExceptionArgs(s, offsetBadChar));
414             }
415             else {
416                 // The error character is an invalid name character
417                 return new XmlException(Res.Xml_BadNameChar, XmlException.BuildCharExceptionArgs(s, offsetBadChar));
418             }
419         }
420
421         /// <summary>
422         /// Returns true if "prefix" starts with the characters 'x', 'm', 'l' (case-insensitive).
423         /// </summary>
424         internal static bool StartsWithXml(string s) {
425             if (s.Length < 3)
426                 return false;
427
428             if (s[0] != 'x' && s[0] != 'X')
429                 return false;
430
431             if (s[1] != 'm' && s[1] != 'M')
432                 return false;
433
434             if (s[2] != 'l' && s[2] != 'L')
435                 return false;
436
437             return true;
438         }
439
440         /// <summary>
441         /// Returns true if "s" is a namespace that is reserved by Xml 1.0 or Namespace 1.0.
442         /// </summary>
443         internal static bool IsReservedNamespace(string s) {
444             return s.Equals(XmlReservedNs.NsXml) || s.Equals(XmlReservedNs.NsXmlNs);
445         }
446
447         /// <summary>
448         /// Throw if the specified name parts are not valid according to the rules of "nodeKind".  Check only rules that are
449         /// specified by the Flags.
450         /// NOTE: Namespaces should be passed using a prefix, ns pair.  "localName" is always string.Empty.
451         /// </summary>
452         internal static void ValidateNameThrow(string prefix, string localName, string ns, XPathNodeType nodeKind, Flags flags) {
453             // throwOnError = true
454             ValidateNameInternal(prefix, localName, ns, nodeKind, flags, true);
455         }
456
457         /// <summary>
458         /// Return false if the specified name parts are not valid according to the rules of "nodeKind".  Check only rules that are
459         /// specified by the Flags.
460         /// NOTE: Namespaces should be passed using a prefix, ns pair.  "localName" is always string.Empty.
461         /// </summary>
462         internal static bool ValidateName(string prefix, string localName, string ns, XPathNodeType nodeKind, Flags flags) {
463             // throwOnError = false
464             return ValidateNameInternal(prefix, localName, ns, nodeKind, flags, false);
465         }
466
467         /// <summary>
468         /// Return false or throw if the specified name parts are not valid according to the rules of "nodeKind".  Check only rules
469         /// that are specified by the Flags.
470         /// NOTE: Namespaces should be passed using a prefix, ns pair.  "localName" is always string.Empty.
471         /// </summary>
472         private static bool ValidateNameInternal(string prefix, string localName, string ns, XPathNodeType nodeKind, Flags flags, bool throwOnError) {
473             Debug.Assert(prefix != null && localName != null && ns != null);
474
475             if ((flags & Flags.NCNames) != 0) {
476
477                 // 1. Verify that each non-empty prefix and localName is a valid NCName
478                 if (prefix.Length != 0)
479                     if (!ParseNCNameInternal(prefix, throwOnError)) {
480                         return false;
481                     }
482
483                 if (localName.Length != 0)
484                     if (!ParseNCNameInternal(localName, throwOnError)) {
485                         return false;
486                     }
487             }
488
489             if ((flags & Flags.CheckLocalName) != 0) {
490
491                 // 2. Determine whether the local name is valid
492                 switch (nodeKind) {
493                     case XPathNodeType.Element:
494                         // Elements and attributes must have a non-empty local name
495                         if (localName.Length == 0) {
496                             if (throwOnError) throw new XmlException(Res.Xdom_Empty_LocalName, string.Empty);
497                             return false;
498                         }
499                         break;
500
501                     case XPathNodeType.Attribute:
502                         // Attribute local name cannot be "xmlns" if namespace is empty
503                         if (ns.Length == 0 && localName.Equals("xmlns")) {
504                             if (throwOnError) throw new XmlException(Res.XmlBadName, new string[] {nodeKind.ToString(), localName});
505                             return false;
506                         }
507                         goto case XPathNodeType.Element;
508
509                     case XPathNodeType.ProcessingInstruction:
510                         // PI's local-name must be non-empty and cannot be 'xml' (case-insensitive)
511                         if (localName.Length == 0 || (localName.Length == 3 && StartsWithXml(localName))) {
512                             if (throwOnError) throw new XmlException(Res.Xml_InvalidPIName, localName);
513                             return false;
514                         }
515                         break;
516
517                     default:
518                         // All other node types must have empty local-name
519                         if (localName.Length != 0) {
520                             if (throwOnError) throw new XmlException(Res.XmlNoNameAllowed, nodeKind.ToString());
521                             return false;
522                         }
523                         break;
524                 }
525             }
526
527             if ((flags & Flags.CheckPrefixMapping) != 0) {
528
529                 // 3. Determine whether the prefix is valid
530                 switch (nodeKind) {
531                     case XPathNodeType.Element:
532                     case XPathNodeType.Attribute:
533                     case XPathNodeType.Namespace:
534                         if (ns.Length == 0) {
535                             // If namespace is empty, then prefix must be empty
536                             if (prefix.Length != 0) {
537                                 if (throwOnError) throw new XmlException(Res.Xml_PrefixForEmptyNs, string.Empty);
538                                 return false;
539                             }
540                         }
541                         else {
542                             // Don't allow empty attribute prefix since namespace is non-empty
543                             if (prefix.Length == 0 && nodeKind == XPathNodeType.Attribute) {
544                                 if (throwOnError) throw new XmlException(Res.XmlBadName, new string[] {nodeKind.ToString(), localName});
545                                 return false;
546                             }
547
548                             if (prefix.Equals("xml")) {
549                                 // xml prefix must be mapped to the xml namespace
550                                 if (!ns.Equals(XmlReservedNs.NsXml)) {
551                                     if (throwOnError) throw new XmlException(Res.Xml_XmlPrefix, string.Empty);
552                                     return false;
553                                 }
554                             }
555                             else if (prefix.Equals("xmlns")) {
556                                 // Prefix may never be 'xmlns'
557                                 if (throwOnError) throw new XmlException(Res.Xml_XmlnsPrefix, string.Empty);
558                                 return false;
559                             }
560                             else if (IsReservedNamespace(ns)) {
561                                 // Don't allow non-reserved prefixes to map to xml or xmlns namespaces
562                                 if (throwOnError) throw new XmlException(Res.Xml_NamespaceDeclXmlXmlns, string.Empty);
563                                 return false;
564                             }
565                         }
566                         break;
567
568                     case XPathNodeType.ProcessingInstruction:
569                         // PI's prefix and namespace must be empty
570                         if (prefix.Length != 0 || ns.Length != 0) {
571                             if (throwOnError) throw new XmlException(Res.Xml_InvalidPIName, CreateName(prefix, localName));
572                             return false;
573                         }
574                         break;
575
576                     default:
577                         // All other node types must have empty prefix and namespace
578                         if (prefix.Length != 0 || ns.Length != 0) {
579                             if (throwOnError) throw new XmlException(Res.XmlNoNameAllowed, nodeKind.ToString());
580                             return false;
581                         }
582                         break;
583                 }
584             }
585
586             return true;
587         }
588
589         /// <summary>
590         /// Creates a colon-delimited qname from prefix and local name parts.
591         /// </summary>
592         private static string CreateName(string prefix, string localName) {
593             return (prefix.Length != 0) ? prefix + ":" + localName : localName;
594         }
595 #endif
596
597
598 #if !SILVERLIGHT || SILVERLIGHT_XPATH
599         /// <summary>
600         /// Split a QualifiedName into prefix and localname, w/o any checking.
601         /// (Used for XmlReader/XPathNavigator MoveTo(name) methods)
602         /// </summary>
603         internal static void SplitQName(string name, out string prefix, out string lname) {
604             int colonPos = name.IndexOf(':');
605             if (-1 == colonPos) {
606                 prefix = string.Empty;
607                 lname = name;
608             }
609             else if (0 == colonPos || (name.Length-1) == colonPos) {
610 #if !SILVERLIGHT_XPATH
611                 throw new ArgumentException(Res.GetString(Res.Xml_BadNameChar, XmlException.BuildCharExceptionArgs(':', '\0')), "name");
612 #else
613                 throw new ArgumentException(Res.GetString(Res.Xml_BadNameChar, XmlExceptionHelper.BuildCharExceptionArgs(':', '\0')), "name");
614 #endif
615             }
616             else {
617                 prefix = name.Substring(0, colonPos);
618                 colonPos++; // move after colon
619                 lname = name.Substring(colonPos, name.Length - colonPos);
620             }
621         }
622 #endif
623     }
624
625 #if SILVERLIGHT_XPATH
626     internal class XmlExceptionHelper
627     {
628         internal static string[] BuildCharExceptionArgs(string data, int invCharIndex)
629         {
630             return BuildCharExceptionArgs(data[invCharIndex], invCharIndex + 1 < data.Length ? data[invCharIndex + 1] : '\0');
631         }
632
633         internal static string[] BuildCharExceptionArgs(char[] data, int invCharIndex)
634         {
635             return BuildCharExceptionArgs(data, data.Length, invCharIndex);
636         }
637
638         internal static string[] BuildCharExceptionArgs(char[] data, int length, int invCharIndex)
639         {
640             Debug.Assert(invCharIndex < data.Length);
641             Debug.Assert(invCharIndex < length);
642             Debug.Assert(length <= data.Length);
643
644             return BuildCharExceptionArgs(data[invCharIndex], invCharIndex + 1 < length ? data[invCharIndex + 1] : '\0');
645         }
646
647         internal static string[] BuildCharExceptionArgs(char invChar, char nextChar)
648         {
649             string[] aStringList = new string[2];
650
651             // for surrogate characters include both high and low char in the message so that a full character is displayed
652             if (XmlCharType.IsHighSurrogate(invChar) && nextChar != 0)
653             {
654                 int combinedChar = XmlCharType.CombineSurrogateChar(nextChar, invChar);
655                 aStringList[0] = new string(new char[] { invChar, nextChar });
656                 aStringList[1] = string.Format(CultureInfo.InvariantCulture, "0x{0:X2}", combinedChar);
657             }
658             else
659             {
660                 // don't include 0 character in the string - in means eof-of-string in native code, where this may bubble up to
661                 if ((int)invChar == 0)
662                 {
663                     aStringList[0] = ".";
664                 }
665                 else
666                 {
667                     aStringList[0] = invChar.ToString(CultureInfo.InvariantCulture);
668                 }
669                 aStringList[1] = string.Format(CultureInfo.InvariantCulture, "0x{0:X2}", (int)invChar);
670             }
671             return aStringList;
672         }
673     }
674 #endif
675 }