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