**** Merged from MCS ****
[mono-project.git] / mcs / class / System / System / Uri.cs
blob6a75cbd6e841256486365a24a1a0963cf02d13a4
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)
11 // (C) 2001 Garrett Rooney
12 // (C) 2003 Ian MacLean
13 // (C) 2003 Ben Maurer
14 // (C) 2003 Novell inc.
18 // Permission is hereby granted, free of charge, to any person obtaining
19 // a copy of this software and associated documentation files (the
20 // "Software"), to deal in the Software without restriction, including
21 // without limitation the rights to use, copy, modify, merge, publish,
22 // distribute, sublicense, and/or sell copies of the Software, and to
23 // permit persons to whom the Software is furnished to do so, subject to
24 // the following conditions:
25 //
26 // The above copyright notice and this permission notice shall be
27 // included in all copies or substantial portions of the Software.
28 //
29 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
31 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
32 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
33 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
34 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
35 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 using System.IO;
38 using System.Net;
39 using System.Runtime.Serialization;
40 using System.Text;
41 using System.Collections;
42 using System.Globalization;
44 // See RFC 2396 for more info on URI's.
46 // TODO: optimize by parsing host string only once
48 namespace System
50 [Serializable]
51 public class Uri : MarshalByRefObject, ISerializable
53 // NOTES:
54 // o scheme excludes the scheme delimiter
55 // o port is -1 to indicate no port is defined
56 // o path is empty or starts with / when scheme delimiter == "://"
57 // o query is empty or starts with ? char, escaped.
58 // o fragment is empty or starts with # char, unescaped.
59 // o all class variables are in escaped format when they are escapable,
60 // except cachedToString.
61 // o UNC is supported, as starts with "\\" for windows,
62 // or "//" with unix.
64 private bool isUnixFilePath = false;
65 private string source;
66 private string scheme = String.Empty;
67 private string host = String.Empty;
68 private int port = -1;
69 private string path = String.Empty;
70 private string query = String.Empty;
71 private string fragment = String.Empty;
72 private string userinfo = String.Empty;
73 private bool isUnc = false;
74 private bool isOpaquePart = false;
76 private string [] segments;
78 private bool userEscaped = false;
79 private string cachedAbsoluteUri = null;
80 private string cachedToString = null;
81 private string cachedLocalPath = null;
82 private int cachedHashCode = 0;
84 private static readonly string hexUpperChars = "0123456789ABCDEF";
86 // Fields
88 public static readonly string SchemeDelimiter = "://";
89 public static readonly string UriSchemeFile = "file";
90 public static readonly string UriSchemeFtp = "ftp";
91 public static readonly string UriSchemeGopher = "gopher";
92 public static readonly string UriSchemeHttp = "http";
93 public static readonly string UriSchemeHttps = "https";
94 public static readonly string UriSchemeMailto = "mailto";
95 public static readonly string UriSchemeNews = "news";
96 public static readonly string UriSchemeNntp = "nntp";
98 // Constructors
100 public Uri (string uriString) : this (uriString, false)
104 protected Uri (SerializationInfo serializationInfo,
105 StreamingContext streamingContext) :
106 this (serializationInfo.GetString ("AbsoluteUri"), true)
110 public Uri (string uriString, bool dontEscape)
112 userEscaped = dontEscape;
113 source = uriString;
114 Parse ();
117 public Uri (Uri baseUri, string relativeUri)
118 : this (baseUri, relativeUri, false)
122 public Uri (Uri baseUri, string relativeUri, bool dontEscape)
124 if (baseUri == null)
125 throw new NullReferenceException ("baseUri");
127 // See RFC 2396 Par 5.2 and Appendix C
129 userEscaped = dontEscape;
131 if (relativeUri == null)
132 throw new NullReferenceException ("relativeUri");
134 // Check Windows UNC (for // it is scheme/host separator)
135 if (relativeUri.StartsWith ("\\\\")) {
136 source = relativeUri;
137 Parse ();
138 return;
141 int pos = relativeUri.IndexOf (':');
142 if (pos != -1) {
144 int pos2 = relativeUri.IndexOfAny (new char [] {'/', '\\', '?'});
146 // pos2 < 0 ... e.g. mailto
147 // pos2 > pos ... to block ':' in query part
148 if (pos2 > pos || pos2 < 0) {
149 // equivalent to new Uri (relativeUri, dontEscape)
150 source = relativeUri;
151 Parse ();
153 return;
157 this.scheme = baseUri.scheme;
158 this.host = baseUri.host;
159 this.port = baseUri.port;
160 this.userinfo = baseUri.userinfo;
161 this.isUnc = baseUri.isUnc;
162 this.isUnixFilePath = baseUri.isUnixFilePath;
163 this.isOpaquePart = baseUri.isOpaquePart;
165 if (relativeUri == String.Empty) {
166 this.path = baseUri.path;
167 this.query = baseUri.query;
168 this.fragment = baseUri.fragment;
169 return;
172 // 8 fragment
173 // Note that in relative constructor, file URI cannot handle '#' as a filename character, but just regarded as a fragment identifier.
174 pos = relativeUri.IndexOf ('#');
175 if (pos != -1) {
176 fragment = relativeUri.Substring (pos);
177 // fragment is not escaped.
178 relativeUri = relativeUri.Substring (0, pos);
181 // 6 query
182 pos = relativeUri.IndexOf ('?');
183 if (pos != -1) {
184 query = relativeUri.Substring (pos);
185 if (!userEscaped)
186 query = EscapeString (query);
187 relativeUri = relativeUri.Substring (0, pos);
190 if (relativeUri.Length > 0 && relativeUri [0] == '/') {
191 if (relativeUri.Length > 1 && relativeUri [1] == '/') {
192 source = scheme + ':' + relativeUri;
193 Parse ();
194 return;
195 } else {
196 path = relativeUri;
197 if (!userEscaped)
198 path = EscapeString (path);
199 return;
203 // par 5.2 step 6 a)
204 path = baseUri.path;
205 if (relativeUri.Length > 0 || query.Length > 0) {
206 pos = path.LastIndexOf ('/');
207 if (pos >= 0)
208 path = path.Substring (0, pos + 1);
211 if(relativeUri.Length == 0)
212 return;
214 // 6 b)
215 path += relativeUri;
217 // 6 c)
218 int startIndex = 0;
219 while (true) {
220 pos = path.IndexOf ("./", startIndex);
221 if (pos == -1)
222 break;
223 if (pos == 0)
224 path = path.Remove (0, 2);
225 else if (path [pos - 1] != '.')
226 path = path.Remove (pos, 2);
227 else
228 startIndex = pos + 1;
231 // 6 d)
232 if (path.Length > 1 &&
233 path [path.Length - 1] == '.' &&
234 path [path.Length - 2] == '/')
235 path = path.Remove (path.Length - 1, 1);
237 // 6 e)
238 startIndex = 0;
239 while (true) {
240 pos = path.IndexOf ("/../", startIndex);
241 if (pos == -1)
242 break;
243 if (pos == 0) {
244 startIndex = 3;
245 continue;
247 int pos2 = path.LastIndexOf ('/', pos - 1);
248 if (pos2 == -1) {
249 startIndex = pos + 1;
250 } else {
251 if (path.Substring (pos2 + 1, pos - pos2 - 1) != "..")
252 path = path.Remove (pos2 + 1, pos - pos2 + 3);
253 else
254 startIndex = pos + 1;
258 // 6 f)
259 if (path.Length > 3 && path.EndsWith ("/..")) {
260 pos = path.LastIndexOf ('/', path.Length - 4);
261 if (pos != -1)
262 if (path.Substring (pos + 1, path.Length - pos - 4) != "..")
263 path = path.Remove (pos + 1, path.Length - pos - 1);
266 if (!userEscaped)
267 path = EscapeString (path);
270 // Properties
272 public string AbsolutePath {
273 get { return path; }
276 public string AbsoluteUri {
277 get {
278 if (cachedAbsoluteUri == null) {
279 cachedAbsoluteUri = GetLeftPart (UriPartial.Path) + query + fragment;
281 return cachedAbsoluteUri;
285 public string Authority {
286 get {
287 return (GetDefaultPort (scheme) == port)
288 ? host : host + ":" + port;
292 public string Fragment {
293 get { return fragment; }
296 public string Host {
297 get { return host; }
300 public UriHostNameType HostNameType {
301 get {
302 UriHostNameType ret = CheckHostName (host);
303 if (ret != UriHostNameType.Unknown)
304 return ret;
306 // looks it always returns Basic...
307 return UriHostNameType.Basic; //.Unknown;
311 public bool IsDefaultPort {
312 get { return GetDefaultPort (scheme) == port; }
315 public bool IsFile {
316 get { return (scheme == UriSchemeFile); }
319 public bool IsLoopback {
320 get {
321 if (host == String.Empty)
322 return false;
324 if (host == "loopback" || host == "localhost")
325 return true;
327 try {
328 if (IPAddress.Loopback.Equals (IPAddress.Parse (host)))
329 return true;
330 } catch (FormatException) {}
332 try {
333 return IPv6Address.IsLoopback (IPv6Address.Parse (host));
334 } catch (FormatException) {}
336 return false;
340 public bool IsUnc {
341 // rule: This should be true only if
342 // - uri string starts from "\\", or
343 // - uri string starts from "//" (Samba way)
344 get { return isUnc; }
347 public string LocalPath {
348 get {
349 if (cachedLocalPath != null)
350 return cachedLocalPath;
351 if (!IsFile)
352 return AbsolutePath;
354 bool windows = (path.Length > 3 && path [1] == ':' &&
355 (path [2] == '\\' || path [2] == '/'));
357 if (!IsUnc) {
358 string p = Unescape (path);
359 if (System.IO.Path.DirectorySeparatorChar == '\\' || windows)
360 cachedLocalPath = p.Replace ('/', '\\');
361 else
362 cachedLocalPath = p;
363 } else {
364 // support *nix and W32 styles
365 if (path.Length > 1 && path [1] == ':')
366 cachedLocalPath = Unescape (path.Replace (Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar));
368 // LAMESPEC: ok, now we cannot determine
369 // if such URI like "file://foo/bar" is
370 // Windows UNC or unix file path, so
371 // they should be handled differently.
372 else if (System.IO.Path.DirectorySeparatorChar == '\\')
373 cachedLocalPath = "\\\\" + Unescape (host + path.Replace ('/', '\\'));
374 else
375 cachedLocalPath = Unescape (path);
377 if (cachedLocalPath == String.Empty)
378 cachedLocalPath = Path.DirectorySeparatorChar.ToString ();
379 return cachedLocalPath;
383 public string PathAndQuery {
384 get { return path + query; }
387 public int Port {
388 get { return port; }
391 public string Query {
392 get { return query; }
395 public string Scheme {
396 get { return scheme; }
399 public string [] Segments {
400 get {
401 if (segments != null)
402 return segments;
404 if (path == "") {
405 segments = new string [0];
406 return segments;
409 string [] parts = path.Split ('/');
410 segments = parts;
411 bool endSlash = path.EndsWith ("/");
412 if (parts.Length > 0 && endSlash) {
413 string [] newParts = new string [parts.Length - 1];
414 Array.Copy (parts, 0, newParts, 0, parts.Length - 1);
415 parts = newParts;
418 int i = 0;
419 if (IsFile && path.Length > 1 && path [1] == ':') {
420 string [] newParts = new string [parts.Length + 1];
421 Array.Copy (parts, 1, newParts, 2, parts.Length - 1);
422 parts = newParts;
423 parts [0] = path.Substring (0, 2);
424 parts [1] = "";
425 i++;
428 int end = parts.Length;
429 for (; i < end; i++)
430 if (i != end - 1 || endSlash)
431 parts [i] += '/';
433 segments = parts;
434 return segments;
438 public bool UserEscaped {
439 get { return userEscaped; }
442 public string UserInfo {
443 get { return userinfo; }
447 // Methods
449 public static UriHostNameType CheckHostName (string name)
451 if (name == null || name.Length == 0)
452 return UriHostNameType.Unknown;
454 if (IsIPv4Address (name))
455 return UriHostNameType.IPv4;
457 if (IsDomainAddress (name))
458 return UriHostNameType.Dns;
460 try {
461 IPv6Address.Parse (name);
462 return UriHostNameType.IPv6;
463 } catch (FormatException) {}
465 return UriHostNameType.Unknown;
468 internal static bool IsIPv4Address (string name)
470 string [] captures = name.Split (new char [] {'.'});
471 if (captures.Length != 4)
472 return false;
473 for (int i = 0; i < 4; i++) {
474 try {
475 int d = Int32.Parse (captures [i], CultureInfo.InvariantCulture);
476 if (d < 0 || d > 255)
477 return false;
478 } catch (Exception) {
479 return false;
482 return true;
485 internal static bool IsDomainAddress (string name)
487 int len = name.Length;
489 if (name [len - 1] == '.')
490 return false;
492 int count = 0;
493 for (int i = 0; i < len; i++) {
494 char c = name [i];
495 if (count == 0) {
496 if (!Char.IsLetterOrDigit (c))
497 return false;
498 } else if (c == '.') {
499 count = 0;
500 } else if (!Char.IsLetterOrDigit (c) && c != '-' && c != '_') {
501 return false;
503 if (++count == 64)
504 return false;
507 return true;
510 [MonoTODO ("Find out what this should do")]
511 protected virtual void Canonicalize ()
515 public static bool CheckSchemeName (string schemeName)
517 if (schemeName == null || schemeName.Length == 0)
518 return false;
520 if (!Char.IsLetter (schemeName [0]))
521 return false;
523 int len = schemeName.Length;
524 for (int i = 1; i < len; i++) {
525 char c = schemeName [i];
526 if (!Char.IsLetterOrDigit (c) && c != '.' && c != '+' && c != '-')
527 return false;
530 return true;
533 [MonoTODO ("Find out what this should do")]
534 protected virtual void CheckSecurity ()
538 public override bool Equals (object comparant)
540 if (comparant == null)
541 return false;
543 Uri uri = comparant as Uri;
544 if (uri == null) {
545 string s = comparant as String;
546 if (s == null)
547 return false;
548 uri = new Uri (s);
551 CultureInfo inv = CultureInfo.InvariantCulture;
552 return ((this.scheme.ToLower (inv) == uri.scheme.ToLower (inv)) &&
553 (this.userinfo.ToLower (inv) == uri.userinfo.ToLower (inv)) &&
554 (this.host.ToLower (inv) == uri.host.ToLower (inv)) &&
555 (this.port == uri.port) &&
556 (this.path == uri.path) &&
557 (this.query.ToLower (inv) == uri.query.ToLower (inv)));
560 public override int GetHashCode ()
562 if (cachedHashCode == 0)
563 cachedHashCode = scheme.GetHashCode ()
564 + userinfo.GetHashCode ()
565 + host.GetHashCode ()
566 + port
567 + path.GetHashCode ()
568 + query.GetHashCode ();
569 return cachedHashCode;
572 public string GetLeftPart (UriPartial part)
574 int defaultPort;
575 switch (part) {
576 case UriPartial.Scheme :
577 return scheme + GetOpaqueWiseSchemeDelimiter ();
578 case UriPartial.Authority :
579 if (host == String.Empty ||
580 scheme == Uri.UriSchemeMailto ||
581 scheme == Uri.UriSchemeNews)
582 return String.Empty;
584 StringBuilder s = new StringBuilder ();
585 s.Append (scheme);
586 s.Append (GetOpaqueWiseSchemeDelimiter ());
587 if (path.Length > 1 && path [1] == ':' && (Uri.UriSchemeFile == scheme))
588 s.Append ('/'); // win32 file
589 if (userinfo.Length > 0)
590 s.Append (userinfo).Append ('@');
591 s.Append (host);
592 defaultPort = GetDefaultPort (scheme);
593 if ((port != -1) && (port != defaultPort))
594 s.Append (':').Append (port);
595 return s.ToString ();
596 case UriPartial.Path :
597 StringBuilder sb = new StringBuilder ();
598 sb.Append (scheme);
599 sb.Append (GetOpaqueWiseSchemeDelimiter ());
600 if (path.Length > 1 && path [1] == ':' && (Uri.UriSchemeFile == scheme))
601 sb.Append ('/'); // win32 file
602 if (userinfo.Length > 0)
603 sb.Append (userinfo).Append ('@');
604 sb.Append (host);
605 defaultPort = GetDefaultPort (scheme);
606 if ((port != -1) && (port != defaultPort))
607 sb.Append (':').Append (port);
608 sb.Append (path);
609 return sb.ToString ();
611 return null;
614 public static int FromHex (char digit)
616 if ('0' <= digit && digit <= '9') {
617 return (int) (digit - '0');
620 if ('a' <= digit && digit <= 'f')
621 return (int) (digit - 'a' + 10);
623 if ('A' <= digit && digit <= 'F')
624 return (int) (digit - 'A' + 10);
626 throw new ArgumentException ("digit");
629 public static string HexEscape (char character)
631 if (character > 255) {
632 throw new ArgumentOutOfRangeException ("character");
635 return "%" + hexUpperChars [((character & 0xf0) >> 4)]
636 + hexUpperChars [((character & 0x0f))];
639 public static char HexUnescape (string pattern, ref int index)
641 if (pattern == null)
642 throw new ArgumentException ("pattern");
644 if (index < 0 || index >= pattern.Length)
645 throw new ArgumentOutOfRangeException ("index");
647 int stage = 0;
648 int c = 0;
649 byte [] bytes = new byte [6];
650 do {
651 if (((index + 3) > pattern.Length) ||
652 (pattern [index] != '%') ||
653 !IsHexDigit (pattern [index + 1]) ||
654 !IsHexDigit (pattern [index + 2]))
656 if (stage == 0)
657 return pattern [index++];
658 break;
661 index++;
662 int msb = FromHex (pattern [index++]);
663 int lsb = FromHex (pattern [index++]);
664 int b = (msb << 4) + lsb;
666 if (stage == 0) {
667 if (b < 0xc0)
668 return (char) b;
669 else if (b < 0xE0) {
670 c = b - 0xc0;
671 stage = 2;
672 } else if (b < 0xF0) {
673 c = b - 0xe0;
674 stage = 3;
675 } else if (b < 0xF8) {
676 c = b - 0xf0;
677 stage = 4;
678 } else if (b < 0xFB) {
679 c = b - 0xf8;
680 stage = 5;
681 } else if (b < 0xFE) {
682 c = b - 0xfc;
683 stage = 6;
685 c <<= (stage - 1) * 6;
687 else
688 c += (b - 0x80) << ((stage - 1) * 6);
689 //Console.WriteLine ("stage {0}: {5:X04} <-- {1:X02}|{2:X01},{3:X01} {4}", new object [] {stage, b, msb, lsb, pattern.Substring (index), c});
690 stage--;
691 } while (stage > 0);
693 return (char) c;
696 public static bool IsHexDigit (char digit)
698 return (('0' <= digit && digit <= '9') ||
699 ('a' <= digit && digit <= 'f') ||
700 ('A' <= digit && digit <= 'F'));
703 public static bool IsHexEncoding (string pattern, int index)
705 if ((index + 3) > pattern.Length)
706 return false;
708 return ((pattern [index++] == '%') &&
709 IsHexDigit (pattern [index++]) &&
710 IsHexDigit (pattern [index]));
713 public string MakeRelative (Uri toUri)
715 if ((this.Scheme != toUri.Scheme) ||
716 (this.Authority != toUri.Authority))
717 return toUri.ToString ();
719 if (this.path == toUri.path)
720 return String.Empty;
722 string [] segments = this.Segments;
723 string [] segments2 = toUri.Segments;
725 int k = 0;
726 int max = Math.Min (segments.Length, segments2.Length);
727 for (; k < max; k++)
728 if (segments [k] != segments2 [k])
729 break;
731 string result = String.Empty;
732 for (int i = k + 1; i < segments.Length; i++)
733 result += "../";
734 for (int i = k; i < segments2.Length; i++)
735 result += segments2 [i];
737 return result;
740 public override string ToString ()
742 if (cachedToString != null)
743 return cachedToString;
744 string q = query.StartsWith ("?") ? '?' + Unescape (query.Substring (1)) : Unescape (query);
745 cachedToString = Unescape (GetLeftPart (UriPartial.Path), true) + q + fragment;
746 return cachedToString;
749 void ISerializable.GetObjectData (SerializationInfo info,
750 StreamingContext context)
752 info.AddValue ("AbsoluteUri", this.AbsoluteUri);
756 // Internal Methods
758 protected virtual void Escape ()
760 path = EscapeString (path);
763 protected static string EscapeString (string str)
765 return EscapeString (str, false, true, true);
768 internal static string EscapeString (string str, bool escapeReserved, bool escapeHex, bool escapeBrackets)
770 if (str == null)
771 return String.Empty;
773 byte [] data = Encoding.UTF8.GetBytes (str.ToCharArray ());
774 StringBuilder s = new StringBuilder ();
775 int len = data.Length;
776 for (int i = 0; i < len; i++) {
777 char c = (char) data [i];
778 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
779 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
780 // control = <US-ASCII coded characters 00-1F and 7F hexadecimal>
781 // space = <US-ASCII coded character 20 hexadecimal>
782 // delims = "<" | ">" | "#" | "%" | <">
783 // unwise = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
785 // check for escape code already placed in str,
786 // i.e. for encoding that follows the pattern
787 // "%hexhex" in a string, where "hex" is a digit from 0-9
788 // or a letter from A-F (case-insensitive).
789 if('%'.Equals(c) && IsHexEncoding(str,i))
791 // if ,yes , copy it as is
792 s.Append(c);
793 s.Append(str[++i]);
794 s.Append(str[++i]);
795 continue;
798 if ((c <= 0x20) || (c >= 0x7f) ||
799 ("<>%\"{}|\\^`".IndexOf (c) != -1) ||
800 (escapeHex && (c == '#')) ||
801 (escapeBrackets && (c == '[' || c == ']')) ||
802 (escapeReserved && (";/?:@&=+$,".IndexOf (c) != -1))) {
803 s.Append (HexEscape (c));
804 continue;
808 s.Append (c);
811 return s.ToString ();
814 // This method is called from .ctor(). When overriden, we can
815 // avoid the "absolute uri" constraints of the .ctor() by
816 // overriding with custom code.
817 protected virtual void Parse ()
819 Parse (source);
821 if (userEscaped)
822 return;
824 host = EscapeString (host, false, true, false);
825 path = EscapeString (path);
828 protected virtual string Unescape (string str)
830 return Unescape (str, false);
833 private string Unescape (string str, bool excludeSharp)
835 if (str == null)
836 return String.Empty;
837 StringBuilder s = new StringBuilder ();
838 int len = str.Length;
839 for (int i = 0; i < len; i++) {
840 char c = str [i];
841 if (c == '%') {
842 char x = HexUnescape (str, ref i);
843 if (excludeSharp && x == '#')
844 s.Append ("%23");
845 else
846 s.Append (x);
847 i--;
848 } else
849 s.Append (c);
851 return s.ToString ();
855 // Private Methods
857 private void ParseAsWindowsUNC (string uriString)
859 scheme = UriSchemeFile;
860 port = -1;
861 fragment = String.Empty;
862 query = String.Empty;
863 isUnc = true;
865 uriString = uriString.TrimStart (new char [] {'\\'});
866 int pos = uriString.IndexOf ('\\');
867 if (pos > 0) {
868 path = uriString.Substring (pos);
869 host = uriString.Substring (0, pos);
870 } else { // "\\\\server"
871 host = uriString;
872 path = String.Empty;
874 path = path.Replace ("\\", "/");
877 private void ParseAsWindowsAbsoluteFilePath (string uriString)
879 if (uriString.Length > 2 && uriString [2] != '\\'
880 && uriString [2] != '/')
881 throw new UriFormatException ("Relative file path is not allowed.");
882 scheme = UriSchemeFile;
883 host = String.Empty;
884 port = -1;
885 path = uriString.Replace ("\\", "/");
886 fragment = String.Empty;
887 query = String.Empty;
890 private void ParseAsUnixAbsoluteFilePath (string uriString)
892 isUnixFilePath = true;
893 scheme = UriSchemeFile;
894 port = -1;
895 fragment = String.Empty;
896 query = String.Empty;
897 host = String.Empty;
898 path = null;
900 if (uriString.StartsWith ("//")) {
901 uriString = uriString.TrimStart (new char [] {'/'});
902 // Now we don't regard //foo/bar as "foo" host.
904 int pos = uriString.IndexOf ('/');
905 if (pos > 0) {
906 path = '/' + uriString.Substring (pos + 1);
907 host = uriString.Substring (0, pos);
908 } else { // "///server"
909 host = uriString;
910 path = String.Empty;
913 path = '/' + uriString;
915 if (path == null)
916 path = uriString;
919 // this parse method is as relaxed as possible about the format
920 // it will hardly ever throw a UriFormatException
921 private void Parse (string uriString)
924 // From RFC 2396 :
926 // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
927 // 12 3 4 5 6 7 8 9
930 if (uriString == null)
931 throw new ArgumentNullException ("uriString");
933 int len = uriString.Length;
934 if (len <= 1)
935 throw new UriFormatException ();
937 int pos = 0;
939 // 1, 2
940 // Identify Windows path, unix path, or standard URI.
941 pos = uriString.IndexOf (':');
942 if (pos < 0) {
943 // It must be Unix file path or Windows UNC
944 if (uriString [0] == '/')
945 ParseAsUnixAbsoluteFilePath (uriString);
946 else if (uriString.StartsWith ("\\\\"))
947 ParseAsWindowsUNC (uriString);
948 else
949 throw new UriFormatException ("URI scheme was not recognized, nor input string is not recognized as an absolute file path.");
950 return;
952 else if (pos == 1) {
953 if (!Char.IsLetter (uriString [0]))
954 throw new UriFormatException ("URI scheme must start with alphabet character.");
955 // This means 'a:' == windows full path.
956 ParseAsWindowsAbsoluteFilePath (uriString);
957 return;
960 // scheme
961 scheme = uriString.Substring (0, pos).ToLower (CultureInfo.InvariantCulture);
962 // Check scheme name characters as specified in RFC2396.
963 if (!Char.IsLetter (scheme [0]))
964 throw new UriFormatException ("URI scheme must start with alphabet character.");
965 for (int i = 1; i < scheme.Length; i++) {
966 if (!Char.IsLetterOrDigit (scheme, i)) {
967 switch (scheme [i]) {
968 case '+':
969 case '-':
970 case '.':
971 break;
972 default:
973 throw new UriFormatException ("URI scheme must consist of one of alphabet, digits, '+', '-' or '.' character.");
977 uriString = uriString.Substring (pos + 1);
979 // 8 fragment
980 pos = uriString.IndexOf ('#');
981 if (!IsUnc && pos != -1) {
982 fragment = uriString.Substring (pos);
983 uriString = uriString.Substring (0, pos);
986 // 6 query
987 pos = uriString.IndexOf ('?');
988 if (pos != -1) {
989 query = uriString.Substring (pos);
990 uriString = uriString.Substring (0, pos);
991 if (!userEscaped)
992 query = EscapeString (query);
995 // 3
996 bool unixAbsPath = scheme == UriSchemeFile && uriString.StartsWith ("///");
997 if (uriString.StartsWith ("//")) {
998 if (uriString.StartsWith ("////"))
999 unixAbsPath = false;
1000 uriString = uriString.TrimStart (new char [] {'/'});
1001 if (uriString.Length > 1 && uriString [1] == ':')
1002 unixAbsPath = false;
1003 } else if (!IsPredefinedScheme (scheme)) {
1004 path = uriString;
1005 isOpaquePart = true;
1006 return;
1009 // 5 path
1010 pos = uriString.IndexOfAny (new char[] {'/'});
1011 if (unixAbsPath)
1012 pos = -1;
1013 if (pos == -1) {
1014 if ((scheme != Uri.UriSchemeMailto) &&
1015 (scheme != Uri.UriSchemeNews) &&
1016 (scheme != Uri.UriSchemeFile))
1017 path = "/";
1018 } else {
1019 path = uriString.Substring (pos);
1020 uriString = uriString.Substring (0, pos);
1023 // 4.a user info
1024 pos = uriString.IndexOf ("@");
1025 if (pos != -1) {
1026 userinfo = uriString.Substring (0, pos);
1027 uriString = uriString.Remove (0, pos + 1);
1030 // 4.b port
1031 port = -1;
1032 pos = uriString.LastIndexOf (":");
1033 if (unixAbsPath)
1034 pos = -1;
1035 if (pos != -1 && pos != (uriString.Length - 1)) {
1036 string portStr = uriString.Remove (0, pos + 1);
1037 if (portStr.Length > 1 && portStr [portStr.Length - 1] != ']') {
1038 try {
1039 port = (int) UInt32.Parse (portStr, CultureInfo.InvariantCulture);
1040 uriString = uriString.Substring (0, pos);
1041 } catch (Exception) {
1042 throw new UriFormatException ("Invalid URI: invalid port number");
1046 if (port == -1) {
1047 port = GetDefaultPort (scheme);
1050 // 4 authority
1051 host = uriString;
1052 if (host.Length > 1 && host [0] == '[' && host [host.Length - 1] == ']') {
1053 try {
1054 host = "[" + IPv6Address.Parse (host).ToString () + "]";
1055 } catch (Exception) {
1056 throw new UriFormatException ("Invalid URI: The hostname could not be parsed");
1060 if (unixAbsPath) {
1061 path = '/' + uriString;
1062 host = String.Empty;
1063 } else if (host.Length == 2 && host [1] == ':') {
1064 // windows filepath
1065 path = host + path;
1066 host = String.Empty;
1067 } else if (isUnixFilePath) {
1068 uriString = "//" + uriString;
1069 host = String.Empty;
1070 } else if (host.Length == 0) {
1071 throw new UriFormatException ("Invalid URI: The hostname could not be parsed");
1072 } else if (scheme == UriSchemeFile) {
1073 isUnc = true;
1076 if ((scheme != Uri.UriSchemeMailto) &&
1077 (scheme != Uri.UriSchemeNews) &&
1078 (scheme != Uri.UriSchemeFile))
1079 path = Reduce (path);
1082 private static string Reduce (string path)
1084 path = path.Replace ('\\','/');
1085 string [] parts = path.Split ('/');
1086 ArrayList result = new ArrayList ();
1088 int end = parts.Length;
1089 for (int i = 0; i < end; i++) {
1090 string current = parts [i];
1091 if (current == "" || current == "." )
1092 continue;
1094 if (current == "..") {
1095 if (result.Count == 0) {
1096 if (i == 1) // see bug 52599
1097 continue;
1098 throw new Exception ("Invalid path.");
1101 result.RemoveAt (result.Count - 1);
1102 continue;
1105 result.Add (current);
1108 if (result.Count == 0)
1109 return "/";
1111 result.Insert (0, "");
1113 string res = String.Join ("/", (string []) result.ToArray (typeof (string)));
1114 if (path.EndsWith ("/"))
1115 res += '/';
1117 return res;
1120 private struct UriScheme
1122 public string scheme;
1123 public string delimiter;
1124 public int defaultPort;
1126 public UriScheme (string s, string d, int p)
1128 scheme = s;
1129 delimiter = d;
1130 defaultPort = p;
1134 static UriScheme [] schemes = new UriScheme [] {
1135 new UriScheme (UriSchemeHttp, SchemeDelimiter, 80),
1136 new UriScheme (UriSchemeHttps, SchemeDelimiter, 443),
1137 new UriScheme (UriSchemeFtp, SchemeDelimiter, 21),
1138 new UriScheme (UriSchemeFile, SchemeDelimiter, -1),
1139 new UriScheme (UriSchemeMailto, ":", 25),
1140 new UriScheme (UriSchemeNews, ":", -1),
1141 new UriScheme (UriSchemeNntp, SchemeDelimiter, 119),
1142 new UriScheme (UriSchemeGopher, SchemeDelimiter, 70),
1145 internal static string GetSchemeDelimiter (string scheme)
1147 for (int i = 0; i < schemes.Length; i++)
1148 if (schemes [i].scheme == scheme)
1149 return schemes [i].delimiter;
1150 return Uri.SchemeDelimiter;
1153 internal static int GetDefaultPort (string scheme)
1155 for (int i = 0; i < schemes.Length; i++)
1156 if (schemes [i].scheme == scheme)
1157 return schemes [i].defaultPort;
1158 return -1;
1161 private string GetOpaqueWiseSchemeDelimiter ()
1163 if (isOpaquePart)
1164 return ":";
1165 else
1166 return GetSchemeDelimiter (scheme);
1169 protected virtual bool IsBadFileSystemCharacter (char ch)
1171 // It does not always overlap with InvalidPathChars.
1172 int chInt = (int) ch;
1173 if (chInt < 32 || (chInt < 64 && chInt > 57))
1174 return true;
1175 switch (chInt) {
1176 case 0:
1177 case 34: // "
1178 case 38: // &
1179 case 42: // *
1180 case 44: // ,
1181 case 47: // /
1182 case 92: // \
1183 case 94: // ^
1184 case 124: // |
1185 return true;
1188 return false;
1192 protected static bool IsExcludedCharacter (char ch)
1194 if (ch <= 32 || ch >= 127)
1195 return true;
1197 if (ch == '"' || ch == '#' || ch == '%' || ch == '<' ||
1198 ch == '>' || ch == '[' || ch == '\\' || ch == ']' ||
1199 ch == '^' || ch == '`' || ch == '{' || ch == '|' ||
1200 ch == '}')
1201 return true;
1202 return false;
1205 private static bool IsPredefinedScheme (string scheme)
1207 switch (scheme) {
1208 case "http":
1209 case "https":
1210 case "file":
1211 case "ftp":
1212 case "nntp":
1213 case "gopher":
1214 case "mailto":
1215 case "news":
1216 return true;
1217 default:
1218 return false;
1222 protected virtual bool IsReservedCharacter (char ch)
1224 if (ch == '$' || ch == '&' || ch == '+' || ch == ',' ||
1225 ch == '/' || ch == ':' || ch == ';' || ch == '=' ||
1226 ch == '@')
1227 return true;
1228 return false;