[System] Fixes UdpClient.Receive with IPv6 endpoint
[mono.git] / mcs / class / corlib / System.IO / Path.cs
1 //------------------------------------------------------------------------------
2 // 
3 // System.IO.Path.cs 
4 //
5 // Copyright (C) 2001 Moonlight Enterprises, All Rights Reserved
6 // Copyright (C) 2002 Ximian, Inc. (http://www.ximian.com)
7 // Copyright (C) 2003 Ben Maurer
8 // Copyright 2011 Xamarin Inc (http://www.xamarin.com).
9 // 
10 // Author:         Jim Richardson, develop@wtfo-guru.com
11 //                 Dan Lewis (dihlewis@yahoo.co.uk)
12 //                 Gonzalo Paniagua Javier (gonzalo@ximian.com)
13 //                 Ben Maurer (bmaurer@users.sourceforge.net)
14 //                 Sebastien Pouliot  <sebastien@ximian.com>
15 // Created:        Saturday, August 11, 2001 
16 //
17 //------------------------------------------------------------------------------
18
19 //
20 // Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
21 //
22 // Permission is hereby granted, free of charge, to any person obtaining
23 // a copy of this software and associated documentation files (the
24 // "Software"), to deal in the Software without restriction, including
25 // without limitation the rights to use, copy, modify, merge, publish,
26 // distribute, sublicense, and/or sell copies of the Software, and to
27 // permit persons to whom the Software is furnished to do so, subject to
28 // the following conditions:
29 // 
30 // The above copyright notice and this permission notice shall be
31 // included in all copies or substantial portions of the Software.
32 // 
33 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
34 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
35 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
36 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
37 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
38 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
39 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
40 //
41
42 using System.Globalization;
43 using System.Runtime.CompilerServices;
44 using System.Runtime.InteropServices;
45 using System.Security;
46 using System.Security.Cryptography;
47 using System.Security.Permissions;
48 using System.Text;
49
50 namespace System.IO {
51
52         [ComVisible (true)]
53         public static class Path {
54
55                 [Obsolete ("see GetInvalidPathChars and GetInvalidFileNameChars methods.")]
56                 public static readonly char[] InvalidPathChars;
57                 public static readonly char AltDirectorySeparatorChar;
58                 public static readonly char DirectorySeparatorChar;
59                 public static readonly char PathSeparator;
60                 internal static readonly string DirectorySeparatorStr;
61                 public static readonly char VolumeSeparatorChar;
62
63                 internal static readonly char[] PathSeparatorChars;
64                 private static readonly bool dirEqualsVolume;
65
66                 // class methods
67                 public static string ChangeExtension (string path, string extension)
68                 {
69                         if (path == null)
70                                 return null;
71
72                         if (path.IndexOfAny (InvalidPathChars) != -1)
73                                 throw new ArgumentException ("Illegal characters in path.");
74
75                         int iExt = findExtension (path);
76
77                         if (extension == null)
78                                 return iExt < 0 ? path : path.Substring (0, iExt);
79                         else if (extension.Length == 0)
80                                 return iExt < 0 ? path + '.' : path.Substring (0, iExt + 1);
81
82                         else if (path.Length != 0) {
83                                 if (extension.Length > 0 && extension [0] != '.')
84                                         extension = "." + extension;
85                         } else
86                                 extension = String.Empty;
87                         
88                         if (iExt < 0) {
89                                 return path + extension;
90                         } else if (iExt > 0) {
91                                 string temp = path.Substring (0, iExt);
92                                 return temp + extension;
93                         }
94
95                         return extension;
96                 }
97
98                 public static string Combine (string path1, string path2)
99                 {
100                         if (path1 == null)
101                                 throw new ArgumentNullException ("path1");
102
103                         if (path2 == null)
104                                 throw new ArgumentNullException ("path2");
105
106                         if (path1.Length == 0)
107                                 return path2;
108
109                         if (path2.Length == 0)
110                                 return path1;
111
112                         if (path1.IndexOfAny (InvalidPathChars) != -1)
113                                 throw new ArgumentException ("Illegal characters in path.");
114
115                         if (path2.IndexOfAny (InvalidPathChars) != -1)
116                                 throw new ArgumentException ("Illegal characters in path.");
117
118                         //TODO???: UNC names
119                         if (IsPathRooted (path2))
120                                 return path2;
121                         
122                         char p1end = path1 [path1.Length - 1];
123                         if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
124                                 return path1 + DirectorySeparatorStr + path2;
125
126                         return path1 + path2;
127                 }
128         
129                 //
130                 // This routine:
131                 //   * Removes duplicat path separators from a string
132                 //   * If the string starts with \\, preserves the first two (hostname on Windows)
133                 //   * Removes the trailing path separator.
134                 //   * Returns the DirectorySeparatorChar for the single input DirectorySeparatorChar or AltDirectorySeparatorChar
135                 //
136                 // Unlike CanonicalizePath, this does not do any path resolution
137                 // (which GetDirectoryName is not supposed to do).
138                 //
139                 internal static string CleanPath (string s)
140                 {
141                         int l = s.Length;
142                         int sub = 0;
143                         int start = 0;
144
145                         // Host prefix?
146                         char s0 = s [0];
147                         if (l > 2 && s0 == '\\' && s [1] == '\\'){
148                                 start = 2;
149                         }
150
151                         // We are only left with root
152                         if (l == 1 && (s0 == DirectorySeparatorChar || s0 == AltDirectorySeparatorChar))
153                                 return s;
154
155                         // Cleanup
156                         for (int i = start; i < l; i++){
157                                 char c = s [i];
158                                 
159                                 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
160                                         continue;
161                                 if (i+1 == l)
162                                         sub++;
163                                 else {
164                                         c = s [i + 1];
165                                         if (c == DirectorySeparatorChar || c == AltDirectorySeparatorChar)
166                                                 sub++;
167                                 }
168                         }
169
170                         if (sub == 0)
171                                 return s;
172
173                         char [] copy = new char [l-sub];
174                         if (start != 0){
175                                 copy [0] = '\\';
176                                 copy [1] = '\\';
177                         }
178                         for (int i = start, j = start; i < l && j < copy.Length; i++){
179                                 char c = s [i];
180
181                                 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar){
182                                         copy [j++] = c;
183                                         continue;
184                                 }
185
186                                 // For non-trailing cases.
187                                 if (j+1 != copy.Length){
188                                         copy [j++] = DirectorySeparatorChar;
189                                         for (;i < l-1; i++){
190                                                 c = s [i+1];
191                                                 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
192                                                         break;
193                                         }
194                                 }
195                         }
196                         return new String (copy);
197                 }
198
199                 public static string GetDirectoryName (string path)
200                 {
201                         // LAMESPEC: For empty string MS docs say both
202                         // return null AND throw exception.  Seems .NET throws.
203                         if (path == String.Empty)
204                                 throw new ArgumentException("Invalid path");
205
206                         if (path == null || GetPathRoot (path) == path)
207                                 return null;
208
209                         if (path.Trim ().Length == 0)
210                                 throw new ArgumentException ("Argument string consists of whitespace characters only.");
211
212                         if (path.IndexOfAny (System.IO.Path.InvalidPathChars) > -1)
213                                 throw new ArgumentException ("Path contains invalid characters");
214
215                         int nLast = path.LastIndexOfAny (PathSeparatorChars);
216                         if (nLast == 0)
217                                 nLast++;
218
219                         if (nLast > 0) {
220                                 string ret = path.Substring (0, nLast);
221                                 int l = ret.Length;
222
223                                 if (l >= 2 && DirectorySeparatorChar == '\\' && ret [l - 1] == VolumeSeparatorChar)
224                                         return ret + DirectorySeparatorChar;
225                                 else if (l == 1 && DirectorySeparatorChar == '\\' && path.Length >= 2 && path [nLast] == VolumeSeparatorChar)
226                                         return ret + VolumeSeparatorChar;
227                                 else {
228                                         //
229                                         // Important: do not use CanonicalizePath here, use
230                                         // the custom CleanPath here, as this should not
231                                         // return absolute paths
232                                         //
233                                         return CleanPath (ret);
234                                 }
235                         }
236
237                         return String.Empty;
238                 }
239
240                 public static string GetExtension (string path)
241                 {
242                         if (path == null)
243                                 return null;
244
245                         if (path.IndexOfAny (InvalidPathChars) != -1)
246                                 throw new ArgumentException ("Illegal characters in path.");
247
248                         int iExt = findExtension (path);
249
250                         if (iExt > -1)
251                         {
252                                 if (iExt < path.Length - 1)
253                                         return path.Substring (iExt);
254                         }
255                         return string.Empty;
256                 }
257
258                 public static string GetFileName (string path)
259                 {
260                         if (path == null || path.Length == 0)
261                                 return path;
262
263                         if (path.IndexOfAny (InvalidPathChars) != -1)
264                                 throw new ArgumentException ("Illegal characters in path.");
265
266                         int nLast = path.LastIndexOfAny (PathSeparatorChars);
267                         if (nLast >= 0)
268                                 return path.Substring (nLast + 1);
269
270                         return path;
271                 }
272
273                 public static string GetFileNameWithoutExtension (string path)
274                 {
275                         return ChangeExtension (GetFileName (path), null);
276                 }
277
278                 public static string GetFullPath (string path)
279                 {
280                         string fullpath = InsecureGetFullPath (path);
281
282                         SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
283
284 #if !NET_2_1
285                         if (SecurityManager.SecurityEnabled) {
286                                 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
287                         }
288 #endif
289                         return fullpath;
290                 }
291
292                 // http://msdn.microsoft.com/en-us/library/windows/desktop/aa364963%28v=vs.85%29.aspx
293                 [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
294                 private static extern int GetFullPathName(string path, int numBufferChars, StringBuilder buffer, ref IntPtr lpFilePartOrNull); 
295
296                 internal static string GetFullPathName(string path)
297                 {
298                         const int MAX_PATH = 260;
299                         StringBuilder buffer = new StringBuilder(MAX_PATH);
300                         IntPtr ptr = IntPtr.Zero;
301                         int length = GetFullPathName(path, MAX_PATH, buffer, ref ptr);
302                         if (length == 0)
303                         {
304                                 int error = Marshal.GetLastWin32Error();
305                                 throw new IOException("Windows API call to GetFullPathName failed, Windows error code: " + error);
306                         }
307                         else if (length > MAX_PATH)
308                         {
309                                 buffer = new StringBuilder(length);
310                                 GetFullPathName(path, length, buffer, ref ptr);
311                         }
312                         return buffer.ToString();
313                 }
314
315                 internal static string WindowsDriveAdjustment (string path)
316                 {
317                         // two special cases to consider when a drive is specified
318                         if (path.Length < 2)
319                                 return path;
320                         if ((path [1] != ':') || !Char.IsLetter (path [0]))
321                                 return path;
322
323                         string current = Directory.InsecureGetCurrentDirectory ();
324                         // first, only the drive is specified
325                         if (path.Length == 2) {
326                                 // then if the current directory is on the same drive
327                                 if (current [0] == path [0])
328                                         path = current; // we return it
329                                 else
330                                         path = GetFullPathName(path); // we have to use the GetFullPathName Windows API
331                         } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
332                                 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
333                                 // If the current directory is on the specified drive...
334                                 if (current [0] == path [0]) {
335                                         // then specified directory is appended to the current drive directory
336                                         path = Path.Combine (current, path.Substring (2, path.Length - 2));
337                                 } else {
338                                         // we have to use the GetFullPathName Windows API
339                                         path = GetFullPathName(path);
340                                 }
341                         }
342                         return path;
343                 }
344
345                 // insecure - do not call directly
346                 internal static string InsecureGetFullPath (string path)
347                 {
348                         if (path == null)
349                                 throw new ArgumentNullException ("path");
350
351                         if (path.Trim ().Length == 0) {
352                                 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
353                                 throw new ArgumentException (msg);
354                         }
355
356                         // adjust for drives, i.e. a special case for windows
357                         if (Environment.IsRunningOnWindows)
358                                 path = WindowsDriveAdjustment (path);
359
360                         // if the supplied path ends with a separator...
361                         char end = path [path.Length - 1];
362
363                         var canonicalize = true;
364                         if (path.Length >= 2 &&
365                                 IsDirectorySeparator (path [0]) &&
366                                 IsDirectorySeparator (path [1])) {
367                                 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
368                                         throw new ArgumentException ("UNC paths should be of the form \\\\server\\share.");
369
370                                 if (path [0] != DirectorySeparatorChar)
371                                         path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
372
373                         } else {
374                                 if (!IsPathRooted (path)) {
375                                         
376                                         // avoid calling expensive CanonicalizePath when possible
377                                         if (!Environment.IsRunningOnWindows) {
378                                                 var start = 0;
379                                                 while ((start = path.IndexOf ('.', start)) != -1) {
380                                                         if (++start == path.Length || path [start] == DirectorySeparatorChar || path [start] == AltDirectorySeparatorChar)
381                                                                 break;
382                                                 }
383                                                 canonicalize = start > 0;
384                                         }
385
386                                         var cwd = Directory.InsecureGetCurrentDirectory();
387                                         if (cwd [cwd.Length - 1] == DirectorySeparatorChar)
388                                                 path = cwd + path;
389                                         else
390                                                 path = cwd + DirectorySeparatorChar + path;                                     
391                                 } else if (DirectorySeparatorChar == '\\' &&
392                                         path.Length >= 2 &&
393                                         IsDirectorySeparator (path [0]) &&
394                                         !IsDirectorySeparator (path [1])) { // like `\abc\def'
395                                         string current = Directory.InsecureGetCurrentDirectory();
396                                         if (current [1] == VolumeSeparatorChar)
397                                                 path = current.Substring (0, 2) + path;
398                                         else
399                                                 path = current.Substring (0, current.IndexOf ('\\', current.IndexOfUnchecked ("\\\\", 0, current.Length) + 1));
400                                 }
401                         }
402                         
403                         if (canonicalize)
404                             path = CanonicalizePath (path);
405
406                         // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
407                         if (IsDirectorySeparator (end) && (path [path.Length - 1] != DirectorySeparatorChar))
408                                 path += DirectorySeparatorChar;
409
410                         return path;
411                 }
412
413                 internal static bool IsDirectorySeparator (char c) {
414                         return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
415                 }
416
417                 public static string GetPathRoot (string path)
418                 {
419                         if (path == null)
420                                 return null;
421
422                         if (path.Trim ().Length == 0)
423                                 throw new ArgumentException ("The specified path is not of a legal form.");
424
425                         if (!IsPathRooted (path))
426                                 return String.Empty;
427                         
428                         if (DirectorySeparatorChar == '/') {
429                                 // UNIX
430                                 return IsDirectorySeparator (path [0]) ? DirectorySeparatorStr : String.Empty;
431                         } else {
432                                 // Windows
433                                 int len = 2;
434
435                                 if (path.Length == 1 && IsDirectorySeparator (path [0]))
436                                         return DirectorySeparatorStr;
437                                 else if (path.Length < 2)
438                                         return String.Empty;
439
440                                 if (IsDirectorySeparator (path [0]) && IsDirectorySeparator (path[1])) {
441                                         // UNC: \\server or \\server\share
442                                         // Get server
443                                         while (len < path.Length && !IsDirectorySeparator (path [len])) len++;
444
445                                         // Get share
446                                         if (len < path.Length) {
447                                                 len++;
448                                                 while (len < path.Length && !IsDirectorySeparator (path [len])) len++;
449                                         }
450
451                                         return DirectorySeparatorStr +
452                                                 DirectorySeparatorStr +
453                                                 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
454                                 } else if (IsDirectorySeparator (path [0])) {
455                                         // path starts with '\' or '/'
456                                         return DirectorySeparatorStr;
457                                 } else if (path[1] == VolumeSeparatorChar) {
458                                         // C:\folder
459                                         if (path.Length >= 3 && (IsDirectorySeparator (path [2]))) len++;
460                                 } else
461                                         return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
462                                 return path.Substring (0, len);
463                         }
464                 }
465
466                 // FIXME: Further limit the assertion when imperative Assert is implemented
467                 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
468                 public static string GetTempFileName ()
469                 {
470                         FileStream f = null;
471                         string path;
472                         Random rnd;
473                         int num;
474                         int count = 0;
475
476                         SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
477
478                         rnd = new Random ();
479                         var tmp_path = GetTempPath ();
480                         do {
481                                 num = rnd.Next ();
482                                 num++;
483                                 path = Path.Combine (tmp_path, "tmp" + num.ToString ("x", CultureInfo.InvariantCulture) + ".tmp");
484
485                                 try {
486                                         f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
487                                                             8192, false, (FileOptions) 1);
488                                 } catch (IOException ex){
489                                         if (ex.hresult != MonoIO.FileAlreadyExistsHResult || count ++ > 65536)
490                                                 throw;
491                                 } catch (UnauthorizedAccessException ex) {
492                                         if (count ++ > 65536)
493                                                 throw new IOException (ex.Message, ex);
494                                 }
495                         } while (f == null);
496                         
497                         f.Close();
498                         return path;
499                 }
500
501                 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
502                 public static string GetTempPath ()
503                 {
504                         SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
505
506                         string p = get_temp_path ();
507                         if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
508                                 return p + DirectorySeparatorChar;
509
510                         return p;
511                 }
512
513                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
514                 private static extern string get_temp_path ();
515
516                 public static bool HasExtension (string path)
517                 {
518                         if (path == null || path.Trim ().Length == 0)
519                                 return false;
520
521                         if (path.IndexOfAny (InvalidPathChars) != -1)
522                                 throw new ArgumentException ("Illegal characters in path.");
523
524                         int pos = findExtension (path);
525                         return 0 <= pos && pos < path.Length - 1;
526                 }
527
528                 public static bool IsPathRooted (string path)
529                 {
530                         if (path == null || path.Length == 0)
531                                 return false;
532
533                         if (path.IndexOfAny (InvalidPathChars) != -1)
534                                 throw new ArgumentException ("Illegal characters in path.");
535
536                         char c = path [0];
537                         return (c == DirectorySeparatorChar     ||
538                                 c == AltDirectorySeparatorChar  ||
539                                 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
540                 }
541
542                 public static char[] GetInvalidFileNameChars ()
543                 {
544                         // return a new array as we do not want anyone to be able to change the values
545                         if (Environment.IsRunningOnWindows) {
546                                 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
547                                         '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', 
548                                         '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', 
549                                         '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
550                         } else {
551                                 return new char [2] { '\x00', '/' };
552                         }
553                 }
554
555                 public static char[] GetInvalidPathChars ()
556                 {
557                         // return a new array as we do not want anyone to be able to change the values
558                         if (Environment.IsRunningOnWindows) {
559                                 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
560                                         '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', 
561                                         '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', 
562                                         '\x1E', '\x1F' };
563                         } else {
564                                 return new char [1] { '\x00' };
565                         }
566                 }
567
568                 public static string GetRandomFileName ()
569                 {
570                         // returns a 8.3 filename (total size 12)
571                         StringBuilder sb = new StringBuilder (12);
572                         // using strong crypto but without creating the file
573                         RandomNumberGenerator rng = RandomNumberGenerator.Create ();
574                         byte [] buffer = new byte [11];
575                         rng.GetBytes (buffer);
576
577                         for (int i = 0; i < buffer.Length; i++) {
578                                 if (sb.Length == 8)
579                                         sb.Append ('.');
580
581                                 // restrict to length of range [a..z0..9]
582                                 int b = (buffer [i] % 36);
583                                 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
584                                 sb.Append (c);
585                         }
586
587                         return sb.ToString ();
588                 }
589
590                 // private class methods
591
592                 private static int findExtension (string path)
593                 {
594                         // method should return the index of the path extension
595                         // start or -1 if no valid extension
596                         if (path != null){
597                                 int iLastDot = path.LastIndexOf ('.');
598                                 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
599
600                                 if (iLastDot > iLastSep)
601                                         return iLastDot;
602                         }
603                         return -1;
604                 }
605
606                 static Path ()
607                 {
608                         VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
609                         DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
610                         AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
611
612                         PathSeparator = MonoIO.PathSeparator;
613                         // this copy will be modifiable ("by design")
614                         InvalidPathChars = GetInvalidPathChars ();
615                         // internal fields
616
617                         DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
618                         PathSeparatorChars = new char [] {
619                                 DirectorySeparatorChar,
620                                 AltDirectorySeparatorChar,
621                                 VolumeSeparatorChar
622                         };
623
624                         dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
625                 }
626
627                 // returns the server and share part of a UNC. Assumes "path" is a UNC.
628                 static string GetServerAndShare (string path)
629                 {
630                         int len = 2;
631                         while (len < path.Length && !IsDirectorySeparator (path [len])) len++;
632
633                         if (len < path.Length) {
634                                 len++;
635                                 while (len < path.Length && !IsDirectorySeparator (path [len])) len++;
636                         }
637
638                         return path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
639                 }
640
641                 // assumes Environment.IsRunningOnWindows == true
642                 static bool SameRoot (string root, string path)
643                 {
644                         // compare root - if enough details are available
645                         if ((root.Length < 2) || (path.Length < 2))
646                                 return false;
647
648                         // UNC handling
649                         if (IsDirectorySeparator (root[0]) && IsDirectorySeparator (root[1])) {
650                                 if (!(IsDirectorySeparator (path[0]) && IsDirectorySeparator (path[1])))
651                                         return false;
652
653                                 string rootShare = GetServerAndShare (root);
654                                 string pathShare = GetServerAndShare (path);
655
656                                 return String.Compare (rootShare, pathShare, true, CultureInfo.InvariantCulture) == 0;
657                         }
658                         
659                         // same volume/drive
660                         if (!root [0].Equals (path [0]))
661                                 return false;
662                         // presence of the separator
663                         if (path[1] != Path.VolumeSeparatorChar)
664                                 return false;
665                         if ((root.Length > 2) && (path.Length > 2)) {
666                                 // but don't directory compare the directory separator
667                                 return (IsDirectorySeparator (root[2]) && IsDirectorySeparator (path[2]));
668                         }
669                         return true;
670                 }
671
672                 static string CanonicalizePath (string path)
673                 {
674                         // STEP 1: Check for empty string
675                         if (path == null)
676                                 return path;
677                         if (Environment.IsRunningOnWindows)
678                                 path = path.Trim ();
679
680                         if (path.Length == 0)
681                                 return path;
682
683                         // STEP 2: Check to see if this is only a root
684                         string root = Path.GetPathRoot (path);
685                         // it will return '\' for path '\', while it should return 'c:\' or so.
686                         // Note: commenting this out makes the need for the (target == 1...) check in step 5
687                         //if (root == path) return path;
688
689                         // STEP 3: split the directories, this gets rid of consecutative "/"'s
690                         string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
691                         // STEP 4: Get rid of directories containing . and ..
692                         int target = 0;
693
694                         bool isUnc = Environment.IsRunningOnWindows &&
695                                 root.Length > 2 && IsDirectorySeparator (root[0]) && IsDirectorySeparator (root[1]);
696
697                         // Set an overwrite limit for UNC paths since '\' + server + share
698                         // must not be eliminated by the '..' elimination algorithm.
699                         int limit = isUnc ? 3 : 0;
700
701                         for (int i = 0; i < dirs.Length; i++) {
702                                 // WIN32 path components must be trimmed
703                                 if (Environment.IsRunningOnWindows)
704                                         dirs[i] = dirs[i].TrimEnd ();
705                                 
706                                 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
707                                         continue;
708                                 else if (dirs[i] == "..") {
709                                         // don't overwrite path segments below the limit
710                                         if (target > limit)
711                                                 target--;
712                                 } else
713                                         dirs[target++] = dirs[i];
714                         }
715
716                         // STEP 5: Combine everything.
717                         if (target == 0 || (target == 1 && dirs[0] == ""))
718                                 return root;
719                         else {
720                                 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
721                                 if (Environment.IsRunningOnWindows) {
722                                         // append leading '\' of the UNC path that was lost in STEP 3.
723                                         if (isUnc)
724                                                 ret = Path.DirectorySeparatorStr + ret;
725
726                                         if (!SameRoot (root, ret))
727                                                 ret = root + ret;
728
729                                         if (isUnc) {
730                                                 return ret;
731                                         } else if (!IsDirectorySeparator (path[0]) && SameRoot (root, path)) {
732                                                 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
733                                                         ret += Path.DirectorySeparatorChar;
734                                                 return ret;
735                                         } else {
736                                                 string current = Directory.GetCurrentDirectory ();
737                                                 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
738                                                         // DOS local file path
739                                                         if (ret.Length == 0 || IsDirectorySeparator (ret[0]))
740                                                                 ret += '\\';
741                                                         return current.Substring (0, 2) + ret;
742                                                 } else if (IsDirectorySeparator (current[current.Length - 1]) && IsDirectorySeparator (ret[0]))
743                                                         return current + ret.Substring (1);
744                                                 else
745                                                         return current + ret;
746                                         }
747                                 } else {
748                                         if (root != "" && ret.Length > 0 && ret [0] != '/')
749                                                 ret = root + ret;
750                                 }
751                                 return ret;
752                         }
753                 }
754
755                 // required for FileIOPermission (and most proibably reusable elsewhere too)
756                 // both path MUST be "full paths"
757                 static internal bool IsPathSubsetOf (string subset, string path)
758                 {
759                         if (subset.Length > path.Length)
760                                 return false;
761
762                         // check that everything up to the last separator match
763                         int slast = subset.LastIndexOfAny (PathSeparatorChars);
764                         if (String.Compare (subset, 0, path, 0, slast) != 0)
765                                 return false;
766
767                         slast++;
768                         // then check if the last segment is identical
769                         int plast = path.IndexOfAny (PathSeparatorChars, slast);
770                         if (plast >= slast) {
771                                 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
772                         }
773                         if (subset.Length != path.Length)
774                                 return false;
775
776                         return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;
777                 }
778
779                 public
780                 static string Combine (params string [] paths)
781                 {
782                         if (paths == null)
783                                 throw new ArgumentNullException ("paths");
784
785                         bool need_sep;
786                         var ret = new StringBuilder ();
787                         int pathsLen = paths.Length;
788                         int slen;
789                         need_sep = false;
790
791                         foreach (var s in paths) {
792                                 if (s == null)
793                                         throw new ArgumentNullException ("One of the paths contains a null value", "paths");
794                                 if (s.Length == 0)
795                                         continue;
796                                 if (s.IndexOfAny (InvalidPathChars) != -1)
797                                         throw new ArgumentException ("Illegal characters in path.");
798
799                                 if (need_sep) {
800                                         need_sep = false;
801                                         ret.Append (DirectorySeparatorStr);
802                                 }
803
804                                 pathsLen--;
805                                 if (IsPathRooted (s))
806                                         ret.Length = 0;
807                                 
808                                 ret.Append (s);
809                                 slen = s.Length;
810                                 if (slen > 0 && pathsLen > 0) {
811                                         char p1end = s [slen - 1];
812                                         if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
813                                                 need_sep = true;
814                                 }
815                         }
816
817                         return ret.ToString ();
818                 }
819
820                 public
821                 static string Combine (string path1, string path2, string path3)
822                 {
823                         if (path1 == null)
824                                 throw new ArgumentNullException ("path1");
825
826                         if (path2 == null)
827                                 throw new ArgumentNullException ("path2");
828
829                         if (path3 == null)
830                                 throw new ArgumentNullException ("path3");
831                         
832                         return Combine (new string [] { path1, path2, path3 });
833                 }
834
835                 public
836                 static string Combine (string path1, string path2, string path3, string path4)
837                 {
838                         if (path1 == null)
839                                 throw new ArgumentNullException ("path1");
840
841                         if (path2 == null)
842                                 throw new ArgumentNullException ("path2");
843
844                         if (path3 == null)
845                                 throw new ArgumentNullException ("path3");
846
847                         if (path4 == null)
848                                 throw new ArgumentNullException ("path4");
849                         
850                         return Combine (new string [] { path1, path2, path3, path4 });
851                 }
852
853                 internal static void Validate (string path)
854                 {
855                         Validate (path, "path");
856                 }
857
858                 internal static void Validate (string path, string parameterName)
859                 {
860                         if (path == null)
861                                 throw new ArgumentNullException (parameterName);
862                         if (String.IsNullOrWhiteSpace (path))
863                                 throw new ArgumentException (Locale.GetText ("Path is empty"));
864                         if (path.IndexOfAny (Path.InvalidPathChars) != -1)
865                                 throw new ArgumentException (Locale.GetText ("Path contains invalid chars"));
866                         if (Environment.IsRunningOnWindows) {
867                                 int idx = path.IndexOf (':');
868                                 if (idx >= 0 && idx != 1)
869                                         throw new ArgumentException (parameterName);
870                         }
871                 }
872         }
873 }