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