2004-06-09 Atsushi Enomoto <atsushi@ximian.com>
[mono.git] / mcs / class / System / System / Uri.cs
1 //
2 // System.Uri
3 //
4 // Authors:
5 //    Lawrence Pit (loz@cable.a2000.nl)
6 //    Garrett Rooney (rooneg@electricjellyfish.net)
7 //    Ian MacLean (ianm@activestate.com)
8 //    Ben Maurer (bmaurer@users.sourceforge.net)
9 //    Atsushi Enomoto (atsushi@ximian.com)
10 //
11 // (C) 2001 Garrett Rooney
12 // (C) 2003 Ian MacLean
13 // (C) 2003 Ben Maurer
14 // (C) 2003 Novell inc.
15 //
16
17 using System.Net;
18 using System.Runtime.Serialization;
19 using System.Text;
20 using System.Collections;
21
22 // See RFC 2396 for more info on URI's.
23
24 // TODO: optimize by parsing host string only once
25
26 namespace System 
27 {
28         [Serializable]
29         public class Uri : MarshalByRefObject, ISerializable 
30         {
31                 // NOTES:
32                 // o  scheme excludes the scheme delimiter
33                 // o  port is -1 to indicate no port is defined
34                 // o  path is empty or starts with / when scheme delimiter == "://"
35                 // o  query is empty or starts with ? char
36                 // o  fragment is empty or starts with # char
37                 // o  all class variables are in escaped format when they are escapable,
38                 //    except cachedToString.
39                 // o  UNC is supported, as starts with "\\" for windows,
40                 //    or "//" with unix.
41
42                 private bool isWindowsFilePath = false;
43                 private bool isUnixFilePath = false;
44                 private string scheme = String.Empty;
45                 private string host = String.Empty;
46                 private int port = -1;
47                 private string path = String.Empty;
48                 private string query = String.Empty;
49                 private string fragment = String.Empty;
50                 private string userinfo = String.Empty;
51                 private bool is_root_path = false;
52                 private bool is_wins_dir = true;
53                 private bool isUnc = false;
54                 private bool isOpaquePart = false;
55
56                 private string [] segments;
57                 
58                 private bool userEscaped = false;
59                 private string cachedAbsoluteUri = null;
60                 private string cachedToString = null;
61                 private int cachedHashCode = 0;
62                 
63                 private static readonly string hexUpperChars = "0123456789ABCDEF";
64         
65                 // Fields
66                 
67                 public static readonly string SchemeDelimiter = "://";
68                 public static readonly string UriSchemeFile = "file";
69                 public static readonly string UriSchemeFtp = "ftp";
70                 public static readonly string UriSchemeGopher = "gopher";
71                 public static readonly string UriSchemeHttp = "http";
72                 public static readonly string UriSchemeHttps = "https";
73                 public static readonly string UriSchemeMailto = "mailto";
74                 public static readonly string UriSchemeNews = "news";
75                 public static readonly string UriSchemeNntp = "nntp";
76
77                 // Constructors         
78
79                 public Uri (string uriString) : this (uriString, false) 
80                 {
81                 }
82
83                 protected Uri (SerializationInfo serializationInfo, 
84                                StreamingContext streamingContext) :
85                         this (serializationInfo.GetString ("AbsoluteUri"), true)
86                 {
87                 }
88
89                 public Uri (string uriString, bool dontEscape) 
90                 {                       
91                         userEscaped = dontEscape;
92                         Parse (uriString);
93                         
94                         if (userEscaped) 
95                                 return;
96
97                         host = EscapeString (host, false, true, false);
98                         path = EscapeString (path);
99                 }
100
101                 public Uri (Uri baseUri, string relativeUri) 
102                         : this (baseUri, relativeUri, false) 
103                 {                       
104                 }
105
106                 public Uri (Uri baseUri, string relativeUri, bool dontEscape) 
107                 {
108                         if (baseUri == null)
109                                 throw new NullReferenceException ("baseUri");
110
111                         // See RFC 2396 Par 5.2 and Appendix C
112
113                         userEscaped = dontEscape;
114
115                         if (relativeUri == null)
116                                 throw new NullReferenceException ("relativeUri");
117
118                         int pos = relativeUri.IndexOf (':');
119                         if (pos != -1) {
120
121                                 int pos2 = relativeUri.IndexOfAny (new char [] {'/', '\\'});
122
123                                 if (pos2 > pos) {
124                                         // equivalent to new Uri (relativeUri, dontEscape)
125                                         Parse (relativeUri);
126
127                                         if (userEscaped) 
128                                                 return;
129
130                                         host = EscapeString (host, false, true, false);
131                                         path = EscapeString (path);
132                                         return;
133                                 }
134                         }
135
136                         this.scheme = baseUri.scheme;
137                         this.host = baseUri.host;
138                         this.port = baseUri.port;
139                         this.userinfo = baseUri.userinfo;
140                         this.isUnc = baseUri.isUnc;
141                         this.isWindowsFilePath = baseUri.isWindowsFilePath;
142                         this.isUnixFilePath = baseUri.isUnixFilePath;
143                         this.isOpaquePart = baseUri.isOpaquePart;
144
145                         if (relativeUri == String.Empty) {
146                                 this.path = baseUri.path;
147                                 this.query = baseUri.query;
148                                 this.fragment = baseUri.fragment;
149                                 return;
150                         }
151                         
152                         // 8 fragment
153                         pos = relativeUri.IndexOf ('#');
154                         if (pos != -1) {
155                                 fragment = relativeUri.Substring (pos);
156                                 relativeUri = relativeUri.Substring (0, pos);
157                         }
158
159                         // 6 query
160                         pos = relativeUri.IndexOf ('?');
161                         if (pos != -1) {
162                                 query = relativeUri.Substring (pos);
163                                 if (!userEscaped)
164                                         query = EscapeString (query);
165                                 relativeUri = relativeUri.Substring (0, pos);
166                         }
167
168                         if (relativeUri.Length > 0 && relativeUri [0] == '/') {
169                                 path = relativeUri;
170                                 if (!userEscaped)
171                                         path = EscapeString (path);
172                                 return;
173                         }
174                         
175                         // par 5.2 step 6 a)
176                         path = baseUri.path;
177                         if (relativeUri.Length > 0 || query.Length > 0) {
178                                 pos = path.LastIndexOf ('/');
179                                 if (pos >= 0) 
180                                         path = path.Substring (0, pos + 1);
181                         }
182
183                         if(relativeUri.Length == 0)
184                                 return;
185         
186                         // 6 b)
187                         path += relativeUri;
188
189                         // 6 c)
190                         int startIndex = 0;
191                         while (true) {
192                                 pos = path.IndexOf ("./", startIndex);
193                                 if (pos == -1)
194                                         break;
195                                 if (pos == 0)
196                                         path = path.Remove (0, 2);
197                                 else if (path [pos - 1] != '.')
198                                         path = path.Remove (pos, 2);
199                                 else
200                                         startIndex = pos + 1;
201                         }
202                         
203                         // 6 d)
204                         if (path.Length > 1 && 
205                             path [path.Length - 1] == '.' &&
206                             path [path.Length - 2] == '/')
207                                 path = path.Remove (path.Length - 1, 1);
208                         
209                         // 6 e)
210                         startIndex = 0;
211                         while (true) {
212                                 pos = path.IndexOf ("/../", startIndex);
213                                 if (pos == -1)
214                                         break;
215                                 if (pos == 0) {
216                                         startIndex = 3;
217                                         continue;
218                                 }
219                                 int pos2 = path.LastIndexOf ('/', pos - 1);
220                                 if (pos2 == -1) {
221                                         startIndex = pos + 1;
222                                 } else {
223                                         if (path.Substring (pos2 + 1, pos - pos2 - 1) != "..")
224                                                 path = path.Remove (pos2 + 1, pos - pos2 + 3);
225                                         else
226                                                 startIndex = pos + 1;
227                                 }
228                         }
229                         
230                         // 6 f)
231                         if (path.Length > 3 && path.EndsWith ("/..")) {
232                                 pos = path.LastIndexOf ('/', path.Length - 4);
233                                 if (pos != -1)
234                                         if (path.Substring (pos + 1, path.Length - pos - 4) != "..")
235                                                 path = path.Remove (pos + 1, path.Length - pos - 1);
236                         }
237                         
238                         if (!userEscaped)
239                                 path = EscapeString (path);
240                 }               
241                 
242                 // Properties
243
244                 public string AbsolutePath { 
245                         get { return path; } 
246                 }
247
248                 public string AbsoluteUri { 
249                         get {
250                                 if (cachedAbsoluteUri == null) {
251 //                                      cachedAbsoluteUri = GetLeftPart (UriPartial.Path) + query + fragment;
252                                         string qf = IsFile ? query + fragment : EscapeString (query + fragment);
253                                         cachedAbsoluteUri = GetLeftPart (UriPartial.Path) + qf;
254                                 }
255                                 return cachedAbsoluteUri;
256                         } 
257                 }
258
259                 public string Authority { 
260                         get { 
261                                 return (GetDefaultPort (scheme) == port)
262                                      ? host : host + ":" + port;
263                         } 
264                 }
265
266                 public string Fragment { 
267                         get { return fragment; } 
268                 }
269
270                 public string Host { 
271                         get { return host; } 
272                 }
273
274                 public UriHostNameType HostNameType { 
275                         get {
276                                 UriHostNameType ret = CheckHostName (host);
277                                 if (ret != UriHostNameType.Unknown)
278                                         return ret;
279
280                                 // looks it always returns Basic...
281                                 return UriHostNameType.Basic; //.Unknown;
282                         } 
283                 }
284
285                 public bool IsDefaultPort { 
286                         get { return GetDefaultPort (scheme) == port; } 
287                 }
288
289                 public bool IsFile { 
290                         get { return (scheme == UriSchemeFile); }
291                 }
292
293                 public bool IsLoopback { 
294                         get { 
295                                 if (host == String.Empty)
296                                         return false;
297                                         
298                                 if (host == "loopback" || host == "localhost") 
299                                         return true;
300                                         
301                                 try {
302                                         if (IPAddress.Loopback.Equals (IPAddress.Parse (host)))
303                                                 return true;
304                                 } catch (FormatException) {}
305
306                                 try {
307                                         return IPv6Address.IsLoopback (IPv6Address.Parse (host));
308                                 } catch (FormatException) {}
309                                 
310                                 return false;
311                         } 
312                 }
313
314                 public bool IsUnc {
315                         // rule: This should be true only if
316                         //   - uri string starts from "\\", or
317                         //   - uri string starts from "//" (Samba way)
318                         get { return isUnc; } 
319                 }
320
321                 public string LocalPath { 
322                         get {
323                                 if (!IsFile)
324                                         return path;
325
326                                 bool windows = (path.Length > 3 && path [1] == ':' &&
327                                                 (path [2] == '\\' || path [2] == '/'));
328
329                                 if (!IsUnc) {
330                                         string p = Unescape (path);
331                                         if (System.IO.Path.DirectorySeparatorChar == '\\' || windows)
332                                                 return p.Replace ('/', '\\');
333                                         else
334                                                 return p;
335                                 }
336
337                                 // support *nix and W32 styles
338                                 if (path.Length > 1 && path [1] == ':')
339                                         return Unescape (path.Replace ('/', '\\'));
340
341                                 if (System.IO.Path.DirectorySeparatorChar == '\\')
342                                         return "\\\\" + Unescape (host + path.Replace ('/', '\\'));
343                                 else 
344                                         return (is_root_path? "/": "") + (is_wins_dir? "/": "") + Unescape (host + path);
345                         } 
346                 }
347
348                 public string PathAndQuery { 
349                         get { return path + query; } 
350                 }
351
352                 public int Port { 
353                         get { return port; } 
354                 }
355
356                 public string Query { 
357                         get { return query; } 
358                 }
359
360                 public string Scheme { 
361                         get { return scheme; } 
362                 }
363
364                 public string [] Segments { 
365                         get { 
366                                 if (segments != null)
367                                         return segments;
368
369                                 if (path == "") {
370                                         segments = new string [0];
371                                         return segments;
372                                 }
373
374                                 string [] parts = path.Split ('/');
375                                 segments = parts;
376                                 bool endSlash = path.EndsWith ("/");
377                                 if (parts.Length > 0 && endSlash) {
378                                         string [] newParts = new string [parts.Length - 1];
379                                         Array.Copy (parts, 0, newParts, 0, parts.Length - 1);
380                                         parts = newParts;
381                                 }
382
383                                 int i = 0;
384                                 if (IsFile && path.Length > 1 && path [1] == ':') {
385                                         string [] newParts = new string [parts.Length + 1];
386                                         Array.Copy (parts, 1, newParts, 2, parts.Length - 1);
387                                         parts = newParts;
388                                         parts [0] = path.Substring (0, 2);
389                                         parts [1] = "";
390                                         i++;
391                                 }
392                                 
393                                 int end = parts.Length;
394                                 for (; i < end; i++) 
395                                         if (i != end - 1 || endSlash)
396                                                 parts [i] += '/';
397
398                                 segments = parts;
399                                 return segments;
400                         } 
401                 }
402
403                 public bool UserEscaped { 
404                         get { return userEscaped; } 
405                 }
406
407                 public string UserInfo { 
408                         get { return userinfo; }
409                 }
410                 
411
412                 // Methods              
413                 
414                 public static UriHostNameType CheckHostName (string name) 
415                 {
416                         if (name == null || name.Length == 0)
417                                 return UriHostNameType.Unknown;
418
419                         if (IsIPv4Address (name)) 
420                                 return UriHostNameType.IPv4;
421                                 
422                         if (IsDomainAddress (name))
423                                 return UriHostNameType.Dns;                             
424                                 
425                         try {
426                                 IPv6Address.Parse (name);
427                                 return UriHostNameType.IPv6;
428                         } catch (FormatException) {}
429                         
430                         return UriHostNameType.Unknown;
431                 }
432                 
433                 internal static bool IsIPv4Address (string name)
434                 {               
435                         string [] captures = name.Split (new char [] {'.'});
436                         if (captures.Length != 4)
437                                 return false;
438                         for (int i = 0; i < 4; i++) {
439                                 try {
440                                         int d = Int32.Parse (captures [i]);
441                                         if (d < 0 || d > 255)
442                                                 return false;
443                                 } catch (Exception) {
444                                         return false;
445                                 }
446                         }
447                         return true;
448                 }                       
449                                 
450                 internal static bool IsDomainAddress (string name)
451                 {
452                         int len = name.Length;
453                         
454                         if (name [len - 1] == '.')
455                                 return false;
456                                 
457                         int count = 0;
458                         for (int i = 0; i < len; i++) {
459                                 char c = name [i];
460                                 if (count == 0) {
461                                         if (!Char.IsLetterOrDigit (c))
462                                                 return false;
463                                 } else if (c == '.') {
464                                         count = 0;
465                                 } else if (!Char.IsLetterOrDigit (c) && c != '-' && c != '_') {
466                                         return false;
467                                 }
468                                 if (++count == 64)
469                                         return false;
470                         }
471                         
472                         return true;
473                 }
474
475                 [MonoTODO ("Find out what this should do")]
476                 protected virtual void Canonicalize ()
477                 {
478                 }
479
480                 public static bool CheckSchemeName (string schemeName) 
481                 {
482                         if (schemeName == null || schemeName.Length == 0)
483                                 return false;
484                         
485                         if (!Char.IsLetter (schemeName [0]))
486                                 return false;
487
488                         int len = schemeName.Length;
489                         for (int i = 1; i < len; i++) {
490                                 char c = schemeName [i];
491                                 if (!Char.IsLetterOrDigit (c) && c != '.' && c != '+' && c != '-')
492                                         return false;
493                         }
494                         
495                         return true;
496                 }
497
498                 [MonoTODO ("Find out what this should do")]
499                 protected virtual void CheckSecurity ()
500                 {
501                 }
502
503                 public override bool Equals (object comparant) 
504                 {
505                         if (comparant == null) 
506                                 return false;
507                                 
508                         Uri uri = comparant as Uri;
509                         if (uri == null) {
510                                 string s = comparant as String;
511                                 if (s == null)
512                                         return false;
513                                 uri = new Uri (s);
514                         }
515                         
516                         return ((this.scheme.ToLower () == uri.scheme.ToLower ()) &&
517                                 (this.userinfo.ToLower () == uri.userinfo.ToLower ()) &&
518                                 (this.host.ToLower () == uri.host.ToLower ()) &&
519                                 (this.port == uri.port) &&
520                                 (this.path == uri.path) &&
521                                 (this.query.ToLower () == uri.query.ToLower ()));
522                 }               
523                 
524                 public override int GetHashCode () 
525                 {
526                         if (cachedHashCode == 0)                        
527                                 cachedHashCode = scheme.GetHashCode ()
528                                                + userinfo.GetHashCode ()
529                                                + host.GetHashCode ()
530                                                + port
531                                                + path.GetHashCode ()
532                                                + query.GetHashCode ();                             
533                         return cachedHashCode;                          
534                 }
535                 
536                 public string GetLeftPart (UriPartial part) 
537                 {
538                         int defaultPort;
539                         switch (part) {                         
540                         case UriPartial.Scheme : 
541                                 return scheme + GetOpaqueWiseSchemeDelimiter ();
542                         case UriPartial.Authority :
543                                 if (host == String.Empty ||
544                                     scheme == Uri.UriSchemeMailto ||
545                                     scheme == Uri.UriSchemeNews)
546                                         return String.Empty;
547                                         
548                                 StringBuilder s = new StringBuilder ();
549                                 s.Append (scheme);
550                                 s.Append (GetOpaqueWiseSchemeDelimiter ());
551                                 if (path.Length > 1 && path [1] == ':' && (Uri.UriSchemeFile == scheme)) 
552                                         s.Append ('/');  // win32 file
553                                 if (userinfo.Length > 0) 
554                                         s.Append (userinfo).Append ('@');
555                                 s.Append (host);
556                                 defaultPort = GetDefaultPort (scheme);
557                                 if ((port != -1) && (port != defaultPort))
558                                         s.Append (':').Append (port);                    
559                                 return s.ToString ();                           
560                         case UriPartial.Path :                  
561                                 StringBuilder sb = new StringBuilder ();
562                                 sb.Append (scheme);
563                                 sb.Append (GetOpaqueWiseSchemeDelimiter ());
564                                 if (path.Length > 1 && path [1] == ':' && (Uri.UriSchemeFile == scheme)) 
565                                         sb.Append ('/');  // win32 file
566                                 if (userinfo.Length > 0) 
567                                         sb.Append (userinfo).Append ('@');
568                                 sb.Append (host);
569                                 defaultPort = GetDefaultPort (scheme);
570                                 if ((port != -1) && (port != defaultPort))
571                                         sb.Append (':').Append (port);                   
572                                 sb.Append (path);
573                                 return sb.ToString ();
574                         }
575                         return null;
576                 }
577
578                 public static int FromHex (char digit) 
579                 {
580                         if ('0' <= digit && digit <= '9') {
581                                 return (int) (digit - '0');
582                         }
583                                 
584                         if ('a' <= digit && digit <= 'f')
585                                 return (int) (digit - 'a' + 10);
586
587                         if ('A' <= digit && digit <= 'F')
588                                 return (int) (digit - 'A' + 10);
589                                 
590                         throw new ArgumentException ("digit");
591                 }
592
593                 public static string HexEscape (char character) 
594                 {
595                         if (character > 255) {
596                                 throw new ArgumentOutOfRangeException ("character");
597                         }
598                         
599                         return "%" + hexUpperChars [((character & 0xf0) >> 4)] 
600                                    + hexUpperChars [((character & 0x0f))];
601                 }
602
603                 public static char HexUnescape (string pattern, ref int index) 
604                 {
605                         if (pattern == null) 
606                                 throw new ArgumentException ("pattern");
607                                 
608                         if (index < 0 || index >= pattern.Length)
609                                 throw new ArgumentOutOfRangeException ("index");        
610                                 
611                         if (((index + 3) > pattern.Length) ||
612                             (pattern [index] != '%') || 
613                             !IsHexDigit (pattern [index + 1]) || 
614                             !IsHexDigit (pattern [index + 2]))
615                         {
616                                 return pattern[index++];
617                         }
618                         
619                         index++;
620                         return (char) ((FromHex (pattern [index++]) << 4) + FromHex (pattern [index++]));
621                 }
622
623                 public static bool IsHexDigit (char digit) 
624                 {
625                         return (('0' <= digit && digit <= '9') ||
626                                 ('a' <= digit && digit <= 'f') ||
627                                 ('A' <= digit && digit <= 'F'));
628                 }
629
630                 public static bool IsHexEncoding (string pattern, int index) 
631                 {
632                         if ((index + 3) > pattern.Length)
633                                 return false;
634
635                         return ((pattern [index++] == '%') &&
636                                 IsHexDigit (pattern [index++]) &&
637                                 IsHexDigit (pattern [index]));
638                 }
639
640                 public string MakeRelative (Uri toUri) 
641                 {
642                         if ((this.Scheme != toUri.Scheme) ||
643                             (this.Authority != toUri.Authority))
644                                 return toUri.ToString ();
645                                 
646                         if (this.path == toUri.path)
647                                 return String.Empty;
648                                 
649                         string [] segments = this.Segments;
650                         string [] segments2 = toUri.Segments;
651                         
652                         int k = 0;
653                         int max = Math.Min (segments.Length, segments2.Length);
654                         for (; k < max; k++)
655                                 if (segments [k] != segments2 [k]) 
656                                         break;
657                         
658                         string result = String.Empty;
659                         for (int i = k + 1; i < segments.Length; i++)
660                                 result += "../";
661                         for (int i = k; i < segments2.Length; i++)
662                                 result += segments2 [i];
663                         
664                         return result;
665                 }
666
667                 public override string ToString () 
668                 {
669                         if (cachedToString != null) 
670                                 return cachedToString;
671                         cachedToString = Unescape (AbsoluteUri);
672
673                         return cachedToString;
674                 }
675
676                 void ISerializable.GetObjectData (SerializationInfo info, 
677                                           StreamingContext context)
678                 {
679                         info.AddValue ("AbsoluteUri", this.AbsoluteUri);
680                 }
681
682
683                 // Internal Methods             
684
685                 protected virtual void Escape ()
686                 {
687                         path = EscapeString (path);
688                 }
689
690                 protected static string EscapeString (string str) 
691                 {
692                         return EscapeString (str, false, true, true);
693                 }
694                 
695                 internal static string EscapeString (string str, bool escapeReserved, bool escapeHex, bool escapeBrackets) 
696                 {
697                         if (str == null)
698                                 return String.Empty;
699                         
700                         byte [] data = Encoding.UTF8.GetBytes (str.ToCharArray ());
701                         StringBuilder s = new StringBuilder ();
702                         int len = data.Length;  
703                         for (int i = 0; i < len; i++) {
704                                 char c = (char) data [i];
705                                 // reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
706                                 // mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
707                                 // control     = <US-ASCII coded characters 00-1F and 7F hexadecimal>
708                                 // space       = <US-ASCII coded character 20 hexadecimal>
709                                 // delims      = "<" | ">" | "#" | "%" | <">
710                                 // unwise      = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
711
712                                 // check for escape code already placed in str, 
713                                 // i.e. for encoding that follows the pattern 
714                 // "%hexhex" in a string, where "hex" is a digit from 0-9 
715                                 // or a letter from A-F (case-insensitive).
716                                 if('%'.Equals(c) && IsHexEncoding(str,i))
717                                 {
718                                         // if ,yes , copy it as is
719                                         s.Append(c);
720                                         s.Append(str[++i]);
721                                         s.Append(str[++i]);
722                                         continue;
723                                 }
724
725                                 if ((c <= 0x20) || (c >= 0x7f) || 
726                                     ("<>%\"{}|\\^`".IndexOf (c) != -1) ||
727                                     (escapeHex && (c == '#')) ||
728                                     (escapeBrackets && (c == '[' || c == ']')) ||
729                                     (escapeReserved && (";/?:@&=+$,".IndexOf (c) != -1))) {
730                                         // FIXME: EscapeString should allow wide characters (> 255). HexEscape rejects it.
731                                         s.Append (HexEscape (c));
732                                         continue;
733                                 }
734                                 
735                                         
736                                 s.Append (c);
737                         }
738                         
739                         return s.ToString ();
740                 }
741
742                 [MonoTODO ("Find out what this should do!")]
743                 protected virtual void Parse ()
744                 {
745                 }       
746                 
747                 protected virtual string Unescape (string str) 
748                 {
749                         if (str == null)
750                                 return String.Empty;
751                         StringBuilder s = new StringBuilder ();
752                         int len = str.Length;
753                         for (int i = 0; i < len; i++) {
754                                 char c = str [i];
755                                 if (c == '%') {
756                                         s.Append (HexUnescape (str, ref i));
757                                         i--;
758                                 } else
759                                         s.Append (c);
760                         }
761                         return s.ToString ();
762                 }
763
764                 
765                 // Private Methods
766                 
767                 // this parse method is as relaxed as possible about the format
768                 // it will hardly ever throw a UriFormatException
769                 private void Parse (string uriString)
770                 {                       
771                         //
772                         // From RFC 2396 :
773                         //
774                         //      ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
775                         //       12            3  4          5       6  7        8 9
776                         //                      
777                         
778                         if (uriString == null)
779                                 throw new ArgumentNullException ("uriString");
780
781                         int len = uriString.Length;
782                         if (len <= 1) 
783                                 throw new UriFormatException ();
784
785                         // 1
786                         char c = 'x';
787                         int pos = 0;
788                         for (; pos < len; pos++) {
789                                 c = uriString [pos];
790                                 if ((c == ':') || (c == '/') || (c == '\\') || (c == '?') || (c == '#')) 
791                                         break;
792                         }
793
794                         if (pos == len)
795                                 throw new UriFormatException ("The format of the URI could not be determined.");
796
797                         if (c == '/') {
798                                 is_root_path = true;
799                         }
800
801                         if ((uriString.Length >= 2) &&
802                                         ((uriString [0] == '/') && (uriString [1] == '/')) ||
803                                         ((uriString [0] == '\\') || (uriString [1] == '\\'))) {
804                                 is_wins_dir = true;
805                                 is_root_path = true;
806                         }
807
808                         // 2 scheme
809                         if (c == ':') {
810                                 if (pos == 1) {
811                                         // a windows filepath
812                                         if (uriString.Length < 3 || (uriString [2] != '\\' && uriString [2] != '/'))
813                                                 throw new UriFormatException ("Invalid URI: The format of the URI could not be determined.");
814                                         isWindowsFilePath = true;
815                                         scheme = Uri.UriSchemeFile;
816                                         path = uriString.Replace ('\\', '/');
817                                         return;
818                                 }
819
820                                 scheme = uriString.Substring (0, pos).ToLower ();
821                                 uriString = uriString.Remove (0, pos + 1);
822                         } else if ((c == '/') && (pos == 0)) {
823                                 scheme = Uri.UriSchemeFile;
824                                 if (uriString.Length > 1 && uriString [1] != '/')
825                                         // unix bare filepath
826                                         isUnixFilePath = true;
827                                 else
828                                         // unix UNC (kind of)
829                                         isUnc = true;
830                         } else {
831                                 if (uriString [0] != '\\' && uriString [0] != '/' && !uriString.StartsWith ("file://"))
832                                         throw new UriFormatException ("Invalid URI: The format of the URI could not be determined.");
833                                 scheme = Uri.UriSchemeFile;
834                                 if (uriString.StartsWith ("\\\\")) {
835                                         isUnc = true;
836                                         isWindowsFilePath = true;
837                                 }
838                                 if (uriString.Length > 8 && uriString [8] != '/')
839                                         isUnc = true;
840                         }
841
842                         // 3
843                         if ((uriString.Length >= 2) && 
844                             ((uriString [0] == '/') && (uriString [1] == '/')) ||
845                             ((uriString [0] == '\\') || (uriString [1] == '\\')))  {
846                                 if (scheme == Uri.UriSchemeFile)
847                                         uriString = uriString.TrimStart (new char [] {'/', '\\'});
848                                 else
849                                         uriString = uriString.Remove (0, 2);
850                         } else if (!IsPredefinedScheme (scheme)) {
851                                 path = uriString;
852                                 isOpaquePart = true;
853                                 return;
854                         }
855
856                         // 8 fragment
857                         pos = uriString.IndexOf ('#');
858                         if (!IsUnc && pos != -1) {
859                                 fragment = uriString.Substring (pos);
860                                 uriString = uriString.Substring (0, pos);
861                         }
862
863                         // 6 query
864                         pos = uriString.IndexOf ('?');
865                         if (pos != -1) {
866                                 query = uriString.Substring (pos);
867                                 uriString = uriString.Substring (0, pos);
868                         }
869                         
870                         // 5 path
871                         pos = uriString.IndexOfAny (new char[] {'/', '\\'});
872                         if (pos == -1) {
873                                 if ((scheme != Uri.UriSchemeMailto) &&
874                                     (scheme != Uri.UriSchemeNews) &&
875                                         (scheme != Uri.UriSchemeFile))
876                                         path = "/";
877                         } else {
878                                 path = uriString.Substring (pos).Replace ('\\', '/');
879                                 uriString = uriString.Substring (0, pos);
880                         }
881
882                         // 4.a user info
883                         pos = uriString.IndexOf ("@");
884                         if (pos != -1) {
885                                 userinfo = uriString.Substring (0, pos);
886                                 uriString = uriString.Remove (0, pos + 1);
887                         }
888
889                         // 4.b port
890                         port = -1;
891                         pos = uriString.LastIndexOf (":");
892                         if (pos != -1 && pos != (uriString.Length - 1)) {
893                                 string portStr = uriString.Remove (0, pos + 1);
894                                 if (portStr.Length > 1 && portStr [portStr.Length - 1] != ']') {
895                                         try {
896                                                 port = (int) UInt32.Parse (portStr);
897                                                 uriString = uriString.Substring (0, pos);
898                                         } catch (Exception) {
899                                                 throw new UriFormatException ("Invalid URI: invalid port number");
900                                         }
901                                 }
902                         }
903                         if (port == -1) {
904                                 port = GetDefaultPort (scheme);
905                         }
906                         
907                         // 4 authority
908                         host = uriString;
909                         if (host.Length > 1 && host [0] == '[' && host [host.Length - 1] == ']') {
910                                 try {
911                                         host = "[" + IPv6Address.Parse (host).ToString () + "]";
912                                 } catch (Exception) {
913                                         throw new UriFormatException ("Invalid URI: The hostname could not be parsed");
914                                 }
915                         }
916
917                         if (host.Length == 2 && host [1] == ':') {
918                                 // windows filepath
919                                 path = host + path;
920                                 host = String.Empty;
921                         } else if (isUnixFilePath) {
922                                 uriString = "//" + uriString;
923                                 host = String.Empty;
924                         } else if (host.Length == 0) {
925                                 throw new UriFormatException ("Invalid URI: The hostname could not be parsed");
926                         } else if (scheme == UriSchemeFile) {
927                                 isUnc = true;
928                         }
929
930                         if ((scheme != Uri.UriSchemeMailto) &&
931                                         (scheme != Uri.UriSchemeNews) &&
932                                         (scheme != Uri.UriSchemeFile))
933                         path = Reduce (path);
934                 }
935
936                 private static string Reduce (string path)
937                 {
938                         path = path.Replace ('\\','/');
939                         string [] parts = path.Split ('/');
940                         ArrayList result = new ArrayList ();
941
942                         int end = parts.Length;
943                         for (int i = 0; i < end; i++) {
944                                 string current = parts [i];
945                                 if (current == "" || current == "." )
946                                         continue;
947
948                                 if (current == "..") {
949                                         if (result.Count == 0) {
950                                                 if (i == 1) // see bug 52599
951                                                         continue;
952                                                 throw new Exception ("Invalid path.");
953                                         }
954
955                                         result.RemoveAt (result.Count - 1);
956                                         continue;
957                                 }
958
959                                 result.Add (current);
960                         }
961
962                         if (result.Count == 0)
963                                 return "/";
964
965                         result.Insert (0, "");
966
967                         string res = String.Join ("/", (string []) result.ToArray (typeof (string)));
968                         if (path.EndsWith ("/"))
969                                 res += '/';
970                                 
971                         return res;
972                 }
973                                 
974                 private struct UriScheme 
975                 {
976                         public string scheme;
977                         public string delimiter;
978                         public int defaultPort;
979
980                         public UriScheme (string s, string d, int p) 
981                         {
982                                 scheme = s;
983                                 delimiter = d;
984                                 defaultPort = p;
985                         }
986                 };
987
988                 static UriScheme [] schemes = new UriScheme [] {
989                         new UriScheme (UriSchemeHttp, SchemeDelimiter, 80),
990                         new UriScheme (UriSchemeHttps, SchemeDelimiter, 443),
991                         new UriScheme (UriSchemeFtp, SchemeDelimiter, 21),
992                         new UriScheme (UriSchemeFile, SchemeDelimiter, -1),
993                         new UriScheme (UriSchemeMailto, ":", 25),
994                         new UriScheme (UriSchemeNews, ":", -1),
995                         new UriScheme (UriSchemeNntp, SchemeDelimiter, 119),
996                         new UriScheme (UriSchemeGopher, SchemeDelimiter, 70),
997                 };
998                                 
999                 internal static string GetSchemeDelimiter (string scheme) 
1000                 {
1001                         for (int i = 0; i < schemes.Length; i++) 
1002                                 if (schemes [i].scheme == scheme)
1003                                         return schemes [i].delimiter;
1004                         return Uri.SchemeDelimiter;
1005                 }
1006                 
1007                 internal static int GetDefaultPort (string scheme)
1008                 {
1009                         for (int i = 0; i < schemes.Length; i++) 
1010                                 if (schemes [i].scheme == scheme)
1011                                         return schemes [i].defaultPort;
1012                         return -1;                      
1013                 }
1014
1015                 private string GetOpaqueWiseSchemeDelimiter ()
1016                 {
1017                         if (isOpaquePart)
1018                                 return ":";
1019                         else
1020                                 return GetSchemeDelimiter (scheme);
1021                 }
1022
1023                 protected virtual bool IsBadFileSystemCharacter (char ch)
1024                 {
1025                         // It does not always overlap with InvalidPathChars.
1026                         int chInt = (int) ch;
1027                         if (chInt < 32 || (chInt < 64 && chInt > 57))
1028                                 return true;
1029                         switch (chInt) {
1030                         case 0:
1031                         case 34: // "
1032                         case 38: // &
1033                         case 42: // *
1034                         case 44: // ,
1035                         case 47: // /
1036                         case 92: // \
1037                         case 94: // ^
1038                         case 124: // |
1039                                 return true;
1040                         }
1041
1042                         return false;
1043                 }
1044
1045                 
1046                 protected static bool IsExcludedCharacter (char ch)
1047                 {
1048                         if (ch <= 32 || ch >= 127)
1049                                 return true;
1050                         
1051                         if (ch == '"' || ch == '#' || ch == '%' || ch == '<' ||
1052                             ch == '>' || ch == '[' || ch == '\\' || ch == ']' ||
1053                             ch == '^' || ch == '`' || ch == '{' || ch == '|' ||
1054                             ch == '}')
1055                                 return true;
1056                         return false;
1057                 }
1058
1059                 private static bool IsPredefinedScheme (string scheme)
1060                 {
1061                         switch (scheme) {
1062                         case "http":
1063                         case "https":
1064                         case "file":
1065                         case "ftp":
1066                         case "nntp":
1067                         case "gopher":
1068                         case "mailto":
1069                         case "news":
1070                                 return true;
1071                         default:
1072                                 return false;
1073                         }
1074                 }
1075
1076                 protected virtual bool IsReservedCharacter (char ch)
1077                 {
1078                         if (ch == '$' || ch == '&' || ch == '+' || ch == ',' ||
1079                             ch == '/' || ch == ':' || ch == ';' || ch == '=' ||
1080                             ch == '@')
1081                                 return true;
1082                         return false;
1083                 }
1084         }
1085 }