1 //------------------------------------------------------------------------------
2 // <copyright file="ValidateNames.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 // <owner current="true" primary="true">Microsoft</owner>
6 //------------------------------------------------------------------------------
10 using System
.Xml
.XPath
;
12 using System
.Diagnostics
;
13 using System
.Globalization
;
16 namespace System
.Xml
.XPath
{
18 namespace System
.Xml
{
22 /// Contains various static functions and methods for parsing and validating:
23 /// NCName (not namespace-aware, no colons allowed)
24 /// QName (prefix:local-name)
26 internal static class ValidateNames
{
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
33 AllExceptNCNames
= 0x6,
34 AllExceptPrefixMapping
= 0x3,
37 static XmlCharType xmlCharType
= XmlCharType
.Instance
;
40 //-----------------------------------------------
42 //-----------------------------------------------
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.
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
53 while (i
< s
.Length
) {
54 if ((xmlCharType
.charProperties
[s
[i
]] & XmlCharType
.fNCNameSC
) != 0) { // if (xmlCharType.IsNCNameSingleChar(s[i])) {
57 #if XML10_FIFTH_EDITION
58 else if (xmlCharType
.IsNCNameSurrogateChar(s
, i
)) {
71 //-----------------------------------------------
72 // Nmtoken parsing (no XML namespaces support)
73 //-----------------------------------------------
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.
81 #if SILVERLIGHT && !SILVERLIGHT_DISABLE_SECURITY && XMLCHARTYPE_USE_RESOURCE
82 [System
.Security
.SecuritySafeCritical
]
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
90 while (i
< s
.Length
) {
91 if ((xmlCharType
.charProperties
[s
[i
]] & XmlCharType
.fNCNameSC
) != 0 || s
[i
] == ':') { // if (xmlCharType.IsNameSingleChar(s[i])) {
94 #if XML10_FIFTH_EDITION
95 else if (xmlCharType
.IsNCNameSurrogateChar(s
, i
)) {
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 //-----------------------------------------------
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.
123 #if SILVERLIGHT && !SILVERLIGHT_DISABLE_SECURITY && XMLCHARTYPE_USE_RESOURCE
124 [System
.Security
.SecuritySafeCritical
]
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
133 if ((xmlCharType
.charProperties
[s
[i
]] & XmlCharType
.fNCStartNameSC
) != 0 || s
[i
] == ':') { // xmlCharType.IsStartNCNameSingleChar(s[i])) {
136 #if XML10_FIFTH_EDITION
137 else if (xmlCharType
.IsNCNameSurrogateChar(s
, i
)) {
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]))
150 #if XML10_FIFTH_EDITION
151 else if (xmlCharType
.IsNCNameSurrogateChar(s
, i
)) {
165 internal static bool IsNameNoNamespaces(string s
) {
166 int endPos
= ParseNameNoNamespaces(s
, 0);
167 return endPos
> 0 && endPos
== s
.Length
;
170 //-----------------------------------------------
172 //-----------------------------------------------
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.
179 #if SILVERLIGHT && !SILVERLIGHT_DISABLE_SECURITY && XMLCHARTYPE_USE_RESOURCE
180 [System
.Security
.SecuritySafeCritical
]
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
189 if ((xmlCharType
.charProperties
[s
[i
]] & XmlCharType
.fNCStartNameSC
) != 0) { // xmlCharType.IsStartNCNameSingleChar(s[i])) {
192 #if XML10_FIFTH_EDITION
193 else if (xmlCharType
.IsNCNameSurrogateChar(s
, i
)) {
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]))
206 #if XML10_FIFTH_EDITION
207 else if (xmlCharType
.IsNCNameSurrogateChar(s
, i
)) {
220 internal static int ParseNCName(string s
) {
221 return ParseNCName(s
, 0);
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.
228 internal static string ParseNCNameThrow(string s
) {
229 // throwOnError = true
230 ParseNCNameInternal(s
, true);
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.
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
);
250 //-----------------------------------------------
252 //-----------------------------------------------
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.
260 internal static int ParseQName(string s
, int offset
, out int colonOffset
) {
266 // Parse NCName (may be prefix, may be local name)
267 len
= ParseNCName(s
, offset
);
270 // Non-empty NCName, so look for colon if there are any characters left
272 if (offset
< s
.Length
&& s
[offset
] == ':') {
274 // First NCName was prefix, so look for local name part
275 lenLocal
= ParseNCName(s
, offset
+ 1);
277 // Local name part found, so increase total QName length (add 1 for colon)
278 colonOffset
= offset
;
288 /// Calls parseQName and throws exception if the resulting name is not a valid QName.
289 /// Returns the prefix and local name parts.
291 internal static void ParseQNameThrow(string s
, out string prefix
, out string localName
) {
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);
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.
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] == '*') {
322 prefix
= localName
= null;
326 // Parse NCName (may be prefix, may be local name)
327 len
= ParseNCName(s
, 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
337 if (offset
< s
.Length
&& s
[offset
] == '*') {
338 // '*' as a local name part, add 2 to len for colon and star
343 lenLocal
= ParseNCName(s
, offset
);
345 // Local name part found, so increase total NameTest length
346 localName
= s
.Substring(offset
, lenLocal
);
352 prefix
= string.Empty
;
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
);
369 /// Throws an invalid name exception.
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
);
380 throw new XmlException(Res
.GetString(Res
.Xml_EmptyName
, string.Empty
));
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
));
390 throw new XmlException(Res
.GetString(Res
.Xml_BadStartNameChar
, XmlExceptionHelper
.BuildCharExceptionArgs(s
, offsetBadChar
)));
394 // The error character is an invalid name character
395 #if !SILVERLIGHT_XPATH
396 throw new XmlException(Res
.Xml_BadNameChar
, XmlException
.BuildCharExceptionArgs(s
, offsetBadChar
));
398 throw new XmlException(Res
.GetString(Res
.Xml_BadNameChar
, XmlExceptionHelper
.BuildCharExceptionArgs(s
, offsetBadChar
)));
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
));
416 // The error character is an invalid name character
417 return new XmlException(Res
.Xml_BadNameChar
, XmlException
.BuildCharExceptionArgs(s
, offsetBadChar
));
422 /// Returns true if "prefix" starts with the characters 'x', 'm', 'l' (case-insensitive).
424 internal static bool StartsWithXml(string s
) {
428 if (s
[0] != 'x' && s
[0] != 'X')
431 if (s
[1] != 'm' && s
[1] != 'M')
434 if (s
[2] != 'l' && s
[2] != 'L')
441 /// Returns true if "s" is a namespace that is reserved by Xml 1.0 or Namespace 1.0.
443 internal static bool IsReservedNamespace(string s
) {
444 return s
.Equals(XmlReservedNs
.NsXml
) || s
.Equals(XmlReservedNs
.NsXmlNs
);
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.
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);
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.
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);
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.
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
)) {
483 if (localName
.Length
!= 0)
484 if (!ParseNCNameInternal(localName
, throwOnError
)) {
489 if ((flags
& Flags
.CheckLocalName
) != 0) {
491 // 2. Determine whether the local name is valid
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
);
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}
);
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
);
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());
527 if ((flags
& Flags
.CheckPrefixMapping
) != 0) {
529 // 3. Determine whether the prefix is valid
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
);
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}
);
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
);
555 else if (prefix
.Equals("xmlns")) {
556 // Prefix may never be 'xmlns'
557 if (throwOnError
) throw new XmlException(Res
.Xml_XmlnsPrefix
, string.Empty
);
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
);
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
));
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());
590 /// Creates a colon-delimited qname from prefix and local name parts.
592 private static string CreateName(string prefix
, string localName
) {
593 return (prefix
.Length
!= 0) ? prefix
+ ":" + localName
: localName
;
598 #if !SILVERLIGHT || SILVERLIGHT_XPATH
600 /// Split a QualifiedName into prefix and localname, w/o any checking.
601 /// (Used for XmlReader/XPathNavigator MoveTo(name) methods)
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
;
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");
613 throw new ArgumentException(Res
.GetString(Res
.Xml_BadNameChar
, XmlExceptionHelper
.BuildCharExceptionArgs(':', '\0')), "name");
617 prefix
= name
.Substring(0, colonPos
);
618 colonPos
++; // move after colon
619 lname
= name
.Substring(colonPos
, name
.Length
- colonPos
);
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
);
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] = ".";
667 aStringList
[0] = invChar
.ToString(CultureInfo
.InvariantCulture
);
669 aStringList
[1] = string.Format(CultureInfo
.InvariantCulture
, "0x{0:X2}", (int)invChar
);