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
8 // Copyright 2011 Xamarin Inc (http://www.xamarin.com).
10 // Author: Jim Richardson, develop@wtfo-guru.com
11 // Dan Lewis (dihlewis@yahoo.co.uk)
12 // Gonzalo Paniagua Javier (gonzalo@ximian.com)
13 // Ben Maurer (bmaurer@users.sourceforge.net)
14 // Sebastien Pouliot <sebastien@ximian.com>
15 // Created: Saturday, August 11, 2001
17 //------------------------------------------------------------------------------
20 // Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
22 // Permission is hereby granted, free of charge, to any person obtaining
23 // a copy of this software and associated documentation files (the
24 // "Software"), to deal in the Software without restriction, including
25 // without limitation the rights to use, copy, modify, merge, publish,
26 // distribute, sublicense, and/or sell copies of the Software, and to
27 // permit persons to whom the Software is furnished to do so, subject to
28 // the following conditions:
30 // The above copyright notice and this permission notice shall be
31 // included in all copies or substantial portions of the Software.
33 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
34 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
35 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
36 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
37 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
38 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
39 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
42 using System.Globalization;
43 using System.Runtime.CompilerServices;
44 using System.Runtime.InteropServices;
45 using System.Security;
46 using System.Security.Cryptography;
47 using System.Security.Permissions;
53 public static class Path {
55 [Obsolete ("see GetInvalidPathChars and GetInvalidFileNameChars methods.")]
56 public static readonly char[] InvalidPathChars;
57 public static readonly char AltDirectorySeparatorChar;
58 public static readonly char DirectorySeparatorChar;
59 public static readonly char PathSeparator;
60 internal static readonly string DirectorySeparatorStr;
61 public static readonly char VolumeSeparatorChar;
63 internal static readonly char[] PathSeparatorChars;
64 private static readonly bool dirEqualsVolume;
67 public static string ChangeExtension (string path, string extension)
72 if (path.IndexOfAny (InvalidPathChars) != -1)
73 throw new ArgumentException ("Illegal characters in path.");
75 int iExt = findExtension (path);
77 if (extension == null)
78 return iExt < 0 ? path : path.Substring (0, iExt);
79 else if (extension.Length == 0)
80 return iExt < 0 ? path + '.' : path.Substring (0, iExt + 1);
82 else if (path.Length != 0) {
83 if (extension.Length > 0 && extension [0] != '.')
84 extension = "." + extension;
86 extension = String.Empty;
89 return path + extension;
90 } else if (iExt > 0) {
91 string temp = path.Substring (0, iExt);
92 return temp + extension;
98 public static string Combine (string path1, string path2)
101 throw new ArgumentNullException ("path1");
104 throw new ArgumentNullException ("path2");
106 if (path1.Length == 0)
109 if (path2.Length == 0)
112 if (path1.IndexOfAny (InvalidPathChars) != -1)
113 throw new ArgumentException ("Illegal characters in path.");
115 if (path2.IndexOfAny (InvalidPathChars) != -1)
116 throw new ArgumentException ("Illegal characters in path.");
119 if (IsPathRooted (path2))
122 char p1end = path1 [path1.Length - 1];
123 if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
124 return path1 + DirectorySeparatorStr + path2;
126 return path1 + path2;
131 // * Removes duplicat path separators from a string
132 // * If the string starts with \\, preserves the first two (hostname on Windows)
133 // * Removes the trailing path separator.
134 // * Returns the DirectorySeparatorChar for the single input DirectorySeparatorChar or AltDirectorySeparatorChar
136 // Unlike CanonicalizePath, this does not do any path resolution
137 // (which GetDirectoryName is not supposed to do).
139 internal static string CleanPath (string s)
147 if (l > 2 && s0 == '\\' && s [1] == '\\'){
151 // We are only left with root
152 if (l == 1 && (s0 == DirectorySeparatorChar || s0 == AltDirectorySeparatorChar))
156 for (int i = start; i < l; i++){
159 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
165 if (c == DirectorySeparatorChar || c == AltDirectorySeparatorChar)
173 char [] copy = new char [l-sub];
178 for (int i = start, j = start; i < l && j < copy.Length; i++){
181 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar){
186 // For non-trailing cases.
187 if (j+1 != copy.Length){
188 copy [j++] = DirectorySeparatorChar;
191 if (c != DirectorySeparatorChar && c != AltDirectorySeparatorChar)
196 return new String (copy);
199 public static string GetDirectoryName (string path)
201 // LAMESPEC: For empty string MS docs say both
202 // return null AND throw exception. Seems .NET throws.
203 if (path == String.Empty)
204 throw new ArgumentException("Invalid path");
206 if (path == null || GetPathRoot (path) == path)
209 if (path.Trim ().Length == 0)
210 throw new ArgumentException ("Argument string consists of whitespace characters only.");
212 if (path.IndexOfAny (System.IO.Path.InvalidPathChars) > -1)
213 throw new ArgumentException ("Path contains invalid characters");
215 int nLast = path.LastIndexOfAny (PathSeparatorChars);
220 string ret = path.Substring (0, nLast);
223 if (l >= 2 && DirectorySeparatorChar == '\\' && ret [l - 1] == VolumeSeparatorChar)
224 return ret + DirectorySeparatorChar;
225 else if (l == 1 && DirectorySeparatorChar == '\\' && path.Length >= 2 && path [nLast] == VolumeSeparatorChar)
226 return ret + VolumeSeparatorChar;
229 // Important: do not use CanonicalizePath here, use
230 // the custom CleanPath here, as this should not
231 // return absolute paths
233 return CleanPath (ret);
240 public static string GetExtension (string path)
245 if (path.IndexOfAny (InvalidPathChars) != -1)
246 throw new ArgumentException ("Illegal characters in path.");
248 int iExt = findExtension (path);
252 if (iExt < path.Length - 1)
253 return path.Substring (iExt);
258 public static string GetFileName (string path)
260 if (path == null || path.Length == 0)
263 if (path.IndexOfAny (InvalidPathChars) != -1)
264 throw new ArgumentException ("Illegal characters in path.");
266 int nLast = path.LastIndexOfAny (PathSeparatorChars);
268 return path.Substring (nLast + 1);
273 public static string GetFileNameWithoutExtension (string path)
275 return ChangeExtension (GetFileName (path), null);
278 public static string GetFullPath (string path)
280 string fullpath = InsecureGetFullPath (path);
282 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
285 if (SecurityManager.SecurityEnabled) {
286 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
292 internal static string WindowsDriveAdjustment (string path)
294 // two special cases to consider when a drive is specified
297 if ((path [1] != ':') || !Char.IsLetter (path [0]))
300 string current = Directory.InsecureGetCurrentDirectory ();
301 // first, only the drive is specified
302 if (path.Length == 2) {
303 // then if the current directory is on the same drive
304 if (current [0] == path [0])
305 path = current; // we return it
308 } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
309 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
310 // If the current directory is on the specified drive...
311 if (current [0] == path [0]) {
312 // then specified directory is appended to the current drive directory
313 path = Path.Combine (current, path.Substring (2, path.Length - 2));
315 // if not, then just pretend there was a separator (Path.Combine won't work in this case)
316 path = String.Concat (path.Substring (0, 2), DirectorySeparatorStr, path.Substring (2, path.Length - 2));
322 // insecure - do not call directly
323 internal static string InsecureGetFullPath (string path)
326 throw new ArgumentNullException ("path");
328 if (path.Trim ().Length == 0) {
329 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
330 throw new ArgumentException (msg);
333 // adjust for drives, i.e. a special case for windows
334 if (Environment.IsRunningOnWindows)
335 path = WindowsDriveAdjustment (path);
337 // if the supplied path ends with a separator...
338 char end = path [path.Length - 1];
340 var canonicalize = true;
341 if (path.Length >= 2 &&
344 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
345 throw new ArgumentException ("UNC paths should be of the form \\\\server\\share.");
347 if (path [0] != DirectorySeparatorChar)
348 path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
351 if (!IsPathRooted (path)) {
353 // avoid calling expensive CanonicalizePath when possible
354 if (!Environment.IsRunningOnWindows) {
356 while ((start = path.IndexOf ('.', start)) != -1) {
357 if (++start == path.Length || path [start] == DirectorySeparatorChar || path [start] == AltDirectorySeparatorChar)
360 canonicalize = start > 0;
363 path = Directory.InsecureGetCurrentDirectory() + DirectorySeparatorStr + path;
364 } else if (DirectorySeparatorChar == '\\' &&
367 !IsDsc (path [1])) { // like `\abc\def'
368 string current = Directory.InsecureGetCurrentDirectory();
369 if (current [1] == VolumeSeparatorChar)
370 path = current.Substring (0, 2) + path;
372 path = current.Substring (0, current.IndexOf ('\\', current.IndexOfOrdinalUnchecked ("\\\\") + 1));
377 path = CanonicalizePath (path);
379 // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
380 if (IsDsc (end) && (path [path.Length - 1] != DirectorySeparatorChar))
381 path += DirectorySeparatorChar;
386 static bool IsDsc (char c) {
387 return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
390 public static string GetPathRoot (string path)
395 if (path.Trim ().Length == 0)
396 throw new ArgumentException ("The specified path is not of a legal form.");
398 if (!IsPathRooted (path))
401 if (DirectorySeparatorChar == '/') {
403 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
408 if (path.Length == 1 && IsDsc (path [0]))
409 return DirectorySeparatorStr;
410 else if (path.Length < 2)
413 if (IsDsc (path [0]) && IsDsc (path[1])) {
414 // UNC: \\server or \\server\share
416 while (len < path.Length && !IsDsc (path [len])) len++;
419 if (len < path.Length) {
421 while (len < path.Length && !IsDsc (path [len])) len++;
424 return DirectorySeparatorStr +
425 DirectorySeparatorStr +
426 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
427 } else if (IsDsc (path [0])) {
428 // path starts with '\' or '/'
429 return DirectorySeparatorStr;
430 } else if (path[1] == VolumeSeparatorChar) {
432 if (path.Length >= 3 && (IsDsc (path [2]))) len++;
434 return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
435 return path.Substring (0, len);
439 // FIXME: Further limit the assertion when imperative Assert is implemented
440 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
441 public static string GetTempFileName ()
449 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
455 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
458 f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
459 8192, false, (FileOptions) 1);
461 catch (IOException ex){
462 if (ex.hresult != MonoIO.FileAlreadyExistsHResult || count ++ > 65536)
471 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
472 public static string GetTempPath ()
474 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
476 string p = get_temp_path ();
477 if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
478 return p + DirectorySeparatorChar;
483 [MethodImplAttribute(MethodImplOptions.InternalCall)]
484 private static extern string get_temp_path ();
486 public static bool HasExtension (string path)
488 if (path == null || path.Trim ().Length == 0)
491 if (path.IndexOfAny (InvalidPathChars) != -1)
492 throw new ArgumentException ("Illegal characters in path.");
494 int pos = findExtension (path);
495 return 0 <= pos && pos < path.Length - 1;
498 public static bool IsPathRooted (string path)
500 if (path == null || path.Length == 0)
503 if (path.IndexOfAny (InvalidPathChars) != -1)
504 throw new ArgumentException ("Illegal characters in path.");
507 return (c == DirectorySeparatorChar ||
508 c == AltDirectorySeparatorChar ||
509 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
512 public static char[] GetInvalidFileNameChars ()
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 [41] { '\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',
519 '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
521 return new char [2] { '\x00', '/' };
525 public static char[] GetInvalidPathChars ()
527 // return a new array as we do not want anyone to be able to change the values
528 if (Environment.IsRunningOnWindows) {
529 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
530 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
531 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
534 return new char [1] { '\x00' };
538 public static string GetRandomFileName ()
540 // returns a 8.3 filename (total size 12)
541 StringBuilder sb = new StringBuilder (12);
542 // using strong crypto but without creating the file
543 RandomNumberGenerator rng = RandomNumberGenerator.Create ();
544 byte [] buffer = new byte [11];
545 rng.GetBytes (buffer);
547 for (int i = 0; i < buffer.Length; i++) {
551 // restrict to length of range [a..z0..9]
552 int b = (buffer [i] % 36);
553 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
557 return sb.ToString ();
560 // private class methods
562 private static int findExtension (string path)
564 // method should return the index of the path extension
565 // start or -1 if no valid extension
567 int iLastDot = path.LastIndexOf ('.');
568 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
570 if (iLastDot > iLastSep)
578 VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
579 DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
580 AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
582 PathSeparator = MonoIO.PathSeparator;
583 // this copy will be modifiable ("by design")
584 InvalidPathChars = GetInvalidPathChars ();
587 DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
588 PathSeparatorChars = new char [] {
589 DirectorySeparatorChar,
590 AltDirectorySeparatorChar,
594 dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
597 // returns the server and share part of a UNC. Assumes "path" is a UNC.
598 static string GetServerAndShare (string path)
601 while (len < path.Length && !IsDsc (path [len])) len++;
603 if (len < path.Length) {
605 while (len < path.Length && !IsDsc (path [len])) len++;
608 return path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
611 // assumes Environment.IsRunningOnWindows == true
612 static bool SameRoot (string root, string path)
614 // compare root - if enough details are available
615 if ((root.Length < 2) || (path.Length < 2))
619 if (IsDsc (root[0]) && IsDsc (root[1])) {
620 if (!(IsDsc (path[0]) && IsDsc (path[1])))
623 string rootShare = GetServerAndShare (root);
624 string pathShare = GetServerAndShare (path);
626 return String.Compare (rootShare, pathShare, true, CultureInfo.InvariantCulture) == 0;
630 if (!root [0].Equals (path [0]))
632 // presence of the separator
633 if (path[1] != Path.VolumeSeparatorChar)
635 if ((root.Length > 2) && (path.Length > 2)) {
636 // but don't directory compare the directory separator
637 return (IsDsc (root[2]) && IsDsc (path[2]));
642 static string CanonicalizePath (string path)
644 // STEP 1: Check for empty string
647 if (Environment.IsRunningOnWindows)
650 if (path.Length == 0)
653 // STEP 2: Check to see if this is only a root
654 string root = Path.GetPathRoot (path);
655 // it will return '\' for path '\', while it should return 'c:\' or so.
656 // Note: commenting this out makes the need for the (target == 1...) check in step 5
657 //if (root == path) return path;
659 // STEP 3: split the directories, this gets rid of consecutative "/"'s
660 string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
661 // STEP 4: Get rid of directories containing . and ..
664 bool isUnc = Environment.IsRunningOnWindows &&
665 root.Length > 2 && IsDsc (root[0]) && IsDsc (root[1]);
667 // Set an overwrite limit for UNC paths since '\' + server + share
668 // must not be eliminated by the '..' elimination algorithm.
669 int limit = isUnc ? 3 : 0;
671 for (int i = 0; i < dirs.Length; i++) {
672 // WIN32 path components must be trimmed
673 if (Environment.IsRunningOnWindows)
674 dirs[i] = dirs[i].TrimEnd ();
676 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
678 else if (dirs[i] == "..") {
679 // don't overwrite path segments below the limit
683 dirs[target++] = dirs[i];
686 // STEP 5: Combine everything.
687 if (target == 0 || (target == 1 && dirs[0] == ""))
690 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
691 if (Environment.IsRunningOnWindows) {
692 // append leading '\' of the UNC path that was lost in STEP 3.
694 ret = Path.DirectorySeparatorStr + ret;
696 if (!SameRoot (root, ret))
701 } else if (!IsDsc (path[0]) && SameRoot (root, path)) {
702 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
703 ret += Path.DirectorySeparatorChar;
706 string current = Directory.GetCurrentDirectory ();
707 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
708 // DOS local file path
709 if (ret.Length == 0 || IsDsc (ret[0]))
711 return current.Substring (0, 2) + ret;
712 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
713 return current + ret.Substring (1);
715 return current + ret;
722 // required for FileIOPermission (and most proibably reusable elsewhere too)
723 // both path MUST be "full paths"
724 static internal bool IsPathSubsetOf (string subset, string path)
726 if (subset.Length > path.Length)
729 // check that everything up to the last separator match
730 int slast = subset.LastIndexOfAny (PathSeparatorChars);
731 if (String.Compare (subset, 0, path, 0, slast) != 0)
735 // then check if the last segment is identical
736 int plast = path.IndexOfAny (PathSeparatorChars, slast);
737 if (plast >= slast) {
738 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
740 if (subset.Length != path.Length)
743 return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;
751 static string Combine (params string [] paths)
754 throw new ArgumentNullException ("paths");
757 var ret = new StringBuilder ();
758 int pathsLen = paths.Length;
760 foreach (var s in paths) {
763 throw new ArgumentNullException ("One of the paths contains a null value", "paths");
764 if (s.IndexOfAny (InvalidPathChars) != -1)
765 throw new ArgumentException ("Illegal characters in path.");
768 if (IsPathRooted (s))
773 if (slen > 0 && pathsLen > 0) {
774 char p1end = s [slen - 1];
775 if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
780 ret.Append (DirectorySeparatorStr);
783 return ret.ToString ();
791 static string Combine (string path1, string path2, string path3)
794 throw new ArgumentNullException ("path1");
797 throw new ArgumentNullException ("path2");
800 throw new ArgumentNullException ("path3");
802 return Combine (new string [] { path1, path2, path3 });
810 static string Combine (string path1, string path2, string path3, string path4)
813 throw new ArgumentNullException ("path1");
816 throw new ArgumentNullException ("path2");
819 throw new ArgumentNullException ("path3");
822 throw new ArgumentNullException ("path4");
824 return Combine (new string [] { path1, path2, path3, path4 });
827 internal static void Validate (string path)
829 Validate (path, "path");
832 internal static void Validate (string path, string parameterName)
835 throw new ArgumentNullException (parameterName);
836 if (String.IsNullOrWhiteSpace (path))
837 throw new ArgumentException (Locale.GetText ("Path is empty"));
838 if (path.IndexOfAny (Path.InvalidPathChars) != -1)
839 throw new ArgumentException (Locale.GetText ("Path contains invalid chars"));
840 if (Environment.IsRunningOnWindows) {
841 int idx = path.IndexOf (':');
842 if (idx >= 0 && idx != 1)
843 throw new ArgumentException (parameterName);