c97e70efcde8f4b6dfe34556343e0d51f15f5771
[mono.git] / mcs / class / referencesource / mscorlib / system / security / util / urlstring.cs
1 // ==++==
2 // 
3 //   Copyright (c) Microsoft Corporation.  All rights reserved.
4 // 
5 // ==--==
6 //  URLString
7 //
8 // <OWNER>Microsoft</OWNER>
9 //
10 //  Implementation of membership condition for zones
11 //
12
13 namespace System.Security.Util {
14     
15     using System;
16     using System.Collections;
17     using System.Collections.Generic;
18     using System.Runtime.CompilerServices;
19     using System.Runtime.InteropServices;
20     using System.Runtime.Versioning;
21     using System.Runtime.Serialization;
22     using System.Globalization;
23     using System.Text;
24     using System.IO;
25     using System.Diagnostics.Contracts;
26     
27     [Serializable]
28     internal sealed class URLString : SiteString
29     {
30         private String m_protocol;
31         [OptionalField(VersionAdded = 2)]
32         private String m_userpass;
33         private SiteString m_siteString;
34         private int m_port;
35         private LocalSiteString m_localSite;
36         private DirectoryString m_directory;
37         
38         private const String m_defaultProtocol = "file";
39
40         [OptionalField(VersionAdded = 2)]
41         private bool m_parseDeferred;
42         [OptionalField(VersionAdded = 2)]
43         private String m_urlOriginal;
44         [OptionalField(VersionAdded = 2)]
45         private bool m_parsedOriginal;
46
47         [OptionalField(VersionAdded = 3)]
48         private bool m_isUncShare;
49
50         // legacy field from v1.x, not used in v2 and beyond. Retained purely for serialization compatability.
51         private String m_fullurl;
52
53
54         [OnDeserialized]
55         public void OnDeserialized(StreamingContext ctx)
56         {
57
58             if (m_urlOriginal == null)
59             {
60                 // pre-v2 deserialization. Need to fix-up fields here
61                 m_parseDeferred = false;
62                 m_parsedOriginal = false; // Dont care what this value is - never used
63                 m_userpass = "";
64                 m_urlOriginal = m_fullurl;
65                 m_fullurl = null;
66             }
67         }
68         [OnSerializing]
69         private void OnSerializing(StreamingContext ctx)
70         {
71
72             if ((ctx.State & ~(StreamingContextStates.Clone|StreamingContextStates.CrossAppDomain)) != 0)
73             {
74                 DoDeferredParse();
75                 m_fullurl = m_urlOriginal;
76             }
77         }   
78         [OnSerialized]
79         private void OnSerialized(StreamingContext ctx)
80         {
81             if ((ctx.State & ~(StreamingContextStates.Clone|StreamingContextStates.CrossAppDomain)) != 0)
82             {
83                 m_fullurl = null;
84             }
85         }
86         
87         public URLString()
88         {
89             m_protocol = "";
90             m_userpass = "";
91             m_siteString = new SiteString();
92             m_port = -1;
93             m_localSite = null;
94             m_directory = new DirectoryString();
95             m_parseDeferred = false;
96         }
97
98         private void DoDeferredParse()
99         {
100             if (m_parseDeferred)
101             {
102                 ParseString(m_urlOriginal, m_parsedOriginal);
103                 m_parseDeferred = false;
104             }
105         }
106
107         public URLString(string url) : this(url, false, false) {}
108         public URLString(string url, bool parsed) : this(url, parsed, false) {}
109
110         internal URLString(string url, bool parsed, bool doDeferredParsing)
111         {
112             m_port = -1;
113             m_userpass = "";
114             DoFastChecks(url);
115             m_urlOriginal = url;
116             m_parsedOriginal = parsed;
117             m_parseDeferred = true;
118             if (doDeferredParsing)
119                 DoDeferredParse();
120         }
121
122         // Converts %XX and %uYYYY to the actual characters (I.e. Unesacpes any escape characters present in the URL)
123         private String UnescapeURL(String url)
124         {
125             StringBuilder intermediate = StringBuilderCache.Acquire(url.Length);
126             int Rindex = 0; // index into temp that gives the rest of the string to be processed
127             int index;
128             int braIndex = -1;
129             int ketIndex = -1;
130             braIndex = url.IndexOf('[',Rindex);
131             if (braIndex != -1)
132                 ketIndex = url.IndexOf(']', braIndex);
133             
134             do
135                 {
136                     index = url.IndexOf( '%', Rindex);
137
138                     if (index == -1)
139                     {
140                         intermediate = intermediate.Append(url, Rindex, (url.Length - Rindex));
141                         break;
142                     }
143                     // if we hit a '%' in the middle of an IPv6 address, dont process that
144                     if (index > braIndex && index < ketIndex)
145                     {
146                         intermediate = intermediate.Append(url, Rindex, (ketIndex - Rindex+1));
147                         Rindex = ketIndex+1;
148                         continue;
149                     }
150
151                     if (url.Length - index < 2) // Check that there is at least 1 char after the '%'
152                         throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
153
154                     if (url[index+1] == 'u' || url[index+1] == 'U')
155                     {
156                         if (url.Length - index < 6) // example: "%u004d" is 6 chars long
157                             throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
158
159                         // We have a unicode character specified in hex
160
161                         try
162                         {
163                             char c = (char)(Hex.ConvertHexDigit( url[index+2] ) << 12 |
164                                             Hex.ConvertHexDigit( url[index+3] ) << 8  |
165                                             Hex.ConvertHexDigit( url[index+4] ) << 4  |
166                                             Hex.ConvertHexDigit( url[index+5] ));
167                             intermediate = intermediate.Append(url, Rindex, index - Rindex);
168                             intermediate = intermediate.Append(c);
169                         }
170                         catch(ArgumentException) // Hex.ConvertHexDigit can throw an "out of range" ArgumentException
171                         {
172                             throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
173                         }
174                                         
175                         Rindex = index + 6 ; //update the 'seen' length
176                     }
177                     else
178                     {
179                         // we have a hex character.
180                                          
181                         if (url.Length - index < 3) // example: "%4d" is 3 chars long
182                              throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
183
184                         try 
185                         {
186                             char c = (char)(Hex.ConvertHexDigit( url[index+1] ) << 4 | Hex.ConvertHexDigit( url[index+2] ));
187
188                             intermediate = intermediate.Append(url, Rindex, index - Rindex);
189                             intermediate = intermediate.Append(c);
190                         }
191                         catch(ArgumentException) // Hex.ConvertHexDigit can throw an "out of range" ArgumentException
192                         {
193                             throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
194                         }
195                         
196                         Rindex = index + 3; // update the 'seen' length
197                     }  
198
199                 } 
200             while (true);
201             return StringBuilderCache.GetStringAndRelease(intermediate);
202         }
203
204         // Helper Function for ParseString: 
205         // Search for the end of the protocol info and grab the actual protocol string
206         // ex. http://www.microsoft.com/complus would have a protocol string of http
207         private String ParseProtocol(String url)
208         {
209             String temp;
210             int index = url.IndexOf( ':' );
211                 
212             if (index == 0)
213             {
214                 throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
215             }
216             else if (index == -1)
217             {
218                 m_protocol = m_defaultProtocol;
219                 temp = url;
220             }
221             else if (url.Length > index + 1)
222             {
223                 if (index == m_defaultProtocol.Length && 
224                     String.Compare(url, 0, m_defaultProtocol, 0, index, StringComparison.OrdinalIgnoreCase) == 0)
225                 {
226                     m_protocol = m_defaultProtocol;
227                     temp = url.Substring( index + 1 );
228
229                     // Since an explicit file:// URL could be immediately followed by a host name, we will be
230                     // conservative and assume that it is on a share rather than a potentally relative local
231                     // URL.
232                     m_isUncShare = true;
233                 }
234                 else if (url[index+1] != '\\')
235                 {
236                     if (url.Length > index + 2 &&
237                         url[index+1] == '/' &&
238                         url[index+2] == '/')
239                     {
240                         m_protocol = url.Substring( 0, index );
241
242                         for (int i = 0; i < m_protocol.Length; ++i)
243                         {
244                             char c = m_protocol[i];
245
246                             if ((c >= 'a' && c <= 'z') ||
247                                 (c >= 'A' && c <= 'Z') ||
248                                 (c >= '0' && c <= '9') ||
249                                 (c == '+') ||
250                                 (c == '.') ||
251                                 (c == '-'))
252                             {
253                                 continue;
254                             }
255                             else
256                             {
257                                 throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
258                             }
259                         }
260                         temp = url.Substring( index + 3 );
261                      }                                
262                     else
263                     {
264                         throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
265                     }
266                 }
267                 else
268                 {
269                     m_protocol = m_defaultProtocol;
270                     temp = url;
271                 }
272             }
273             else
274             {
275                 throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
276             }
277
278             return temp;
279         }
280
281         private String ParsePort(String url)
282         {
283             String temp = url;
284             char[] separators = new char[] { ':', '/' };
285             int Rindex = 0;
286             int userpassIndex = temp.IndexOf('@');
287             if (userpassIndex != -1) {
288                 if (temp.IndexOf('/',0,userpassIndex) == -1) {
289                     // this is a user:pass type of string 
290                     m_userpass = temp.Substring(0,userpassIndex);
291                     Rindex = userpassIndex + 1;
292                 }
293             }
294
295             int braIndex = -1;
296             int ketIndex = -1;
297             int portIndex = -1;
298             braIndex = url.IndexOf('[',Rindex);
299             if (braIndex != -1)
300                 ketIndex = url.IndexOf(']', braIndex);
301             if (ketIndex != -1)
302             {
303                 // IPv6 address...ignore the IPv6 block when searching for the port
304                 portIndex = temp.IndexOfAny(separators,ketIndex);
305             }
306             else
307             {
308                 portIndex = temp.IndexOfAny(separators,Rindex);
309             }
310
311             
312
313             if (portIndex != -1 && temp[portIndex] == ':')
314             {
315                 // make sure it really is a port, and has a number after the :
316                 if ( temp[portIndex+1] >= '0' && temp[portIndex+1] <= '9' )
317                 {
318                     int tempIndex = temp.IndexOf( '/', Rindex);
319
320                     if (tempIndex == -1)
321                     {
322                         m_port = Int32.Parse( temp.Substring(portIndex + 1), CultureInfo.InvariantCulture );
323
324                         if (m_port < 0)
325                             throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
326
327                         temp = temp.Substring( Rindex, portIndex - Rindex );
328                     }
329                     else if (tempIndex > portIndex)
330                     {
331                         m_port = Int32.Parse( temp.Substring(portIndex + 1, tempIndex - portIndex - 1), CultureInfo.InvariantCulture );
332                         temp = temp.Substring( Rindex, portIndex - Rindex ) + temp.Substring( tempIndex );
333                     }
334                     else 
335                         throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );                            
336                 }
337                 else
338                     throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
339             }
340             else {
341                 // Chop of the user/pass portion if any
342                 temp = temp.Substring(Rindex);
343             }
344                 
345             return temp;
346         }                
347
348         // This does three things:
349         // 1. It makes the following modifications to the start of the string:
350         //      a. \\?\ and \\?/ => <empty>
351         //      b. \\.\ and \\./ => <empty>
352         // 2. If isFileUrl is true, converts all slashes to front slashes and strips leading
353         //    front slashes. See comment by code.
354         // 3. Throws a PathTooLongException if the length of the resulting URL is >= MAX_PATH.
355         //    This is done to prevent security issues due to canonicalization truncations.
356         // Remove this method when the Path class supports "\\?\"
357         internal static String PreProcessForExtendedPathRemoval(String url, bool isFileUrl)
358         {
359             bool uncShare = false;
360             return PreProcessForExtendedPathRemoval(url, isFileUrl, ref uncShare);
361         }
362
363         private static String PreProcessForExtendedPathRemoval(String url, bool isFileUrl, ref bool isUncShare)
364         {
365             // This is the modified URL that we will return
366             StringBuilder modifiedUrl = new StringBuilder(url);
367
368             // ITEM 1 - remove extended path characters.
369             {
370                 // Keep track of where we are in both the comparison and altered strings.
371                 int curCmpIdx = 0;
372                 int curModIdx = 0;
373
374                 // If all the '\' have already been converted to '/', just check for //?/ or //./
375                 if ((url.Length - curCmpIdx) >= 4 &&
376                     (String.Compare(url, curCmpIdx, "//?/", 0, 4, StringComparison.OrdinalIgnoreCase) == 0 ||
377                      String.Compare(url, curCmpIdx, "//./", 0, 4, StringComparison.OrdinalIgnoreCase) == 0))
378                 {
379                     modifiedUrl.Remove(curModIdx, 4);
380                     curCmpIdx += 4;
381                 }
382                 else
383                 {
384                     if (isFileUrl) {
385                         // We need to handle an indefinite number of leading front slashes for file URLs since we could
386                         // get something like:
387                         //      file://\\?\
388                         //      file:/\\?\
389                         //      file:\\?\
390                         //      etc...
391                         while (url[curCmpIdx] == '/')
392                         {
393                             curCmpIdx++;
394                             curModIdx++;
395                         }
396                     }
397
398                     // Remove the extended path characters
399                     if ((url.Length - curCmpIdx) >= 4 &&
400                         (String.Compare(url, curCmpIdx, "\\\\?\\", 0, 4, StringComparison.OrdinalIgnoreCase) == 0 ||
401                          String.Compare(url, curCmpIdx, "\\\\?/", 0, 4, StringComparison.OrdinalIgnoreCase) == 0 ||
402                          String.Compare(url, curCmpIdx, "\\\\.\\", 0, 4, StringComparison.OrdinalIgnoreCase) == 0 ||
403                          String.Compare(url, curCmpIdx, "\\\\./", 0, 4, StringComparison.OrdinalIgnoreCase) == 0))
404                     {
405                         modifiedUrl.Remove(curModIdx, 4);
406                         curCmpIdx += 4;
407                     }
408                 }
409             }
410
411             // ITEM 2 - convert all slashes to forward slashes, and strip leading slashes.
412             if (isFileUrl)
413             {
414                 int slashCount = 0;
415                 bool seenFirstBackslash = false;
416                 
417                 while (slashCount < modifiedUrl.Length && (modifiedUrl[slashCount] == '/' || modifiedUrl[slashCount] == '\\'))
418                 {
419                     // Look for sets of consecutive backslashes. We can't just look for these at the start
420                     // of the string, since file:// might come first.  Instead, once we see the first \, look
421                     // for a second one following it.
422                     if (!seenFirstBackslash && modifiedUrl[slashCount] == '\\')
423                 {
424                         seenFirstBackslash = true;
425                         if (slashCount + 1 < modifiedUrl.Length && modifiedUrl[slashCount + 1] == '\\')
426                             isUncShare = true;
427                     }
428
429                     slashCount++;
430                 }
431
432                 modifiedUrl.Remove(0, slashCount);
433                 modifiedUrl.Replace('\\', '/');
434             }
435
436             // ITEM 3 - If the path is greater than or equal (due to terminating NULL in windows) MAX_PATH, we throw.
437             if (modifiedUrl.Length >= Path.MAX_PATH)
438             {
439                 throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
440             }
441
442             // Create the result string from the StringBuilder
443             return modifiedUrl.ToString();
444         }
445
446
447         // Do any misc massaging of data in the URL
448         private String PreProcessURL(String url, bool isFileURL)
449         {
450            if (isFileURL) {
451                 // Remove when the Path class supports "\\?\"
452                 url = PreProcessForExtendedPathRemoval(url, true, ref m_isUncShare);
453             }
454             else {
455                 url = url.Replace('\\', '/');
456             }
457             return url;
458         }
459
460         private void ParseFileURL(String url)
461         {
462             String temp = url;
463
464             int index = temp.IndexOf( '/');
465
466             if (index != -1 &&
467                 ((index == 2 &&
468                   temp[index-1] != ':' &&
469                   temp[index-1] != '|') ||
470                  index != 2) &&
471                 index != temp.Length - 1)
472             {
473                 // Also, if it is a UNC share, we want m_localSite to
474                 // be of the form "computername/share", so if the first
475                 // fileEnd character found is a slash, do some more parsing
476                 // to find the proper end character.
477
478                 int tempIndex = temp.IndexOf( '/', index+1);
479
480                 if (tempIndex != -1)
481                     index = tempIndex;
482                 else
483                     index = -1;
484             }
485
486             String localSite;
487             if (index == -1)
488                 localSite = temp;
489             else
490                 localSite = temp.Substring(0,index);
491
492             if (localSite.Length == 0)
493                 throw new ArgumentException( Environment.GetResourceString( "Argument_InvalidUrl" ) );
494
495             int i;
496             bool spacesAllowed;
497
498             if (localSite[0] == '\\' && localSite[1] == '\\')
499             {
500                 spacesAllowed = true;
501                 i = 2;
502             }
503             else
504             {
505                 i = 0;
506                 spacesAllowed = false;
507             }
508
509             bool useSmallCharToUpper = true;
510
511             for (; i < localSite.Length; ++i)
512             {
513                 char c = localSite[i];
514
515                 if ((c >= 'A' && c <= 'Z') ||
516                     (c >= 'a' && c <= 'z') ||
517                     (c >= '0' && c <= '9') ||
518                     (c == '-') || (c == '/') ||
519                     (c == ':') || (c == '|') ||
520                     (c == '.') || (c == '*') ||
521                     (c == '$') || (spacesAllowed && c == ' '))
522                 {
523                     continue;
524                 }
525                 else
526                 {
527                     useSmallCharToUpper = false;
528                     break;
529                 }
530             }
531
532             if (useSmallCharToUpper)
533                 localSite = String.SmallCharToUpper( localSite );
534             else
535                 localSite = localSite.ToUpper(CultureInfo.InvariantCulture);
536
537             m_localSite = new LocalSiteString( localSite );
538
539             if (index == -1)
540             {
541                 if (localSite[localSite.Length-1] == '*')
542                     m_directory = new DirectoryString( "*", false );
543                 else 
544                     m_directory = new DirectoryString();
545             }
546             else
547             {
548                 String directoryString = temp.Substring( index + 1 );
549                 if (directoryString.Length == 0)
550                 {
551                     m_directory = new DirectoryString();
552                 }
553                 else
554                 {
555                     m_directory = new DirectoryString( directoryString, true);
556                 }
557             }
558
559             m_siteString = null;
560             return;
561         }
562
563         private void ParseNonFileURL(String url)
564         {
565             String temp = url;
566             int index = temp.IndexOf('/');
567
568             if (index == -1)
569             {
570                 m_localSite = null;    // for drive letter
571                 m_siteString = new SiteString( temp );
572                 m_directory = new DirectoryString();
573             }
574             else
575             {
576                 String site = temp.Substring( 0, index );
577                 m_localSite = null;
578                 m_siteString = new SiteString( site );
579
580                 String directoryString = temp.Substring( index + 1 );
581
582                 if (directoryString.Length == 0)
583                 {
584                     m_directory = new DirectoryString();
585                 }
586                 else
587                 {
588                     m_directory = new DirectoryString( directoryString, false );
589                 }
590             }
591             return;
592         }
593
594         void DoFastChecks( String url )
595         {
596             if (url == null)
597             {
598                 throw new ArgumentNullException( "url" );
599             }
600             Contract.EndContractBlock();
601             
602             if (url.Length == 0)
603             {
604                 throw new FormatException(Environment.GetResourceString("Format_StringZeroLength"));
605             }
606         }
607
608         // NOTE:
609         // 1. We support URLs that follow the common Internet scheme syntax
610         //     (<scheme>://user:pass@<host>:<port>/<url-path>) and all windows file URLs.
611         // 2.  In the general case we parse of the site and create a SiteString out of it
612         //      (which supports our wildcarding scheme).  In the case of files we don't support
613         //      wildcarding and furthermore SiteString doesn't like ':' and '|' which can appear
614         //      in file urls so we just keep that info in a separate string and set the
615         //      SiteString to null.
616         //
617         // ex. http://www.microsoft.com/complus  -> m_siteString = "www.microsoft.com" m_localSite = null
618         // ex. file:///c:/complus/mscorlib.dll  -> m_siteString = null m_localSite = "c:"
619         // ex. file:///c|/complus/mscorlib.dll  -> m_siteString = null m_localSite = "c:"
620         void ParseString( String url, bool parsed )
621         {
622             // If there are any escaped hex or unicode characters in the url, translate those
623             // into the proper character.
624
625             if (!parsed)
626             {
627                 url = UnescapeURL(url);
628             }
629
630             // Identify the protocol and strip the protocol info from the string, if present.
631             String temp = ParseProtocol(url); 
632
633             bool fileProtocol = (String.Compare( m_protocol, "file", StringComparison.OrdinalIgnoreCase) == 0);
634             
635             // handle any special  preocessing...removing extra characters, etc.
636             temp = PreProcessURL(temp, fileProtocol);
637             
638             if (fileProtocol)
639             {
640                 ParseFileURL(temp);
641             }
642             else 
643             {
644                 // Check if there is a port number and parse that out.
645                 temp = ParsePort(temp);
646                 ParseNonFileURL(temp);
647                 // Note: that we allow DNS and Netbios names for non-file protocols (since sitestring will check
648                 // that the hostname satisfies these two protocols. DNS-only checking can theoretically be added
649                 // here but that would break all the programs that use '_' (which is fairly common, yet illegal).
650                 // If this needs to be done at any point, add a call to m_siteString.IsLegalDNSName().
651             }
652
653
654         }
655
656         public String Scheme
657         {
658             get
659             {
660                 DoDeferredParse();
661
662                 return m_protocol;
663             }
664         }
665
666         public String Host
667         {
668             get
669             {
670                 DoDeferredParse();
671
672                 if (m_siteString != null)
673                 {
674                     return m_siteString.ToString();
675                 }
676                 else
677                 {
678                     return m_localSite.ToString();
679                 }
680             }
681         }
682
683         public String Port 
684         {
685             get 
686             {
687                 DoDeferredParse();
688
689                 if (m_port == -1)
690                     return null;
691                 else
692                     return m_port.ToString(CultureInfo.InvariantCulture);
693             }
694         }
695
696         public String Directory
697         {
698             get
699             {
700                 DoDeferredParse();
701
702                 return m_directory.ToString();
703             }
704         }
705
706         /// <summary>
707         ///     Make a best guess at determining if this is URL refers to a file with a relative path. Since
708         ///     this is a guess to help out users of UrlMembershipCondition who may accidentally supply a
709         ///     relative URL, we'd rather err on the side of absolute than relative. (We'd rather accept some
710         ///     meaningless membership conditions rather than reject meaningful ones).
711         /// 
712         ///     In order to be a relative file URL, the URL needs to have a protocol of file, and not be on a
713         ///     UNC share.
714         /// 
715         ///     If both of the above are true, then the heuristics we'll use to detect an absolute URL are:
716         ///         1. A host name which is:
717         ///              a. greater than one character and ends in a colon (representing the drive letter) OR
718         ///              b. ends with a * (so we match any file with the given prefix if any)
719         ///         2. Has a directory name (cannot be simply file://c:)
720         /// </summary>
721         public bool IsRelativeFileUrl
722         {
723             get
724             {
725                 DoDeferredParse();
726
727                 if (String.Equals(m_protocol, "file", StringComparison.OrdinalIgnoreCase) && !m_isUncShare)
728                 {
729                     string host = m_localSite != null ? m_localSite.ToString() : null;
730                     // If the host name ends with the * character, treat this as an absolute URL since the *
731                     // could represent the rest of the full path.
732                     if (host.EndsWith('*'))
733                         return false;
734                     string directory = m_directory != null ? m_directory.ToString() : null;
735
736                     return host == null || host.Length < 2 || !host.EndsWith(':') ||
737                            String.IsNullOrEmpty(directory);
738                 }
739
740                 // Since this is not a local URL, it cannot be relative
741                 return false;
742             }
743         }
744
745         public String GetFileName()
746         {
747             DoDeferredParse();
748
749             if (String.Compare( m_protocol, "file", StringComparison.OrdinalIgnoreCase) != 0)
750                 return null;
751          
752             String intermediateDirectory = this.Directory.Replace( '/', '\\' );
753
754             String directory = this.Host.Replace( '/', '\\' );
755
756             int directorySlashIndex = directory.IndexOf( '\\' );
757             if (directorySlashIndex == -1)
758             {
759                 if (directory.Length != 2 ||
760                     !(directory[1] == ':' || directory[1] == '|'))
761                 {
762                     directory = "\\\\" + directory;
763                 }
764             }
765             else if (directorySlashIndex != 2 ||
766                      (directorySlashIndex == 2 && directory[1] != ':' && directory[1] != '|'))
767             {
768                 directory = "\\\\" + directory;
769             }
770
771             directory += "\\" + intermediateDirectory;
772             
773             return directory;
774     }
775
776
777         public String GetDirectoryName()
778         {
779             DoDeferredParse();
780
781             if (String.Compare( m_protocol, "file", StringComparison.OrdinalIgnoreCase ) != 0)
782                 return null;
783
784             String intermediateDirectory = this.Directory.Replace( '/', '\\' );
785
786             int slashIndex = 0;
787             for (int i = intermediateDirectory.Length; i > 0; i--)
788             {
789                if (intermediateDirectory[i-1] == '\\')
790                {
791                    slashIndex = i;
792                    break;
793                }
794             }
795
796             String directory = this.Host.Replace( '/', '\\' );
797
798             int directorySlashIndex = directory.IndexOf( '\\' );
799             if (directorySlashIndex == -1)
800             {
801                 if (directory.Length != 2 ||
802                     !(directory[1] == ':' || directory[1] == '|'))
803                 {
804                     directory = "\\\\" + directory;
805                 }
806             }
807             else if (directorySlashIndex > 2 ||
808                     (directorySlashIndex == 2 && directory[1] != ':' && directory[1] != '|'))
809             {
810                 directory = "\\\\" + directory;
811             }
812
813             directory += "\\";
814             
815             if (slashIndex > 0)
816             {
817                 directory += intermediateDirectory.Substring( 0, slashIndex );
818             }
819
820             return directory;
821         }
822
823         public override SiteString Copy()
824         {
825             return new URLString( m_urlOriginal, m_parsedOriginal );
826         }            
827         
828         [ResourceExposure(ResourceScope.None)]
829         [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
830         public override bool IsSubsetOf( SiteString site )
831         {
832             if (site == null)
833             {
834                 return false;
835             }
836             
837             URLString url = site as URLString;
838             
839             if (url == null)
840             {
841                 return false;
842             }
843
844             DoDeferredParse();
845             url.DoDeferredParse();
846
847             URLString normalUrl1 = this.SpecialNormalizeUrl();
848             URLString normalUrl2 = url.SpecialNormalizeUrl();
849             
850             if (String.Compare( normalUrl1.m_protocol, normalUrl2.m_protocol, StringComparison.OrdinalIgnoreCase) == 0 &&
851                 normalUrl1.m_directory.IsSubsetOf( normalUrl2.m_directory ))
852             {
853                 if (normalUrl1.m_localSite != null)
854                 {
855                     // We do a little extra processing in here for local files since we allow
856                     // both <drive_letter>: and <drive_letter>| forms of urls.
857                     
858                     return normalUrl1.m_localSite.IsSubsetOf( normalUrl2.m_localSite );
859                 }
860                 else
861                 {
862                     if (normalUrl1.m_port != normalUrl2.m_port)
863                         return false;
864
865                     return normalUrl2.m_siteString != null && normalUrl1.m_siteString.IsSubsetOf( normalUrl2.m_siteString );
866                 }
867             }
868             else
869             {
870                 return false;
871             }
872         }
873         
874         public override String ToString()
875         {
876             return m_urlOriginal;
877         }
878         
879         public override bool Equals(Object o)
880         {
881             DoDeferredParse();
882
883             if (o == null || !(o is URLString))
884                 return false;
885             else
886                 return this.Equals( (URLString)o );
887         }
888
889         public override int GetHashCode()
890         {
891             DoDeferredParse();
892
893             TextInfo info = CultureInfo.InvariantCulture.TextInfo;
894             int accumulator = 0;
895
896             if (this.m_protocol != null)
897                 accumulator = info.GetCaseInsensitiveHashCode( this.m_protocol );
898
899             if (this.m_localSite != null)
900             {
901                 accumulator = accumulator ^ this.m_localSite.GetHashCode();
902             }
903             else
904             {
905                 accumulator = accumulator ^ this.m_siteString.GetHashCode();
906             }
907             accumulator = accumulator ^ this.m_directory.GetHashCode();
908
909             return accumulator;
910         }    
911         
912         public bool Equals( URLString url )
913         {
914             return CompareUrls( this, url );
915         }
916
917         [ResourceExposure(ResourceScope.None)]
918         [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
919         public static bool CompareUrls( URLString url1, URLString url2 )
920         {
921             if (url1 == null && url2 == null)
922                 return true;
923
924             if (url1 == null || url2 == null)
925                 return false;
926
927             url1.DoDeferredParse();
928             url2.DoDeferredParse();
929
930             URLString normalUrl1 = url1.SpecialNormalizeUrl();
931             URLString normalUrl2 = url2.SpecialNormalizeUrl();
932
933             // Compare protocol (case insensitive)
934
935             if (String.Compare( normalUrl1.m_protocol, normalUrl2.m_protocol, StringComparison.OrdinalIgnoreCase) != 0)
936                 return false;
937
938             // Do special processing for file urls
939
940             if (String.Compare( normalUrl1.m_protocol, "file", StringComparison.OrdinalIgnoreCase) == 0)
941             {
942                 if (!normalUrl1.m_localSite.IsSubsetOf( normalUrl2.m_localSite ) ||
943                     !normalUrl2.m_localSite.IsSubsetOf( normalUrl1.m_localSite ))
944                      return false;
945             }
946             else
947             {
948                 if (String.Compare( normalUrl1.m_userpass, normalUrl2.m_userpass, StringComparison.Ordinal) != 0)
949                     return false;
950                 
951                 if (!normalUrl1.m_siteString.IsSubsetOf( normalUrl2.m_siteString ) ||
952                     !normalUrl2.m_siteString.IsSubsetOf( normalUrl1.m_siteString ))
953                     return false;
954
955                 if (url1.m_port != url2.m_port)
956                     return false;
957             }
958
959             if (!normalUrl1.m_directory.IsSubsetOf( normalUrl2.m_directory ) ||
960                 !normalUrl2.m_directory.IsSubsetOf( normalUrl1.m_directory ))
961                 return false;
962
963             return true;
964         }
965
966         internal String NormalizeUrl()
967         {
968             DoDeferredParse();
969             StringBuilder builtUrl = StringBuilderCache.Acquire();
970
971             if (String.Compare( m_protocol, "file", StringComparison.OrdinalIgnoreCase) == 0)
972             {
973                 builtUrl = builtUrl.AppendFormat("FILE:///{0}/{1}", m_localSite.ToString(), m_directory.ToString());
974             }
975             else
976             {
977                 builtUrl = builtUrl.AppendFormat("{0}://{1}{2}", m_protocol, m_userpass, m_siteString.ToString());
978
979                 if (m_port != -1)
980                     builtUrl = builtUrl.AppendFormat("{0}",m_port);
981
982                 builtUrl = builtUrl.AppendFormat("/{0}", m_directory.ToString());
983             }
984
985             return StringBuilderCache.GetStringAndRelease(builtUrl).ToUpper(CultureInfo.InvariantCulture);
986         }
987
988         [System.Security.SecuritySafeCritical]  // auto-generated
989         [ResourceExposure(ResourceScope.Machine)]
990         [ResourceConsumption(ResourceScope.Machine)]
991         internal URLString SpecialNormalizeUrl()
992         {
993             // Under WinXP, file protocol urls can be mapped to
994             // drives that aren't actually file protocol underneath
995             // due to drive mounting.  This code attempts to figure
996             // out what a drive is mounted to and create the
997             // url is maps to.
998
999             DoDeferredParse();
1000             if (String.Compare( m_protocol, "file", StringComparison.OrdinalIgnoreCase) != 0)
1001             {
1002                 return this;
1003             }
1004             else
1005             {
1006                 String localSite = m_localSite.ToString();
1007
1008                 if (localSite.Length == 2 &&
1009                     (localSite[1] == '|' ||
1010                      localSite[1] == ':'))
1011                 {
1012                     String deviceName = null;
1013                     GetDeviceName(localSite, JitHelpers.GetStringHandleOnStack(ref deviceName));
1014
1015                     if (deviceName != null)
1016                     {
1017                         if (deviceName.IndexOf( "://", StringComparison.Ordinal ) != -1)
1018                         {
1019                             URLString u = new URLString( deviceName + "/" + this.m_directory.ToString() );
1020                             u.DoDeferredParse(); // Presumably the caller of SpecialNormalizeUrl wants a fully parsed URL
1021                             return u;
1022                         }
1023                         else
1024                         {
1025                             URLString u = new URLString( "file://" + deviceName + "/" + this.m_directory.ToString() );
1026                             u.DoDeferredParse();// Presumably the caller of SpecialNormalizeUrl wants a fully parsed URL
1027                             return u;
1028                         }
1029                     }
1030                     else
1031                         return this;
1032                 }
1033                 else
1034                 {
1035                     return this;
1036                 }
1037             }
1038         }
1039                 
1040         [System.Security.SecurityCritical]  // auto-generated
1041         [ResourceExposure(ResourceScope.Machine)]
1042         [DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)]
1043         [SuppressUnmanagedCodeSecurity]
1044         private static extern void GetDeviceName( String driveLetter, StringHandleOnStack retDeviceName );
1045     }
1046
1047     
1048     [Serializable]
1049     internal class DirectoryString : SiteString
1050     {
1051         private bool m_checkForIllegalChars;
1052
1053         private new static char[] m_separators = { '/' };
1054
1055         // From KB #Q177506, file/folder illegal characters are \ / : * ? " < > | 
1056         protected static char[] m_illegalDirectoryCharacters = { '\\', ':', '*', '?', '"', '<', '>', '|' };
1057         
1058         public DirectoryString()
1059         {
1060             m_site = "";
1061             m_separatedSite = new ArrayList();
1062         }
1063         
1064         public DirectoryString( String directory, bool checkForIllegalChars )
1065         {
1066             m_site = directory;
1067             m_checkForIllegalChars = checkForIllegalChars;
1068             m_separatedSite = CreateSeparatedString(directory);
1069         }
1070
1071         private ArrayList CreateSeparatedString(String directory)
1072         {
1073             if (directory == null || directory.Length == 0)
1074             {
1075                 throw new ArgumentException(Environment.GetResourceString("Argument_InvalidDirectoryOnUrl"));
1076             }
1077             Contract.EndContractBlock();
1078
1079             ArrayList list = new ArrayList();
1080             String[] separatedArray = directory.Split(m_separators);
1081             
1082             for (int index = 0; index < separatedArray.Length; ++index)
1083             {
1084                 if (separatedArray[index] == null || separatedArray[index].Equals( "" ))
1085                 {
1086                     // this case is fine, we just ignore it the extra separators.
1087                 }
1088                 else if (separatedArray[index].Equals( "*" ))
1089                 {
1090                     if (index != separatedArray.Length-1)
1091                     {
1092                         throw new ArgumentException(Environment.GetResourceString("Argument_InvalidDirectoryOnUrl"));
1093                     }
1094                     list.Add( separatedArray[index] );
1095                 }
1096                 else if (m_checkForIllegalChars && separatedArray[index].IndexOfAny( m_illegalDirectoryCharacters ) != -1)
1097                 {
1098                     throw new ArgumentException(Environment.GetResourceString("Argument_InvalidDirectoryOnUrl"));
1099                 }
1100                 else
1101                 {
1102                     list.Add( separatedArray[index] );
1103                 }
1104             }
1105             
1106             return list;
1107         }
1108         
1109         public virtual bool IsSubsetOf( DirectoryString operand )
1110         {
1111             return this.IsSubsetOf( operand, true );
1112         }
1113
1114         public virtual bool IsSubsetOf( DirectoryString operand, bool ignoreCase )
1115         {
1116             if (operand == null)
1117             {
1118                 return false;
1119             }
1120             else if (operand.m_separatedSite.Count == 0)
1121             {
1122                 return this.m_separatedSite.Count == 0 || this.m_separatedSite.Count > 0 && String.Compare((String)this.m_separatedSite[0], "*", StringComparison.Ordinal) == 0;
1123             }
1124             else if (this.m_separatedSite.Count == 0)
1125             {
1126                 return String.Compare((String)operand.m_separatedSite[0], "*", StringComparison.Ordinal) == 0;
1127             }
1128             else
1129             {
1130                 return base.IsSubsetOf( operand, ignoreCase );
1131             }
1132         }
1133     }
1134
1135     [Serializable]
1136     internal class LocalSiteString : SiteString
1137     {
1138         private new static char[] m_separators = { '/' };
1139
1140         public LocalSiteString( String site )
1141         {
1142             m_site = site.Replace( '|', ':');
1143
1144             if (m_site.Length > 2 && m_site.IndexOf( ':' ) != -1)
1145                 throw new ArgumentException(Environment.GetResourceString("Argument_InvalidDirectoryOnUrl"));
1146
1147             m_separatedSite = CreateSeparatedString(m_site);
1148         }
1149
1150         private ArrayList CreateSeparatedString(String directory)
1151         {
1152             if (directory == null || directory.Length == 0)
1153             {
1154                 throw new ArgumentException(Environment.GetResourceString("Argument_InvalidDirectoryOnUrl"));
1155             }
1156             Contract.EndContractBlock();
1157
1158             ArrayList list = new ArrayList();
1159             String[] separatedArray = directory.Split(m_separators);
1160             
1161             for (int index = 0; index < separatedArray.Length; ++index)
1162             {
1163                 if (separatedArray[index] == null || separatedArray[index].Equals( "" ))
1164                 {
1165                     if (index < 2 &&
1166                         directory[index] == '/')
1167                     {
1168                         list.Add( "//" );
1169                     }
1170                     else if (index != separatedArray.Length-1)
1171                     {
1172                         throw new ArgumentException(Environment.GetResourceString("Argument_InvalidDirectoryOnUrl"));
1173                     }
1174                 }
1175                 else if (separatedArray[index].Equals( "*" ))
1176                 {
1177                     if (index != separatedArray.Length-1)
1178                     {
1179                         throw new ArgumentException(Environment.GetResourceString("Argument_InvalidDirectoryOnUrl"));
1180                     }
1181                     list.Add( separatedArray[index] );
1182                 }
1183                 else
1184                 {
1185                     list.Add( separatedArray[index] );
1186                 }
1187             }
1188             
1189             return list;
1190         }
1191         
1192         public virtual bool IsSubsetOf( LocalSiteString operand )
1193         {
1194             return this.IsSubsetOf( operand, true );
1195         }
1196
1197         public virtual bool IsSubsetOf( LocalSiteString operand, bool ignoreCase )
1198         {
1199             if (operand == null)
1200             {
1201                 return false;
1202             }
1203             else if (operand.m_separatedSite.Count == 0)
1204             {
1205                 return this.m_separatedSite.Count == 0 || this.m_separatedSite.Count > 0 && String.Compare((String)this.m_separatedSite[0], "*", StringComparison.Ordinal) == 0;
1206             }
1207             else if (this.m_separatedSite.Count == 0)
1208             {
1209                 return String.Compare((String)operand.m_separatedSite[0], "*", StringComparison.Ordinal) == 0;
1210             }
1211             else
1212             {
1213                 return base.IsSubsetOf( operand, ignoreCase );
1214             }
1215         }
1216     }
1217 }