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;
21 // See RFC 2396 for more info on URI's.
23 // TODO: optimize by parsing host string only once
28 public class Uri : MarshalByRefObject, ISerializable
31 // o scheme excludes the scheme delimiter
32 // o port is -1 to indicate no port is defined
33 // o path is empty or starts with / when scheme delimiter == "://"
34 // o query is empty or starts with ? char
35 // o fragment is empty or starts with # char
36 // o all class variables are in escaped format when they are escapable,
37 // except cachedToString.
38 // o UNC is supported, as starts with "\\" for windows,
41 private bool isWindowsFilePath = false;
42 private bool isUnixFilePath = false;
43 private string scheme = String.Empty;
44 private string host = String.Empty;
45 private int port = -1;
46 private string path = String.Empty;
47 private string query = String.Empty;
48 private string fragment = String.Empty;
49 private string userinfo = String.Empty;
50 private bool is_root_path = false;
51 private bool is_wins_dir = true;
52 private bool isUnc = false;
53 private bool isOpaquePart = false;
55 private string [] segments;
57 private bool userEscaped = false;
58 private string cachedAbsoluteUri = null;
59 private string cachedToString = null;
60 private int cachedHashCode = 0;
62 private static readonly string hexUpperChars = "0123456789ABCDEF";
66 public static readonly string SchemeDelimiter = "://";
67 public static readonly string UriSchemeFile = "file";
68 public static readonly string UriSchemeFtp = "ftp";
69 public static readonly string UriSchemeGopher = "gopher";
70 public static readonly string UriSchemeHttp = "http";
71 public static readonly string UriSchemeHttps = "https";
72 public static readonly string UriSchemeMailto = "mailto";
73 public static readonly string UriSchemeNews = "news";
74 public static readonly string UriSchemeNntp = "nntp";
78 public Uri (string uriString) : this (uriString, false)
82 protected Uri (SerializationInfo serializationInfo,
83 StreamingContext streamingContext) :
84 this (serializationInfo.GetString ("AbsoluteUri"), true)
88 public Uri (string uriString, bool dontEscape)
90 userEscaped = dontEscape;
96 host = EscapeString (host, false, true, false);
97 path = EscapeString (path);
98 query = EscapeString (query);
99 fragment = EscapeString (fragment, false, false, true);
102 public Uri (Uri baseUri, string relativeUri)
103 : this (baseUri, relativeUri, false)
107 public Uri (Uri baseUri, string relativeUri, bool dontEscape)
110 throw new NullReferenceException ("baseUri");
112 // See RFC 2396 Par 5.2 and Appendix C
114 userEscaped = dontEscape;
116 this.scheme = baseUri.scheme;
117 this.host = baseUri.host;
118 this.port = baseUri.port;
119 this.userinfo = baseUri.userinfo;
120 this.isUnc = baseUri.isUnc;
121 this.isWindowsFilePath = baseUri.isWindowsFilePath;
122 this.isUnixFilePath = baseUri.isUnixFilePath;
123 this.isOpaquePart = baseUri.isOpaquePart;
125 if (relativeUri == null)
126 throw new NullReferenceException ("relativeUri");
128 if (relativeUri == String.Empty) {
129 this.path = baseUri.path;
130 this.query = baseUri.query;
131 this.fragment = baseUri.fragment;
135 int pos = relativeUri.IndexOf (':');
138 int pos2 = relativeUri.IndexOfAny (new char [] {'/', '\\'});
141 // equivalent to new Uri (relativeUri, dontEscape)
147 host = EscapeString (host, false, true, false);
148 path = EscapeString (path);
149 query = EscapeString (query);
150 fragment = EscapeString (fragment, false, false, true);
156 pos = relativeUri.IndexOf ('#');
158 fragment = relativeUri.Substring (pos);
160 fragment = EscapeString (fragment, false, false, true);
161 relativeUri = relativeUri.Substring (0, pos);
165 pos = relativeUri.IndexOf ('?');
167 query = relativeUri.Substring (pos);
169 query = EscapeString (query);
170 relativeUri = relativeUri.Substring (0, pos);
173 if (relativeUri.Length > 0 && relativeUri [0] == '/') {
176 path = EscapeString (path);
182 if (relativeUri.Length > 0 || query.Length > 0) {
183 pos = path.LastIndexOf ('/');
185 path = path.Substring (0, pos + 1);
188 if(relativeUri.Length == 0)
197 pos = path.IndexOf ("./", startIndex);
201 path = path.Remove (0, 2);
202 else if (path [pos - 1] != '.')
203 path = path.Remove (pos, 2);
205 startIndex = pos + 1;
209 if (path.Length > 1 &&
210 path [path.Length - 1] == '.' &&
211 path [path.Length - 2] == '/')
212 path = path.Remove (path.Length - 1, 1);
217 pos = path.IndexOf ("/../", startIndex);
224 int pos2 = path.LastIndexOf ('/', pos - 1);
226 startIndex = pos + 1;
228 if (path.Substring (pos2 + 1, pos - pos2 - 1) != "..")
229 path = path.Remove (pos2 + 1, pos - pos2 + 3);
231 startIndex = pos + 1;
236 if (path.Length > 3 && path.EndsWith ("/..")) {
237 pos = path.LastIndexOf ('/', path.Length - 4);
239 if (path.Substring (pos + 1, path.Length - pos - 4) != "..")
240 path = path.Remove (pos + 1, path.Length - pos - 1);
244 path = EscapeString (path);
249 public string AbsolutePath {
253 public string AbsoluteUri {
255 if (cachedAbsoluteUri == null)
256 cachedAbsoluteUri = GetLeftPart (UriPartial.Path) + query + fragment;
257 return cachedAbsoluteUri;
261 public string Authority {
263 return (GetDefaultPort (scheme) == port)
264 ? host : host + ":" + port;
268 public string Fragment {
269 get { return fragment; }
276 public UriHostNameType HostNameType {
278 UriHostNameType ret = CheckHostName (host);
279 if (ret != UriHostNameType.Unknown)
282 // looks it always returns Basic...
283 return UriHostNameType.Basic; //.Unknown;
287 public bool IsDefaultPort {
288 get { return GetDefaultPort (scheme) == port; }
292 get { return (scheme == UriSchemeFile); }
295 public bool IsLoopback {
297 if (host == String.Empty)
300 if (host == "loopback" || host == "localhost")
304 return IPAddress.IsLoopback (IPAddress.Parse (host));
305 } catch (FormatException) {}
308 return IPv6Address.IsLoopback (IPv6Address.Parse (host));
309 } catch (FormatException) {}
316 // rule: This should be true only if
317 // - uri string starts from "\\", or
318 // - uri string starts from "//" (Samba way)
319 get { return isUnc; }
322 public string LocalPath {
327 if (System.IO.Path.DirectorySeparatorChar == '\\')
328 return path.Replace ('/', '\\');
333 // support *nix and W32 styles
334 if (path.Length > 1 && path [1] == ':')
335 return Unescape (path.Replace ('/', '\\'));
337 if (System.IO.Path.DirectorySeparatorChar == '\\')
338 return "\\\\" + Unescape (host + path.Replace ('/', '\\'));
340 return (is_root_path? "/": "") + (is_wins_dir? "/": "") + Unescape (host + path);
344 public string PathAndQuery {
345 get { return path + query; }
352 public string Query {
353 get { return query; }
356 public string Scheme {
357 get { return scheme; }
360 public string [] Segments {
362 if (segments != null)
366 segments = new string [0];
370 string [] parts = path.Split ('/');
372 bool endSlash = path.EndsWith ("/");
373 if (parts.Length > 0 && endSlash) {
374 string [] newParts = new string [parts.Length - 1];
375 Array.Copy (parts, 0, newParts, 0, parts.Length - 1);
380 if (IsFile && path.Length > 1 && path [1] == ':') {
381 string [] newParts = new string [parts.Length + 1];
382 Array.Copy (parts, 1, newParts, 2, parts.Length - 1);
384 parts [0] = path.Substring (0, 2);
389 int end = parts.Length;
391 if (i != end - 1 || endSlash)
399 public bool UserEscaped {
400 get { return userEscaped; }
403 public string UserInfo {
404 get { return userinfo; }
410 public static UriHostNameType CheckHostName (string name)
412 if (name == null || name.Length == 0)
413 return UriHostNameType.Unknown;
415 if (IsIPv4Address (name))
416 return UriHostNameType.IPv4;
418 if (IsDomainAddress (name))
419 return UriHostNameType.Dns;
422 IPv6Address.Parse (name);
423 return UriHostNameType.IPv6;
424 } catch (FormatException) {}
426 return UriHostNameType.Unknown;
429 internal static bool IsIPv4Address (string name)
431 string [] captures = name.Split (new char [] {'.'});
432 if (captures.Length != 4)
434 for (int i = 0; i < 4; i++) {
436 int d = Int32.Parse (captures [i]);
437 if (d < 0 || d > 255)
439 } catch (Exception) {
446 internal static bool IsDomainAddress (string name)
448 int len = name.Length;
450 if (name [len - 1] == '.')
454 for (int i = 0; i < len; i++) {
457 if (!Char.IsLetterOrDigit (c))
459 } else if (c == '.') {
461 } else if (!Char.IsLetterOrDigit (c) && c != '-' && c != '_') {
471 [MonoTODO ("Find out what this should do")]
472 protected virtual void Canonicalize ()
476 public static bool CheckSchemeName (string schemeName)
478 if (schemeName == null || schemeName.Length == 0)
481 if (!Char.IsLetter (schemeName [0]))
484 int len = schemeName.Length;
485 for (int i = 1; i < len; i++) {
486 char c = schemeName [i];
487 if (!Char.IsLetterOrDigit (c) && c != '.' && c != '+' && c != '-')
494 [MonoTODO ("Find out what this should do")]
495 protected virtual void CheckSecurity ()
499 public override bool Equals (object comparant)
501 if (comparant == null)
504 Uri uri = comparant as Uri;
506 string s = comparant as String;
512 return ((this.scheme == uri.scheme) &&
513 (this.userinfo == uri.userinfo) &&
514 (this.host == uri.host) &&
515 (this.port == uri.port) &&
516 (this.path == uri.path) &&
517 (this.query == uri.query));
520 public override int GetHashCode ()
522 if (cachedHashCode == 0)
523 cachedHashCode = scheme.GetHashCode ()
524 + userinfo.GetHashCode ()
525 + host.GetHashCode ()
527 + path.GetHashCode ()
528 + query.GetHashCode ();
529 return cachedHashCode;
532 public string GetLeftPart (UriPartial part)
536 case UriPartial.Scheme :
537 return scheme + GetOpaqueWiseSchemeDelimiter ();
538 case UriPartial.Authority :
539 if (host == String.Empty ||
540 scheme == Uri.UriSchemeMailto ||
541 scheme == Uri.UriSchemeNews)
544 StringBuilder s = new StringBuilder ();
546 s.Append (GetOpaqueWiseSchemeDelimiter ());
547 if (path.Length > 1 && path [1] == ':' && (Uri.UriSchemeFile == scheme))
548 s.Append ('/'); // win32 file
549 if (userinfo.Length > 0)
550 s.Append (userinfo).Append ('@');
552 defaultPort = GetDefaultPort (scheme);
553 if ((port != -1) && (port != defaultPort))
554 s.Append (':').Append (port);
555 return s.ToString ();
556 case UriPartial.Path :
557 StringBuilder sb = new StringBuilder ();
559 sb.Append (GetOpaqueWiseSchemeDelimiter ());
560 if (path.Length > 1 && path [1] == ':' && (Uri.UriSchemeFile == scheme))
561 sb.Append ('/'); // win32 file
562 if (userinfo.Length > 0)
563 sb.Append (userinfo).Append ('@');
565 defaultPort = GetDefaultPort (scheme);
566 if ((port != -1) && (port != defaultPort))
567 sb.Append (':').Append (port);
569 return sb.ToString ();
574 public static int FromHex (char digit)
576 if ('0' <= digit && digit <= '9') {
577 return (int) (digit - '0');
580 if ('a' <= digit && digit <= 'f')
581 return (int) (digit - 'a' + 10);
583 if ('A' <= digit && digit <= 'F')
584 return (int) (digit - 'A' + 10);
586 throw new ArgumentException ("digit");
589 public static string HexEscape (char character)
591 if (character > 255) {
592 throw new ArgumentOutOfRangeException ("character");
595 return "%" + hexUpperChars [((character & 0xf0) >> 4)]
596 + hexUpperChars [((character & 0x0f))];
599 public static char HexUnescape (string pattern, ref int index)
602 throw new ArgumentException ("pattern");
604 if (index < 0 || index >= pattern.Length)
605 throw new ArgumentOutOfRangeException ("index");
607 if (((index + 3) > pattern.Length) ||
608 (pattern [index] != '%') ||
609 !IsHexDigit (pattern [index + 1]) ||
610 !IsHexDigit (pattern [index + 2]))
612 return pattern[index++];
616 return (char) ((FromHex (pattern [index++]) << 4) + FromHex (pattern [index++]));
619 public static bool IsHexDigit (char digit)
621 return (('0' <= digit && digit <= '9') ||
622 ('a' <= digit && digit <= 'f') ||
623 ('A' <= digit && digit <= 'F'));
626 public static bool IsHexEncoding (string pattern, int index)
628 if ((index + 3) > pattern.Length)
631 return ((pattern [index++] == '%') &&
632 IsHexDigit (pattern [index++]) &&
633 IsHexDigit (pattern [index]));
636 public string MakeRelative (Uri toUri)
638 if ((this.Scheme != toUri.Scheme) ||
639 (this.Authority != toUri.Authority))
640 return toUri.ToString ();
642 if (this.path == toUri.path)
645 string [] segments = this.Segments;
646 string [] segments2 = toUri.Segments;
649 int max = Math.Min (segments.Length, segments2.Length);
651 if (segments [k] != segments2 [k])
654 string result = String.Empty;
655 for (int i = k + 1; i < segments.Length; i++)
657 for (int i = k; i < segments2.Length; i++)
658 result += segments2 [i];
663 public override string ToString ()
665 if (cachedToString != null)
666 return cachedToString;
667 cachedToString = AbsoluteUri;
669 return cachedToString;
672 void ISerializable.GetObjectData (SerializationInfo info,
673 StreamingContext context)
675 info.AddValue ("AbsoluteUri", this.AbsoluteUri);
681 protected virtual void Escape ()
683 path = EscapeString (path);
686 protected static string EscapeString (string str)
688 return EscapeString (str, false, true, true);
691 internal static string EscapeString (string str, bool escapeReserved, bool escapeHex, bool escapeBrackets)
696 StringBuilder s = new StringBuilder ();
697 int len = str.Length;
698 for (int i = 0; i < len; i++) {
700 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
701 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
702 // control = <US-ASCII coded characters 00-1F and 7F hexadecimal>
703 // space = <US-ASCII coded character 20 hexadecimal>
704 // delims = "<" | ">" | "#" | "%" | <">
705 // unwise = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
707 if ((c <= 0x20) || (c >= 0x7f) ||
708 ("<>%\"{}|\\^`".IndexOf (c) != -1) ||
709 (escapeHex && (c == '#')) ||
710 (escapeBrackets && (c == '[' || c == ']')) ||
711 (escapeReserved && (";/?:@&=+$,".IndexOf (c) != -1))) {
712 // FIXME: EscapeString should allow wide characters (> 255). HexEscape rejects it.
713 s.Append (HexEscape (c));
720 return s.ToString ();
723 [MonoTODO ("Find out what this should do!")]
724 protected virtual void Parse ()
728 protected virtual string Unescape (string str)
732 StringBuilder s = new StringBuilder ();
733 int len = str.Length;
734 for (int i = 0; i < len; i++) {
737 s.Append (HexUnescape (str, ref i));
742 return s.ToString ();
748 // this parse method is as relaxed as possible about the format
749 // it will hardly ever throw a UriFormatException
750 private void Parse (string uriString)
755 // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
759 if (uriString == null)
760 throw new ArgumentNullException ("uriString");
762 int len = uriString.Length;
764 throw new UriFormatException ();
769 for (; pos < len; pos++) {
771 if ((c == ':') || (c == '/') || (c == '\\') || (c == '?') || (c == '#'))
776 throw new UriFormatException ("The format of the URI could not be determined.");
782 if ((uriString.Length >= 2) &&
783 ((uriString [0] == '/') && (uriString [1] == '/')) ||
784 ((uriString [0] == '\\') || (uriString [1] == '\\'))) {
792 // a windows filepath
793 isWindowsFilePath = true;
794 scheme = Uri.UriSchemeFile;
795 path = uriString.Replace ('\\', '/');
799 scheme = uriString.Substring (0, pos).ToLower ();
800 uriString = uriString.Remove (0, pos + 1);
801 } else if ((c == '/') && (pos == 0)) {
802 scheme = Uri.UriSchemeFile;
803 if (uriString.Length > 1 && uriString [1] != '/')
804 // unix bare filepath
805 isUnixFilePath = true;
807 // unix UNC (kind of)
810 scheme = Uri.UriSchemeFile;
811 if (uriString.StartsWith ("\\\\")) {
813 isWindowsFilePath = true;
815 if (uriString.Length > 8 && uriString [8] != '/')
820 if ((uriString.Length >= 2) &&
821 ((uriString [0] == '/') && (uriString [1] == '/')) ||
822 ((uriString [0] == '\\') || (uriString [1] == '\\'))) {
823 if (scheme == Uri.UriSchemeFile)
824 uriString = uriString.TrimStart (new char [] {'/', '\\'});
826 uriString = uriString.Remove (0, 2);
827 } else if (!IsPredefinedScheme (scheme)) {
834 pos = uriString.IndexOf ('#');
835 if (!IsUnc && pos != -1) {
836 fragment = uriString.Substring (pos);
837 uriString = uriString.Substring (0, pos);
841 pos = uriString.IndexOf ('?');
843 query = uriString.Substring (pos);
844 uriString = uriString.Substring (0, pos);
848 pos = uriString.IndexOfAny (new char[] {'/', '\\'});
850 if ((scheme != Uri.UriSchemeMailto) &&
851 (scheme != Uri.UriSchemeNews) &&
852 (scheme != Uri.UriSchemeFile))
855 path = uriString.Substring (pos).Replace ('\\', '/');
856 uriString = uriString.Substring (0, pos);
860 pos = uriString.IndexOf ("@");
862 userinfo = uriString.Substring (0, pos);
863 uriString = uriString.Remove (0, pos + 1);
868 pos = uriString.LastIndexOf (":");
869 if (pos != -1 && pos != (uriString.Length - 1)) {
870 string portStr = uriString.Remove (0, pos + 1);
871 if (portStr.Length > 1 && portStr [portStr.Length - 1] != ']') {
873 port = (int) UInt32.Parse (portStr);
874 uriString = uriString.Substring (0, pos);
875 } catch (Exception) {
876 throw new UriFormatException ("Invalid URI: invalid port number");
881 port = GetDefaultPort (scheme);
886 if (host.Length > 1 && host [0] == '[' && host [host.Length - 1] == ']') {
888 host = "[" + IPv6Address.Parse (host).ToString () + "]";
889 } catch (Exception) {
890 throw new UriFormatException ("Invalid URI: The hostname could not be parsed");
894 if (host.Length == 2 && host [1] == ':') {
898 } else if (isUnixFilePath) {
899 uriString = "//" + uriString;
901 } else if (host.Length == 0) {
902 throw new UriFormatException ("Invalid URI: The hostname could not be parsed");
903 } else if (scheme == UriSchemeFile) {
909 private struct UriScheme
911 public string scheme;
912 public string delimiter;
913 public int defaultPort;
915 public UriScheme (string s, string d, int p)
923 static UriScheme [] schemes = new UriScheme [] {
924 new UriScheme (UriSchemeHttp, SchemeDelimiter, 80),
925 new UriScheme (UriSchemeHttps, SchemeDelimiter, 443),
926 new UriScheme (UriSchemeFtp, SchemeDelimiter, 21),
927 new UriScheme (UriSchemeFile, SchemeDelimiter, -1),
928 new UriScheme (UriSchemeMailto, ":", 25),
929 new UriScheme (UriSchemeNews, ":", -1),
930 new UriScheme (UriSchemeNntp, SchemeDelimiter, 119),
931 new UriScheme (UriSchemeGopher, SchemeDelimiter, 70),
934 internal static string GetSchemeDelimiter (string scheme)
936 for (int i = 0; i < schemes.Length; i++)
937 if (schemes [i].scheme == scheme)
938 return schemes [i].delimiter;
939 return Uri.SchemeDelimiter;
942 internal static int GetDefaultPort (string scheme)
944 for (int i = 0; i < schemes.Length; i++)
945 if (schemes [i].scheme == scheme)
946 return schemes [i].defaultPort;
950 private string GetOpaqueWiseSchemeDelimiter ()
955 return GetSchemeDelimiter (scheme);
958 protected virtual bool IsBadFileSystemCharacter (char ch)
960 foreach (char c in System.IO.Path.InvalidPathChars)
967 protected static bool IsExcludedCharacter (char ch)
969 if (ch <= 32 || ch >= 127)
972 if (ch == '"' || ch == '#' || ch == '%' || ch == '<' ||
973 ch == '>' || ch == '[' || ch == '\\' || ch == ']' ||
974 ch == '^' || ch == '`' || ch == '{' || ch == '|' ||
980 private static bool IsPredefinedScheme (string scheme)
997 protected virtual bool IsReservedCharacter (char ch)
999 if (ch == '$' || ch == '&' || ch == '+' || ch == ',' ||
1000 ch == '/' || ch == ':' || ch == ';' || ch == '=' ||