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)
11 // (C) 2001 Garrett Rooney
12 // (C) 2003 Ian MacLean
13 // (C) 2003 Ben Maurer
14 // (C) 2003 Novell inc.
18 using System.Runtime.Serialization;
20 using System.Collections;
22 // See RFC 2396 for more info on URI's.
24 // TODO: optimize by parsing host string only once
29 public class Uri : MarshalByRefObject, ISerializable
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,
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;
56 private string [] segments;
58 private bool userEscaped = false;
59 private string cachedAbsoluteUri = null;
60 private string cachedToString = null;
61 private int cachedHashCode = 0;
63 private static readonly string hexUpperChars = "0123456789ABCDEF";
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";
79 public Uri (string uriString) : this (uriString, false)
83 protected Uri (SerializationInfo serializationInfo,
84 StreamingContext streamingContext) :
85 this (serializationInfo.GetString ("AbsoluteUri"), true)
89 public Uri (string uriString, bool dontEscape)
91 userEscaped = dontEscape;
97 host = EscapeString (host, false, true, false);
98 path = EscapeString (path);
101 public Uri (Uri baseUri, string relativeUri)
102 : this (baseUri, relativeUri, false)
106 public Uri (Uri baseUri, string relativeUri, bool dontEscape)
109 throw new NullReferenceException ("baseUri");
111 // See RFC 2396 Par 5.2 and Appendix C
113 userEscaped = dontEscape;
115 if (relativeUri == null)
116 throw new NullReferenceException ("relativeUri");
118 int pos = relativeUri.IndexOf (':');
121 int pos2 = relativeUri.IndexOfAny (new char [] {'/', '\\'});
124 // equivalent to new Uri (relativeUri, dontEscape)
130 host = EscapeString (host, false, true, false);
131 path = EscapeString (path);
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;
145 if (relativeUri == String.Empty) {
146 this.path = baseUri.path;
147 this.query = baseUri.query;
148 this.fragment = baseUri.fragment;
153 pos = relativeUri.IndexOf ('#');
155 fragment = relativeUri.Substring (pos);
156 relativeUri = relativeUri.Substring (0, pos);
160 pos = relativeUri.IndexOf ('?');
162 query = relativeUri.Substring (pos);
164 query = EscapeString (query);
165 relativeUri = relativeUri.Substring (0, pos);
168 if (relativeUri.Length > 0 && relativeUri [0] == '/') {
171 path = EscapeString (path);
177 if (relativeUri.Length > 0 || query.Length > 0) {
178 pos = path.LastIndexOf ('/');
180 path = path.Substring (0, pos + 1);
183 if(relativeUri.Length == 0)
192 pos = path.IndexOf ("./", startIndex);
196 path = path.Remove (0, 2);
197 else if (path [pos - 1] != '.')
198 path = path.Remove (pos, 2);
200 startIndex = pos + 1;
204 if (path.Length > 1 &&
205 path [path.Length - 1] == '.' &&
206 path [path.Length - 2] == '/')
207 path = path.Remove (path.Length - 1, 1);
212 pos = path.IndexOf ("/../", startIndex);
219 int pos2 = path.LastIndexOf ('/', pos - 1);
221 startIndex = pos + 1;
223 if (path.Substring (pos2 + 1, pos - pos2 - 1) != "..")
224 path = path.Remove (pos2 + 1, pos - pos2 + 3);
226 startIndex = pos + 1;
231 if (path.Length > 3 && path.EndsWith ("/..")) {
232 pos = path.LastIndexOf ('/', path.Length - 4);
234 if (path.Substring (pos + 1, path.Length - pos - 4) != "..")
235 path = path.Remove (pos + 1, path.Length - pos - 1);
239 path = EscapeString (path);
244 public string AbsolutePath {
248 public string AbsoluteUri {
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;
255 return cachedAbsoluteUri;
259 public string Authority {
261 return (GetDefaultPort (scheme) == port)
262 ? host : host + ":" + port;
266 public string Fragment {
267 get { return fragment; }
274 public UriHostNameType HostNameType {
276 UriHostNameType ret = CheckHostName (host);
277 if (ret != UriHostNameType.Unknown)
280 // looks it always returns Basic...
281 return UriHostNameType.Basic; //.Unknown;
285 public bool IsDefaultPort {
286 get { return GetDefaultPort (scheme) == port; }
290 get { return (scheme == UriSchemeFile); }
293 public bool IsLoopback {
295 if (host == String.Empty)
298 if (host == "loopback" || host == "localhost")
302 if (IPAddress.Loopback.Equals (IPAddress.Parse (host)))
304 } catch (FormatException) {}
307 return IPv6Address.IsLoopback (IPv6Address.Parse (host));
308 } catch (FormatException) {}
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; }
321 public string LocalPath {
326 bool windows = (path.Length > 3 && path [1] == ':' &&
327 (path [2] == '\\' || path [2] == '/'));
330 string p = Unescape (path);
331 if (System.IO.Path.DirectorySeparatorChar == '\\' || windows)
332 return p.Replace ('/', '\\');
337 // support *nix and W32 styles
338 if (path.Length > 1 && path [1] == ':')
339 return Unescape (path.Replace ('/', '\\'));
341 if (System.IO.Path.DirectorySeparatorChar == '\\')
342 return "\\\\" + Unescape (host + path.Replace ('/', '\\'));
344 return (is_root_path? "/": "") + (is_wins_dir? "/": "") + Unescape (host + path);
348 public string PathAndQuery {
349 get { return path + query; }
356 public string Query {
357 get { return query; }
360 public string Scheme {
361 get { return scheme; }
364 public string [] Segments {
366 if (segments != null)
370 segments = new string [0];
374 string [] parts = path.Split ('/');
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);
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);
388 parts [0] = path.Substring (0, 2);
393 int end = parts.Length;
395 if (i != end - 1 || endSlash)
403 public bool UserEscaped {
404 get { return userEscaped; }
407 public string UserInfo {
408 get { return userinfo; }
414 public static UriHostNameType CheckHostName (string name)
416 if (name == null || name.Length == 0)
417 return UriHostNameType.Unknown;
419 if (IsIPv4Address (name))
420 return UriHostNameType.IPv4;
422 if (IsDomainAddress (name))
423 return UriHostNameType.Dns;
426 IPv6Address.Parse (name);
427 return UriHostNameType.IPv6;
428 } catch (FormatException) {}
430 return UriHostNameType.Unknown;
433 internal static bool IsIPv4Address (string name)
435 string [] captures = name.Split (new char [] {'.'});
436 if (captures.Length != 4)
438 for (int i = 0; i < 4; i++) {
440 int d = Int32.Parse (captures [i]);
441 if (d < 0 || d > 255)
443 } catch (Exception) {
450 internal static bool IsDomainAddress (string name)
452 int len = name.Length;
454 if (name [len - 1] == '.')
458 for (int i = 0; i < len; i++) {
461 if (!Char.IsLetterOrDigit (c))
463 } else if (c == '.') {
465 } else if (!Char.IsLetterOrDigit (c) && c != '-' && c != '_') {
475 [MonoTODO ("Find out what this should do")]
476 protected virtual void Canonicalize ()
480 public static bool CheckSchemeName (string schemeName)
482 if (schemeName == null || schemeName.Length == 0)
485 if (!Char.IsLetter (schemeName [0]))
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 != '-')
498 [MonoTODO ("Find out what this should do")]
499 protected virtual void CheckSecurity ()
503 public override bool Equals (object comparant)
505 if (comparant == null)
508 Uri uri = comparant as Uri;
510 string s = comparant as String;
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 ()));
524 public override int GetHashCode ()
526 if (cachedHashCode == 0)
527 cachedHashCode = scheme.GetHashCode ()
528 + userinfo.GetHashCode ()
529 + host.GetHashCode ()
531 + path.GetHashCode ()
532 + query.GetHashCode ();
533 return cachedHashCode;
536 public string GetLeftPart (UriPartial 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)
548 StringBuilder s = new StringBuilder ();
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 ('@');
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 ();
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 ('@');
569 defaultPort = GetDefaultPort (scheme);
570 if ((port != -1) && (port != defaultPort))
571 sb.Append (':').Append (port);
573 return sb.ToString ();
578 public static int FromHex (char digit)
580 if ('0' <= digit && digit <= '9') {
581 return (int) (digit - '0');
584 if ('a' <= digit && digit <= 'f')
585 return (int) (digit - 'a' + 10);
587 if ('A' <= digit && digit <= 'F')
588 return (int) (digit - 'A' + 10);
590 throw new ArgumentException ("digit");
593 public static string HexEscape (char character)
595 if (character > 255) {
596 throw new ArgumentOutOfRangeException ("character");
599 return "%" + hexUpperChars [((character & 0xf0) >> 4)]
600 + hexUpperChars [((character & 0x0f))];
603 public static char HexUnescape (string pattern, ref int index)
606 throw new ArgumentException ("pattern");
608 if (index < 0 || index >= pattern.Length)
609 throw new ArgumentOutOfRangeException ("index");
611 if (((index + 3) > pattern.Length) ||
612 (pattern [index] != '%') ||
613 !IsHexDigit (pattern [index + 1]) ||
614 !IsHexDigit (pattern [index + 2]))
616 return pattern[index++];
620 return (char) ((FromHex (pattern [index++]) << 4) + FromHex (pattern [index++]));
623 public static bool IsHexDigit (char digit)
625 return (('0' <= digit && digit <= '9') ||
626 ('a' <= digit && digit <= 'f') ||
627 ('A' <= digit && digit <= 'F'));
630 public static bool IsHexEncoding (string pattern, int index)
632 if ((index + 3) > pattern.Length)
635 return ((pattern [index++] == '%') &&
636 IsHexDigit (pattern [index++]) &&
637 IsHexDigit (pattern [index]));
640 public string MakeRelative (Uri toUri)
642 if ((this.Scheme != toUri.Scheme) ||
643 (this.Authority != toUri.Authority))
644 return toUri.ToString ();
646 if (this.path == toUri.path)
649 string [] segments = this.Segments;
650 string [] segments2 = toUri.Segments;
653 int max = Math.Min (segments.Length, segments2.Length);
655 if (segments [k] != segments2 [k])
658 string result = String.Empty;
659 for (int i = k + 1; i < segments.Length; i++)
661 for (int i = k; i < segments2.Length; i++)
662 result += segments2 [i];
667 public override string ToString ()
669 if (cachedToString != null)
670 return cachedToString;
671 cachedToString = Unescape (AbsoluteUri);
673 return cachedToString;
676 void ISerializable.GetObjectData (SerializationInfo info,
677 StreamingContext context)
679 info.AddValue ("AbsoluteUri", this.AbsoluteUri);
685 protected virtual void Escape ()
687 path = EscapeString (path);
690 protected static string EscapeString (string str)
692 return EscapeString (str, false, true, true);
695 internal static string EscapeString (string str, bool escapeReserved, bool escapeHex, bool escapeBrackets)
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 = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
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))
718 // if ,yes , copy it as is
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));
739 return s.ToString ();
742 [MonoTODO ("Find out what this should do!")]
743 protected virtual void Parse ()
747 protected virtual string Unescape (string str)
751 StringBuilder s = new StringBuilder ();
752 int len = str.Length;
753 for (int i = 0; i < len; i++) {
756 s.Append (HexUnescape (str, ref i));
761 return s.ToString ();
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)
774 // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
778 if (uriString == null)
779 throw new ArgumentNullException ("uriString");
781 int len = uriString.Length;
783 throw new UriFormatException ();
788 for (; pos < len; pos++) {
790 if ((c == ':') || (c == '/') || (c == '\\') || (c == '?') || (c == '#'))
795 throw new UriFormatException ("The format of the URI could not be determined.");
801 if ((uriString.Length >= 2) &&
802 ((uriString [0] == '/') && (uriString [1] == '/')) ||
803 ((uriString [0] == '\\') || (uriString [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 ('\\', '/');
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;
828 // unix UNC (kind of)
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 ("\\\\")) {
836 isWindowsFilePath = true;
838 if (uriString.Length > 8 && uriString [8] != '/')
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 [] {'/', '\\'});
849 uriString = uriString.Remove (0, 2);
850 } else if (!IsPredefinedScheme (scheme)) {
857 pos = uriString.IndexOf ('#');
858 if (!IsUnc && pos != -1) {
859 fragment = uriString.Substring (pos);
860 uriString = uriString.Substring (0, pos);
864 pos = uriString.IndexOf ('?');
866 query = uriString.Substring (pos);
867 uriString = uriString.Substring (0, pos);
871 pos = uriString.IndexOfAny (new char[] {'/', '\\'});
873 if ((scheme != Uri.UriSchemeMailto) &&
874 (scheme != Uri.UriSchemeNews) &&
875 (scheme != Uri.UriSchemeFile))
878 path = uriString.Substring (pos).Replace ('\\', '/');
879 uriString = uriString.Substring (0, pos);
883 pos = uriString.IndexOf ("@");
885 userinfo = uriString.Substring (0, pos);
886 uriString = uriString.Remove (0, pos + 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] != ']') {
896 port = (int) UInt32.Parse (portStr);
897 uriString = uriString.Substring (0, pos);
898 } catch (Exception) {
899 throw new UriFormatException ("Invalid URI: invalid port number");
904 port = GetDefaultPort (scheme);
909 if (host.Length > 1 && host [0] == '[' && host [host.Length - 1] == ']') {
911 host = "[" + IPv6Address.Parse (host).ToString () + "]";
912 } catch (Exception) {
913 throw new UriFormatException ("Invalid URI: The hostname could not be parsed");
917 if (host.Length == 2 && host [1] == ':') {
921 } else if (isUnixFilePath) {
922 uriString = "//" + uriString;
924 } else if (host.Length == 0) {
925 throw new UriFormatException ("Invalid URI: The hostname could not be parsed");
926 } else if (scheme == UriSchemeFile) {
930 if ((scheme != Uri.UriSchemeMailto) &&
931 (scheme != Uri.UriSchemeNews) &&
932 (scheme != Uri.UriSchemeFile))
933 path = Reduce (path);
936 private static string Reduce (string path)
938 path = path.Replace ('\\','/');
939 string [] parts = path.Split ('/');
940 ArrayList result = new ArrayList ();
942 int end = parts.Length;
943 for (int i = 0; i < end; i++) {
944 string current = parts [i];
945 if (current == "" || current == "." )
948 if (current == "..") {
949 if (result.Count == 0) {
950 if (i == 1) // see bug 52599
952 throw new Exception ("Invalid path.");
955 result.RemoveAt (result.Count - 1);
959 result.Add (current);
962 if (result.Count == 0)
965 result.Insert (0, "");
967 string res = String.Join ("/", (string []) result.ToArray (typeof (string)));
968 if (path.EndsWith ("/"))
974 private struct UriScheme
976 public string scheme;
977 public string delimiter;
978 public int defaultPort;
980 public UriScheme (string s, string d, int p)
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),
999 internal static string GetSchemeDelimiter (string scheme)
1001 for (int i = 0; i < schemes.Length; i++)
1002 if (schemes [i].scheme == scheme)
1003 return schemes [i].delimiter;
1004 return Uri.SchemeDelimiter;
1007 internal static int GetDefaultPort (string scheme)
1009 for (int i = 0; i < schemes.Length; i++)
1010 if (schemes [i].scheme == scheme)
1011 return schemes [i].defaultPort;
1015 private string GetOpaqueWiseSchemeDelimiter ()
1020 return GetSchemeDelimiter (scheme);
1023 protected virtual bool IsBadFileSystemCharacter (char ch)
1025 // It does not always overlap with InvalidPathChars.
1026 int chInt = (int) ch;
1027 if (chInt < 32 || (chInt < 64 && chInt > 57))
1046 protected static bool IsExcludedCharacter (char ch)
1048 if (ch <= 32 || ch >= 127)
1051 if (ch == '"' || ch == '#' || ch == '%' || ch == '<' ||
1052 ch == '>' || ch == '[' || ch == '\\' || ch == ']' ||
1053 ch == '^' || ch == '`' || ch == '{' || ch == '|' ||
1059 private static bool IsPredefinedScheme (string scheme)
1076 protected virtual bool IsReservedCharacter (char ch)
1078 if (ch == '$' || ch == '&' || ch == '+' || ch == ',' ||
1079 ch == '/' || ch == ':' || ch == ';' || ch == '=' ||