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 internal 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.");
84 int iExt = findExtension (path);
86 if (extension == null)
87 return iExt < 0 ? path : path.Substring (0, iExt);
88 else if (extension.Length == 0)
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.Length == 0)
118 if (path2.Length == 0)
121 if (path1.IndexOfAny (InvalidPathChars) != -1)
122 throw new ArgumentException ("Illegal characters in path.");
124 if (path2.IndexOfAny (InvalidPathChars) != -1)
125 throw new ArgumentException ("Illegal characters in path.");
128 if (IsPathRooted (path2))
131 char p1end = path1 [path1.Length - 1];
132 if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
133 return path1 + DirectorySeparatorStr + path2;
135 return path1 + path2;
140 // * Removes duplicat path separators from a string
141 // * If the string starts with \\, preserves the first two (hostname on Windows)
142 // * Removes the trailing path separator.
143 // * Returns the DirectorySeparatorChar for the single input DirectorySeparatorChar or AltDirectorySeparatorChar
145 // Unlike CanonicalizePath, this does not do any path resolution
146 // (which GetDirectoryName is not supposed to do).
148 internal static string CleanPath (string s)
156 if (l > 2 && s0 == '\\' && s [1] == '\\'){
160 // We are only left with root
161 if (l == 1 && (s0 == DirectorySeparatorChar || s0 == AltDirectorySeparatorChar))
165 for (int i = start; i < l; i++){
168 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
174 if (c == DirectorySeparatorChar || c == AltDirectorySeparatorChar)
182 char [] copy = new char [l-sub];
187 for (int i = start, j = start; i < l && j < copy.Length; i++){
190 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar){
195 // For non-trailing cases.
196 if (j+1 != copy.Length){
197 copy [j++] = DirectorySeparatorChar;
200 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
205 return new String (copy);
208 public static string GetDirectoryName (string path)
210 // LAMESPEC: For empty string MS docs say both
211 // return null AND throw exception. Seems .NET throws.
212 if (path == String.Empty)
213 throw new ArgumentException("Invalid path");
215 if (path == null || GetPathRoot (path) == path)
218 if (path.Trim ().Length == 0)
219 throw new ArgumentException ("Argument string consists of whitespace characters only.");
221 if (path.IndexOfAny (System.IO.Path.InvalidPathChars) > -1)
222 throw new ArgumentException ("Path contains invalid characters");
224 int nLast = path.LastIndexOfAny (PathSeparatorChars);
229 string ret = path.Substring (0, nLast);
232 if (l >= 2 && DirectorySeparatorChar == '\\' && ret [l - 1] == VolumeSeparatorChar)
233 return ret + DirectorySeparatorChar;
236 // Important: do not use CanonicalizePath here, use
237 // the custom CleanPath here, as this should not
238 // return absolute paths
240 return CleanPath (ret);
247 public static string GetExtension (string path)
252 if (path.IndexOfAny (InvalidPathChars) != -1)
253 throw new ArgumentException ("Illegal characters in path.");
255 int iExt = findExtension (path);
259 if (iExt < path.Length - 1)
260 return path.Substring (iExt);
265 public static string GetFileName (string path)
267 if (path == null || path.Length == 0)
270 if (path.IndexOfAny (InvalidPathChars) != -1)
271 throw new ArgumentException ("Illegal characters in path.");
273 int nLast = path.LastIndexOfAny (PathSeparatorChars);
275 return path.Substring (nLast + 1);
280 public static string GetFileNameWithoutExtension (string path)
282 return ChangeExtension (GetFileName (path), null);
285 public static string GetFullPath (string path)
287 string fullpath = InsecureGetFullPath (path);
288 if (SecurityManager.SecurityEnabled) {
289 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
294 internal static string WindowsDriveAdjustment (string path)
296 // two special cases to consider when a drive is specified
299 if ((path [1] != ':') || !Char.IsLetter (path [0]))
302 string current = Directory.GetCurrentDirectory ();
303 // first, only the drive is specified
304 if (path.Length == 2) {
305 // then if the current directory is on the same drive
306 if (current [0] == path [0])
307 path = current; // we return it
310 } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
311 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
312 // If the current directory is on the specified drive...
313 if (current [0] == path [0]) {
314 // then specified directory is appended to the current drive directory
315 path = Path.Combine (current, path.Substring (2, path.Length - 2));
317 // if not, then just pretend there was a separator (Path.Combine won't work in this case)
318 path = String.Concat (path.Substring (0, 2), DirectorySeparatorStr, path.Substring (2, path.Length - 2));
324 // insecure - do not call directly
325 internal static string InsecureGetFullPath (string path)
328 throw new ArgumentNullException ("path");
330 if (path.Trim ().Length == 0) {
331 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
332 throw new ArgumentException (msg);
335 // adjust for drives, i.e. a special case for windows
336 if (Environment.IsRunningOnWindows)
337 path = WindowsDriveAdjustment (path);
339 // if the supplied path ends with a separator...
340 char end = path [path.Length - 1];
342 if (path.Length >= 2 &&
345 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
346 throw new ArgumentException ("UNC pass should be of the form \\\\server\\share.");
348 if (path [0] != DirectorySeparatorChar)
349 path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
351 path = CanonicalizePath (path);
353 if (!IsPathRooted (path))
354 path = Directory.GetCurrentDirectory () + DirectorySeparatorStr + path;
355 else if (DirectorySeparatorChar == '\\' &&
358 !IsDsc (path [1])) { // like `\abc\def'
359 string current = Directory.GetCurrentDirectory ();
360 if (current [1] == VolumeSeparatorChar)
361 path = current.Substring (0, 2) + path;
363 path = current.Substring (0, current.IndexOf ('\\', current.IndexOf ("\\\\") + 1));
365 path = CanonicalizePath (path);
368 // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
369 if (IsDsc (end) && (path [path.Length - 1] != DirectorySeparatorChar))
370 path += DirectorySeparatorChar;
375 static bool IsDsc (char c) {
376 return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
379 public static string GetPathRoot (string path)
384 if (path.Trim ().Length == 0)
385 throw new ArgumentException ("The specified path is not of a legal form.");
387 if (!IsPathRooted (path))
390 if (DirectorySeparatorChar == '/') {
392 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
397 if (path.Length == 1 && IsDsc (path [0]))
398 return DirectorySeparatorStr;
399 else if (path.Length < 2)
402 if (IsDsc (path [0]) && IsDsc (path[1])) {
403 // UNC: \\server or \\server\share
405 while (len < path.Length && !IsDsc (path [len])) len++;
408 if (len < path.Length) {
410 while (len < path.Length && !IsDsc (path [len])) len++;
413 return DirectorySeparatorStr +
414 DirectorySeparatorStr +
415 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
416 } else if (IsDsc (path [0])) {
417 // path starts with '\' or '/'
418 return DirectorySeparatorStr;
419 } else if (path[1] == VolumeSeparatorChar) {
421 if (path.Length >= 3 && (IsDsc (path [2]))) len++;
423 return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
424 return path.Substring (0, len);
428 // FIXME: Further limit the assertion when imperative Assert is implemented
429 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
430 public static string GetTempFileName ()
441 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
444 f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
445 8192, false, (FileOptions) 1);
447 catch (SecurityException) {
448 // avoid an endless loop
459 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
460 public static string GetTempPath ()
462 string p = get_temp_path ();
463 if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
464 return p + DirectorySeparatorChar;
469 [MethodImplAttribute(MethodImplOptions.InternalCall)]
470 private static extern string get_temp_path ();
472 public static bool HasExtension (string path)
474 if (path == null || path.Trim ().Length == 0)
477 if (path.IndexOfAny (InvalidPathChars) != -1)
478 throw new ArgumentException ("Illegal characters in path.");
480 int pos = findExtension (path);
481 return 0 <= pos && pos < path.Length - 1;
484 public static bool IsPathRooted (string path)
486 if (path == null || path.Length == 0)
489 if (path.IndexOfAny (InvalidPathChars) != -1)
490 throw new ArgumentException ("Illegal characters in path.");
493 return (c == DirectorySeparatorChar ||
494 c == AltDirectorySeparatorChar ||
495 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
499 public static char[] GetInvalidFileNameChars ()
501 // return a new array as we do not want anyone to be able to change the values
502 if (Environment.IsRunningOnWindows) {
503 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
504 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
505 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
506 '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
508 return new char [2] { '\x00', '/' };
512 public static char[] GetInvalidPathChars ()
514 // return a new array as we do not want anyone to be able to change the values
515 if (Environment.IsRunningOnWindows) {
516 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
517 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
518 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
521 return new char [1] { '\x00' };
525 public static string GetRandomFileName ()
527 // returns a 8.3 filename (total size 12)
528 StringBuilder sb = new StringBuilder (12);
529 // using strong crypto but without creating the file
530 RandomNumberGenerator rng = RandomNumberGenerator.Create ();
531 byte [] buffer = new byte [11];
532 rng.GetBytes (buffer);
534 for (int i = 0; i < buffer.Length; i++) {
538 // restrict to length of range [a..z0..9]
539 int b = (buffer [i] % 36);
540 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
544 return sb.ToString ();
547 // private class methods
549 private static int findExtension (string path)
551 // method should return the index of the path extension
552 // start or -1 if no valid extension
554 int iLastDot = path.LastIndexOf ('.');
555 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
557 if (iLastDot > iLastSep)
565 VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
566 DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
567 AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
569 PathSeparator = MonoIO.PathSeparator;
571 // this copy will be modifiable ("by design")
572 InvalidPathChars = GetInvalidPathChars ();
574 if (Environment.IsRunningOnWindows) {
575 InvalidPathChars = new char [15] { '\x00', '\x08', '\x10', '\x11', '\x12', '\x14', '\x15', '\x16',
576 '\x17', '\x18', '\x19', '\x22', '\x3C', '\x3E', '\x7C' };
578 InvalidPathChars = new char [1] { '\x00' };
583 DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
584 PathSeparatorChars = new char [] {
585 DirectorySeparatorChar,
586 AltDirectorySeparatorChar,
590 dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
593 // returns the server and share part of a UNC. Assumes "path" is a UNC.
594 static string GetServerAndShare (string path)
597 while (len < path.Length && !IsDsc (path [len])) len++;
599 if (len < path.Length) {
601 while (len < path.Length && !IsDsc (path [len])) len++;
604 return path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
607 // assumes Environment.IsRunningOnWindows == true
608 static bool SameRoot (string root, string path)
610 // compare root - if enough details are available
611 if ((root.Length < 2) || (path.Length < 2))
615 if (IsDsc (root[0]) && IsDsc (root[1])) {
616 if (!(IsDsc (path[0]) && IsDsc (path[1])))
619 string rootShare = GetServerAndShare (root);
620 string pathShare = GetServerAndShare (path);
622 return String.Compare (rootShare, pathShare, true, CultureInfo.InvariantCulture) == 0;
626 if (!root [0].Equals (path [0]))
628 // presence of the separator
629 if (path[1] != Path.VolumeSeparatorChar)
631 if ((root.Length > 2) && (path.Length > 2)) {
632 // but don't directory compare the directory separator
633 return (IsDsc (root[2]) && IsDsc (path[2]));
638 static string CanonicalizePath (string path)
640 // STEP 1: Check for empty string
643 if (Environment.IsRunningOnWindows)
646 if (path.Length == 0)
649 // STEP 2: Check to see if this is only a root
650 string root = Path.GetPathRoot (path);
651 // it will return '\' for path '\', while it should return 'c:\' or so.
652 // Note: commenting this out makes the need for the (target == 1...) check in step 5
653 //if (root == path) return path;
655 // STEP 3: split the directories, this gets rid of consecutative "/"'s
656 string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
657 // STEP 4: Get rid of directories containing . and ..
660 bool isUnc = Environment.IsRunningOnWindows &&
661 root.Length > 2 && IsDsc (root[0]) && IsDsc (root[1]);
663 // Set an overwrite limit for UNC paths since '\' + server + share
664 // must not be eliminated by the '..' elimination algorithm.
665 int limit = isUnc ? 3 : 0;
667 for (int i = 0; i < dirs.Length; i++) {
668 // WIN32 path components must be trimmed
669 if (Environment.IsRunningOnWindows)
670 dirs[i] = dirs[i].Trim ();
672 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
674 else if (dirs[i] == "..") {
675 // don't overwrite path segments below the limit
679 dirs[target++] = dirs[i];
682 // STEP 5: Combine everything.
683 if (target == 0 || (target == 1 && dirs[0] == ""))
686 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
687 if (Environment.IsRunningOnWindows) {
688 // append leading '\' of the UNC path that was lost in STEP 3.
690 ret = Path.DirectorySeparatorStr + ret;
692 if (!SameRoot (root, ret))
697 } else if (!IsDsc (path[0]) && SameRoot (root, path)) {
698 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
699 ret += Path.DirectorySeparatorChar;
702 string current = Directory.GetCurrentDirectory ();
703 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
704 // DOS local file path
705 if (ret.Length == 0 || IsDsc (ret[0]))
707 return current.Substring (0, 2) + ret;
708 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
709 return current + ret.Substring (1);
711 return current + ret;
718 // required for FileIOPermission (and most proibably reusable elsewhere too)
719 // both path MUST be "full paths"
720 static internal bool IsPathSubsetOf (string subset, string path)
722 if (subset.Length > path.Length)
725 // check that everything up to the last separator match
726 int slast = subset.LastIndexOfAny (PathSeparatorChars);
727 if (String.Compare (subset, 0, path, 0, slast) != 0)
731 // then check if the last segment is identical
732 int plast = path.IndexOfAny (PathSeparatorChars, slast);
733 if (plast >= slast) {
734 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
736 if (subset.Length != path.Length)
739 return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;