Merge pull request #2034 from alexrp/ctx-cleanup
[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 #if !MOBILE
293                 // http://msdn.microsoft.com/en-us/library/windows/desktop/aa364963%28v=vs.85%29.aspx
294                 [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
295                 private static extern int GetFullPathName(string path, int numBufferChars, StringBuilder buffer, ref IntPtr lpFilePartOrNull); 
296
297                 internal static string GetFullPathName(string path)
298                 {
299                         const int MAX_PATH = 260;
300                         StringBuilder buffer = new StringBuilder(MAX_PATH);
301                         IntPtr ptr = IntPtr.Zero;
302                         int length = GetFullPathName(path, MAX_PATH, buffer, ref ptr);
303                         if (length == 0)
304                         {
305                                 int error = Marshal.GetLastWin32Error();
306                                 throw new IOException("Windows API call to GetFullPathName failed, Windows error code: " + error);
307                         }
308                         else if (length > MAX_PATH)
309                         {
310                                 buffer = new StringBuilder(length);
311                                 GetFullPathName(path, length, buffer, ref ptr);
312                         }
313                         return buffer.ToString();
314                 }
315
316                 internal static string WindowsDriveAdjustment (string path)
317                 {
318                         // two special cases to consider when a drive is specified
319                         if (path.Length < 2)
320                                 return path;
321                         if ((path [1] != ':') || !Char.IsLetter (path [0]))
322                                 return path;
323
324                         string current = Directory.InsecureGetCurrentDirectory ();
325                         // first, only the drive is specified
326                         if (path.Length == 2) {
327                                 // then if the current directory is on the same drive
328                                 if (current [0] == path [0])
329                                         path = current; // we return it
330                                 else
331                                         path = GetFullPathName(path); // we have to use the GetFullPathName Windows API
332                         } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
333                                 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
334                                 // If the current directory is on the specified drive...
335                                 if (current [0] == path [0]) {
336                                         // then specified directory is appended to the current drive directory
337                                         path = Path.Combine (current, path.Substring (2, path.Length - 2));
338                                 } else {
339                                         // we have to use the GetFullPathName Windows API
340                                         path = GetFullPathName(path);
341                                 }
342                         }
343                         return path;
344                 }
345 #endif
346
347                 // insecure - do not call directly
348                 internal static string InsecureGetFullPath (string path)
349                 {
350                         if (path == null)
351                                 throw new ArgumentNullException ("path");
352
353                         if (path.Trim ().Length == 0) {
354                                 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
355                                 throw new ArgumentException (msg);
356                         }
357 #if !MOBILE
358                         // adjust for drives, i.e. a special case for windows
359                         if (Environment.IsRunningOnWindows)
360                                 path = WindowsDriveAdjustment (path);
361 #endif
362                         // if the supplied path ends with a separator...
363                         char end = path [path.Length - 1];
364
365                         var canonicalize = true;
366                         if (path.Length >= 2 &&
367                                 IsDirectorySeparator (path [0]) &&
368                                 IsDirectorySeparator (path [1])) {
369                                 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
370                                         throw new ArgumentException ("UNC paths should be of the form \\\\server\\share.");
371
372                                 if (path [0] != DirectorySeparatorChar)
373                                         path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
374
375                         } else {
376                                 if (!IsPathRooted (path)) {
377                                         
378                                         // avoid calling expensive CanonicalizePath when possible
379                                         if (!Environment.IsRunningOnWindows) {
380                                                 var start = 0;
381                                                 while ((start = path.IndexOf ('.', start)) != -1) {
382                                                         if (++start == path.Length || path [start] == DirectorySeparatorChar || path [start] == AltDirectorySeparatorChar)
383                                                                 break;
384                                                 }
385                                                 canonicalize = start > 0;
386                                         }
387
388                                         var cwd = Directory.InsecureGetCurrentDirectory();
389                                         if (cwd [cwd.Length - 1] == DirectorySeparatorChar)
390                                                 path = cwd + path;
391                                         else
392                                                 path = cwd + DirectorySeparatorChar + path;                                     
393                                 } else if (DirectorySeparatorChar == '\\' &&
394                                         path.Length >= 2 &&
395                                         IsDirectorySeparator (path [0]) &&
396                                         !IsDirectorySeparator (path [1])) { // like `\abc\def'
397                                         string current = Directory.InsecureGetCurrentDirectory();
398                                         if (current [1] == VolumeSeparatorChar)
399                                                 path = current.Substring (0, 2) + path;
400                                         else
401                                                 path = current.Substring (0, current.IndexOf ('\\', current.IndexOfUnchecked ("\\\\", 0, current.Length) + 1));
402                                 }
403                         }
404                         
405                         if (canonicalize)
406                             path = CanonicalizePath (path);
407
408                         // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
409                         if (IsDirectorySeparator (end) && (path [path.Length - 1] != DirectorySeparatorChar))
410                                 path += DirectorySeparatorChar;
411
412                         return path;
413                 }
414
415                 internal static bool IsDirectorySeparator (char c) {
416                         return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
417                 }
418
419                 public static string GetPathRoot (string path)
420                 {
421                         if (path == null)
422                                 return null;
423
424                         if (path.Trim ().Length == 0)
425                                 throw new ArgumentException ("The specified path is not of a legal form.");
426
427                         if (!IsPathRooted (path))
428                                 return String.Empty;
429                         
430                         if (DirectorySeparatorChar == '/') {
431                                 // UNIX
432                                 return IsDirectorySeparator (path [0]) ? DirectorySeparatorStr : String.Empty;
433                         } else {
434                                 // Windows
435                                 int len = 2;
436
437                                 if (path.Length == 1 && IsDirectorySeparator (path [0]))
438                                         return DirectorySeparatorStr;
439                                 else if (path.Length < 2)
440                                         return String.Empty;
441
442                                 if (IsDirectorySeparator (path [0]) && IsDirectorySeparator (path[1])) {
443                                         // UNC: \\server or \\server\share
444                                         // Get server
445                                         while (len < path.Length && !IsDirectorySeparator (path [len])) len++;
446
447                                         // Get share
448                                         if (len < path.Length) {
449                                                 len++;
450                                                 while (len < path.Length && !IsDirectorySeparator (path [len])) len++;
451                                         }
452
453                                         return DirectorySeparatorStr +
454                                                 DirectorySeparatorStr +
455                                                 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
456                                 } else if (IsDirectorySeparator (path [0])) {
457                                         // path starts with '\' or '/'
458                                         return DirectorySeparatorStr;
459                                 } else if (path[1] == VolumeSeparatorChar) {
460                                         // C:\folder
461                                         if (path.Length >= 3 && (IsDirectorySeparator (path [2]))) len++;
462                                 } else
463                                         return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
464                                 return path.Substring (0, len);
465                         }
466                 }
467
468                 // FIXME: Further limit the assertion when imperative Assert is implemented
469                 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
470                 public static string GetTempFileName ()
471                 {
472                         FileStream f = null;
473                         string path;
474                         Random rnd;
475                         int num;
476                         int count = 0;
477
478                         SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
479
480                         rnd = new Random ();
481                         var tmp_path = GetTempPath ();
482                         do {
483                                 num = rnd.Next ();
484                                 num++;
485                                 path = Path.Combine (tmp_path, "tmp" + num.ToString ("x", CultureInfo.InvariantCulture) + ".tmp");
486
487                                 try {
488                                         f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
489                                                             8192, false, (FileOptions) 1);
490                                 } catch (IOException ex){
491                                         if (ex.hresult != MonoIO.FileAlreadyExistsHResult || count ++ > 65536)
492                                                 throw;
493                                 } catch (UnauthorizedAccessException ex) {
494                                         if (count ++ > 65536)
495                                                 throw new IOException (ex.Message, ex);
496                                 }
497                         } while (f == null);
498                         
499                         f.Close();
500                         return path;
501                 }
502
503                 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
504                 public static string GetTempPath ()
505                 {
506                         SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
507
508                         string p = get_temp_path ();
509                         if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
510                                 return p + DirectorySeparatorChar;
511
512                         return p;
513                 }
514
515                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
516                 private static extern string get_temp_path ();
517
518                 public static bool HasExtension (string path)
519                 {
520                         if (path == null || path.Trim ().Length == 0)
521                                 return false;
522
523                         if (path.IndexOfAny (InvalidPathChars) != -1)
524                                 throw new ArgumentException ("Illegal characters in path.");
525
526                         int pos = findExtension (path);
527                         return 0 <= pos && pos < path.Length - 1;
528                 }
529
530                 public static bool IsPathRooted (string path)
531                 {
532                         if (path == null || path.Length == 0)
533                                 return false;
534
535                         if (path.IndexOfAny (InvalidPathChars) != -1)
536                                 throw new ArgumentException ("Illegal characters in path.");
537
538                         char c = path [0];
539                         return (c == DirectorySeparatorChar     ||
540                                 c == AltDirectorySeparatorChar  ||
541                                 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
542                 }
543
544                 public static char[] GetInvalidFileNameChars ()
545                 {
546                         // return a new array as we do not want anyone to be able to change the values
547                         if (Environment.IsRunningOnWindows) {
548                                 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
549                                         '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', 
550                                         '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', 
551                                         '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
552                         } else {
553                                 return new char [2] { '\x00', '/' };
554                         }
555                 }
556
557                 public static char[] GetInvalidPathChars ()
558                 {
559                         // return a new array as we do not want anyone to be able to change the values
560                         if (Environment.IsRunningOnWindows) {
561                                 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
562                                         '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', 
563                                         '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', 
564                                         '\x1E', '\x1F' };
565                         } else {
566                                 return new char [1] { '\x00' };
567                         }
568                 }
569
570                 public static string GetRandomFileName ()
571                 {
572                         // returns a 8.3 filename (total size 12)
573                         StringBuilder sb = new StringBuilder (12);
574                         // using strong crypto but without creating the file
575                         RandomNumberGenerator rng = RandomNumberGenerator.Create ();
576                         byte [] buffer = new byte [11];
577                         rng.GetBytes (buffer);
578
579                         for (int i = 0; i < buffer.Length; i++) {
580                                 if (sb.Length == 8)
581                                         sb.Append ('.');
582
583                                 // restrict to length of range [a..z0..9]
584                                 int b = (buffer [i] % 36);
585                                 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
586                                 sb.Append (c);
587                         }
588
589                         return sb.ToString ();
590                 }
591
592                 // private class methods
593
594                 private static int findExtension (string path)
595                 {
596                         // method should return the index of the path extension
597                         // start or -1 if no valid extension
598                         if (path != null){
599                                 int iLastDot = path.LastIndexOf ('.');
600                                 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
601
602                                 if (iLastDot > iLastSep)
603                                         return iLastDot;
604                         }
605                         return -1;
606                 }
607
608                 static Path ()
609                 {
610                         VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
611                         DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
612                         AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
613
614                         PathSeparator = MonoIO.PathSeparator;
615                         // this copy will be modifiable ("by design")
616                         InvalidPathChars = GetInvalidPathChars ();
617                         // internal fields
618
619                         DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
620                         PathSeparatorChars = new char [] {
621                                 DirectorySeparatorChar,
622                                 AltDirectorySeparatorChar,
623                                 VolumeSeparatorChar
624                         };
625
626                         dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
627                 }
628
629                 // returns the server and share part of a UNC. Assumes "path" is a UNC.
630                 static string GetServerAndShare (string path)
631                 {
632                         int len = 2;
633                         while (len < path.Length && !IsDirectorySeparator (path [len])) len++;
634
635                         if (len < path.Length) {
636                                 len++;
637                                 while (len < path.Length && !IsDirectorySeparator (path [len])) len++;
638                         }
639
640                         return path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
641                 }
642
643                 // assumes Environment.IsRunningOnWindows == true
644                 static bool SameRoot (string root, string path)
645                 {
646                         // compare root - if enough details are available
647                         if ((root.Length < 2) || (path.Length < 2))
648                                 return false;
649
650                         // UNC handling
651                         if (IsDirectorySeparator (root[0]) && IsDirectorySeparator (root[1])) {
652                                 if (!(IsDirectorySeparator (path[0]) && IsDirectorySeparator (path[1])))
653                                         return false;
654
655                                 string rootShare = GetServerAndShare (root);
656                                 string pathShare = GetServerAndShare (path);
657
658                                 return String.Compare (rootShare, pathShare, true, CultureInfo.InvariantCulture) == 0;
659                         }
660                         
661                         // same volume/drive
662                         if (!root [0].Equals (path [0]))
663                                 return false;
664                         // presence of the separator
665                         if (path[1] != Path.VolumeSeparatorChar)
666                                 return false;
667                         if ((root.Length > 2) && (path.Length > 2)) {
668                                 // but don't directory compare the directory separator
669                                 return (IsDirectorySeparator (root[2]) && IsDirectorySeparator (path[2]));
670                         }
671                         return true;
672                 }
673
674                 static string CanonicalizePath (string path)
675                 {
676                         // STEP 1: Check for empty string
677                         if (path == null)
678                                 return path;
679                         if (Environment.IsRunningOnWindows)
680                                 path = path.Trim ();
681
682                         if (path.Length == 0)
683                                 return path;
684
685                         // STEP 2: Check to see if this is only a root
686                         string root = Path.GetPathRoot (path);
687                         // it will return '\' for path '\', while it should return 'c:\' or so.
688                         // Note: commenting this out makes the need for the (target == 1...) check in step 5
689                         //if (root == path) return path;
690
691                         // STEP 3: split the directories, this gets rid of consecutative "/"'s
692                         string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
693                         // STEP 4: Get rid of directories containing . and ..
694                         int target = 0;
695
696                         bool isUnc = Environment.IsRunningOnWindows &&
697                                 root.Length > 2 && IsDirectorySeparator (root[0]) && IsDirectorySeparator (root[1]);
698
699                         // Set an overwrite limit for UNC paths since '\' + server + share
700                         // must not be eliminated by the '..' elimination algorithm.
701                         int limit = isUnc ? 3 : 0;
702
703                         for (int i = 0; i < dirs.Length; i++) {
704                                 // WIN32 path components must be trimmed
705                                 if (Environment.IsRunningOnWindows)
706                                         dirs[i] = dirs[i].TrimEnd ();
707                                 
708                                 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
709                                         continue;
710                                 else if (dirs[i] == "..") {
711                                         // don't overwrite path segments below the limit
712                                         if (target > limit)
713                                                 target--;
714                                 } else
715                                         dirs[target++] = dirs[i];
716                         }
717
718                         // STEP 5: Combine everything.
719                         if (target == 0 || (target == 1 && dirs[0] == ""))
720                                 return root;
721                         else {
722                                 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
723                                 if (Environment.IsRunningOnWindows) {
724                                         // append leading '\' of the UNC path that was lost in STEP 3.
725                                         if (isUnc)
726                                                 ret = Path.DirectorySeparatorStr + ret;
727
728                                         if (!SameRoot (root, ret))
729                                                 ret = root + ret;
730
731                                         if (isUnc) {
732                                                 return ret;
733                                         } else if (!IsDirectorySeparator (path[0]) && SameRoot (root, path)) {
734                                                 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
735                                                         ret += Path.DirectorySeparatorChar;
736                                                 return ret;
737                                         } else {
738                                                 string current = Directory.GetCurrentDirectory ();
739                                                 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
740                                                         // DOS local file path
741                                                         if (ret.Length == 0 || IsDirectorySeparator (ret[0]))
742                                                                 ret += '\\';
743                                                         return current.Substring (0, 2) + ret;
744                                                 } else if (IsDirectorySeparator (current[current.Length - 1]) && IsDirectorySeparator (ret[0]))
745                                                         return current + ret.Substring (1);
746                                                 else
747                                                         return current + ret;
748                                         }
749                                 } else {
750                                         if (root != "" && ret.Length > 0 && ret [0] != '/')
751                                                 ret = root + ret;
752                                 }
753                                 return ret;
754                         }
755                 }
756
757                 // required for FileIOPermission (and most proibably reusable elsewhere too)
758                 // both path MUST be "full paths"
759                 static internal bool IsPathSubsetOf (string subset, string path)
760                 {
761                         if (subset.Length > path.Length)
762                                 return false;
763
764                         // check that everything up to the last separator match
765                         int slast = subset.LastIndexOfAny (PathSeparatorChars);
766                         if (String.Compare (subset, 0, path, 0, slast) != 0)
767                                 return false;
768
769                         slast++;
770                         // then check if the last segment is identical
771                         int plast = path.IndexOfAny (PathSeparatorChars, slast);
772                         if (plast >= slast) {
773                                 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
774                         }
775                         if (subset.Length != path.Length)
776                                 return false;
777
778                         return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;
779                 }
780
781                 public
782                 static string Combine (params string [] paths)
783                 {
784                         if (paths == null)
785                                 throw new ArgumentNullException ("paths");
786
787                         bool need_sep;
788                         var ret = new StringBuilder ();
789                         int pathsLen = paths.Length;
790                         int slen;
791                         need_sep = false;
792
793                         foreach (var s in paths) {
794                                 if (s == null)
795                                         throw new ArgumentNullException ("One of the paths contains a null value", "paths");
796                                 if (s.Length == 0)
797                                         continue;
798                                 if (s.IndexOfAny (InvalidPathChars) != -1)
799                                         throw new ArgumentException ("Illegal characters in path.");
800
801                                 if (need_sep) {
802                                         need_sep = false;
803                                         ret.Append (DirectorySeparatorStr);
804                                 }
805
806                                 pathsLen--;
807                                 if (IsPathRooted (s))
808                                         ret.Length = 0;
809                                 
810                                 ret.Append (s);
811                                 slen = s.Length;
812                                 if (slen > 0 && pathsLen > 0) {
813                                         char p1end = s [slen - 1];
814                                         if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
815                                                 need_sep = true;
816                                 }
817                         }
818
819                         return ret.ToString ();
820                 }
821
822                 public
823                 static string Combine (string path1, string path2, string path3)
824                 {
825                         if (path1 == null)
826                                 throw new ArgumentNullException ("path1");
827
828                         if (path2 == null)
829                                 throw new ArgumentNullException ("path2");
830
831                         if (path3 == null)
832                                 throw new ArgumentNullException ("path3");
833                         
834                         return Combine (new string [] { path1, path2, path3 });
835                 }
836
837                 public
838                 static string Combine (string path1, string path2, string path3, string path4)
839                 {
840                         if (path1 == null)
841                                 throw new ArgumentNullException ("path1");
842
843                         if (path2 == null)
844                                 throw new ArgumentNullException ("path2");
845
846                         if (path3 == null)
847                                 throw new ArgumentNullException ("path3");
848
849                         if (path4 == null)
850                                 throw new ArgumentNullException ("path4");
851                         
852                         return Combine (new string [] { path1, path2, path3, path4 });
853                 }
854
855                 internal static void Validate (string path)
856                 {
857                         Validate (path, "path");
858                 }
859
860                 internal static void Validate (string path, string parameterName)
861                 {
862                         if (path == null)
863                                 throw new ArgumentNullException (parameterName);
864                         if (String.IsNullOrWhiteSpace (path))
865                                 throw new ArgumentException (Locale.GetText ("Path is empty"));
866                         if (path.IndexOfAny (Path.InvalidPathChars) != -1)
867                                 throw new ArgumentException (Locale.GetText ("Path contains invalid chars"));
868                         if (Environment.IsRunningOnWindows) {
869                                 int idx = path.IndexOf (':');
870                                 if (idx >= 0 && idx != 1)
871                                         throw new ArgumentException (parameterName);
872                         }
873                 }
874         }
875 }