Updates referencesource to .NET 4.7
[mono-project.git] / mcs / class / referencesource / System.Xml / System / Xml / ValidateNames.cs
blobbf034e162b4c9ec66a4a9fba8b550b5bc5fd35c6
1 //------------------------------------------------------------------------------
2 // <copyright file="ValidateNames.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
4 // </copyright>
5 // <owner current="true" primary="true">Microsoft</owner>
6 //------------------------------------------------------------------------------
8 using System;
9 #if !SILVERLIGHT
10 using System.Xml.XPath;
11 #endif
12 using System.Diagnostics;
13 using System.Globalization;
15 #if SILVERLIGHT_XPATH
16 namespace System.Xml.XPath {
17 #else
18 namespace System.Xml {
19 #endif
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 {
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,
37 static XmlCharType xmlCharType = XmlCharType.Instance;
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);
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++;
57 #if XML10_FIFTH_EDITION
58 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
59 i += 2;
61 #endif
62 else {
63 break;
67 return i - offset;
69 #endif
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) {
86 Debug.Assert(s != null && offset <= s.Length);
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++;
94 #if XML10_FIFTH_EDITION
95 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
96 i += 2;
98 #endif
99 else {
100 break;
104 return i - offset;
107 // helper methods
108 internal static bool IsNmtokenNoNamespaces(string s) {
109 int endPos = ParseNmtokenNoNamespaces(s, 0);
110 return endPos > 0 && endPos == s.Length;
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) {
128 Debug.Assert(s != null && offset <= s.Length);
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++;
136 #if XML10_FIFTH_EDITION
137 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
138 i += 2;
140 #endif
141 else {
142 return 0; // no valid StartNCName char
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++;
150 #if XML10_FIFTH_EDITION
151 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
152 i += 2;
154 #endif
155 else {
156 break;
161 return i - offset;
164 // helper methods
165 internal static bool IsNameNoNamespaces(string s) {
166 int endPos = ParseNameNoNamespaces(s, 0);
167 return endPos > 0 && endPos == s.Length;
170 //-----------------------------------------------
171 // NCName parsing
172 //-----------------------------------------------
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) {
184 Debug.Assert(s != null && offset <= s.Length);
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++;
192 #if XML10_FIFTH_EDITION
193 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
194 i += 2;
196 #endif
197 else {
198 return 0; // no valid StartNCName char
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++;
206 #if XML10_FIFTH_EDITION
207 else if (xmlCharType.IsNCNameSurrogateChar(s, i)) {
208 i += 2;
210 #endif
211 else {
212 break;
217 return i - offset;
220 internal static int ParseNCName(string s) {
221 return ParseNCName(s, 0);
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;
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);
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;
247 return true;
250 //-----------------------------------------------
251 // QName parsing
252 //-----------------------------------------------
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;
263 // Assume no colon
264 colonOffset = 0;
266 // Parse NCName (may be prefix, may be local name)
267 len = ParseNCName(s, offset);
268 if (len != 0) {
270 // Non-empty NCName, so look for colon if there are any characters left
271 offset += len;
272 if (offset < s.Length && s[offset] == ':') {
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;
284 return len;
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);
295 if (len == 0 || len != s.Length) {
296 // If the string is not a valid QName, then throw
297 ThrowInvalidName(s, 0, len);
300 if (colonOffset != 0) {
301 prefix = s.Substring(0, colonOffset);
302 localName = s.Substring(colonOffset + 1);
304 else {
305 prefix = "";
306 localName = s;
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;
320 if (s.Length != 0 && s[0] == '*') {
321 // '*' as a NameTest
322 prefix = localName = null;
323 len = 1;
325 else {
326 // Parse NCName (may be prefix, may be local name)
327 len = ParseNCName(s, 0);
328 if (len != 0) {
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] == ':') {
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;
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;
351 else {
352 prefix = string.Empty;
355 else {
356 // Make the compiler happy
357 prefix = localName = null;
361 if (len == 0 || len != s.Length) {
362 // If the string is not a valid NameTest, then throw
363 ThrowInvalidName(s, 0, len);
366 #endif
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
383 Debug.Assert(offsetBadChar < s.Length);
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
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
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);
409 Debug.Assert(offsetBadChar < s.Length);
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));
415 else {
416 // The error character is an invalid name character
417 return new XmlException(Res.Xml_BadNameChar, XmlException.BuildCharExceptionArgs(s, offsetBadChar));
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;
428 if (s[0] != 'x' && s[0] != 'X')
429 return false;
431 if (s[1] != 'm' && s[1] != 'M')
432 return false;
434 if (s[2] != 'l' && s[2] != 'L')
435 return false;
437 return true;
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);
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);
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);
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);
475 if ((flags & Flags.NCNames) != 0) {
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;
483 if (localName.Length != 0)
484 if (!ParseNCNameInternal(localName, throwOnError)) {
485 return false;
489 if ((flags & Flags.CheckLocalName) != 0) {
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;
499 break;
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;
507 goto case XPathNodeType.Element;
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;
515 break;
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;
523 break;
527 if ((flags & Flags.CheckPrefixMapping) != 0) {
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;
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;
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;
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;
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;
566 break;
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;
574 break;
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;
582 break;
586 return true;
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;
595 #endif
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;
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
616 else {
617 prefix = name.Substring(0, colonPos);
618 colonPos++; // move after colon
619 lname = name.Substring(colonPos, name.Length - colonPos);
622 #endif
625 #if SILVERLIGHT_XPATH
626 internal class XmlExceptionHelper
628 internal static string[] BuildCharExceptionArgs(string data, int invCharIndex)
630 return BuildCharExceptionArgs(data[invCharIndex], invCharIndex + 1 < data.Length ? data[invCharIndex + 1] : '\0');
633 internal static string[] BuildCharExceptionArgs(char[] data, int invCharIndex)
635 return BuildCharExceptionArgs(data, data.Length, invCharIndex);
638 internal static string[] BuildCharExceptionArgs(char[] data, int length, int invCharIndex)
640 Debug.Assert(invCharIndex < data.Length);
641 Debug.Assert(invCharIndex < length);
642 Debug.Assert(length <= data.Length);
644 return BuildCharExceptionArgs(data[invCharIndex], invCharIndex + 1 < length ? data[invCharIndex + 1] : '\0');
647 internal static string[] BuildCharExceptionArgs(char invChar, char nextChar)
649 string[] aStringList = new string[2];
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)
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);
658 else
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)
663 aStringList[0] = ".";
665 else
667 aStringList[0] = invChar.ToString(CultureInfo.InvariantCulture);
669 aStringList[1] = string.Format(CultureInfo.InvariantCulture, "0x{0:X2}", (int)invChar);
671 return aStringList;
674 #endif