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];
328 if (!Uri.IsHexEncoding (part, index))
330 var oldIndex = index;
331 ch = Uri.HexUnescape (part, ref index);
334 sb.Append (part.Substring (oldIndex, index - oldIndex + 1));
339 if (Char.IsLetterOrDigit (ch) || IsUnreserved (ch) || IsSubDelim (ch) || ch == ':'){
341 sb = new StringBuilder ();
347 if (index + 1 <= part.Length && part [index] == '@') {
348 if (state.elements.scheme == Uri.UriSchemeFile) {
349 state.error = "Invalid URI: The hostname could not be parsed.";
353 state.elements.user = sb == null ? "" : sb.ToString ();
354 state.remaining = state.remaining.Substring (index + 1);
357 return state.remaining.Length > 0;
360 // host = IP-literal / IPv4address / reg-name
361 private static bool ParseHost (ParserState state)
363 string part = state.remaining;
365 if (state.elements.scheme == Uri.UriSchemeFile && part.Length >= 2 &&
366 (part [0] == '\\' || part [0] == '/') && part [1] == part [0]) {
367 part = part.TrimStart (part [0]);
368 state.remaining = part;
371 if (!ParseWindowsFilePath (state))
374 StringBuilder sb = new StringBuilder ();
378 var possibleIpv6 = false;
381 for (index = 0; index < part.Length; index++) {
383 char ch = part [index];
385 if (ch == '/' || ch == '#' || ch == '?')
389 if (string.IsNullOrEmpty (tmpHost) && ch == ':') {
390 tmpHost = sb.ToString ();
396 if (possibleIpv6 && ch == ']')
401 IPv6Address ipv6addr;
402 if (IPv6Address.TryParse (sb.ToString (), out ipv6addr)) {
404 var ipStr = ipv6addr.ToString (false);
406 var ipStr = ipv6addr.ToString (true);
409 ipStr = ipStr.Split ('%') [0];
411 state.elements.host = "[" + ipStr + "]";
412 state.elements.scopeId = ipv6addr.ScopeId;
414 state.remaining = part.Substring (sb.Length);
415 return state.remaining.Length > 0;
417 state.elements.host = tmpHost;
419 state.elements.host = sb.ToString ();
421 state.elements.host = state.elements.host.ToLowerInvariant ();
423 state.remaining = part.Substring (state.elements.host.Length);
425 if (state.elements.scheme == Uri.UriSchemeFile &&
426 state.elements.host != "") {
427 // under Windows all file://host URI are considered UNC, which is not the case other MacOS (e.g. Silverlight)
429 state.elements.isUnc = (Path.DirectorySeparatorChar == '\\');
431 state.elements.isUnc = Environment.IsRunningOnWindows;
435 return state.remaining.Length > 0;
439 private static bool ParsePort (ParserState state)
441 string part = state.remaining;
442 if (part.Length == 0 || part [0] != ':')
443 return part.Length > 0;
445 StringBuilder sb = new StringBuilder ();
448 for (index = 1; index < part.Length; index++ ) {
449 char ch = part [index];
451 if (!char.IsDigit (ch)) {
452 if (ch == '/' || ch == '#' || ch == '?')
455 state.error = "Invalid URI: Invalid port specified.";
462 if (index <= part.Length)
463 state.remaining = part.Substring (index);
466 return state.remaining.Length > 0;
469 if (!Int32.TryParse (sb.ToString (), NumberStyles.None, CultureInfo.InvariantCulture, out port) ||
470 port < 0 || port > UInt16.MaxValue) {
471 state.error = "Invalid URI: Invalid port number";
475 state.elements.port = port;
477 return state.remaining.Length > 0;
480 private static bool ParsePath (ParserState state)
482 string part = state.remaining;
483 StringBuilder sb = new StringBuilder ();
486 for (index = 0; index < part.Length; index++) {
488 char ch = part [index];
490 var supportsQuery = UriHelper.SupportsQuery (state.elements.scheme);
492 if (ch == '#' || (supportsQuery && ch == '?'))
498 if (index <= part.Length)
499 state.remaining = part.Substring (index);
501 state.elements.path = sb.ToString ();
503 return state.remaining.Length > 0;
506 private static bool ParseQuery (ParserState state)
508 string part = state.remaining;
510 if (!UriHelper.SupportsQuery (state.elements.scheme))
511 return part.Length > 0;
513 if (part.Length == 0 || part [0] != '?')
514 return part.Length > 0;
516 StringBuilder sb = new StringBuilder ();
519 for (index = 1; index < part.Length; index++) {
521 char ch = part [index];
529 if (index <= part.Length)
530 state.remaining = part.Substring (index);
532 state.elements.query = sb.ToString ();
534 return state.remaining.Length > 0;
537 private static bool ParseFragment (ParserState state)
539 string part = state.remaining;
541 if (part.Length == 0 || part [0] != '#')
542 return part.Length > 0;
544 StringBuilder sb = new StringBuilder ();
547 for (index = 1; index < part.Length; index++) {
549 char ch = part [index];
554 state.elements.fragment = sb.ToString ();