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 [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
293 // http://msdn.microsoft.com/en-us/library/windows/desktop/aa364963%28v=vs.85%29.aspx
294 // http://www.codeproject.com/Tips/223321/Win32-API-GetFullPathName
295 private static extern int GetFullPathName(string path, int numBufferChars, StringBuilder buffer, ref IntPtr lpFilePartOrNull);
297 internal static string GetFullPathName(string path)
299 const int MAX_PATH = 260;
300 StringBuilder buffer = new StringBuilder(MAX_PATH);
301 IntPtr ptr = IntPtr.Zero;
302 int length = GetFullPathName(path, MAX_PATH, buffer, ref ptr);
305 int error = Marshal.GetLastWin32Error();
306 throw new IOException("Windows API call to GetFullPathName failed, Windows error code: " + error);
308 else if (length > MAX_PATH)
310 buffer = new StringBuilder(length);
311 GetFullPathName(path, length, buffer, ref ptr);
313 return buffer.ToString();
316 internal static string WindowsDriveAdjustment (string path)
318 // two special cases to consider when a drive is specified
321 if ((path [1] != ':') || !Char.IsLetter (path [0]))
324 string current = Directory.InsecureGetCurrentDirectory ();
325 // first, only the drive is specified
326 if (path.Length == 2) {
327 // then if the current directory is on the same drive
328 if (current [0] == path [0])
329 path = current; // we return it
331 path = GetFullPathName(path); // we have to use the GetFullPathName Windows API
332 } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
333 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
334 // If the current directory is on the specified drive...
335 if (current [0] == path [0]) {
336 // then specified directory is appended to the current drive directory
337 path = Path.Combine (current, path.Substring (2, path.Length - 2));
339 // we have to use the GetFullPathName Windows API
340 path = GetFullPathName(path);
346 // insecure - do not call directly
347 internal static string InsecureGetFullPath (string path)
350 throw new ArgumentNullException ("path");
352 if (path.Trim ().Length == 0) {
353 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
354 throw new ArgumentException (msg);
357 // adjust for drives, i.e. a special case for windows
358 if (Environment.IsRunningOnWindows)
359 path = WindowsDriveAdjustment (path);
361 // if the supplied path ends with a separator...
362 char end = path [path.Length - 1];
364 var canonicalize = true;
365 if (path.Length >= 2 &&
368 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
369 throw new ArgumentException ("UNC paths should be of the form \\\\server\\share.");
371 if (path [0] != DirectorySeparatorChar)
372 path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
375 if (!IsPathRooted (path)) {
377 // avoid calling expensive CanonicalizePath when possible
378 if (!Environment.IsRunningOnWindows) {
380 while ((start = path.IndexOf ('.', start)) != -1) {
381 if (++start == path.Length || path [start] == DirectorySeparatorChar || path [start] == AltDirectorySeparatorChar)
384 canonicalize = start > 0;
387 path = Directory.InsecureGetCurrentDirectory() + DirectorySeparatorStr + path;
388 } else if (DirectorySeparatorChar == '\\' &&
391 !IsDsc (path [1])) { // like `\abc\def'
392 string current = Directory.InsecureGetCurrentDirectory();
393 if (current [1] == VolumeSeparatorChar)
394 path = current.Substring (0, 2) + path;
396 path = current.Substring (0, current.IndexOf ('\\', current.IndexOfOrdinalUnchecked ("\\\\") + 1));
401 path = CanonicalizePath (path);
403 // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
404 if (IsDsc (end) && (path [path.Length - 1] != DirectorySeparatorChar))
405 path += DirectorySeparatorChar;
410 static bool IsDsc (char c) {
411 return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
414 public static string GetPathRoot (string path)
419 if (path.Trim ().Length == 0)
420 throw new ArgumentException ("The specified path is not of a legal form.");
422 if (!IsPathRooted (path))
425 if (DirectorySeparatorChar == '/') {
427 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
432 if (path.Length == 1 && IsDsc (path [0]))
433 return DirectorySeparatorStr;
434 else if (path.Length < 2)
437 if (IsDsc (path [0]) && IsDsc (path[1])) {
438 // UNC: \\server or \\server\share
440 while (len < path.Length && !IsDsc (path [len])) len++;
443 if (len < path.Length) {
445 while (len < path.Length && !IsDsc (path [len])) len++;
448 return DirectorySeparatorStr +
449 DirectorySeparatorStr +
450 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
451 } else if (IsDsc (path [0])) {
452 // path starts with '\' or '/'
453 return DirectorySeparatorStr;
454 } else if (path[1] == VolumeSeparatorChar) {
456 if (path.Length >= 3 && (IsDsc (path [2]))) len++;
458 return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
459 return path.Substring (0, len);
463 // FIXME: Further limit the assertion when imperative Assert is implemented
464 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
465 public static string GetTempFileName ()
473 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
479 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
482 f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
483 8192, false, (FileOptions) 1);
485 catch (IOException ex){
486 if (ex.hresult != MonoIO.FileAlreadyExistsHResult || count ++ > 65536)
489 catch (UnauthorizedAccessException ex) {
490 if (count ++ > 65536)
491 throw new IOException (ex.Message, ex);
499 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
500 public static string GetTempPath ()
502 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
504 string p = get_temp_path ();
505 if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
506 return p + DirectorySeparatorChar;
511 [MethodImplAttribute(MethodImplOptions.InternalCall)]
512 private static extern string get_temp_path ();
514 public static bool HasExtension (string path)
516 if (path == null || path.Trim ().Length == 0)
519 if (path.IndexOfAny (InvalidPathChars) != -1)
520 throw new ArgumentException ("Illegal characters in path.");
522 int pos = findExtension (path);
523 return 0 <= pos && pos < path.Length - 1;
526 public static bool IsPathRooted (string path)
528 if (path == null || path.Length == 0)
531 if (path.IndexOfAny (InvalidPathChars) != -1)
532 throw new ArgumentException ("Illegal characters in path.");
535 return (c == DirectorySeparatorChar ||
536 c == AltDirectorySeparatorChar ||
537 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
540 public static char[] GetInvalidFileNameChars ()
542 // return a new array as we do not want anyone to be able to change the values
543 if (Environment.IsRunningOnWindows) {
544 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
545 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
546 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
547 '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
549 return new char [2] { '\x00', '/' };
553 public static char[] GetInvalidPathChars ()
555 // return a new array as we do not want anyone to be able to change the values
556 if (Environment.IsRunningOnWindows) {
557 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
558 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
559 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
562 return new char [1] { '\x00' };
566 public static string GetRandomFileName ()
568 // returns a 8.3 filename (total size 12)
569 StringBuilder sb = new StringBuilder (12);
570 // using strong crypto but without creating the file
571 RandomNumberGenerator rng = RandomNumberGenerator.Create ();
572 byte [] buffer = new byte [11];
573 rng.GetBytes (buffer);
575 for (int i = 0; i < buffer.Length; i++) {
579 // restrict to length of range [a..z0..9]
580 int b = (buffer [i] % 36);
581 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
585 return sb.ToString ();
588 // private class methods
590 private static int findExtension (string path)
592 // method should return the index of the path extension
593 // start or -1 if no valid extension
595 int iLastDot = path.LastIndexOf ('.');
596 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
598 if (iLastDot > iLastSep)
606 VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
607 DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
608 AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
610 PathSeparator = MonoIO.PathSeparator;
611 // this copy will be modifiable ("by design")
612 InvalidPathChars = GetInvalidPathChars ();
615 DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
616 PathSeparatorChars = new char [] {
617 DirectorySeparatorChar,
618 AltDirectorySeparatorChar,
622 dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
625 // returns the server and share part of a UNC. Assumes "path" is a UNC.
626 static string GetServerAndShare (string path)
629 while (len < path.Length && !IsDsc (path [len])) len++;
631 if (len < path.Length) {
633 while (len < path.Length && !IsDsc (path [len])) len++;
636 return path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
639 // assumes Environment.IsRunningOnWindows == true
640 static bool SameRoot (string root, string path)
642 // compare root - if enough details are available
643 if ((root.Length < 2) || (path.Length < 2))
647 if (IsDsc (root[0]) && IsDsc (root[1])) {
648 if (!(IsDsc (path[0]) && IsDsc (path[1])))
651 string rootShare = GetServerAndShare (root);
652 string pathShare = GetServerAndShare (path);
654 return String.Compare (rootShare, pathShare, true, CultureInfo.InvariantCulture) == 0;
658 if (!root [0].Equals (path [0]))
660 // presence of the separator
661 if (path[1] != Path.VolumeSeparatorChar)
663 if ((root.Length > 2) && (path.Length > 2)) {
664 // but don't directory compare the directory separator
665 return (IsDsc (root[2]) && IsDsc (path[2]));
670 static string CanonicalizePath (string path)
672 // STEP 1: Check for empty string
675 if (Environment.IsRunningOnWindows)
678 if (path.Length == 0)
681 // STEP 2: Check to see if this is only a root
682 string root = Path.GetPathRoot (path);
683 // it will return '\' for path '\', while it should return 'c:\' or so.
684 // Note: commenting this out makes the need for the (target == 1...) check in step 5
685 //if (root == path) return path;
687 // STEP 3: split the directories, this gets rid of consecutative "/"'s
688 string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
689 // STEP 4: Get rid of directories containing . and ..
692 bool isUnc = Environment.IsRunningOnWindows &&
693 root.Length > 2 && IsDsc (root[0]) && IsDsc (root[1]);
695 // Set an overwrite limit for UNC paths since '\' + server + share
696 // must not be eliminated by the '..' elimination algorithm.
697 int limit = isUnc ? 3 : 0;
699 for (int i = 0; i < dirs.Length; i++) {
700 // WIN32 path components must be trimmed
701 if (Environment.IsRunningOnWindows)
702 dirs[i] = dirs[i].TrimEnd ();
704 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
706 else if (dirs[i] == "..") {
707 // don't overwrite path segments below the limit
711 dirs[target++] = dirs[i];
714 // STEP 5: Combine everything.
715 if (target == 0 || (target == 1 && dirs[0] == ""))
718 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
719 if (Environment.IsRunningOnWindows) {
720 // append leading '\' of the UNC path that was lost in STEP 3.
722 ret = Path.DirectorySeparatorStr + ret;
724 if (!SameRoot (root, ret))
729 } else if (!IsDsc (path[0]) && SameRoot (root, path)) {
730 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
731 ret += Path.DirectorySeparatorChar;
734 string current = Directory.GetCurrentDirectory ();
735 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
736 // DOS local file path
737 if (ret.Length == 0 || IsDsc (ret[0]))
739 return current.Substring (0, 2) + ret;
740 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
741 return current + ret.Substring (1);
743 return current + ret;
750 // required for FileIOPermission (and most proibably reusable elsewhere too)
751 // both path MUST be "full paths"
752 static internal bool IsPathSubsetOf (string subset, string path)
754 if (subset.Length > path.Length)
757 // check that everything up to the last separator match
758 int slast = subset.LastIndexOfAny (PathSeparatorChars);
759 if (String.Compare (subset, 0, path, 0, slast) != 0)
763 // then check if the last segment is identical
764 int plast = path.IndexOfAny (PathSeparatorChars, slast);
765 if (plast >= slast) {
766 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
768 if (subset.Length != path.Length)
771 return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;
779 static string Combine (params string [] paths)
782 throw new ArgumentNullException ("paths");
785 var ret = new StringBuilder ();
786 int pathsLen = paths.Length;
788 foreach (var s in paths) {
791 throw new ArgumentNullException ("One of the paths contains a null value", "paths");
792 if (s.IndexOfAny (InvalidPathChars) != -1)
793 throw new ArgumentException ("Illegal characters in path.");
796 if (IsPathRooted (s))
801 if (slen > 0 && pathsLen > 0) {
802 char p1end = s [slen - 1];
803 if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
808 ret.Append (DirectorySeparatorStr);
811 return ret.ToString ();
819 static string Combine (string path1, string path2, string path3)
822 throw new ArgumentNullException ("path1");
825 throw new ArgumentNullException ("path2");
828 throw new ArgumentNullException ("path3");
830 return Combine (new string [] { path1, path2, path3 });
838 static string Combine (string path1, string path2, string path3, string path4)
841 throw new ArgumentNullException ("path1");
844 throw new ArgumentNullException ("path2");
847 throw new ArgumentNullException ("path3");
850 throw new ArgumentNullException ("path4");
852 return Combine (new string [] { path1, path2, path3, path4 });
855 internal static void Validate (string path)
857 Validate (path, "path");
860 internal static void Validate (string path, string parameterName)
863 throw new ArgumentNullException (parameterName);
864 if (String.IsNullOrWhiteSpace (path))
865 throw new ArgumentException (Locale.GetText ("Path is empty"));
866 if (path.IndexOfAny (Path.InvalidPathChars) != -1)
867 throw new ArgumentException (Locale.GetText ("Path contains invalid chars"));
868 if (Environment.IsRunningOnWindows) {
869 int idx = path.IndexOf (':');
870 if (idx >= 0 && idx != 1)
871 throw new ArgumentException (parameterName);