1 //------------------------------------------------------------------------------
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
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
16 //------------------------------------------------------------------------------
19 // Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
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:
29 // The above copyright notice and this permission notice shall be
30 // included in all copies or substantial portions of the Software.
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.
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;
53 public static class Path {
55 [Obsolete ("see GetInvalidPathChars and GetInvalidFileNameChars methods.")]
56 public static readonly char[] InvalidPathChars;
58 public sealed class Path {
64 public static readonly char[] InvalidPathChars;
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;
72 private static readonly char[] PathSeparatorChars;
73 private static readonly bool dirEqualsVolume;
76 public static string ChangeExtension (string path, string extension)
81 if (path.IndexOfAny (InvalidPathChars) != -1)
82 throw new ArgumentException ("Illegal characters in path", "path");
84 int iExt = findExtension (path);
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);
91 else if (path.Length != 0) {
92 if (extension.Length > 0 && extension [0] != '.')
93 extension = "." + extension;
95 extension = String.Empty;
98 return path + extension;
99 } else if (iExt > 0) {
100 string temp = path.Substring (0, iExt);
101 return temp + extension;
107 public static string Combine (string path1, string path2)
110 throw new ArgumentNullException ("path1");
113 throw new ArgumentNullException ("path2");
115 if (path1 == String.Empty)
118 if (path2 == String.Empty)
121 if (path1.IndexOfAny (InvalidPathChars) != -1)
122 throw new ArgumentException ("Illegal characters in path", "path1");
124 if (path2.IndexOfAny (InvalidPathChars) != -1)
125 throw new ArgumentException ("Illegal characters in path", "path2");
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))
133 char p1end = path1 [path1.Length - 1];
134 if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
135 return path1 + DirectorySeparatorChar + path2;
137 return path1 + path2;
140 public static string GetDirectoryName (string path)
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();
147 if (path == null || GetPathRoot (path) == path)
150 CheckArgument.WhitespaceOnly (path);
151 CheckArgument.PathChars (path);
153 int nLast = path.LastIndexOfAny (PathSeparatorChars);
158 string ret = path.Substring (0, nLast);
160 if (l >= 2 && ret [l - 1] == VolumeSeparatorChar)
161 return ret + DirectorySeparatorChar;
169 public static string GetExtension (string path)
174 if (path.IndexOfAny (InvalidPathChars) != -1)
175 throw new ArgumentException ("Illegal characters in path", "path");
177 int iExt = findExtension (path);
181 if (iExt < path.Length - 1)
182 return path.Substring (iExt);
187 public static string GetFileName (string path)
189 if (path == null || path == String.Empty)
192 if (path.IndexOfAny (InvalidPathChars) != -1)
193 throw new ArgumentException ("Illegal characters in path", "path");
195 int nLast = path.LastIndexOfAny (PathSeparatorChars);
197 return path.Substring (nLast + 1);
202 public static string GetFileNameWithoutExtension (string path)
204 return ChangeExtension (GetFileName (path), null);
207 public static string GetFullPath (string path)
209 string fullpath = InsecureGetFullPath (path);
210 if (SecurityManager.SecurityEnabled) {
211 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
216 internal static string WindowsDriveAdjustment (string path)
218 // two special cases to consider when a drive is specified
221 if ((path [1] != ':') || !Char.IsLetter (path [0]))
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
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));
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));
246 // insecure - do not call directly
247 internal static string InsecureGetFullPath (string path)
250 throw new ArgumentNullException ("path");
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");
257 // adjust for drives, i.e. a special case for windows
258 if (Environment.IsRunningOnWindows)
259 path = WindowsDriveAdjustment (path);
261 // if the supplied path ends with a separator...
262 char end = path [path.Length - 1];
264 if (path.Length >= 2 &&
267 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
268 throw new ArgumentException ("UNC pass should be of the form \\\\server\\share.");
270 if (path [0] != DirectorySeparatorChar)
271 path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
273 if (!IsPathRooted (path))
274 path = Directory.GetCurrentDirectory () + DirectorySeparatorStr + path;
275 else if (DirectorySeparatorChar == '\\' &&
278 !IsDsc (path [1])) { // like `\abc\def'
279 string current = Directory.GetCurrentDirectory ();
280 if (current [1] == VolumeSeparatorChar)
281 path = current.Substring (0, 2) + path;
283 path = current.Substring (0, current.IndexOf ('\\', current.IndexOf ("\\\\") + 1));
285 path = CanonicalizePath (path);
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;
295 static bool IsDsc (char c) {
296 return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
299 public static string GetPathRoot (string path)
304 if (path == String.Empty)
305 throw new ArgumentException ("This specified path is invalid.");
307 if (!IsPathRooted (path))
310 if (DirectorySeparatorChar == '/') {
312 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
317 if (path.Length == 1 && IsDsc (path [0]))
318 return DirectorySeparatorStr;
319 else if (path.Length < 2)
322 if (IsDsc (path [0]) && IsDsc (path[1])) {
323 // UNC: \\server or \\server\share
325 while (len < path.Length && !IsDsc (path [len])) len++;
328 if (len < path.Length) {
330 while (len < path.Length && !IsDsc (path [len])) len++;
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) {
341 if (path.Length >= 3 && (IsDsc (path [2]))) len++;
343 return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
344 return path.Substring (0, len);
348 // FIXME: Further limit the assertion when imperative Assert is implemented
349 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
350 public static string GetTempFileName ()
361 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
364 f = new FileStream (path, FileMode.CreateNew);
366 catch (SecurityException) {
367 // avoid an endless loop
378 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
379 public static string GetTempPath ()
381 string p = get_temp_path ();
382 if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
383 return p + DirectorySeparatorChar;
388 [MethodImplAttribute(MethodImplOptions.InternalCall)]
389 private static extern string get_temp_path ();
391 public static bool HasExtension (string path)
393 if (path == null || path.Trim () == String.Empty)
396 int pos = findExtension (path);
397 return 0 <= pos && pos < path.Length - 1;
400 public static bool IsPathRooted (string path)
402 if (path == null || path.Length == 0)
405 if (path.IndexOfAny (InvalidPathChars) != -1)
406 throw new ArgumentException ("Illegal characters in path", "path");
409 return (c == DirectorySeparatorChar ||
410 c == AltDirectorySeparatorChar ||
411 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
415 public static char[] GetInvalidFileNameChars ()
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', ':', '*', '?', '\\', '/' };
424 return new char [2] { '\x00', '/' };
428 public static char[] GetInvalidPathChars ()
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] { '\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', '\x22', '\x3C', '\x3E', '\x7C' };
437 return new char [1] { '\x00' };
441 public static string GetRandomFileName ()
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) {
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)
461 return sb.ToString ();
464 // private class methods
466 private static int findExtension (string path)
468 // method should return the index of the path extension
469 // start or -1 if no valid extension
471 int iLastDot = path.LastIndexOf ('.');
472 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
474 if (iLastDot > iLastSep)
482 VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
483 DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
484 AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
486 PathSeparator = MonoIO.PathSeparator;
488 // this copy will be modifiable ("by design")
489 InvalidPathChars = GetInvalidPathChars ();
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' };
495 InvalidPathChars = new char [1] { '\x00' };
500 DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
501 PathSeparatorChars = new char [] {
502 DirectorySeparatorChar,
503 AltDirectorySeparatorChar,
507 dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
510 static bool SameRoot (string root, string path)
512 // compare root - if enough details are available
513 if ((root.Length < 2) || (path.Length < 2))
516 if (!root [0].Equals (path [0]))
518 // presence if the separator
519 if (path[1] != Path.VolumeSeparatorChar)
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]));
528 static string CanonicalizePath (string path)
530 // STEP 1: Check for empty string
533 if (Environment.IsRunningOnWindows)
536 if (path.Length == 0)
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;
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 ..
550 for (int i = 0; i < dirs.Length; i++) {
551 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
553 else if (dirs[i] == "..") {
557 dirs[target++] = dirs[i];
560 // STEP 5: Combine everything.
561 if (target == 0 || (target == 1 && dirs[0] == ""))
564 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
565 if (Environment.IsRunningOnWindows) {
566 if (!SameRoot (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;
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]))
579 return current.Substring (0, 2) + ret;
580 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
581 return current + ret.Substring (1);
583 return current + ret;
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)
594 if (subset.Length > path.Length)
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)
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;
608 if (subset.Length != path.Length)
611 return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;