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;
142 // * Removes duplicat path separators from a string
143 // * If the string starts with \\, preserves the first two (hostname on Windows)
144 // * Removes the trailing path separator.
145 // * Returns the DirectorySeparatorChar for the single input DirectorySeparatorChar or AltDirectorySeparatorChar
147 // Unlike CanonicalizePath, this does not do any path resolution
148 // (which GetDirectoryName is not supposed to do).
150 internal static string CleanPath (string s)
158 if (l > 2 && s0 == '\\' && s [1] == '\\'){
162 // We are only left with root
163 if (l == 1 && (s0 == DirectorySeparatorChar || s0 == AltDirectorySeparatorChar))
167 for (int i = start; i < l; i++){
170 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
176 if (c == DirectorySeparatorChar || c == AltDirectorySeparatorChar)
184 char [] copy = new char [l-sub];
189 for (int i = start, j = start; i < l && j < copy.Length; i++){
192 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar){
197 // For non-trailing cases.
198 if (j+1 != copy.Length){
199 copy [j++] = DirectorySeparatorChar;
202 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
207 return new String (copy);
210 public static string GetDirectoryName (string path)
212 // LAMESPEC: For empty string MS docs say both
213 // return null AND throw exception. Seems .NET throws.
214 if (path == String.Empty)
215 throw new ArgumentException("Invalid path");
217 if (path == null || GetPathRoot (path) == path)
220 CheckArgument.WhitespaceOnly (path);
221 CheckArgument.PathChars (path);
223 int nLast = path.LastIndexOfAny (PathSeparatorChars);
228 string ret = path.Substring (0, nLast);
231 if (l >= 2 && ret [l - 1] == VolumeSeparatorChar)
232 return ret + DirectorySeparatorChar;
235 // Important: do not use CanonicalizePath here, use
236 // the custom CleanPath here, as this should not
237 // return absolute paths
239 return CleanPath (ret);
246 public static string GetExtension (string path)
251 if (path.IndexOfAny (InvalidPathChars) != -1)
252 throw new ArgumentException ("Illegal characters in path", "path");
254 int iExt = findExtension (path);
258 if (iExt < path.Length - 1)
259 return path.Substring (iExt);
264 public static string GetFileName (string path)
266 if (path == null || path == String.Empty)
269 if (path.IndexOfAny (InvalidPathChars) != -1)
270 throw new ArgumentException ("Illegal characters in path", "path");
272 int nLast = path.LastIndexOfAny (PathSeparatorChars);
274 return path.Substring (nLast + 1);
279 public static string GetFileNameWithoutExtension (string path)
281 return ChangeExtension (GetFileName (path), null);
284 public static string GetFullPath (string path)
286 string fullpath = InsecureGetFullPath (path);
287 if (SecurityManager.SecurityEnabled) {
288 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
293 internal static string WindowsDriveAdjustment (string path)
295 // two special cases to consider when a drive is specified
298 if ((path [1] != ':') || !Char.IsLetter (path [0]))
301 string current = Directory.GetCurrentDirectory ();
302 // first, only the drive is specified
303 if (path.Length == 2) {
304 // then if the current directory is on the same drive
305 if (current [0] == path [0])
306 path = current; // we return it
309 } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
310 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
311 // If the current directory is on the specified drive...
312 if (current [0] == path [0]) {
313 // then specified directory is appended to the current drive directory
314 path = Path.Combine (current, path.Substring (2, path.Length - 2));
316 // if not, then just pretend there was a separator (Path.Combine won't work in this case)
317 path = String.Concat (path.Substring (0, 2), DirectorySeparatorStr, path.Substring (2, path.Length - 2));
323 // insecure - do not call directly
324 internal static string InsecureGetFullPath (string path)
327 throw new ArgumentNullException ("path");
329 if (path.Trim ().Length == 0) {
330 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
331 throw new ArgumentException (msg, "path");
334 // adjust for drives, i.e. a special case for windows
335 if (Environment.IsRunningOnWindows)
336 path = WindowsDriveAdjustment (path);
338 // if the supplied path ends with a separator...
339 char end = path [path.Length - 1];
341 if (path.Length >= 2 &&
344 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
345 throw new ArgumentException ("UNC pass should be of the form \\\\server\\share.");
347 if (path [0] != DirectorySeparatorChar)
348 path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
350 if (!IsPathRooted (path))
351 path = Directory.GetCurrentDirectory () + DirectorySeparatorStr + path;
352 else if (DirectorySeparatorChar == '\\' &&
355 !IsDsc (path [1])) { // like `\abc\def'
356 string current = Directory.GetCurrentDirectory ();
357 if (current [1] == VolumeSeparatorChar)
358 path = current.Substring (0, 2) + path;
360 path = current.Substring (0, current.IndexOf ('\\', current.IndexOf ("\\\\") + 1));
362 path = CanonicalizePath (path);
365 // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
366 if (IsDsc (end) && (path [path.Length - 1] != DirectorySeparatorChar))
367 path += DirectorySeparatorChar;
372 static bool IsDsc (char c) {
373 return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
376 public static string GetPathRoot (string path)
381 if (path == String.Empty)
382 throw new ArgumentException ("This specified path is invalid.");
384 if (!IsPathRooted (path))
387 if (DirectorySeparatorChar == '/') {
389 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
394 if (path.Length == 1 && IsDsc (path [0]))
395 return DirectorySeparatorStr;
396 else if (path.Length < 2)
399 if (IsDsc (path [0]) && IsDsc (path[1])) {
400 // UNC: \\server or \\server\share
402 while (len < path.Length && !IsDsc (path [len])) len++;
405 if (len < path.Length) {
407 while (len < path.Length && !IsDsc (path [len])) len++;
410 return DirectorySeparatorStr +
411 DirectorySeparatorStr +
412 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
413 } else if (IsDsc (path [0])) {
414 // path starts with '\' or '/'
415 return DirectorySeparatorStr;
416 } else if (path[1] == VolumeSeparatorChar) {
418 if (path.Length >= 3 && (IsDsc (path [2]))) len++;
420 return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
421 return path.Substring (0, len);
425 // FIXME: Further limit the assertion when imperative Assert is implemented
426 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
427 public static string GetTempFileName ()
438 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
441 f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
442 8192, false, (FileOptions) 1);
444 catch (SecurityException) {
445 // avoid an endless loop
456 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
457 public static string GetTempPath ()
459 string p = get_temp_path ();
460 if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
461 return p + DirectorySeparatorChar;
466 [MethodImplAttribute(MethodImplOptions.InternalCall)]
467 private static extern string get_temp_path ();
469 public static bool HasExtension (string path)
471 if (path == null || path.Trim () == String.Empty)
474 int pos = findExtension (path);
475 return 0 <= pos && pos < path.Length - 1;
478 public static bool IsPathRooted (string path)
480 if (path == null || path.Length == 0)
483 if (path.IndexOfAny (InvalidPathChars) != -1)
484 throw new ArgumentException ("Illegal characters in path", "path");
487 return (c == DirectorySeparatorChar ||
488 c == AltDirectorySeparatorChar ||
489 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
493 public static char[] GetInvalidFileNameChars ()
495 // return a new array as we do not want anyone to be able to change the values
496 if (Environment.IsRunningOnWindows) {
497 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
498 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
499 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
500 '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
502 return new char [2] { '\x00', '/' };
506 public static char[] GetInvalidPathChars ()
508 // return a new array as we do not want anyone to be able to change the values
509 if (Environment.IsRunningOnWindows) {
510 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
511 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
512 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
515 return new char [1] { '\x00' };
519 public static string GetRandomFileName ()
521 // returns a 8.3 filename (total size 12)
522 StringBuilder sb = new StringBuilder (12);
523 // using strong crypto but without creating the file
524 RandomNumberGenerator rng = RandomNumberGenerator.Create ();
525 byte [] buffer = new byte [11];
526 rng.GetBytes (buffer);
528 for (int i = 0; i < buffer.Length; i++) {
532 // restrict to length of range [a..z0..9]
533 int b = (buffer [i] % 36);
534 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
538 return sb.ToString ();
541 // private class methods
543 private static int findExtension (string path)
545 // method should return the index of the path extension
546 // start or -1 if no valid extension
548 int iLastDot = path.LastIndexOf ('.');
549 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
551 if (iLastDot > iLastSep)
559 VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
560 DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
561 AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
563 PathSeparator = MonoIO.PathSeparator;
565 // this copy will be modifiable ("by design")
566 InvalidPathChars = GetInvalidPathChars ();
568 if (Environment.IsRunningOnWindows) {
569 InvalidPathChars = new char [15] { '\x00', '\x08', '\x10', '\x11', '\x12', '\x14', '\x15', '\x16',
570 '\x17', '\x18', '\x19', '\x22', '\x3C', '\x3E', '\x7C' };
572 InvalidPathChars = new char [1] { '\x00' };
577 DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
578 PathSeparatorChars = new char [] {
579 DirectorySeparatorChar,
580 AltDirectorySeparatorChar,
584 dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
587 static bool SameRoot (string root, string path)
589 // compare root - if enough details are available
590 if ((root.Length < 2) || (path.Length < 2))
593 if (!root [0].Equals (path [0]))
595 // presence if the separator
596 if (path[1] != Path.VolumeSeparatorChar)
598 if ((root.Length > 2) && (path.Length > 2)) {
599 // but don't directory compare the directory separator
600 return (IsDsc (root[2]) && IsDsc (path[2]));
605 static string CanonicalizePath (string path)
607 // STEP 1: Check for empty string
610 if (Environment.IsRunningOnWindows)
613 if (path.Length == 0)
616 // STEP 2: Check to see if this is only a root
617 string root = Path.GetPathRoot (path);
618 // it will return '\' for path '\', while it should return 'c:\' or so.
619 // Note: commenting this out makes the ened for the (target == 1...) check in step 5
620 //if (root == path) return path;
622 // STEP 3: split the directories, this gets rid of consecutative "/"'s
623 string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
624 // STEP 4: Get rid of directories containing . and ..
627 for (int i = 0; i < dirs.Length; i++) {
628 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
630 else if (dirs[i] == "..") {
634 dirs[target++] = dirs[i];
637 // STEP 5: Combine everything.
638 if (target == 0 || (target == 1 && dirs[0] == ""))
641 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
642 if (Environment.IsRunningOnWindows) {
643 if (!SameRoot (root, ret))
645 // In GetFullPath(), it is assured that here never comes UNC. So this must only applied to such path that starts with '\', without drive specification.
646 if (!IsDsc (path[0]) && SameRoot (root, path)) {
647 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
648 ret += Path.DirectorySeparatorChar;
651 string current = Directory.GetCurrentDirectory ();
652 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
653 // DOS local file path
654 if (ret.Length == 0 || IsDsc (ret[0]))
656 return current.Substring (0, 2) + ret;
657 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
658 return current + ret.Substring (1);
660 return current + ret;
667 // required for FileIOPermission (and most proibably reusable elsewhere too)
668 // both path MUST be "full paths"
669 static internal bool IsPathSubsetOf (string subset, string path)
671 if (subset.Length > path.Length)
674 // check that everything up to the last separator match
675 int slast = subset.LastIndexOfAny (PathSeparatorChars);
676 if (String.Compare (subset, 0, path, 0, slast) != 0)
680 // then check if the last segment is identical
681 int plast = path.IndexOfAny (PathSeparatorChars, slast);
682 if (plast >= slast) {
683 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
685 if (subset.Length != path.Length)
688 return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;