2009-12-02 Jb Evain <jbevain@novell.com>
[mcs.git] / class / corlib / Mono.Security / Uri.cs
blobe9ac8ae299ec3d8f9ab6261687556d05d8eb82b9
1 //
2 // Mono.Security.Uri
3 // Adapted from System.Uri (in System.dll assembly) for its use in corlib
4 //
5 // Authors:
6 // Lawrence Pit (loz@cable.a2000.nl)
7 // Garrett Rooney (rooneg@electricjellyfish.net)
8 // Ian MacLean (ianm@activestate.com)
9 // Ben Maurer (bmaurer@users.sourceforge.net)
10 // Atsushi Enomoto (atsushi@ximian.com)
11 // Stephane Delcroix <stephane@delcroix.org>
13 // (C) 2001 Garrett Rooney
14 // (C) 2003 Ian MacLean
15 // (C) 2003 Ben Maurer
16 // (C) 2003 Novell inc.
17 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
19 // Permission is hereby granted, free of charge, to any person obtaining
20 // a copy of this software and associated documentation files (the
21 // "Software"), to deal in the Software without restriction, including
22 // without limitation the rights to use, copy, modify, merge, publish,
23 // distribute, sublicense, and/or sell copies of the Software, and to
24 // permit persons to whom the Software is furnished to do so, subject to
25 // the following conditions:
26 //
27 // The above copyright notice and this permission notice shall be
28 // included in all copies or substantial portions of the Software.
29 //
30 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
31 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
32 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
33 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
34 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
35 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
36 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
39 using System;
40 using System.IO;
41 using System.Text;
42 using System.Collections;
43 using System.Globalization;
45 // See RFC 2396 for more info on URI's.
47 // TODO: optimize by parsing host string only once
49 namespace Mono.Security {
51 #if INSIDE_CORLIB
52 internal
53 #else
54 public
55 #endif
56 enum UriPartial {
57 Scheme = 0,
58 Authority = 1,
59 Path = 2,
62 #if INSIDE_CORLIB
63 internal
64 #else
65 public
66 #endif
67 class Uri {
69 // NOTES:
70 // o scheme excludes the scheme delimiter
71 // o port is -1 to indicate no port is defined
72 // o path is empty or starts with / when scheme delimiter == "://"
73 // o query is empty or starts with ? char, escaped.
74 // o fragment is empty or starts with # char, unescaped.
75 // o all class variables are in escaped format when they are escapable,
76 // except cachedToString.
77 // o UNC is supported, as starts with "\\" for windows,
78 // or "//" with unix.
80 private bool isUnixFilePath = false;
81 private string source;
82 private string scheme = String.Empty;
83 private string host = String.Empty;
84 private int port = -1;
85 private string path = String.Empty;
86 private string query = String.Empty;
87 private string fragment = String.Empty;
88 private string userinfo = String.Empty;
89 private bool isUnc = false;
90 private bool isOpaquePart = false;
92 private string [] segments;
94 private bool userEscaped = false;
95 private string cachedAbsoluteUri = null;
96 private string cachedToString = null;
97 private string cachedLocalPath = null;
98 private int cachedHashCode = 0;
99 private bool reduce = true;
101 private static readonly string hexUpperChars = "0123456789ABCDEF";
103 // Fields
105 public static readonly string SchemeDelimiter = "://";
106 public static readonly string UriSchemeFile = "file";
107 public static readonly string UriSchemeFtp = "ftp";
108 public static readonly string UriSchemeGopher = "gopher";
109 public static readonly string UriSchemeHttp = "http";
110 public static readonly string UriSchemeHttps = "https";
111 public static readonly string UriSchemeMailto = "mailto";
112 public static readonly string UriSchemeNews = "news";
113 public static readonly string UriSchemeNntp = "nntp";
115 // Constructors
117 public Uri (string uriString) : this (uriString, false)
121 public Uri (string uriString, bool dontEscape)
123 userEscaped = dontEscape;
124 source = uriString;
125 Parse ();
128 public Uri (string uriString, bool dontEscape, bool reduce)
130 userEscaped = dontEscape;
131 source = uriString;
132 this.reduce = reduce;
133 Parse ();
136 public Uri (Uri baseUri, string relativeUri)
137 : this (baseUri, relativeUri, false)
141 public Uri (Uri baseUri, string relativeUri, bool dontEscape)
143 if (baseUri == null)
144 throw new NullReferenceException ("baseUri");
146 // See RFC 2396 Par 5.2 and Appendix C
148 userEscaped = dontEscape;
150 if (relativeUri == null)
151 throw new NullReferenceException ("relativeUri");
153 // Check Windows UNC (for // it is scheme/host separator)
154 if (relativeUri.StartsWith ("\\\\")) {
155 source = relativeUri;
156 Parse ();
157 return;
160 int pos = relativeUri.IndexOf (':');
161 if (pos != -1) {
163 int pos2 = relativeUri.IndexOfAny (new char [] {'/', '\\', '?'});
165 // pos2 < 0 ... e.g. mailto
166 // pos2 > pos ... to block ':' in query part
167 if (pos2 > pos || pos2 < 0) {
168 // equivalent to new Uri (relativeUri, dontEscape)
169 source = relativeUri;
170 Parse ();
172 return;
176 this.scheme = baseUri.scheme;
177 this.host = baseUri.host;
178 this.port = baseUri.port;
179 this.userinfo = baseUri.userinfo;
180 this.isUnc = baseUri.isUnc;
181 this.isUnixFilePath = baseUri.isUnixFilePath;
182 this.isOpaquePart = baseUri.isOpaquePart;
184 if (relativeUri == String.Empty) {
185 this.path = baseUri.path;
186 this.query = baseUri.query;
187 this.fragment = baseUri.fragment;
188 return;
191 // 8 fragment
192 // Note that in relative constructor, file URI cannot handle '#' as a filename character, but just regarded as a fragment identifier.
193 pos = relativeUri.IndexOf ('#');
194 if (pos != -1) {
195 fragment = relativeUri.Substring (pos);
196 // fragment is not escaped.
197 relativeUri = relativeUri.Substring (0, pos);
200 // 6 query
201 pos = relativeUri.IndexOf ('?');
202 if (pos != -1) {
203 query = relativeUri.Substring (pos);
204 if (!userEscaped)
205 query = EscapeString (query);
206 relativeUri = relativeUri.Substring (0, pos);
209 if (relativeUri.Length > 0 && relativeUri [0] == '/') {
210 if (relativeUri.Length > 1 && relativeUri [1] == '/') {
211 source = scheme + ':' + relativeUri;
212 Parse ();
213 return;
214 } else {
215 path = relativeUri;
216 if (!userEscaped)
217 path = EscapeString (path);
218 return;
222 // par 5.2 step 6 a)
223 path = baseUri.path;
224 if (relativeUri.Length > 0 || query.Length > 0) {
225 pos = path.LastIndexOf ('/');
226 if (pos >= 0)
227 path = path.Substring (0, pos + 1);
230 if(relativeUri.Length == 0)
231 return;
233 // 6 b)
234 path += relativeUri;
236 // 6 c)
237 int startIndex = 0;
238 while (true) {
239 pos = path.IndexOf ("./", startIndex);
240 if (pos == -1)
241 break;
242 if (pos == 0)
243 path = path.Remove (0, 2);
244 else if (path [pos - 1] != '.')
245 path = path.Remove (pos, 2);
246 else
247 startIndex = pos + 1;
250 // 6 d)
251 if (path.Length > 1 &&
252 path [path.Length - 1] == '.' &&
253 path [path.Length - 2] == '/')
254 path = path.Remove (path.Length - 1, 1);
256 // 6 e)
257 startIndex = 0;
258 while (true) {
259 pos = path.IndexOf ("/../", startIndex);
260 if (pos == -1)
261 break;
262 if (pos == 0) {
263 startIndex = 3;
264 continue;
266 int pos2 = path.LastIndexOf ('/', pos - 1);
267 if (pos2 == -1) {
268 startIndex = pos + 1;
269 } else {
270 if (path.Substring (pos2 + 1, pos - pos2 - 1) != "..")
271 path = path.Remove (pos2 + 1, pos - pos2 + 3);
272 else
273 startIndex = pos + 1;
277 // 6 f)
278 if (path.Length > 3 && path.EndsWith ("/..")) {
279 pos = path.LastIndexOf ('/', path.Length - 4);
280 if (pos != -1)
281 if (path.Substring (pos + 1, path.Length - pos - 4) != "..")
282 path = path.Remove (pos + 1, path.Length - pos - 1);
285 if (!userEscaped)
286 path = EscapeString (path);
289 // Properties
291 public string AbsolutePath {
292 get { return path; }
295 public string AbsoluteUri {
296 get {
297 if (cachedAbsoluteUri == null) {
298 cachedAbsoluteUri = GetLeftPart (UriPartial.Path) + query + fragment;
300 return cachedAbsoluteUri;
304 public string Authority {
305 get {
306 return (GetDefaultPort (scheme) == port)
307 ? host : host + ":" + port;
311 public string Fragment {
312 get { return fragment; }
315 public string Host {
316 get { return host; }
319 /* public UriHostNameType HostNameType {
320 get {
321 UriHostNameType ret = CheckHostName (host);
322 if (ret != UriHostNameType.Unknown)
323 return ret;
325 // looks it always returns Basic...
326 return UriHostNameType.Basic; //.Unknown;
330 public bool IsDefaultPort {
331 get { return GetDefaultPort (scheme) == port; }
334 public bool IsFile {
335 get { return (scheme == UriSchemeFile); }
338 public bool IsLoopback {
339 get {
340 if (host == String.Empty)
341 return false;
343 if (host == "loopback" || host == "localhost")
344 return true;
346 try {
347 if (IPAddress.Loopback.Equals (IPAddress.Parse (host)))
348 return true;
349 } catch (FormatException) {}
351 try {
352 return IPv6Address.IsLoopback (IPv6Address.Parse (host));
353 } catch (FormatException) {}
355 return false;
359 public bool IsUnc {
360 // rule: This should be true only if
361 // - uri string starts from "\\", or
362 // - uri string starts from "//" (Samba way)
363 get { return isUnc; }
366 public string LocalPath {
367 get {
368 if (cachedLocalPath != null)
369 return cachedLocalPath;
370 if (!IsFile)
371 return AbsolutePath;
373 bool windows = (path.Length > 3 && path [1] == ':' &&
374 (path [2] == '\\' || path [2] == '/'));
376 if (!IsUnc) {
377 string p = Unescape (path);
378 if (System.IO.Path.DirectorySeparatorChar == '\\' || windows)
379 cachedLocalPath = p.Replace ('/', '\\');
380 else
381 cachedLocalPath = p;
382 } else {
383 // support *nix and W32 styles
384 if (path.Length > 1 && path [1] == ':')
385 cachedLocalPath = Unescape (path.Replace (Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar));
387 // LAMESPEC: ok, now we cannot determine
388 // if such URI like "file://foo/bar" is
389 // Windows UNC or unix file path, so
390 // they should be handled differently.
391 else if (System.IO.Path.DirectorySeparatorChar == '\\')
392 cachedLocalPath = "\\\\" + Unescape (host + path.Replace ('/', '\\'));
393 else
394 cachedLocalPath = Unescape (path);
396 if (cachedLocalPath == String.Empty)
397 cachedLocalPath = Path.DirectorySeparatorChar.ToString ();
398 return cachedLocalPath;
402 public string PathAndQuery {
403 get { return path + query; }
406 public int Port {
407 get { return port; }
410 public string Query {
411 get { return query; }
414 public string Scheme {
415 get { return scheme; }
418 public string [] Segments {
419 get {
420 if (segments != null)
421 return segments;
423 if (path.Length == 0) {
424 segments = new string [0];
425 return segments;
428 string [] parts = path.Split ('/');
429 segments = parts;
430 bool endSlash = path.EndsWith ("/");
431 if (parts.Length > 0 && endSlash) {
432 string [] newParts = new string [parts.Length - 1];
433 Array.Copy (parts, 0, newParts, 0, parts.Length - 1);
434 parts = newParts;
437 int i = 0;
438 if (IsFile && path.Length > 1 && path [1] == ':') {
439 string [] newParts = new string [parts.Length + 1];
440 Array.Copy (parts, 1, newParts, 2, parts.Length - 1);
441 parts = newParts;
442 parts [0] = path.Substring (0, 2);
443 parts [1] = String.Empty;
444 i++;
447 int end = parts.Length;
448 for (; i < end; i++)
449 if (i != end - 1 || endSlash)
450 parts [i] += '/';
452 segments = parts;
453 return segments;
457 public bool UserEscaped {
458 get { return userEscaped; }
461 public string UserInfo {
462 get { return userinfo; }
466 // Methods
468 /* public static UriHostNameType CheckHostName (string name)
470 if (name == null || name.Length == 0)
471 return UriHostNameType.Unknown;
473 if (IsIPv4Address (name))
474 return UriHostNameType.IPv4;
476 if (IsDomainAddress (name))
477 return UriHostNameType.Dns;
479 try {
480 IPv6Address.Parse (name);
481 return UriHostNameType.IPv6;
482 } catch (FormatException) {}
484 return UriHostNameType.Unknown;
487 internal static bool IsIPv4Address (string name)
489 string [] captures = name.Split (new char [] {'.'});
490 if (captures.Length != 4)
491 return false;
492 for (int i = 0; i < 4; i++) {
493 try {
494 int d = Int32.Parse (captures [i], CultureInfo.InvariantCulture);
495 if (d < 0 || d > 255)
496 return false;
497 } catch (Exception) {
498 return false;
501 return true;
504 internal static bool IsDomainAddress (string name)
506 int len = name.Length;
508 if (name [len - 1] == '.')
509 return false;
511 int count = 0;
512 for (int i = 0; i < len; i++) {
513 char c = name [i];
514 if (count == 0) {
515 if (!Char.IsLetterOrDigit (c))
516 return false;
517 } else if (c == '.') {
518 count = 0;
519 } else if (!Char.IsLetterOrDigit (c) && c != '-' && c != '_') {
520 return false;
522 if (++count == 64)
523 return false;
526 return true;
529 public static bool CheckSchemeName (string schemeName)
531 if (schemeName == null || schemeName.Length == 0)
532 return false;
534 if (!Char.IsLetter (schemeName [0]))
535 return false;
537 int len = schemeName.Length;
538 for (int i = 1; i < len; i++) {
539 char c = schemeName [i];
540 if (!Char.IsLetterOrDigit (c) && c != '.' && c != '+' && c != '-')
541 return false;
544 return true;
547 public override bool Equals (object comparant)
549 if (comparant == null)
550 return false;
552 Uri uri = comparant as Uri;
553 if (uri == null) {
554 string s = comparant as String;
555 if (s == null)
556 return false;
557 uri = new Uri (s);
560 CultureInfo inv = CultureInfo.InvariantCulture;
561 return ((this.scheme.ToLower (inv) == uri.scheme.ToLower (inv)) &&
562 (this.userinfo.ToLower (inv) == uri.userinfo.ToLower (inv)) &&
563 (this.host.ToLower (inv) == uri.host.ToLower (inv)) &&
564 (this.port == uri.port) &&
565 (this.path == uri.path) &&
566 (this.query.ToLower (inv) == uri.query.ToLower (inv)));
569 public override int GetHashCode ()
571 if (cachedHashCode == 0)
572 cachedHashCode = scheme.GetHashCode ()
573 + userinfo.GetHashCode ()
574 + host.GetHashCode ()
575 + port
576 + path.GetHashCode ()
577 + query.GetHashCode ();
578 return cachedHashCode;
581 public string GetLeftPart (UriPartial part)
583 int defaultPort;
584 switch (part) {
585 case UriPartial.Scheme :
586 return scheme + GetOpaqueWiseSchemeDelimiter ();
587 case UriPartial.Authority :
588 if (host == String.Empty ||
589 scheme == Uri.UriSchemeMailto ||
590 scheme == Uri.UriSchemeNews)
591 return String.Empty;
593 StringBuilder s = new StringBuilder ();
594 s.Append (scheme);
595 s.Append (GetOpaqueWiseSchemeDelimiter ());
596 if (path.Length > 1 && path [1] == ':' && (Uri.UriSchemeFile == scheme))
597 s.Append ('/'); // win32 file
598 if (userinfo.Length > 0)
599 s.Append (userinfo).Append ('@');
600 s.Append (host);
601 defaultPort = GetDefaultPort (scheme);
602 if ((port != -1) && (port != defaultPort))
603 s.Append (':').Append (port);
604 return s.ToString ();
605 case UriPartial.Path :
606 StringBuilder sb = new StringBuilder ();
607 sb.Append (scheme);
608 sb.Append (GetOpaqueWiseSchemeDelimiter ());
609 if (path.Length > 1 && path [1] == ':' && (Uri.UriSchemeFile == scheme))
610 sb.Append ('/'); // win32 file
611 if (userinfo.Length > 0)
612 sb.Append (userinfo).Append ('@');
613 sb.Append (host);
614 defaultPort = GetDefaultPort (scheme);
615 if ((port != -1) && (port != defaultPort))
616 sb.Append (':').Append (port);
617 sb.Append (path);
618 return sb.ToString ();
620 return null;
623 public static int FromHex (char digit)
625 if ('0' <= digit && digit <= '9') {
626 return (int) (digit - '0');
629 if ('a' <= digit && digit <= 'f')
630 return (int) (digit - 'a' + 10);
632 if ('A' <= digit && digit <= 'F')
633 return (int) (digit - 'A' + 10);
635 throw new ArgumentException ("digit");
638 public static string HexEscape (char character)
640 if (character > 255) {
641 throw new ArgumentOutOfRangeException ("character");
644 return "%" + hexUpperChars [((character & 0xf0) >> 4)]
645 + hexUpperChars [((character & 0x0f))];
648 public static char HexUnescape (string pattern, ref int index)
650 if (pattern == null)
651 throw new ArgumentException ("pattern");
653 if (index < 0 || index >= pattern.Length)
654 throw new ArgumentOutOfRangeException ("index");
656 int stage = 0;
657 int c = 0;
658 do {
659 if (((index + 3) > pattern.Length) ||
660 (pattern [index] != '%') ||
661 !IsHexDigit (pattern [index + 1]) ||
662 !IsHexDigit (pattern [index + 2]))
664 if (stage == 0)
665 return pattern [index++];
666 break;
669 index++;
670 int msb = FromHex (pattern [index++]);
671 int lsb = FromHex (pattern [index++]);
672 int b = (msb << 4) + lsb;
674 if (stage == 0) {
675 if (b < 0xc0)
676 return (char) b;
677 else if (b < 0xE0) {
678 c = b - 0xc0;
679 stage = 2;
680 } else if (b < 0xF0) {
681 c = b - 0xe0;
682 stage = 3;
683 } else if (b < 0xF8) {
684 c = b - 0xf0;
685 stage = 4;
686 } else if (b < 0xFB) {
687 c = b - 0xf8;
688 stage = 5;
689 } else if (b < 0xFE) {
690 c = b - 0xfc;
691 stage = 6;
693 c <<= (stage - 1) * 6;
695 else
696 c += (b - 0x80) << ((stage - 1) * 6);
697 //Console.WriteLine ("stage {0}: {5:X04} <-- {1:X02}|{2:X01},{3:X01} {4}", new object [] {stage, b, msb, lsb, pattern.Substring (index), c});
698 stage--;
699 } while (stage > 0);
701 return (char) c;
704 public static bool IsHexDigit (char digit)
706 return (('0' <= digit && digit <= '9') ||
707 ('a' <= digit && digit <= 'f') ||
708 ('A' <= digit && digit <= 'F'));
711 public static bool IsHexEncoding (string pattern, int index)
713 if ((index + 3) > pattern.Length)
714 return false;
716 return ((pattern [index++] == '%') &&
717 IsHexDigit (pattern [index++]) &&
718 IsHexDigit (pattern [index]));
721 public string MakeRelative (Uri toUri)
723 if ((this.Scheme != toUri.Scheme) ||
724 (this.Authority != toUri.Authority))
725 return toUri.ToString ();
727 if (this.path == toUri.path)
728 return String.Empty;
730 string [] segments = this.Segments;
731 string [] segments2 = toUri.Segments;
733 int k = 0;
734 int max = System.Math.Min (segments.Length, segments2.Length);
735 for (; k < max; k++)
736 if (segments [k] != segments2 [k])
737 break;
739 string result = String.Empty;
740 for (int i = k + 1; i < segments.Length; i++)
741 result += "../";
742 for (int i = k; i < segments2.Length; i++)
743 result += segments2 [i];
745 return result;
748 public override string ToString ()
750 if (cachedToString != null)
751 return cachedToString;
752 string q = query.StartsWith ("?") ? '?' + Unescape (query.Substring (1)) : Unescape (query);
753 cachedToString = Unescape (GetLeftPart (UriPartial.Path), true) + q + fragment;
754 return cachedToString;
757 /* void ISerializable.GetObjectData (SerializationInfo info,
758 StreamingContext context)
760 info.AddValue ("AbsoluteUri", this.AbsoluteUri);
764 // Internal Methods
766 protected void Escape ()
768 path = EscapeString (path);
771 protected static string EscapeString (string str)
773 return EscapeString (str, false, true, true);
776 internal static string EscapeString (string str, bool escapeReserved, bool escapeHex, bool escapeBrackets)
778 if (str == null)
779 return String.Empty;
781 StringBuilder s = new StringBuilder ();
782 int len = str.Length;
783 for (int i = 0; i < len; i++) {
784 // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
785 // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
786 // control = <US-ASCII coded characters 00-1F and 7F hexadecimal>
787 // space = <US-ASCII coded character 20 hexadecimal>
788 // delims = "<" | ">" | "#" | "%" | <">
789 // unwise = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
791 // check for escape code already placed in str,
792 // i.e. for encoding that follows the pattern
793 // "%hexhex" in a string, where "hex" is a digit from 0-9
794 // or a letter from A-F (case-insensitive).
795 if (IsHexEncoding (str,i)) {
796 // if ,yes , copy it as is
797 s.Append(str.Substring (i, 3));
798 i += 2;
799 continue;
802 byte [] data = Encoding.UTF8.GetBytes (new char[] {str[i]});
803 int length = data.Length;
804 for (int j = 0; j < length; j++) {
805 char c = (char) data [j];
806 if ((c <= 0x20) || (c >= 0x7f) ||
807 ("<>%\"{}|\\^`".IndexOf (c) != -1) ||
808 (escapeHex && (c == '#')) ||
809 (escapeBrackets && (c == '[' || c == ']')) ||
810 (escapeReserved && (";/?:@&=+$,".IndexOf (c) != -1))) {
811 s.Append (HexEscape (c));
812 continue;
814 s.Append (c);
818 return s.ToString ();
821 // This method is called from .ctor(). When overriden, we can
822 // avoid the "absolute uri" constraints of the .ctor() by
823 // overriding with custom code.
824 protected void Parse ()
826 Parse (source);
828 if (userEscaped)
829 return;
831 host = EscapeString (host, false, true, false);
832 path = EscapeString (path);
835 protected string Unescape (string str)
837 return Unescape (str, false);
840 internal string Unescape (string str, bool excludeSharp)
842 if (str == null)
843 return String.Empty;
844 StringBuilder s = new StringBuilder ();
845 int len = str.Length;
846 for (int i = 0; i < len; i++) {
847 char c = str [i];
848 if (c == '%') {
849 char x = HexUnescape (str, ref i);
850 if (excludeSharp && x == '#')
851 s.Append ("%23");
852 else
853 s.Append (x);
854 i--;
855 } else
856 s.Append (c);
858 return s.ToString ();
862 // Private Methods
864 private void ParseAsWindowsUNC (string uriString)
866 scheme = UriSchemeFile;
867 port = -1;
868 fragment = String.Empty;
869 query = String.Empty;
870 isUnc = true;
872 uriString = uriString.TrimStart (new char [] {'\\'});
873 int pos = uriString.IndexOf ('\\');
874 if (pos > 0) {
875 path = uriString.Substring (pos);
876 host = uriString.Substring (0, pos);
877 } else { // "\\\\server"
878 host = uriString;
879 path = String.Empty;
881 path = path.Replace ("\\", "/");
884 private void ParseAsWindowsAbsoluteFilePath (string uriString)
886 if (uriString.Length > 2 && uriString [2] != '\\'
887 && uriString [2] != '/')
888 throw new FormatException ("Relative file path is not allowed.");
889 scheme = UriSchemeFile;
890 host = String.Empty;
891 port = -1;
892 path = uriString.Replace ("\\", "/");
893 fragment = String.Empty;
894 query = String.Empty;
897 private void ParseAsUnixAbsoluteFilePath (string uriString)
899 isUnixFilePath = true;
900 scheme = UriSchemeFile;
901 port = -1;
902 fragment = String.Empty;
903 query = String.Empty;
904 host = String.Empty;
905 path = null;
907 if (uriString.StartsWith ("//")) {
908 uriString = uriString.TrimStart (new char [] {'/'});
909 // Now we don't regard //foo/bar as "foo" host.
911 int pos = uriString.IndexOf ('/');
912 if (pos > 0) {
913 path = '/' + uriString.Substring (pos + 1);
914 host = uriString.Substring (0, pos);
915 } else { // "///server"
916 host = uriString;
917 path = String.Empty;
920 path = '/' + uriString;
922 if (path == null)
923 path = uriString;
926 // this parse method is as relaxed as possible about the format
927 // it will hardly ever throw a UriFormatException
928 private void Parse (string uriString)
931 // From RFC 2396 :
933 // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
934 // 12 3 4 5 6 7 8 9
937 if (uriString == null)
938 throw new ArgumentNullException ("uriString");
940 int len = uriString.Length;
941 if (len <= 1)
942 throw new FormatException ();
944 int pos = 0;
946 // 1, 2
947 // Identify Windows path, unix path, or standard URI.
948 pos = uriString.IndexOf (':');
949 if (pos < 0) {
950 // It must be Unix file path or Windows UNC
951 if (uriString [0] == '/')
952 ParseAsUnixAbsoluteFilePath (uriString);
953 else if (uriString.StartsWith ("\\\\"))
954 ParseAsWindowsUNC (uriString);
955 else
956 throw new FormatException ("URI scheme was not recognized, nor input string is not recognized as an absolute file path.");
957 return;
959 else if (pos == 1) {
960 if (!Char.IsLetter (uriString [0]))
961 throw new FormatException ("URI scheme must start with alphabet character.");
962 // This means 'a:' == windows full path.
963 ParseAsWindowsAbsoluteFilePath (uriString);
964 return;
967 // scheme
968 scheme = uriString.Substring (0, pos).ToLower (CultureInfo.InvariantCulture);
969 // Check scheme name characters as specified in RFC2396.
970 if (!Char.IsLetter (scheme [0]))
971 throw new FormatException ("URI scheme must start with alphabet character.");
972 for (int i = 1; i < scheme.Length; i++) {
973 if (!Char.IsLetterOrDigit (scheme, i)) {
974 switch (scheme [i]) {
975 case '+':
976 case '-':
977 case '.':
978 break;
979 default:
980 throw new FormatException ("URI scheme must consist of one of alphabet, digits, '+', '-' or '.' character.");
984 uriString = uriString.Substring (pos + 1);
986 // 8 fragment
987 pos = uriString.IndexOf ('#');
988 if (!IsUnc && pos != -1) {
989 fragment = uriString.Substring (pos);
990 uriString = uriString.Substring (0, pos);
993 // 6 query
994 pos = uriString.IndexOf ('?');
995 if (pos != -1) {
996 query = uriString.Substring (pos);
997 uriString = uriString.Substring (0, pos);
998 if (!userEscaped)
999 query = EscapeString (query);
1002 // 3
1003 bool unixAbsPath = scheme == UriSchemeFile && uriString.StartsWith ("///");
1004 if (uriString.StartsWith ("//")) {
1005 if (uriString.StartsWith ("////"))
1006 unixAbsPath = false;
1007 uriString = uriString.TrimStart (new char [] {'/'});
1008 if (uriString.Length > 1 && uriString [1] == ':')
1009 unixAbsPath = false;
1010 } else if (!IsPredefinedScheme (scheme)) {
1011 path = uriString;
1012 isOpaquePart = true;
1013 return;
1016 // 5 path
1017 pos = uriString.IndexOfAny (new char[] {'/'});
1018 if (unixAbsPath)
1019 pos = -1;
1020 if (pos == -1) {
1021 if ((scheme != Uri.UriSchemeMailto) &&
1022 (scheme != Uri.UriSchemeNews) &&
1023 (scheme != Uri.UriSchemeFile))
1024 path = "/";
1025 } else {
1026 path = uriString.Substring (pos);
1027 uriString = uriString.Substring (0, pos);
1030 // 4.a user info
1031 pos = uriString.IndexOf ("@");
1032 if (pos != -1) {
1033 userinfo = uriString.Substring (0, pos);
1034 uriString = uriString.Remove (0, pos + 1);
1037 // 4.b port
1038 port = -1;
1039 pos = uriString.LastIndexOf (":");
1040 if (unixAbsPath)
1041 pos = -1;
1042 if (pos != -1 && pos != (uriString.Length - 1)) {
1043 string portStr = uriString.Remove (0, pos + 1);
1044 if (portStr.Length > 1 && portStr [portStr.Length - 1] != ']') {
1045 try {
1046 port = (int) UInt32.Parse (portStr, CultureInfo.InvariantCulture);
1047 uriString = uriString.Substring (0, pos);
1048 } catch (Exception) {
1049 throw new FormatException ("Invalid URI: invalid port number");
1053 if (port == -1) {
1054 port = GetDefaultPort (scheme);
1057 // 4 authority
1058 host = uriString;
1059 /* if (host.Length > 1 && host [0] == '[' && host [host.Length - 1] == ']') {
1060 try {
1061 host = "[" + IPv6Address.Parse (host).ToString () + "]";
1062 } catch (Exception) {
1063 throw new FormatException ("Invalid URI: The hostname could not be parsed");
1067 if (unixAbsPath) {
1068 path = '/' + uriString;
1069 host = String.Empty;
1070 } else if (host.Length == 2 && host [1] == ':') {
1071 // windows filepath
1072 path = host + path;
1073 host = String.Empty;
1074 } else if (isUnixFilePath) {
1075 uriString = "//" + uriString;
1076 host = String.Empty;
1077 } else if (host.Length == 0) {
1078 throw new FormatException ("Invalid URI: The hostname could not be parsed");
1079 } else if (scheme == UriSchemeFile) {
1080 isUnc = true;
1083 if ((scheme != Uri.UriSchemeMailto) &&
1084 (scheme != Uri.UriSchemeNews) &&
1085 (scheme != Uri.UriSchemeFile))
1087 if (reduce)
1088 path = Reduce (path);
1091 private static string Reduce (string path)
1093 path = path.Replace ('\\','/');
1094 string [] parts = path.Split ('/');
1095 ArrayList result = new ArrayList ();
1097 int end = parts.Length;
1098 for (int i = 0; i < end; i++) {
1099 string current = parts [i];
1100 if (current.Length == 0 || current == "." )
1101 continue;
1103 if (current == "..") {
1104 if (result.Count == 0) {
1105 if (i == 1) // see bug 52599
1106 continue;
1107 throw new Exception ("Invalid path.");
1110 result.RemoveAt (result.Count - 1);
1111 continue;
1114 result.Add (current);
1117 if (result.Count == 0)
1118 return "/";
1120 result.Insert (0, String.Empty);
1122 string res = String.Join ("/", (string []) result.ToArray (typeof (string)));
1123 if (path.EndsWith ("/"))
1124 res += '/';
1126 return res;
1129 private struct UriScheme
1131 public string scheme;
1132 public string delimiter;
1133 public int defaultPort;
1135 public UriScheme (string s, string d, int p)
1137 scheme = s;
1138 delimiter = d;
1139 defaultPort = p;
1143 static UriScheme [] schemes = new UriScheme [] {
1144 new UriScheme (UriSchemeHttp, SchemeDelimiter, 80),
1145 new UriScheme (UriSchemeHttps, SchemeDelimiter, 443),
1146 new UriScheme (UriSchemeFtp, SchemeDelimiter, 21),
1147 new UriScheme (UriSchemeFile, SchemeDelimiter, -1),
1148 new UriScheme (UriSchemeMailto, ":", 25),
1149 new UriScheme (UriSchemeNews, ":", -1),
1150 new UriScheme (UriSchemeNntp, SchemeDelimiter, 119),
1151 new UriScheme (UriSchemeGopher, SchemeDelimiter, 70),
1154 internal static string GetSchemeDelimiter (string scheme)
1156 for (int i = 0; i < schemes.Length; i++)
1157 if (schemes [i].scheme == scheme)
1158 return schemes [i].delimiter;
1159 return Uri.SchemeDelimiter;
1162 internal static int GetDefaultPort (string scheme)
1164 for (int i = 0; i < schemes.Length; i++)
1165 if (schemes [i].scheme == scheme)
1166 return schemes [i].defaultPort;
1167 return -1;
1170 private string GetOpaqueWiseSchemeDelimiter ()
1172 if (isOpaquePart)
1173 return ":";
1174 else
1175 return GetSchemeDelimiter (scheme);
1178 protected bool IsBadFileSystemCharacter (char ch)
1180 // It does not always overlap with InvalidPathChars.
1181 int chInt = (int) ch;
1182 if (chInt < 32 || (chInt < 64 && chInt > 57))
1183 return true;
1184 switch (chInt) {
1185 case 0:
1186 case 34: // "
1187 case 38: // &
1188 case 42: // *
1189 case 44: // ,
1190 case 47: // /
1191 case 92: // \
1192 case 94: // ^
1193 case 124: // |
1194 return true;
1197 return false;
1201 protected static bool IsExcludedCharacter (char ch)
1203 if (ch <= 32 || ch >= 127)
1204 return true;
1206 if (ch == '"' || ch == '#' || ch == '%' || ch == '<' ||
1207 ch == '>' || ch == '[' || ch == '\\' || ch == ']' ||
1208 ch == '^' || ch == '`' || ch == '{' || ch == '|' ||
1209 ch == '}')
1210 return true;
1211 return false;
1214 private static bool IsPredefinedScheme (string scheme)
1216 switch (scheme) {
1217 case "http":
1218 case "https":
1219 case "file":
1220 case "ftp":
1221 case "nntp":
1222 case "gopher":
1223 case "mailto":
1224 case "news":
1225 return true;
1226 default:
1227 return false;
1231 protected bool IsReservedCharacter (char ch)
1233 if (ch == '$' || ch == '&' || ch == '+' || ch == ',' ||
1234 ch == '/' || ch == ':' || ch == ';' || ch == '=' ||
1235 ch == '@')
1236 return true;
1237 return false;