Merge pull request #1949 from lewurm/fixtype
[mono.git] / mcs / class / System / System / UriParseComponents.cs
1 //
2 // Internal UriParseComponents class
3 //
4 // Author:
5 //      Vinicius Jarina  <vinicius.jarina@xamarin.com>
6 //
7 // Copyright (C) 2012 Xamarin, Inc (http://www.xamarin.com)
8 //
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:
16 // 
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 // 
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.
27 //
28
29 using System.IO;
30 using System.Net;
31 using System.Text;
32 using System.Globalization;
33
34 namespace System {
35         
36         internal class ParserState
37         {
38                 public ParserState (string uri, UriKind kind)
39                 {
40                         remaining = uri;
41                         this.kind = kind;
42                         elements  = new UriElements ();
43                 }
44                 
45                 public string remaining;
46                 public UriKind kind;
47                 public UriElements elements;
48                 public string error;
49         }
50         
51         // Parse Uri components (scheme, userinfo, host, query, fragment)
52         // http://www.ietf.org/rfc/rfc3986.txt
53         internal static class UriParseComponents
54         {
55                 public static UriElements ParseComponents (string uri, UriKind kind)
56                 {
57                         UriElements elements;
58                         string error;
59
60                         if (!TryParseComponents (uri, kind, out elements, out error))
61                                 throw new UriFormatException (error);
62
63                         return elements;
64                 }
65
66                 public static bool TryParseComponents (string uri, UriKind kind, out UriElements elements, out string error)
67                 {
68                         uri = uri.Trim ();
69
70                         ParserState state = new ParserState (uri, kind);
71                         elements = state.elements;
72                         error = null;
73
74                         if (uri.Length == 0 && (kind == UriKind.Relative || kind == UriKind.RelativeOrAbsolute)){
75                                 state.elements.isAbsoluteUri = false;
76                                 return true;
77                         }
78                         
79                         if (uri.Length <= 1 && kind == UriKind.Absolute) {
80                                 error = "Absolute URI is too short";
81                                 return false;
82                         }
83
84                         bool ok = ParseFilePath (state) &&
85                                 ParseScheme (state);
86
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))
92                                         return true;
93                         }
94
95                         ok = ok &&
96                                 ParseAuthority (state) &&
97                                 ParsePath (state) &&
98                                 ParseQuery (state) &&
99                                 ParseFragment (state);
100
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.";
105
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.";
109
110                         if (!string.IsNullOrEmpty (state.error)) {
111                                 elements = null;
112                                 error = state.error;
113                                 return false;
114                         }
115                         
116                         return true;
117                 }
118
119                                 // ALPHA
120                 private static bool IsAlpha (char ch)
121                 {
122                         return (('a' <= ch) && (ch <= 'z')) ||
123                                    (('A' <= ch) && (ch <= 'Z'));
124                 }
125
126                 private static bool ParseFilePath (ParserState state)
127                 {
128                         return ParseWindowsFilePath (state) &&
129                                 ParseWindowsUNC (state) &&
130                                 ParseUnixFilePath (state);
131                 }
132
133                 private static bool ParseWindowsFilePath (ParserState state)
134                 {
135                         var scheme = state.elements.scheme;
136
137                         if (!string.IsNullOrEmpty (scheme) &&
138                                  scheme != Uri.UriSchemeFile && UriHelper.IsKnownScheme (scheme))
139                                 return state.remaining.Length > 0;
140
141                         string part = state.remaining;
142
143                         if (part.Length > 0 && (part [0] == '/' || part [0] == '\\'))
144                                 part = part.Substring (1);
145
146                         if (part.Length < 2 || part [1] != ':')
147                                 return state.remaining.Length > 0;
148
149                         if (!IsAlpha (part [0])) {
150                                 if (state.kind == UriKind.Absolute) {
151                                         state.error = "Invalid URI: The URI scheme is not valid.";
152                                         return false;
153                                 }
154                                 state.elements.isAbsoluteUri = false;
155                                 state.elements.path = part;
156                                 return false;
157                         }
158
159                         if (part.Length > 2 && part [2] != '\\' && part [2] != '/') {
160                                 state.error = "Relative file path is not allowed.";
161                                 return false;
162                         }
163
164                         if (string.IsNullOrEmpty (scheme)) {
165                                 state.elements.scheme = Uri.UriSchemeFile;
166                                 state.elements.delimiter = "://";
167                         }
168
169                         state.elements.path = part.Replace ("\\", "/");
170
171                         return false;
172                 }
173
174                 private static bool ParseWindowsUNC (ParserState state)
175                 {
176                         string part = state.remaining;
177
178                         if (part.Length < 2 || part [0] != '\\' || part [1] != '\\')
179                                 return state.remaining.Length > 0;
180
181                         state.elements.scheme = Uri.UriSchemeFile;
182                         state.elements.delimiter = "://";
183                         state.elements.isUnc = true;
184
185                         part = part.TrimStart ('\\');
186                         int pos = part.IndexOf ('\\');
187                         if (pos > 0) {
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;
193                         }
194                         state.elements.path = state.elements.path.Replace ("\\", "/");
195
196                         return false;
197                 }
198
199                 private static bool ParseUnixFilePath (ParserState state)
200                 {
201                         string part = state.remaining;
202
203                         if (part.Length < 1 || part [0] != '/' || Path.DirectorySeparatorChar != '/')
204                                 return state.remaining.Length > 0;
205
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;
210
211                         if (part.Length >= 2 && part [0] == '/' && part [1] == '/') {
212                                 part = part.TrimStart (new char [] {'/'});
213                                 state.elements.path = '/' + part;
214                         } else
215                                 state.elements.path = part;
216
217                         return false;
218                 }
219                 
220                 // 3.1) scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
221                 private static bool ParseScheme (ParserState state)
222                 {
223                         string part = state.remaining;
224                         
225                         StringBuilder sb = new StringBuilder ();
226                         sb.Append (part [0]);
227                         
228                         int index;
229                         for (index = 1; index < part.Length; index++ ) {
230                                 char ch = part [index];
231                                 if (ch != '.' && ch != '-' && ch != '+' && !IsAlpha (ch) && !Char.IsDigit (ch))
232                                         break;
233                                 
234                                 sb.Append (ch);
235                         }
236                         
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.";
240                                         return false;
241                                 }
242
243                                 state.elements.isAbsoluteUri = false;
244                                 return state.remaining.Length > 0;
245                         }
246
247                         if (part [index] != ':') {
248                                 if (state.kind == UriKind.Absolute) {
249                                         state.error = "Invalid URI: The URI scheme is not valid.";
250                                         return false;
251                                 }
252
253                                 state.elements.isAbsoluteUri = false;
254                                 return state.remaining.Length > 0;
255                         }
256
257                         state.elements.scheme = sb.ToString ().ToLowerInvariant ();
258                         state.remaining = part.Substring (index);
259
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.";
265                                         return false;
266                                 }
267
268                                 state.elements.isAbsoluteUri = false;
269                                 return state.remaining.Length > 0;
270                         }
271
272                         return ParseDelimiter (state);
273                 }
274
275                 private static bool ParseDelimiter (ParserState state)
276                 {
277                         var delimiter = Uri.GetSchemeDelimiter (state.elements.scheme);
278
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.";
282                                         return false;
283                                 }
284
285                                 delimiter = ":";
286                         }
287                                 
288                         state.elements.delimiter = delimiter;
289
290                         state.remaining = state.remaining.Substring (delimiter.Length);
291
292                         return state.remaining.Length > 0;
293                 }
294                 
295                 private static bool ParseAuthority (ParserState state)
296                 {
297                         if (state.elements.delimiter != Uri.SchemeDelimiter && state.elements.scheme != Uri.UriSchemeMailto)
298                                 return state.remaining.Length > 0;
299                         
300                         return ParseUser (state) &&
301                                 ParseHost (state) &&
302                                 ParsePort (state);
303                 }
304
305                 static bool IsUnreserved (char ch)
306                 {
307                         return ch == '-' || ch == '.' || ch == '_' || ch == '~';
308                 }
309
310
311                 static bool IsSubDelim (char ch)
312                 {
313                         return ch == '!' || ch == '$' || ch == '&' || ch == '\'' || ch == '(' || ch == ')' ||
314                                 ch == '*' || ch == '+' || ch == ',' || ch == ';' || ch == '=';
315                 }
316                 
317                 // userinfo    = *( unreserved / pct-encoded / sub-delims / ":" )
318                 private static bool ParseUser (ParserState state)
319                 {
320                         string part = state.remaining;
321                         StringBuilder sb = null;
322
323                         int index;
324                         for (index = 0; index < part.Length; index++) {
325                                 char ch = part [index];
326                                 bool isEscapedChar = false;
327                                 var oldIndex = index;
328
329                                 // Spaces should be percentage encoded #31172
330                                 if (ch == ' ') {
331                                         if (sb == null)
332                                                 sb = new StringBuilder ();
333                                         sb.Append ("%20");
334                                         continue;
335                                 }
336
337                                 if (ch == '%'){
338                                         if (!Uri.IsHexEncoding (part, index))
339                                                 return false;
340                                         ch = Uri.HexUnescape (part, ref index);
341                                         index--;
342                                         isEscapedChar = true;
343                                 }
344
345                                 if (!Char.IsLetterOrDigit (ch) && !IsUnreserved (ch) && !IsSubDelim (ch) && ch != ':'){
346                                         if (!isEscapedChar)
347                                                 break;
348
349                                         ch = '%';
350                                         index = oldIndex;
351                                 }
352
353                                 if (sb == null)
354                                         sb = new StringBuilder ();
355                                 sb.Append (ch);
356                         }
357
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.";
361                                         return false;
362                                 }
363
364                                 state.elements.user = sb == null ? "" : sb.ToString ();
365                                 state.remaining = state.remaining.Substring (index + 1);
366                         }
367                                 
368                         return state.remaining.Length > 0;
369                 }
370                 
371                 // host        = IP-literal / IPv4address / reg-name
372                 private static bool ParseHost (ParserState state)
373                 {
374                         string part = state.remaining;
375
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;
380                         }
381
382                         if (!ParseWindowsFilePath (state))
383                                 return false;
384
385                         StringBuilder sb = new StringBuilder ();
386                         
387                         var tmpHost = "";
388
389                         var possibleIpv6 = false;
390
391                         int index;
392                         for (index = 0; index < part.Length; index++) { 
393                                 
394                                 char ch = part [index];
395                                 
396                                 if (ch == '/' || ch == '#' || ch == '?')
397                                         break;
398
399                                 // Possible IPv6
400                                 if (string.IsNullOrEmpty (tmpHost) && ch == ':') {
401                                         tmpHost = sb.ToString ();
402                                         possibleIpv6 = true;
403                                 }
404                                 
405                                 sb.Append (ch);
406
407                                 if (possibleIpv6 && ch == ']')
408                                         break;
409                         }
410                         
411                         if (possibleIpv6) {
412                                 IPv6Address ipv6addr;
413                                 if (IPv6Address.TryParse (sb.ToString (), out ipv6addr)) {
414                                         var ipStr = ipv6addr.ToString (false);
415                                         //remove scope
416                                         ipStr = ipStr.Split ('%') [0];
417
418                                         state.elements.host = "[" + ipStr + "]";
419                                         state.elements.scopeId = ipv6addr.ScopeId;
420
421                                         state.remaining = part.Substring (sb.Length);
422                                         return state.remaining.Length > 0;
423                                 }
424                                 state.elements.host = tmpHost;
425                         } else
426                                 state.elements.host = sb.ToString ();
427
428                         state.elements.host = state.elements.host.ToLowerInvariant ();
429
430                         state.remaining = part.Substring (state.elements.host.Length);
431
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)
435 #if BOOTSTRAP_BASIC
436                                 state.elements.isUnc = (Path.DirectorySeparatorChar == '\\');
437 #else
438                                 state.elements.isUnc = Environment.IsRunningOnWindows;
439 #endif
440                         }
441
442                         return state.remaining.Length > 0;
443                 }
444                 
445                 // port          = *DIGIT
446                 private static bool ParsePort (ParserState state)
447                 {
448                         string part = state.remaining;
449                         if (part.Length == 0 || part [0] != ':')
450                                 return part.Length > 0;
451                         
452                         StringBuilder sb = new StringBuilder ();
453                         
454                         int index;
455                         for (index = 1; index < part.Length; index++ ) {
456                                 char ch = part [index];
457                                 
458                                 if (!char.IsDigit (ch)) {
459                                         if (ch == '/' || ch == '#' || ch == '?')
460                                                 break;
461
462                                         state.error = "Invalid URI: Invalid port specified.";
463                                         return false;
464                                 }
465                                 
466                                 sb.Append (ch);
467                         }
468
469                         if (index <= part.Length)
470                                 state.remaining = part.Substring (index);
471
472                         if (sb.Length == 0)
473                                 return state.remaining.Length > 0;
474                         
475                         int port;
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";
479                                 return false;
480                         }
481
482                         state.elements.port = port;
483                                 
484                         return state.remaining.Length > 0;
485                 }
486                 
487                 private static bool ParsePath (ParserState state)
488                 {
489                         string part = state.remaining;
490                         StringBuilder sb = new StringBuilder ();
491                         
492                         int index;
493                         for (index = 0; index < part.Length; index++) {
494                                 
495                                 char ch = part [index];
496                                 
497                                 var supportsQuery = UriHelper.SupportsQuery (state.elements.scheme);
498
499                                 if (ch == '#' || (supportsQuery && ch == '?'))
500                                         break;
501                                 
502                                 sb.Append (ch);
503                         }
504                         
505                         if (index <= part.Length)
506                                 state.remaining = part.Substring (index);
507                         
508                         state.elements.path  = sb.ToString ();
509                                 
510                         return state.remaining.Length > 0;
511                 }
512                 
513                 private static bool ParseQuery (ParserState state)
514                 {
515                         string part = state.remaining;
516
517                         if (!UriHelper.SupportsQuery (state.elements.scheme))
518                                 return part.Length > 0;
519                         
520                         if (part.Length == 0 || part [0] != '?')
521                                 return part.Length > 0;
522                         
523                         StringBuilder sb = new StringBuilder ();
524                         
525                         int index;
526                         for (index = 1; index < part.Length; index++) {
527                                 
528                                 char ch = part [index];
529                                 
530                                 if (ch == '#')
531                                         break;
532                                 
533                                 sb.Append (ch);
534                         }
535                         
536                         if (index <= part.Length)
537                                 state.remaining = part.Substring (index);
538                         
539                         state.elements.query  = sb.ToString ();
540                                 
541                         return state.remaining.Length > 0;
542                 }
543                 
544                 private static bool ParseFragment (ParserState state)
545                 {
546                         string part = state.remaining;
547                         
548                         if (part.Length == 0 || part [0] != '#')
549                                 return part.Length > 0;
550                         
551                         StringBuilder sb = new StringBuilder ();
552                         
553                         int index;
554                         for (index = 1; index < part.Length; index++) { 
555                                 
556                                 char ch = part [index];
557                                 
558                                 sb.Append (ch);
559                         }
560                         
561                         state.elements.fragment = sb.ToString ();
562                         
563                         return false;
564                 }
565         }
566 }