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;
227 // Important: do not use CanonicalizePath here, use
228 // the custom CleanPath here, as this should not
229 // return absolute paths
231 return CleanPath (ret);
238 public static string GetExtension (string path)
243 if (path.IndexOfAny (InvalidPathChars) != -1)
244 throw new ArgumentException ("Illegal characters in path.");
246 int iExt = findExtension (path);
250 if (iExt < path.Length - 1)
251 return path.Substring (iExt);
256 public static string GetFileName (string path)
258 if (path == null || path.Length == 0)
261 if (path.IndexOfAny (InvalidPathChars) != -1)
262 throw new ArgumentException ("Illegal characters in path.");
264 int nLast = path.LastIndexOfAny (PathSeparatorChars);
266 return path.Substring (nLast + 1);
271 public static string GetFileNameWithoutExtension (string path)
273 return ChangeExtension (GetFileName (path), null);
276 public static string GetFullPath (string path)
278 string fullpath = InsecureGetFullPath (path);
280 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
283 if (SecurityManager.SecurityEnabled) {
284 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
290 internal static string WindowsDriveAdjustment (string path)
292 // two special cases to consider when a drive is specified
295 if ((path [1] != ':') || !Char.IsLetter (path [0]))
298 string current = Directory.InsecureGetCurrentDirectory ();
299 // first, only the drive is specified
300 if (path.Length == 2) {
301 // then if the current directory is on the same drive
302 if (current [0] == path [0])
303 path = current; // we return it
306 } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
307 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
308 // If the current directory is on the specified drive...
309 if (current [0] == path [0]) {
310 // then specified directory is appended to the current drive directory
311 path = Path.Combine (current, path.Substring (2, path.Length - 2));
313 // if not, then just pretend there was a separator (Path.Combine won't work in this case)
314 path = String.Concat (path.Substring (0, 2), DirectorySeparatorStr, path.Substring (2, path.Length - 2));
320 // insecure - do not call directly
321 internal static string InsecureGetFullPath (string path)
324 throw new ArgumentNullException ("path");
326 if (path.Trim ().Length == 0) {
327 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
328 throw new ArgumentException (msg);
331 // adjust for drives, i.e. a special case for windows
332 if (Environment.IsRunningOnWindows)
333 path = WindowsDriveAdjustment (path);
335 // if the supplied path ends with a separator...
336 char end = path [path.Length - 1];
338 var canonicalize = true;
339 if (path.Length >= 2 &&
342 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
343 throw new ArgumentException ("UNC paths should be of the form \\\\server\\share.");
345 if (path [0] != DirectorySeparatorChar)
346 path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
349 if (!IsPathRooted (path)) {
351 // avoid calling expensive CanonicalizePath when possible
352 if (!Environment.IsRunningOnWindows) {
354 while ((start = path.IndexOf ('.', start)) != -1) {
355 if (++start == path.Length || path [start] == DirectorySeparatorChar || path [start] == AltDirectorySeparatorChar)
358 canonicalize = start > 0;
361 path = Directory.InsecureGetCurrentDirectory() + DirectorySeparatorStr + path;
362 } else if (DirectorySeparatorChar == '\\' &&
365 !IsDsc (path [1])) { // like `\abc\def'
366 string current = Directory.InsecureGetCurrentDirectory();
367 if (current [1] == VolumeSeparatorChar)
368 path = current.Substring (0, 2) + path;
370 path = current.Substring (0, current.IndexOf ('\\', current.IndexOf ("\\\\") + 1));
375 path = CanonicalizePath (path);
377 // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
378 if (IsDsc (end) && (path [path.Length - 1] != DirectorySeparatorChar))
379 path += DirectorySeparatorChar;
384 static bool IsDsc (char c) {
385 return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
388 public static string GetPathRoot (string path)
393 if (path.Trim ().Length == 0)
394 throw new ArgumentException ("The specified path is not of a legal form.");
396 if (!IsPathRooted (path))
399 if (DirectorySeparatorChar == '/') {
401 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
406 if (path.Length == 1 && IsDsc (path [0]))
407 return DirectorySeparatorStr;
408 else if (path.Length < 2)
411 if (IsDsc (path [0]) && IsDsc (path[1])) {
412 // UNC: \\server or \\server\share
414 while (len < path.Length && !IsDsc (path [len])) len++;
417 if (len < path.Length) {
419 while (len < path.Length && !IsDsc (path [len])) len++;
422 return DirectorySeparatorStr +
423 DirectorySeparatorStr +
424 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
425 } else if (IsDsc (path [0])) {
426 // path starts with '\' or '/'
427 return DirectorySeparatorStr;
428 } else if (path[1] == VolumeSeparatorChar) {
430 if (path.Length >= 3 && (IsDsc (path [2]))) len++;
432 return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
433 return path.Substring (0, len);
437 // FIXME: Further limit the assertion when imperative Assert is implemented
438 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
439 public static string GetTempFileName ()
447 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
453 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
456 f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
457 8192, false, (FileOptions) 1);
459 catch (IOException ex){
460 if (ex.hresult != MonoIO.FileAlreadyExistsHResult || count ++ > 65536)
469 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
470 public static string GetTempPath ()
472 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
474 string p = get_temp_path ();
475 if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
476 return p + DirectorySeparatorChar;
481 [MethodImplAttribute(MethodImplOptions.InternalCall)]
482 private static extern string get_temp_path ();
484 public static bool HasExtension (string path)
486 if (path == null || path.Trim ().Length == 0)
489 if (path.IndexOfAny (InvalidPathChars) != -1)
490 throw new ArgumentException ("Illegal characters in path.");
492 int pos = findExtension (path);
493 return 0 <= pos && pos < path.Length - 1;
496 public static bool IsPathRooted (string path)
498 if (path == null || path.Length == 0)
501 if (path.IndexOfAny (InvalidPathChars) != -1)
502 throw new ArgumentException ("Illegal characters in path.");
505 return (c == DirectorySeparatorChar ||
506 c == AltDirectorySeparatorChar ||
507 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
510 public static char[] GetInvalidFileNameChars ()
512 // return a new array as we do not want anyone to be able to change the values
513 if (Environment.IsRunningOnWindows) {
514 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
515 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
516 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
517 '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
519 return new char [2] { '\x00', '/' };
523 public static char[] GetInvalidPathChars ()
525 // return a new array as we do not want anyone to be able to change the values
526 if (Environment.IsRunningOnWindows) {
527 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
528 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
529 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
532 return new char [1] { '\x00' };
536 public static string GetRandomFileName ()
538 // returns a 8.3 filename (total size 12)
539 StringBuilder sb = new StringBuilder (12);
540 // using strong crypto but without creating the file
541 RandomNumberGenerator rng = RandomNumberGenerator.Create ();
542 byte [] buffer = new byte [11];
543 rng.GetBytes (buffer);
545 for (int i = 0; i < buffer.Length; i++) {
549 // restrict to length of range [a..z0..9]
550 int b = (buffer [i] % 36);
551 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
555 return sb.ToString ();
558 // private class methods
560 private static int findExtension (string path)
562 // method should return the index of the path extension
563 // start or -1 if no valid extension
565 int iLastDot = path.LastIndexOf ('.');
566 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
568 if (iLastDot > iLastSep)
576 VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
577 DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
578 AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
580 PathSeparator = MonoIO.PathSeparator;
581 // this copy will be modifiable ("by design")
582 InvalidPathChars = GetInvalidPathChars ();
585 DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
586 PathSeparatorChars = new char [] {
587 DirectorySeparatorChar,
588 AltDirectorySeparatorChar,
592 dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
595 // returns the server and share part of a UNC. Assumes "path" is a UNC.
596 static string GetServerAndShare (string path)
599 while (len < path.Length && !IsDsc (path [len])) len++;
601 if (len < path.Length) {
603 while (len < path.Length && !IsDsc (path [len])) len++;
606 return path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
609 // assumes Environment.IsRunningOnWindows == true
610 static bool SameRoot (string root, string path)
612 // compare root - if enough details are available
613 if ((root.Length < 2) || (path.Length < 2))
617 if (IsDsc (root[0]) && IsDsc (root[1])) {
618 if (!(IsDsc (path[0]) && IsDsc (path[1])))
621 string rootShare = GetServerAndShare (root);
622 string pathShare = GetServerAndShare (path);
624 return String.Compare (rootShare, pathShare, true, CultureInfo.InvariantCulture) == 0;
628 if (!root [0].Equals (path [0]))
630 // presence of the separator
631 if (path[1] != Path.VolumeSeparatorChar)
633 if ((root.Length > 2) && (path.Length > 2)) {
634 // but don't directory compare the directory separator
635 return (IsDsc (root[2]) && IsDsc (path[2]));
640 static string CanonicalizePath (string path)
642 // STEP 1: Check for empty string
645 if (Environment.IsRunningOnWindows)
648 if (path.Length == 0)
651 // STEP 2: Check to see if this is only a root
652 string root = Path.GetPathRoot (path);
653 // it will return '\' for path '\', while it should return 'c:\' or so.
654 // Note: commenting this out makes the need for the (target == 1...) check in step 5
655 //if (root == path) return path;
657 // STEP 3: split the directories, this gets rid of consecutative "/"'s
658 string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
659 // STEP 4: Get rid of directories containing . and ..
662 bool isUnc = Environment.IsRunningOnWindows &&
663 root.Length > 2 && IsDsc (root[0]) && IsDsc (root[1]);
665 // Set an overwrite limit for UNC paths since '\' + server + share
666 // must not be eliminated by the '..' elimination algorithm.
667 int limit = isUnc ? 3 : 0;
669 for (int i = 0; i < dirs.Length; i++) {
670 // WIN32 path components must be trimmed
671 if (Environment.IsRunningOnWindows)
672 dirs[i] = dirs[i].TrimEnd ();
674 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
676 else if (dirs[i] == "..") {
677 // don't overwrite path segments below the limit
681 dirs[target++] = dirs[i];
684 // STEP 5: Combine everything.
685 if (target == 0 || (target == 1 && dirs[0] == ""))
688 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
689 if (Environment.IsRunningOnWindows) {
690 // append leading '\' of the UNC path that was lost in STEP 3.
692 ret = Path.DirectorySeparatorStr + ret;
694 if (!SameRoot (root, ret))
699 } else if (!IsDsc (path[0]) && SameRoot (root, path)) {
700 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
701 ret += Path.DirectorySeparatorChar;
704 string current = Directory.GetCurrentDirectory ();
705 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
706 // DOS local file path
707 if (ret.Length == 0 || IsDsc (ret[0]))
709 return current.Substring (0, 2) + ret;
710 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
711 return current + ret.Substring (1);
713 return current + ret;
720 // required for FileIOPermission (and most proibably reusable elsewhere too)
721 // both path MUST be "full paths"
722 static internal bool IsPathSubsetOf (string subset, string path)
724 if (subset.Length > path.Length)
727 // check that everything up to the last separator match
728 int slast = subset.LastIndexOfAny (PathSeparatorChars);
729 if (String.Compare (subset, 0, path, 0, slast) != 0)
733 // then check if the last segment is identical
734 int plast = path.IndexOfAny (PathSeparatorChars, slast);
735 if (plast >= slast) {
736 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
738 if (subset.Length != path.Length)
741 return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;
749 static string Combine (params string [] paths)
752 throw new ArgumentNullException ("paths");
755 var ret = new StringBuilder ();
756 int pathsLen = paths.Length;
758 foreach (var s in paths) {
761 throw new ArgumentNullException ("One of the paths contains a null value", "paths");
762 if (s.IndexOfAny (InvalidPathChars) != -1)
763 throw new ArgumentException ("Illegal characters in path.");
766 if (IsPathRooted (s))
771 if (slen > 0 && pathsLen > 0) {
772 char p1end = s [slen - 1];
773 if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
778 ret.Append (DirectorySeparatorStr);
781 return ret.ToString ();
789 static string Combine (string path1, string path2, string path3)
792 throw new ArgumentNullException ("path1");
795 throw new ArgumentNullException ("path2");
798 throw new ArgumentNullException ("path3");
800 return Combine (new string [] { path1, path2, path3 });
808 static string Combine (string path1, string path2, string path3, string path4)
811 throw new ArgumentNullException ("path1");
814 throw new ArgumentNullException ("path2");
817 throw new ArgumentNullException ("path3");
820 throw new ArgumentNullException ("path4");
822 return Combine (new string [] { path1, path2, path3, path4 });
825 internal static void Validate (string path)
827 Validate (path, "path");
830 internal static void Validate (string path, string parameterName)
833 throw new ArgumentNullException (parameterName);
834 if (String.IsNullOrWhiteSpace (path))
835 throw new ArgumentException (Locale.GetText ("Path is empty"));
836 if (path.IndexOfAny (Path.InvalidPathChars) != -1)
837 throw new ArgumentException (Locale.GetText ("Path contains invalid chars"));
838 if (Environment.IsRunningOnWindows) {
839 int idx = path.IndexOf (':');
840 if (idx >= 0 && idx != 1)
841 throw new ArgumentException (parameterName);