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