2 // Internal UriParseComponents class
5 // Vinicius Jarina <vinicius.jarina@xamarin.com>
7 // Copyright (C) 2012 Xamarin, Inc (http://www.xamarin.com)
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 using System.Globalization;
36 internal class ParserState
38 public ParserState (string uri, UriKind kind)
42 elements = new UriElements ();
45 public string remaining;
47 public UriElements elements;
51 // Parse Uri components (scheme, userinfo, host, query, fragment)
52 // http://www.ietf.org/rfc/rfc3986.txt
53 internal static class UriParseComponents
55 public static UriElements ParseComponents (string uri, UriKind kind)
60 if (!TryParseComponents (uri, kind, out elements, out error))
61 throw new UriFormatException (error);
66 public static bool TryParseComponents (string uri, UriKind kind, out UriElements elements, out string error)
70 ParserState state = new ParserState (uri, kind);
71 elements = state.elements;
74 if (uri.Length == 0 && (kind == UriKind.Relative || kind == UriKind.RelativeOrAbsolute)){
75 state.elements.isAbsoluteUri = false;
79 if (uri.Length <= 1 && kind == UriKind.Absolute) {
80 error = "Absolute URI is too short";
84 bool ok = ParseFilePath (state) &&
87 var scheme = state.elements.scheme;
88 UriParser parser = null;
89 if (!string.IsNullOrEmpty (scheme)) {
90 parser = UriParser.GetParser (scheme);
91 if (parser != null && !(parser is DefaultUriParser))
96 ParseAuthority (state) &&
99 ParseFragment (state);
101 if (string.IsNullOrEmpty (state.elements.host) &&
102 (scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeGopher || scheme == Uri.UriSchemeNntp ||
103 scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeFtp))
104 state.error = "Invalid URI: The Authority/Host could not be parsed.";
106 if (!string.IsNullOrEmpty (state.elements.host) &&
107 Uri.CheckHostName (state.elements.host) == UriHostNameType.Unknown)
108 state.error = "Invalid URI: The hostname could not be parsed.";
110 if (!string.IsNullOrEmpty (state.error)) {
120 private static bool IsAlpha (char ch)
122 return (('a' <= ch) && (ch <= 'z')) ||
123 (('A' <= ch) && (ch <= 'Z'));
126 private static bool ParseFilePath (ParserState state)
128 return ParseWindowsFilePath (state) &&
129 ParseWindowsUNC (state) &&
130 ParseUnixFilePath (state);
133 private static bool ParseWindowsFilePath (ParserState state)
135 var scheme = state.elements.scheme;
137 if (!string.IsNullOrEmpty (scheme) &&
138 scheme != Uri.UriSchemeFile && UriHelper.IsKnownScheme (scheme))
139 return state.remaining.Length > 0;
141 string part = state.remaining;
143 if (part.Length > 0 && (part [0] == '/' || part [0] == '\\'))
144 part = part.Substring (1);
146 if (part.Length < 2 || part [1] != ':')
147 return state.remaining.Length > 0;
149 if (!IsAlpha (part [0])) {
150 if (state.kind == UriKind.Absolute) {
151 state.error = "Invalid URI: The URI scheme is not valid.";
154 state.elements.isAbsoluteUri = false;
155 state.elements.path = part;
159 if (part.Length > 2 && part [2] != '\\' && part [2] != '/') {
160 state.error = "Relative file path is not allowed.";
164 if (string.IsNullOrEmpty (scheme)) {
165 state.elements.scheme = Uri.UriSchemeFile;
166 state.elements.delimiter = "://";
169 state.elements.path = part.Replace ("\\", "/");
174 private static bool ParseWindowsUNC (ParserState state)
176 string part = state.remaining;
178 if (part.Length < 2 || part [0] != '\\' || part [1] != '\\')
179 return state.remaining.Length > 0;
181 state.elements.scheme = Uri.UriSchemeFile;
182 state.elements.delimiter = "://";
183 state.elements.isUnc = true;
185 part = part.TrimStart ('\\');
186 int pos = part.IndexOf ('\\');
188 state.elements.path = part.Substring (pos);
189 state.elements.host = part.Substring (0, pos);
190 } else { // "\\\\server"
191 state.elements.host = part;
192 state.elements.path = String.Empty;
194 state.elements.path = state.elements.path.Replace ("\\", "/");
199 private static bool ParseUnixFilePath (ParserState state)
201 string part = state.remaining;
203 if (part.Length < 1 || part [0] != '/' || Path.DirectorySeparatorChar != '/')
204 return state.remaining.Length > 0;
206 state.elements.scheme = Uri.UriSchemeFile;
207 state.elements.delimiter = "://";
208 state.elements.isUnixFilePath = true;
209 state.elements.isAbsoluteUri = (state.kind == UriKind.Relative)? false : true;
211 if (part.Length >= 2 && part [0] == '/' && part [1] == '/') {
212 part = part.TrimStart (new char [] {'/'});
213 state.elements.path = '/' + part;
215 state.elements.path = part;
220 // 3.1) scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
221 private static bool ParseScheme (ParserState state)
223 string part = state.remaining;
225 StringBuilder sb = new StringBuilder ();
226 sb.Append (part [0]);
229 for (index = 1; index < part.Length; index++ ) {
230 char ch = part [index];
231 if (ch != '.' && ch != '-' && ch != '+' && !IsAlpha (ch) && !Char.IsDigit (ch))
237 if (index == 0 || index >= part.Length) {
238 if (state.kind == UriKind.Absolute) {
239 state.error = "Invalid URI: The format of the URI could not be determined.";
243 state.elements.isAbsoluteUri = false;
244 return state.remaining.Length > 0;
247 if (part [index] != ':') {
248 if (state.kind == UriKind.Absolute) {
249 state.error = "Invalid URI: The URI scheme is not valid.";
253 state.elements.isAbsoluteUri = false;
254 return state.remaining.Length > 0;
257 state.elements.scheme = sb.ToString ().ToLowerInvariant ();
258 state.remaining = part.Substring (index);
260 // Check scheme name characters as specified in RFC2396.
261 // Note: different checks in 1.x and 2.0
262 if (!Uri.CheckSchemeName (state.elements.scheme)) {
263 if (state.kind == UriKind.Absolute) {
264 state.error = "Invalid URI: The URI scheme is not valid.";
268 state.elements.isAbsoluteUri = false;
269 return state.remaining.Length > 0;
272 return ParseDelimiter (state);
275 private static bool ParseDelimiter (ParserState state)
277 var delimiter = Uri.GetSchemeDelimiter (state.elements.scheme);
279 if (!state.remaining.StartsWith (delimiter, StringComparison.Ordinal)) {
280 if (UriHelper.IsKnownScheme (state.elements.scheme)) {
281 state.error = "Invalid URI: The Authority/Host could not be parsed.";
288 state.elements.delimiter = delimiter;
290 state.remaining = state.remaining.Substring (delimiter.Length);
292 return state.remaining.Length > 0;
295 private static bool ParseAuthority (ParserState state)
297 if (state.elements.delimiter != Uri.SchemeDelimiter && state.elements.scheme != Uri.UriSchemeMailto)
298 return state.remaining.Length > 0;
300 return ParseUser (state) &&
305 static bool IsUnreserved (char ch)
307 return ch == '-' || ch == '.' || ch == '_' || ch == '~';
311 static bool IsSubDelim (char ch)
313 return ch == '!' || ch == '$' || ch == '&' || ch == '\'' || ch == '(' || ch == ')' ||
314 ch == '*' || ch == '+' || ch == ',' || ch == ';' || ch == '=';
317 // userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
318 private static bool ParseUser (ParserState state)
320 string part = state.remaining;
321 StringBuilder sb = null;
324 for (index = 0; index < part.Length; index++) {
325 char ch = part [index];
326 bool isEscapedChar = false;
327 var oldIndex = index;
329 // Spaces should be percentage encoded #31172
332 sb = new StringBuilder ();
338 if (!Uri.IsHexEncoding (part, index))
340 ch = Uri.HexUnescape (part, ref index);
342 isEscapedChar = true;
345 if (!Char.IsLetterOrDigit (ch) && !IsUnreserved (ch) && !IsSubDelim (ch) && ch != ':'){
354 sb = new StringBuilder ();
358 if (index + 1 <= part.Length && part [index] == '@') {
359 if (state.elements.scheme == Uri.UriSchemeFile) {
360 state.error = "Invalid URI: The hostname could not be parsed.";
364 state.elements.user = sb == null ? "" : sb.ToString ();
365 state.remaining = state.remaining.Substring (index + 1);
368 return state.remaining.Length > 0;
371 // host = IP-literal / IPv4address / reg-name
372 private static bool ParseHost (ParserState state)
374 string part = state.remaining;
376 if (state.elements.scheme == Uri.UriSchemeFile && part.Length >= 2 &&
377 (part [0] == '\\' || part [0] == '/') && part [1] == part [0]) {
378 part = part.TrimStart (part [0]);
379 state.remaining = part;
382 if (!ParseWindowsFilePath (state))
385 StringBuilder sb = new StringBuilder ();
389 var possibleIpv6 = false;
392 for (index = 0; index < part.Length; index++) {
394 char ch = part [index];
396 if (ch == '/' || ch == '#' || ch == '?')
400 if (string.IsNullOrEmpty (tmpHost) && ch == ':') {
401 tmpHost = sb.ToString ();
407 if (possibleIpv6 && ch == ']')
412 IPv6Address ipv6addr;
413 if (IPv6Address.TryParse (sb.ToString (), out ipv6addr)) {
414 var ipStr = ipv6addr.ToString (false);
416 ipStr = ipStr.Split ('%') [0];
418 state.elements.host = "[" + ipStr + "]";
419 state.elements.scopeId = ipv6addr.ScopeId;
421 state.remaining = part.Substring (sb.Length);
422 return state.remaining.Length > 0;
424 state.elements.host = tmpHost;
426 state.elements.host = sb.ToString ();
428 state.elements.host = state.elements.host.ToLowerInvariant ();
430 state.remaining = part.Substring (state.elements.host.Length);
432 if (state.elements.scheme == Uri.UriSchemeFile &&
433 state.elements.host != "") {
434 // under Windows all file://host URI are considered UNC, which is not the case other MacOS (e.g. Silverlight)
436 state.elements.isUnc = (Path.DirectorySeparatorChar == '\\');
438 state.elements.isUnc = Environment.IsRunningOnWindows;
442 return state.remaining.Length > 0;
446 private static bool ParsePort (ParserState state)
448 string part = state.remaining;
449 if (part.Length == 0 || part [0] != ':')
450 return part.Length > 0;
452 StringBuilder sb = new StringBuilder ();
455 for (index = 1; index < part.Length; index++ ) {
456 char ch = part [index];
458 if (!char.IsDigit (ch)) {
459 if (ch == '/' || ch == '#' || ch == '?')
462 state.error = "Invalid URI: Invalid port specified.";
469 if (index <= part.Length)
470 state.remaining = part.Substring (index);
473 return state.remaining.Length > 0;
476 if (!Int32.TryParse (sb.ToString (), NumberStyles.None, CultureInfo.InvariantCulture, out port) ||
477 port < 0 || port > UInt16.MaxValue) {
478 state.error = "Invalid URI: Invalid port number";
482 state.elements.port = port;
484 return state.remaining.Length > 0;
487 private static bool ParsePath (ParserState state)
489 string part = state.remaining;
490 StringBuilder sb = new StringBuilder ();
493 for (index = 0; index < part.Length; index++) {
495 char ch = part [index];
497 var supportsQuery = UriHelper.SupportsQuery (state.elements.scheme);
499 if (ch == '#' || (supportsQuery && ch == '?'))
505 if (index <= part.Length)
506 state.remaining = part.Substring (index);
508 state.elements.path = sb.ToString ();
510 return state.remaining.Length > 0;
513 private static bool ParseQuery (ParserState state)
515 string part = state.remaining;
517 if (!UriHelper.SupportsQuery (state.elements.scheme))
518 return part.Length > 0;
520 if (part.Length == 0 || part [0] != '?')
521 return part.Length > 0;
523 StringBuilder sb = new StringBuilder ();
526 for (index = 1; index < part.Length; index++) {
528 char ch = part [index];
536 if (index <= part.Length)
537 state.remaining = part.Substring (index);
539 state.elements.query = sb.ToString ();
541 return state.remaining.Length > 0;
544 private static bool ParseFragment (ParserState state)
546 string part = state.remaining;
548 if (part.Length == 0 || part [0] != '#')
549 return part.Length > 0;
551 StringBuilder sb = new StringBuilder ();
554 for (index = 1; index < part.Length; index++) {
556 char ch = part [index];
561 state.elements.fragment = sb.ToString ();