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 if (state.elements.scheme == Uri.UriSchemeFile) {
273 // under Windows all file:// URI are considered UNC, which is not the case other MacOS (e.g. Silverlight)
275 state.elements.isUnc = (Path.DirectorySeparatorChar == '\\');
277 state.elements.isUnc = Environment.IsRunningOnWindows;
281 return ParseDelimiter (state);
284 private static bool ParseDelimiter (ParserState state)
286 var delimiter = Uri.GetSchemeDelimiter (state.elements.scheme);
288 if (!state.remaining.StartsWith (delimiter, StringComparison.Ordinal)) {
289 if (UriHelper.IsKnownScheme (state.elements.scheme)) {
290 state.error = "Invalid URI: The Authority/Host could not be parsed.";
297 state.elements.delimiter = delimiter;
299 state.remaining = state.remaining.Substring (delimiter.Length);
301 return state.remaining.Length > 0;
304 private static bool ParseAuthority (ParserState state)
306 if (state.elements.delimiter != Uri.SchemeDelimiter && state.elements.scheme != Uri.UriSchemeMailto)
307 return state.remaining.Length > 0;
309 return ParseUser (state) &&
314 static bool IsUnreserved (char ch)
316 return ch == '-' || ch == '.' || ch == '_' || ch == '~';
320 static bool IsSubDelim (char ch)
322 return ch == '!' || ch == '$' || ch == '&' || ch == '\'' || ch == '(' || ch == ')' ||
323 ch == '*' || ch == '+' || ch == ',' || ch == ';' || ch == '=';
326 // userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
327 private static bool ParseUser (ParserState state)
329 string part = state.remaining;
330 StringBuilder sb = null;
333 for (index = 0; index < part.Length; index++) {
334 char ch = part [index];
337 if (!Uri.IsHexEncoding (part, index))
339 var oldIndex = index;
340 ch = Uri.HexUnescape (part, ref index);
343 sb.Append (part.Substring (oldIndex, index - oldIndex + 1));
348 if (Char.IsLetterOrDigit (ch) || IsUnreserved (ch) || IsSubDelim (ch) || ch == ':'){
350 sb = new StringBuilder ();
356 if (index + 1 <= part.Length && part [index] == '@') {
357 if (state.elements.scheme == Uri.UriSchemeFile) {
358 state.error = "Invalid URI: The hostname could not be parsed.";
362 state.elements.user = sb == null ? "" : sb.ToString ();
363 state.remaining = state.remaining.Substring (index + 1);
366 return state.remaining.Length > 0;
369 // host = IP-literal / IPv4address / reg-name
370 private static bool ParseHost (ParserState state)
372 string part = state.remaining;
374 if (state.elements.scheme == Uri.UriSchemeFile && part.Length >= 2 &&
375 (part [0] == '\\' || part [0] == '/') && part [1] == part [0]) {
376 part = part.TrimStart (part [0]);
377 state.remaining = part;
380 if (!ParseWindowsFilePath (state))
383 StringBuilder sb = new StringBuilder ();
387 var possibleIpv6 = false;
390 for (index = 0; index < part.Length; index++) {
392 char ch = part [index];
394 if (ch == '/' || ch == '#' || ch == '?')
398 if (string.IsNullOrEmpty (tmpHost) && ch == ':') {
399 tmpHost = sb.ToString ();
405 if (possibleIpv6 && ch == ']')
410 IPv6Address ipv6addr;
411 if (IPv6Address.TryParse (sb.ToString (), out ipv6addr)) {
413 var ipStr = ipv6addr.ToString (false);
415 var ipStr = ipv6addr.ToString (true);
418 ipStr = ipStr.Split ('%') [0];
420 state.elements.host = "[" + ipStr + "]";
421 state.elements.scopeId = ipv6addr.ScopeId;
423 state.remaining = part.Substring (sb.Length);
424 return state.remaining.Length > 0;
426 state.elements.host = tmpHost;
428 state.elements.host = sb.ToString ();
430 state.elements.host = state.elements.host.ToLowerInvariant ();
432 state.remaining = part.Substring (state.elements.host.Length);
434 return state.remaining.Length > 0;
438 private static bool ParsePort (ParserState state)
440 string part = state.remaining;
441 if (part.Length == 0 || part [0] != ':')
442 return part.Length > 0;
444 StringBuilder sb = new StringBuilder ();
447 for (index = 1; index < part.Length; index++ ) {
448 char ch = part [index];
450 if (!char.IsDigit (ch)) {
451 if (ch == '/' || ch == '#' || ch == '?')
454 state.error = "Invalid URI: Invalid port specified.";
461 if (index <= part.Length)
462 state.remaining = part.Substring (index);
465 return state.remaining.Length > 0;
468 if (!Int32.TryParse (sb.ToString (), NumberStyles.None, CultureInfo.InvariantCulture, out port) ||
469 port < 0 || port > UInt16.MaxValue) {
470 state.error = "Invalid URI: Invalid port number";
474 state.elements.port = port;
476 return state.remaining.Length > 0;
479 private static bool ParsePath (ParserState state)
481 string part = state.remaining;
482 StringBuilder sb = new StringBuilder ();
485 for (index = 0; index < part.Length; index++) {
487 char ch = part [index];
489 var supportsQuery = UriHelper.SupportsQuery (state.elements.scheme);
491 if (ch == '#' || (supportsQuery && ch == '?'))
497 if (index <= part.Length)
498 state.remaining = part.Substring (index);
500 state.elements.path = sb.ToString ();
502 return state.remaining.Length > 0;
505 private static bool ParseQuery (ParserState state)
507 string part = state.remaining;
509 if (!UriHelper.SupportsQuery (state.elements.scheme))
510 return part.Length > 0;
512 if (part.Length == 0 || part [0] != '?')
513 return part.Length > 0;
515 StringBuilder sb = new StringBuilder ();
518 for (index = 1; index < part.Length; index++) {
520 char ch = part [index];
528 if (index <= part.Length)
529 state.remaining = part.Substring (index);
531 state.elements.query = sb.ToString ();
533 return state.remaining.Length > 0;
536 private static bool ParseFragment (ParserState state)
538 string part = state.remaining;
540 if (part.Length == 0 || part [0] != '#')
541 return part.Length > 0;
543 StringBuilder sb = new StringBuilder ();
546 for (index = 1; index < part.Length; index++) {
548 char ch = part [index];
553 state.elements.fragment = sb.ToString ();