fixed tests
[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 // 
9 // Author:         Jim Richardson, develop@wtfo-guru.com
10 //                 Dan Lewis (dihlewis@yahoo.co.uk)
11 //                 Gonzalo Paniagua Javier (gonzalo@ximian.com)
12 //                 Ben Maurer (bmaurer@users.sourceforge.net)
13 //                 Sebastien Pouliot  <sebastien@ximian.com>
14 // Created:        Saturday, August 11, 2001 
15 //
16 //------------------------------------------------------------------------------
17
18 //
19 // Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
20 //
21 // Permission is hereby granted, free of charge, to any person obtaining
22 // a copy of this software and associated documentation files (the
23 // "Software"), to deal in the Software without restriction, including
24 // without limitation the rights to use, copy, modify, merge, publish,
25 // distribute, sublicense, and/or sell copies of the Software, and to
26 // permit persons to whom the Software is furnished to do so, subject to
27 // the following conditions:
28 // 
29 // The above copyright notice and this permission notice shall be
30 // included in all copies or substantial portions of the Software.
31 // 
32 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
33 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
34 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
35 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
36 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
37 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
38 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
39 //
40
41 using System.Globalization;
42 using System.Runtime.CompilerServices;
43 using System.Runtime.InteropServices;
44 using System.Security;
45 using System.Security.Cryptography;
46 using System.Security.Permissions;
47 using System.Text;
48
49 namespace System.IO {
50
51 #if NET_2_0
52         [ComVisible (true)]
53         public static class Path {
54
55                 [Obsolete ("see GetInvalidPathChars and GetInvalidFileNameChars methods.")]
56                 public static readonly char[] InvalidPathChars;
57 #else
58         public sealed class Path {
59
60                 private Path ()
61                 {
62                 }
63
64                 public static readonly char[] InvalidPathChars;
65 #endif
66                 public static readonly char AltDirectorySeparatorChar;
67                 public static readonly char DirectorySeparatorChar;
68                 public static readonly char PathSeparator;
69                 internal static readonly string DirectorySeparatorStr;
70                 public static readonly char VolumeSeparatorChar;
71
72                 private static readonly char[] PathSeparatorChars;
73                 private static readonly bool dirEqualsVolume;
74
75                 // class methods
76                 public static string ChangeExtension (string path, string extension)
77                 {
78                         if (path == null)
79                                 return null;
80
81                         if (path.IndexOfAny (InvalidPathChars) != -1)
82                                 throw new ArgumentException ("Illegal characters in path", "path");
83
84                         int iExt = findExtension (path);
85
86                         if (extension == null)
87                                 return iExt < 0 ? path : path.Substring (0, iExt);
88                         else if (extension == String.Empty)
89                                 return iExt < 0 ? path + '.' : path.Substring (0, iExt + 1);
90
91                         else if (path.Length != 0) {
92                                 if (extension.Length > 0 && extension [0] != '.')
93                                         extension = "." + extension;
94                         } else
95                                 extension = String.Empty;
96                         
97                         if (iExt < 0) {
98                                 return path + extension;
99                         } else if (iExt > 0) {
100                                 string temp = path.Substring (0, iExt);
101                                 return temp + extension;
102                         }
103
104                         return extension;
105                 }
106
107                 public static string Combine (string path1, string path2)
108                 {
109                         if (path1 == null)
110                                 throw new ArgumentNullException ("path1");
111
112                         if (path2 == null)
113                                 throw new ArgumentNullException ("path2");
114
115                         if (path1 == String.Empty)
116                                 return path2;
117
118                         if (path2 == String.Empty)
119                                 return path1;
120
121                         if (path1.IndexOfAny (InvalidPathChars) != -1)
122                                 throw new ArgumentException ("Illegal characters in path", "path1");
123
124                         if (path2.IndexOfAny (InvalidPathChars) != -1)
125                                 throw new ArgumentException ("Illegal characters in path", "path2");
126
127                         //TODO???: UNC names
128                         // LAMESPEC: MS says that if path1 is not empty and path2 is a full path
129                         // it should throw ArgumentException
130                         if (IsPathRooted (path2))
131                                 return path2;
132                         
133                         char p1end = path1 [path1.Length - 1];
134                         if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
135                                 return path1 + DirectorySeparatorChar + path2;
136
137                         return path1 + path2;
138                 }
139
140                 public static string GetDirectoryName (string path)
141                 {
142                         // LAMESPEC: For empty string MS docs say both
143                         // return null AND throw exception.  Seems .NET throws.
144                         if (path == String.Empty)
145                                 throw new ArgumentException();
146
147                         if (path == null || GetPathRoot (path) == path)
148                                 return null;
149
150                         CheckArgument.WhitespaceOnly (path);
151                         CheckArgument.PathChars (path);
152
153                         int nLast = path.LastIndexOfAny (PathSeparatorChars);
154                         if (nLast == 0)
155                                 nLast++;
156
157                         if (nLast > 0) {
158                                 string ret = path.Substring (0, nLast);
159                                 int l = ret.Length;
160                                 if (l >= 2 && ret [l - 1] == VolumeSeparatorChar)
161                                         return ret + DirectorySeparatorChar;
162                                 else
163                                         return ret;
164                         }
165
166                         return String.Empty;
167                 }
168
169                 public static string GetExtension (string path)
170                 {
171                         if (path == null)
172                                 return null;
173
174                         if (path.IndexOfAny (InvalidPathChars) != -1)
175                                 throw new ArgumentException ("Illegal characters in path", "path");
176
177                         int iExt = findExtension (path);
178
179                         if (iExt > -1)
180                         {
181                                 if (iExt < path.Length - 1)
182                                         return path.Substring (iExt);
183                         }
184                         return string.Empty;
185                 }
186
187                 public static string GetFileName (string path)
188                 {
189                         if (path == null || path == String.Empty)
190                                 return path;
191
192                         if (path.IndexOfAny (InvalidPathChars) != -1)
193                                 throw new ArgumentException ("Illegal characters in path", "path");
194
195                         int nLast = path.LastIndexOfAny (PathSeparatorChars);
196                         if (nLast >= 0)
197                                 return path.Substring (nLast + 1);
198
199                         return path;
200                 }
201
202                 public static string GetFileNameWithoutExtension (string path)
203                 {
204                         return ChangeExtension (GetFileName (path), null);
205                 }
206
207                 public static string GetFullPath (string path)
208                 {
209                         string fullpath = InsecureGetFullPath (path);
210                         if (SecurityManager.SecurityEnabled) {
211                                 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
212                         }
213                         return fullpath;
214                 }
215
216                 internal static string WindowsDriveAdjustment (string path)
217                 {
218                         // two special cases to consider when a drive is specified
219                         if (path.Length < 2)
220                                 return path;
221                         if ((path [1] != ':') || !Char.IsLetter (path [0]))
222                                 return path;
223
224                         string current = Directory.GetCurrentDirectory ();
225                         // first, only the drive is specified
226                         if (path.Length == 2) {
227                                 // then if the current directory is on the same drive
228                                 if (current [0] == path [0])
229                                         path = current; // we return it
230                                 else
231                                         path += '\\';
232                         } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
233                                 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
234                                 // If the current directory is on the specified drive...
235                                 if (current [0] == path [0]) {
236                                         // then specified directory is appended to the current drive directory
237                                         path = Path.Combine (current, path.Substring (2, path.Length - 2));
238                                 } else {
239                                         // if not, then just pretend there was a separator (Path.Combine won't work in this case)
240                                         path = String.Concat (path.Substring (0, 2), DirectorySeparatorStr, path.Substring (2, path.Length - 2));
241                                 }
242                         }
243                         return path;
244                 }
245
246                 // insecure - do not call directly
247                 internal static string InsecureGetFullPath (string path)
248                 {
249                         if (path == null)
250                                 throw new ArgumentNullException ("path");
251
252                         if (path.Trim ().Length == 0) {
253                                 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
254                                 throw new ArgumentException (msg, "path");
255                         }
256
257                         // adjust for drives, i.e. a special case for windows
258                         if (Environment.IsRunningOnWindows)
259                                 path = WindowsDriveAdjustment (path);
260
261                         // if the supplied path ends with a separator...
262                         char end = path [path.Length - 1];
263
264                         if (path.Length >= 2 &&
265                                 IsDsc (path [0]) &&
266                                 IsDsc (path [1])) {
267                                 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
268                                         throw new ArgumentException ("UNC pass should be of the form \\\\server\\share.");
269
270                                 if (path [0] != DirectorySeparatorChar)
271                                         path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
272                         } else {
273                                 if (!IsPathRooted (path))
274                                         path = Directory.GetCurrentDirectory () + DirectorySeparatorStr + path;
275                                 else if (DirectorySeparatorChar == '\\' &&
276                                         path.Length >= 2 &&
277                                         IsDsc (path [0]) &&
278                                         !IsDsc (path [1])) { // like `\abc\def'
279                                         string current = Directory.GetCurrentDirectory ();
280                                         if (current [1] == VolumeSeparatorChar)
281                                                 path = current.Substring (0, 2) + path;
282                                         else
283                                                 path = current.Substring (0, current.IndexOf ('\\', current.IndexOf ("\\\\") + 1));
284                                 }
285                                 path = CanonicalizePath (path);
286                         }
287
288                         // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
289                         if (IsDsc (end) && (path [path.Length - 1] != DirectorySeparatorChar))
290                                 path += DirectorySeparatorChar;
291
292                         return path;
293                 }
294
295                 static bool IsDsc (char c) {
296                         return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
297                 }
298                 
299                 public static string GetPathRoot (string path)
300                 {
301                         if (path == null)
302                                 return null;
303
304                         if (path == String.Empty)
305                                 throw new ArgumentException ("This specified path is invalid.");
306
307                         if (!IsPathRooted (path))
308                                 return String.Empty;
309                         
310                         if (DirectorySeparatorChar == '/') {
311                                 // UNIX
312                                 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
313                         } else {
314                                 // Windows
315                                 int len = 2;
316
317                                 if (path.Length == 1 && IsDsc (path [0]))
318                                         return DirectorySeparatorStr;
319                                 else if (path.Length < 2)
320                                         return String.Empty;
321
322                                 if (IsDsc (path [0]) && IsDsc (path[1])) {
323                                         // UNC: \\server or \\server\share
324                                         // Get server
325                                         while (len < path.Length && !IsDsc (path [len])) len++;
326
327                                         // Get share
328                                         if (len < path.Length) {
329                                                 len++;
330                                                 while (len < path.Length && !IsDsc (path [len])) len++;
331                                         }
332
333                                         return DirectorySeparatorStr +
334                                                 DirectorySeparatorStr +
335                                                 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
336                                 } else if (IsDsc (path [0])) {
337                                         // path starts with '\' or '/'
338                                         return DirectorySeparatorStr;
339                                 } else if (path[1] == VolumeSeparatorChar) {
340                                         // C:\folder
341                                         if (path.Length >= 3 && (IsDsc (path [2]))) len++;
342                                 } else
343                                         return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
344                                 return path.Substring (0, len);
345                         }
346                 }
347
348                 // FIXME: Further limit the assertion when imperative Assert is implemented
349                 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
350                 public static string GetTempFileName ()
351                 {
352                         FileStream f = null;
353                         string path;
354                         Random rnd;
355                         int num = 0;
356
357                         rnd = new Random ();
358                         do {
359                                 num = rnd.Next ();
360                                 num++;
361                                 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
362
363                                 try {
364                                         f = new FileStream (path, FileMode.CreateNew);
365                                 }
366                                 catch (SecurityException) {
367                                         // avoid an endless loop
368                                         throw;
369                                 }
370                                 catch {
371                                 }
372                         } while (f == null);
373                         
374                         f.Close();
375                         return path;
376                 }
377
378                 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
379                 public static string GetTempPath ()
380                 {
381                         string p = get_temp_path ();
382                         if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
383                                 return p + DirectorySeparatorChar;
384
385                         return p;
386                 }
387
388                 [MethodImplAttribute(MethodImplOptions.InternalCall)]
389                 private static extern string get_temp_path ();
390
391                 public static bool HasExtension (string path)
392                 {  
393                         if (path == null || path.Trim () == String.Empty)
394                                 return false;
395
396                         int pos = findExtension (path);
397                         return 0 <= pos && pos < path.Length - 1;
398                 }
399
400                 public static bool IsPathRooted (string path)
401                 {
402                         if (path == null || path.Length == 0)
403                                 return false;
404
405                         if (path.IndexOfAny (InvalidPathChars) != -1)
406                                 throw new ArgumentException ("Illegal characters in path", "path");
407
408                         char c = path [0];
409                         return (c == DirectorySeparatorChar     ||
410                                 c == AltDirectorySeparatorChar  ||
411                                 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
412                 }
413
414 #if NET_2_0
415                 public static char[] GetInvalidFileNameChars ()
416                 {
417                         // return a new array as we do not want anyone to be able to change the values
418                         if (Environment.IsRunningOnWindows) {
419                                 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
420                                         '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', 
421                                         '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', 
422                                         '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
423                         } else {
424                                 return new char [2] { '\x00', '/' };
425                         }
426                 }
427
428                 public static char[] GetInvalidPathChars ()
429                 {
430                         // return a new array as we do not want anyone to be able to change the values
431                         if (Environment.IsRunningOnWindows) {
432                                 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
433                                         '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', 
434                                         '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', 
435                                         '\x1E', '\x1F' };
436                         } else {
437                                 return new char [1] { '\x00' };
438                         }
439                 }
440
441                 public static string GetRandomFileName ()
442                 {
443                         char[] invalid = GetInvalidFileNameChars ();
444                         // returns a 8.3 filename (total size 12)
445                         StringBuilder sb = new StringBuilder (12);
446                         // using strong crypto but without creating the file
447                         RandomNumberGenerator rng = RandomNumberGenerator.Create ();
448                         byte[] buffer = new byte [16];
449                         while (sb.Length < 12) {
450                                 int i = 0;
451                                 rng.GetNonZeroBytes (buffer);
452                                 while ((i < buffer.Length) && (sb.Length < 12)) {
453                                         char c = (char) buffer [i];
454                                         if (Array.IndexOf (invalid, c) == -1)
455                                                 sb.Append (c);
456                                         i++;
457                                         if (sb.Length == 8)
458                                                 sb.Append ('.');
459                                 }
460                         }
461                         return sb.ToString ();
462                 }
463 #endif
464                 // private class methods
465
466                 private static int findExtension (string path)
467                 {
468                         // method should return the index of the path extension
469                         // start or -1 if no valid extension
470                         if (path != null){
471                                 int iLastDot = path.LastIndexOf ('.');
472                                 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
473
474                                 if (iLastDot > iLastSep)
475                                         return iLastDot;
476                         }
477                         return -1;
478                 }
479
480                 static Path ()
481                 {
482                         VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
483                         DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
484                         AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
485
486                         PathSeparator = MonoIO.PathSeparator;
487 #if NET_2_0
488                         // this copy will be modifiable ("by design")
489                         InvalidPathChars = GetInvalidPathChars ();
490 #else
491                         if (Environment.IsRunningOnWindows) {
492                                 InvalidPathChars = new char [15] { '\x00', '\x08', '\x10', '\x11', '\x12', '\x14', '\x15', '\x16',
493                                         '\x17', '\x18', '\x19', '\x22', '\x3C', '\x3E', '\x7C' };
494                         } else {
495                                 InvalidPathChars = new char [1] { '\x00' };
496                         }
497 #endif
498                         // internal fields
499
500                         DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
501                         PathSeparatorChars = new char [] {
502                                 DirectorySeparatorChar,
503                                 AltDirectorySeparatorChar,
504                                 VolumeSeparatorChar
505                         };
506
507                         dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
508                 }
509                 
510                 static bool SameRoot (string root, string path)
511                 {
512                         // compare root - if enough details are available
513                         if ((root.Length < 2) || (path.Length < 2))
514                                 return false;
515                         // same volume/drive
516                         if (!root [0].Equals (path [0]))
517                                 return false;
518                         // presence if the separator
519                         if (path[1] != Path.VolumeSeparatorChar)
520                                 return false;
521                         if ((root.Length > 2) && (path.Length > 2)) {
522                                 // but don't directory compare the directory separator
523                                 return (IsDsc (root[2]) && IsDsc (path[2]));
524                         }
525                         return true;
526                 }
527
528                 static string CanonicalizePath (string path)
529                 {
530                         // STEP 1: Check for empty string
531                         if (path == null)
532                                 return path;
533                         if (Environment.IsRunningOnWindows)
534                                 path = path.Trim ();
535
536                         if (path.Length == 0)
537                                 return path;
538
539                         // STEP 2: Check to see if this is only a root
540                         string root = Path.GetPathRoot (path);
541                         // it will return '\' for path '\', while it should return 'c:\' or so.
542                         // Note: commenting this out makes the ened for the (target == 1...) check in step 5
543                         //if (root == path) return path;
544
545                         // STEP 3: split the directories, this gets rid of consecutative "/"'s
546                         string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
547                         // STEP 4: Get rid of directories containing . and ..
548                         int target = 0;
549
550                         for (int i = 0; i < dirs.Length; i++) {
551                                 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
552                                         continue;
553                                 else if (dirs[i] == "..") {
554                                         if (target != 0)
555                                                 target--;
556                                 } else
557                                         dirs[target++] = dirs[i];
558                         }
559
560                         // STEP 5: Combine everything.
561                         if (target == 0 || (target == 1 && dirs[0] == ""))
562                                 return root;
563                         else {
564                                 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
565                                 if (Environment.IsRunningOnWindows) {
566                                         if (!SameRoot (root, ret))
567                                                 ret = root + ret;
568                                         // In GetFullPath(), it is assured that here never comes UNC. So this must only applied to such path that starts with '\', without drive specification.
569                                         if (!IsDsc (path[0]) && SameRoot (root, path)) {
570                                                 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
571                                                         ret += Path.DirectorySeparatorChar;
572                                                 return ret;
573                                         } else {
574                                                 string current = Directory.GetCurrentDirectory ();
575                                                 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
576                                                         // DOS local file path
577                                                         if (ret.Length == 0 || IsDsc (ret[0]))
578                                                                 ret += '\\';
579                                                         return current.Substring (0, 2) + ret;
580                                                 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
581                                                         return current + ret.Substring (1);
582                                                 else
583                                                         return current + ret;
584                                         }
585                                 }
586                                 return ret;
587                         }
588                 }
589
590                 // required for FileIOPermission (and most proibably reusable elsewhere too)
591                 // both path MUST be "full paths"
592                 static internal bool IsPathSubsetOf (string subset, string path)
593                 {
594                         if (subset.Length > path.Length)
595                                 return false;
596
597                         // check that everything up to the last separator match
598                         int slast = subset.LastIndexOfAny (PathSeparatorChars);
599                         if (String.Compare (subset, 0, path, 0, slast) != 0)
600                                 return false;
601
602                         slast++;
603                         // then check if the last segment is identical
604                         int plast = path.IndexOfAny (PathSeparatorChars, slast);
605                         if (plast >= slast) {
606                                 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
607                         }
608                         if (subset.Length != path.Length)
609                                 return false;
610
611                         return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;
612                 }
613         }
614 }