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;
52 public static class Path {
54 [Obsolete ("see GetInvalidPathChars and GetInvalidFileNameChars methods.")]
55 public static readonly char[] InvalidPathChars;
56 public static readonly char AltDirectorySeparatorChar;
57 public static readonly char DirectorySeparatorChar;
58 public static readonly char PathSeparator;
59 internal static readonly string DirectorySeparatorStr;
60 public static readonly char VolumeSeparatorChar;
62 internal static readonly char[] PathSeparatorChars;
63 private static readonly bool dirEqualsVolume;
66 public static string ChangeExtension (string path, string extension)
71 if (path.IndexOfAny (InvalidPathChars) != -1)
72 throw new ArgumentException ("Illegal characters in path.");
74 int iExt = findExtension (path);
76 if (extension == null)
77 return iExt < 0 ? path : path.Substring (0, iExt);
78 else if (extension.Length == 0)
79 return iExt < 0 ? path + '.' : path.Substring (0, iExt + 1);
81 else if (path.Length != 0) {
82 if (extension.Length > 0 && extension [0] != '.')
83 extension = "." + extension;
85 extension = String.Empty;
88 return path + extension;
89 } else if (iExt > 0) {
90 string temp = path.Substring (0, iExt);
91 return temp + extension;
97 public static string Combine (string path1, string path2)
100 throw new ArgumentNullException ("path1");
103 throw new ArgumentNullException ("path2");
105 if (path1.Length == 0)
108 if (path2.Length == 0)
111 if (path1.IndexOfAny (InvalidPathChars) != -1)
112 throw new ArgumentException ("Illegal characters in path.");
114 if (path2.IndexOfAny (InvalidPathChars) != -1)
115 throw new ArgumentException ("Illegal characters in path.");
118 if (IsPathRooted (path2))
121 char p1end = path1 [path1.Length - 1];
122 if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
123 return path1 + DirectorySeparatorStr + path2;
125 return path1 + path2;
130 // * Removes duplicat path separators from a string
131 // * If the string starts with \\, preserves the first two (hostname on Windows)
132 // * Removes the trailing path separator.
133 // * Returns the DirectorySeparatorChar for the single input DirectorySeparatorChar or AltDirectorySeparatorChar
135 // Unlike CanonicalizePath, this does not do any path resolution
136 // (which GetDirectoryName is not supposed to do).
138 internal static string CleanPath (string s)
146 if (l > 2 && s0 == '\\' && s [1] == '\\'){
150 // We are only left with root
151 if (l == 1 && (s0 == DirectorySeparatorChar || s0 == AltDirectorySeparatorChar))
155 for (int i = start; i < l; i++){
158 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
164 if (c == DirectorySeparatorChar || c == AltDirectorySeparatorChar)
172 char [] copy = new char [l-sub];
177 for (int i = start, j = start; i < l && j < copy.Length; i++){
180 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar){
185 // For non-trailing cases.
186 if (j+1 != copy.Length){
187 copy [j++] = DirectorySeparatorChar;
190 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
195 return new String (copy);
198 public static string GetDirectoryName (string path)
200 // LAMESPEC: For empty string MS docs say both
201 // return null AND throw exception. Seems .NET throws.
202 if (path == String.Empty)
203 throw new ArgumentException("Invalid path");
205 if (path == null || GetPathRoot (path) == path)
208 if (path.Trim ().Length == 0)
209 throw new ArgumentException ("Argument string consists of whitespace characters only.");
211 if (path.IndexOfAny (System.IO.Path.InvalidPathChars) > -1)
212 throw new ArgumentException ("Path contains invalid characters");
214 int nLast = path.LastIndexOfAny (PathSeparatorChars);
219 string ret = path.Substring (0, nLast);
222 if (l >= 2 && DirectorySeparatorChar == '\\' && ret [l - 1] == VolumeSeparatorChar)
223 return ret + DirectorySeparatorChar;
226 // Important: do not use CanonicalizePath here, use
227 // the custom CleanPath here, as this should not
228 // return absolute paths
230 return CleanPath (ret);
237 public static string GetExtension (string path)
242 if (path.IndexOfAny (InvalidPathChars) != -1)
243 throw new ArgumentException ("Illegal characters in path.");
245 int iExt = findExtension (path);
249 if (iExt < path.Length - 1)
250 return path.Substring (iExt);
255 public static string GetFileName (string path)
257 if (path == null || path.Length == 0)
260 if (path.IndexOfAny (InvalidPathChars) != -1)
261 throw new ArgumentException ("Illegal characters in path.");
263 int nLast = path.LastIndexOfAny (PathSeparatorChars);
265 return path.Substring (nLast + 1);
270 public static string GetFileNameWithoutExtension (string path)
272 return ChangeExtension (GetFileName (path), null);
275 public static string GetFullPath (string path)
277 string fullpath = InsecureGetFullPath (path);
279 if (SecurityManager.SecurityEnabled) {
280 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
286 internal static string WindowsDriveAdjustment (string path)
288 // two special cases to consider when a drive is specified
291 if ((path [1] != ':') || !Char.IsLetter (path [0]))
294 string current = Directory.GetCurrentDirectory ();
295 // first, only the drive is specified
296 if (path.Length == 2) {
297 // then if the current directory is on the same drive
298 if (current [0] == path [0])
299 path = current; // we return it
302 } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
303 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
304 // If the current directory is on the specified drive...
305 if (current [0] == path [0]) {
306 // then specified directory is appended to the current drive directory
307 path = Path.Combine (current, path.Substring (2, path.Length - 2));
309 // if not, then just pretend there was a separator (Path.Combine won't work in this case)
310 path = String.Concat (path.Substring (0, 2), DirectorySeparatorStr, path.Substring (2, path.Length - 2));
316 // insecure - do not call directly
317 internal static string InsecureGetFullPath (string path)
320 throw new ArgumentNullException ("path");
322 if (path.Trim ().Length == 0) {
323 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
324 throw new ArgumentException (msg);
327 // adjust for drives, i.e. a special case for windows
328 if (Environment.IsRunningOnWindows)
329 path = WindowsDriveAdjustment (path);
331 // if the supplied path ends with a separator...
332 char end = path [path.Length - 1];
334 if (path.Length >= 2 &&
337 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
338 throw new ArgumentException ("UNC pass should be of the form \\\\server\\share.");
340 if (path [0] != DirectorySeparatorChar)
341 path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
343 path = CanonicalizePath (path);
345 if (!IsPathRooted (path))
346 path = Directory.GetCurrentDirectory () + DirectorySeparatorStr + path;
347 else if (DirectorySeparatorChar == '\\' &&
350 !IsDsc (path [1])) { // like `\abc\def'
351 string current = Directory.GetCurrentDirectory ();
352 if (current [1] == VolumeSeparatorChar)
353 path = current.Substring (0, 2) + path;
355 path = current.Substring (0, current.IndexOf ('\\', current.IndexOf ("\\\\") + 1));
357 path = CanonicalizePath (path);
360 // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
361 if (IsDsc (end) && (path [path.Length - 1] != DirectorySeparatorChar))
362 path += DirectorySeparatorChar;
367 static bool IsDsc (char c) {
368 return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
371 public static string GetPathRoot (string path)
376 if (path.Trim ().Length == 0)
377 throw new ArgumentException ("The specified path is not of a legal form.");
379 if (!IsPathRooted (path))
382 if (DirectorySeparatorChar == '/') {
384 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
389 if (path.Length == 1 && IsDsc (path [0]))
390 return DirectorySeparatorStr;
391 else if (path.Length < 2)
394 if (IsDsc (path [0]) && IsDsc (path[1])) {
395 // UNC: \\server or \\server\share
397 while (len < path.Length && !IsDsc (path [len])) len++;
400 if (len < path.Length) {
402 while (len < path.Length && !IsDsc (path [len])) len++;
405 return DirectorySeparatorStr +
406 DirectorySeparatorStr +
407 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
408 } else if (IsDsc (path [0])) {
409 // path starts with '\' or '/'
410 return DirectorySeparatorStr;
411 } else if (path[1] == VolumeSeparatorChar) {
413 if (path.Length >= 3 && (IsDsc (path [2]))) len++;
415 return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
416 return path.Substring (0, len);
420 // FIXME: Further limit the assertion when imperative Assert is implemented
421 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
422 public static string GetTempFileName ()
433 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
436 f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
437 8192, false, (FileOptions) 1);
439 catch (SecurityException) {
440 // avoid an endless loop
451 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
452 public static string GetTempPath ()
454 string p = get_temp_path ();
455 if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
456 return p + DirectorySeparatorChar;
461 [MethodImplAttribute(MethodImplOptions.InternalCall)]
462 private static extern string get_temp_path ();
464 public static bool HasExtension (string path)
466 if (path == null || path.Trim ().Length == 0)
469 if (path.IndexOfAny (InvalidPathChars) != -1)
470 throw new ArgumentException ("Illegal characters in path.");
472 int pos = findExtension (path);
473 return 0 <= pos && pos < path.Length - 1;
476 public static bool IsPathRooted (string path)
478 if (path == null || path.Length == 0)
481 if (path.IndexOfAny (InvalidPathChars) != -1)
482 throw new ArgumentException ("Illegal characters in path.");
485 return (c == DirectorySeparatorChar ||
486 c == AltDirectorySeparatorChar ||
487 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
490 public static char[] GetInvalidFileNameChars ()
492 // return a new array as we do not want anyone to be able to change the values
493 if (Environment.IsRunningOnWindows) {
494 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
495 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
496 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
497 '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
499 return new char [2] { '\x00', '/' };
503 public static char[] GetInvalidPathChars ()
505 // return a new array as we do not want anyone to be able to change the values
506 if (Environment.IsRunningOnWindows) {
507 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
508 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
509 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
512 return new char [1] { '\x00' };
516 public static string GetRandomFileName ()
518 // returns a 8.3 filename (total size 12)
519 StringBuilder sb = new StringBuilder (12);
520 // using strong crypto but without creating the file
521 RandomNumberGenerator rng = RandomNumberGenerator.Create ();
522 byte [] buffer = new byte [11];
523 rng.GetBytes (buffer);
525 for (int i = 0; i < buffer.Length; i++) {
529 // restrict to length of range [a..z0..9]
530 int b = (buffer [i] % 36);
531 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
535 return sb.ToString ();
538 // private class methods
540 private static int findExtension (string path)
542 // method should return the index of the path extension
543 // start or -1 if no valid extension
545 int iLastDot = path.LastIndexOf ('.');
546 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
548 if (iLastDot > iLastSep)
556 VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
557 DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
558 AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
560 PathSeparator = MonoIO.PathSeparator;
561 // this copy will be modifiable ("by design")
562 InvalidPathChars = GetInvalidPathChars ();
565 DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
566 PathSeparatorChars = new char [] {
567 DirectorySeparatorChar,
568 AltDirectorySeparatorChar,
572 dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
575 // returns the server and share part of a UNC. Assumes "path" is a UNC.
576 static string GetServerAndShare (string path)
579 while (len < path.Length && !IsDsc (path [len])) len++;
581 if (len < path.Length) {
583 while (len < path.Length && !IsDsc (path [len])) len++;
586 return path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
589 // assumes Environment.IsRunningOnWindows == true
590 static bool SameRoot (string root, string path)
592 // compare root - if enough details are available
593 if ((root.Length < 2) || (path.Length < 2))
597 if (IsDsc (root[0]) && IsDsc (root[1])) {
598 if (!(IsDsc (path[0]) && IsDsc (path[1])))
601 string rootShare = GetServerAndShare (root);
602 string pathShare = GetServerAndShare (path);
604 return String.Compare (rootShare, pathShare, true, CultureInfo.InvariantCulture) == 0;
608 if (!root [0].Equals (path [0]))
610 // presence of the separator
611 if (path[1] != Path.VolumeSeparatorChar)
613 if ((root.Length > 2) && (path.Length > 2)) {
614 // but don't directory compare the directory separator
615 return (IsDsc (root[2]) && IsDsc (path[2]));
620 static string CanonicalizePath (string path)
622 // STEP 1: Check for empty string
625 if (Environment.IsRunningOnWindows)
628 if (path.Length == 0)
631 // STEP 2: Check to see if this is only a root
632 string root = Path.GetPathRoot (path);
633 // it will return '\' for path '\', while it should return 'c:\' or so.
634 // Note: commenting this out makes the need for the (target == 1...) check in step 5
635 //if (root == path) return path;
637 // STEP 3: split the directories, this gets rid of consecutative "/"'s
638 string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
639 // STEP 4: Get rid of directories containing . and ..
642 bool isUnc = Environment.IsRunningOnWindows &&
643 root.Length > 2 && IsDsc (root[0]) && IsDsc (root[1]);
645 // Set an overwrite limit for UNC paths since '\' + server + share
646 // must not be eliminated by the '..' elimination algorithm.
647 int limit = isUnc ? 3 : 0;
649 for (int i = 0; i < dirs.Length; i++) {
650 // WIN32 path components must be trimmed
651 if (Environment.IsRunningOnWindows)
652 dirs[i] = dirs[i].TrimEnd ();
654 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
656 else if (dirs[i] == "..") {
657 // don't overwrite path segments below the limit
661 dirs[target++] = dirs[i];
664 // STEP 5: Combine everything.
665 if (target == 0 || (target == 1 && dirs[0] == ""))
668 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
669 if (Environment.IsRunningOnWindows) {
670 // append leading '\' of the UNC path that was lost in STEP 3.
672 ret = Path.DirectorySeparatorStr + ret;
674 if (!SameRoot (root, ret))
679 } else if (!IsDsc (path[0]) && SameRoot (root, path)) {
680 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
681 ret += Path.DirectorySeparatorChar;
684 string current = Directory.GetCurrentDirectory ();
685 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
686 // DOS local file path
687 if (ret.Length == 0 || IsDsc (ret[0]))
689 return current.Substring (0, 2) + ret;
690 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
691 return current + ret.Substring (1);
693 return current + ret;
700 // required for FileIOPermission (and most proibably reusable elsewhere too)
701 // both path MUST be "full paths"
702 static internal bool IsPathSubsetOf (string subset, string path)
704 if (subset.Length > path.Length)
707 // check that everything up to the last separator match
708 int slast = subset.LastIndexOfAny (PathSeparatorChars);
709 if (String.Compare (subset, 0, path, 0, slast) != 0)
713 // then check if the last segment is identical
714 int plast = path.IndexOfAny (PathSeparatorChars, slast);
715 if (plast >= slast) {
716 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
718 if (subset.Length != path.Length)
721 return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;