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 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
282 if (SecurityManager.SecurityEnabled) {
283 new FileIOPermission (FileIOPermissionAccess.PathDiscovery, fullpath).Demand ();
289 internal static string WindowsDriveAdjustment (string path)
291 // two special cases to consider when a drive is specified
294 if ((path [1] != ':') || !Char.IsLetter (path [0]))
297 string current = Directory.GetCurrentDirectory ();
298 // first, only the drive is specified
299 if (path.Length == 2) {
300 // then if the current directory is on the same drive
301 if (current [0] == path [0])
302 path = current; // we return it
305 } else if ((path [2] != Path.DirectorySeparatorChar) && (path [2] != Path.AltDirectorySeparatorChar)) {
306 // second, the drive + a directory is specified *without* a separator between them (e.g. C:dir).
307 // If the current directory is on the specified drive...
308 if (current [0] == path [0]) {
309 // then specified directory is appended to the current drive directory
310 path = Path.Combine (current, path.Substring (2, path.Length - 2));
312 // if not, then just pretend there was a separator (Path.Combine won't work in this case)
313 path = String.Concat (path.Substring (0, 2), DirectorySeparatorStr, path.Substring (2, path.Length - 2));
319 // insecure - do not call directly
320 internal static string InsecureGetFullPath (string path)
323 throw new ArgumentNullException ("path");
325 if (path.Trim ().Length == 0) {
326 string msg = Locale.GetText ("The specified path is not of a legal form (empty).");
327 throw new ArgumentException (msg);
330 // adjust for drives, i.e. a special case for windows
331 if (Environment.IsRunningOnWindows)
332 path = WindowsDriveAdjustment (path);
334 // if the supplied path ends with a separator...
335 char end = path [path.Length - 1];
337 var canonicalize = true;
338 if (path.Length >= 2 &&
341 if (path.Length == 2 || path.IndexOf (path [0], 2) < 0)
342 throw new ArgumentException ("UNC paths should be of the form \\\\server\\share.");
344 if (path [0] != DirectorySeparatorChar)
345 path = path.Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
348 if (!IsPathRooted (path)) {
350 // avoid calling expensive CanonicalizePath when possible
352 while ((start = path.IndexOf ('.', start)) != -1) {
353 if (++start == path.Length || path [start] == DirectorySeparatorChar || path [start] == AltDirectorySeparatorChar)
356 canonicalize = start > 0;
358 path = Directory.GetCurrentDirectory () + DirectorySeparatorStr + path;
359 } else if (DirectorySeparatorChar == '\\' &&
362 !IsDsc (path [1])) { // like `\abc\def'
363 string current = Directory.GetCurrentDirectory ();
364 if (current [1] == VolumeSeparatorChar)
365 path = current.Substring (0, 2) + path;
367 path = current.Substring (0, current.IndexOf ('\\', current.IndexOf ("\\\\") + 1));
372 path = CanonicalizePath (path);
374 // if the original ended with a [Alt]DirectorySeparatorChar then ensure the full path also ends with one
375 if (IsDsc (end) && (path [path.Length - 1] != DirectorySeparatorChar))
376 path += DirectorySeparatorChar;
381 static bool IsDsc (char c) {
382 return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar;
385 public static string GetPathRoot (string path)
390 if (path.Trim ().Length == 0)
391 throw new ArgumentException ("The specified path is not of a legal form.");
393 if (!IsPathRooted (path))
396 if (DirectorySeparatorChar == '/') {
398 return IsDsc (path [0]) ? DirectorySeparatorStr : String.Empty;
403 if (path.Length == 1 && IsDsc (path [0]))
404 return DirectorySeparatorStr;
405 else if (path.Length < 2)
408 if (IsDsc (path [0]) && IsDsc (path[1])) {
409 // UNC: \\server or \\server\share
411 while (len < path.Length && !IsDsc (path [len])) len++;
414 if (len < path.Length) {
416 while (len < path.Length && !IsDsc (path [len])) len++;
419 return DirectorySeparatorStr +
420 DirectorySeparatorStr +
421 path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
422 } else if (IsDsc (path [0])) {
423 // path starts with '\' or '/'
424 return DirectorySeparatorStr;
425 } else if (path[1] == VolumeSeparatorChar) {
427 if (path.Length >= 3 && (IsDsc (path [2]))) len++;
429 return Directory.GetCurrentDirectory ().Substring (0, 2);// + path.Substring (0, len);
430 return path.Substring (0, len);
434 // FIXME: Further limit the assertion when imperative Assert is implemented
435 [FileIOPermission (SecurityAction.Assert, Unrestricted = true)]
436 public static string GetTempFileName ()
443 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
449 path = Path.Combine (GetTempPath(), "tmp" + num.ToString("x") + ".tmp");
452 f = new FileStream (path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read,
453 8192, false, (FileOptions) 1);
455 catch (SecurityException) {
456 // avoid an endless loop
459 catch (UnauthorizedAccessException) {
460 // This can happen if we don't have write permission to /tmp
463 catch (DirectoryNotFoundException) {
464 // This happens when TMPDIR does not exist
475 [EnvironmentPermission (SecurityAction.Demand, Unrestricted = true)]
476 public static string GetTempPath ()
478 SecurityManager.EnsureElevatedPermissions (); // this is a no-op outside moonlight
480 string p = get_temp_path ();
481 if (p.Length > 0 && p [p.Length - 1] != DirectorySeparatorChar)
482 return p + DirectorySeparatorChar;
487 [MethodImplAttribute(MethodImplOptions.InternalCall)]
488 private static extern string get_temp_path ();
490 public static bool HasExtension (string path)
492 if (path == null || path.Trim ().Length == 0)
495 if (path.IndexOfAny (InvalidPathChars) != -1)
496 throw new ArgumentException ("Illegal characters in path.");
498 int pos = findExtension (path);
499 return 0 <= pos && pos < path.Length - 1;
502 public static bool IsPathRooted (string path)
504 if (path == null || path.Length == 0)
507 if (path.IndexOfAny (InvalidPathChars) != -1)
508 throw new ArgumentException ("Illegal characters in path.");
511 return (c == DirectorySeparatorChar ||
512 c == AltDirectorySeparatorChar ||
513 (!dirEqualsVolume && path.Length > 1 && path [1] == VolumeSeparatorChar));
516 public static char[] GetInvalidFileNameChars ()
518 // return a new array as we do not want anyone to be able to change the values
519 if (Environment.IsRunningOnWindows) {
520 return new char [41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
521 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
522 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
523 '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' };
525 return new char [2] { '\x00', '/' };
529 public static char[] GetInvalidPathChars ()
531 // return a new array as we do not want anyone to be able to change the values
532 if (Environment.IsRunningOnWindows) {
533 return new char [36] { '\x22', '\x3C', '\x3E', '\x7C', '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
534 '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
535 '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
538 return new char [1] { '\x00' };
542 public static string GetRandomFileName ()
544 // returns a 8.3 filename (total size 12)
545 StringBuilder sb = new StringBuilder (12);
546 // using strong crypto but without creating the file
547 RandomNumberGenerator rng = RandomNumberGenerator.Create ();
548 byte [] buffer = new byte [11];
549 rng.GetBytes (buffer);
551 for (int i = 0; i < buffer.Length; i++) {
555 // restrict to length of range [a..z0..9]
556 int b = (buffer [i] % 36);
557 char c = (char) (b < 26 ? (b + 'a') : (b - 26 + '0'));
561 return sb.ToString ();
564 // private class methods
566 private static int findExtension (string path)
568 // method should return the index of the path extension
569 // start or -1 if no valid extension
571 int iLastDot = path.LastIndexOf ('.');
572 int iLastSep = path.LastIndexOfAny ( PathSeparatorChars );
574 if (iLastDot > iLastSep)
582 VolumeSeparatorChar = MonoIO.VolumeSeparatorChar;
583 DirectorySeparatorChar = MonoIO.DirectorySeparatorChar;
584 AltDirectorySeparatorChar = MonoIO.AltDirectorySeparatorChar;
586 PathSeparator = MonoIO.PathSeparator;
587 // this copy will be modifiable ("by design")
588 InvalidPathChars = GetInvalidPathChars ();
591 DirectorySeparatorStr = DirectorySeparatorChar.ToString ();
592 PathSeparatorChars = new char [] {
593 DirectorySeparatorChar,
594 AltDirectorySeparatorChar,
598 dirEqualsVolume = (DirectorySeparatorChar == VolumeSeparatorChar);
601 // returns the server and share part of a UNC. Assumes "path" is a UNC.
602 static string GetServerAndShare (string path)
605 while (len < path.Length && !IsDsc (path [len])) len++;
607 if (len < path.Length) {
609 while (len < path.Length && !IsDsc (path [len])) len++;
612 return path.Substring (2, len - 2).Replace (AltDirectorySeparatorChar, DirectorySeparatorChar);
615 // assumes Environment.IsRunningOnWindows == true
616 static bool SameRoot (string root, string path)
618 // compare root - if enough details are available
619 if ((root.Length < 2) || (path.Length < 2))
623 if (IsDsc (root[0]) && IsDsc (root[1])) {
624 if (!(IsDsc (path[0]) && IsDsc (path[1])))
627 string rootShare = GetServerAndShare (root);
628 string pathShare = GetServerAndShare (path);
630 return String.Compare (rootShare, pathShare, true, CultureInfo.InvariantCulture) == 0;
634 if (!root [0].Equals (path [0]))
636 // presence of the separator
637 if (path[1] != Path.VolumeSeparatorChar)
639 if ((root.Length > 2) && (path.Length > 2)) {
640 // but don't directory compare the directory separator
641 return (IsDsc (root[2]) && IsDsc (path[2]));
646 static string CanonicalizePath (string path)
648 // STEP 1: Check for empty string
651 if (Environment.IsRunningOnWindows)
654 if (path.Length == 0)
657 // STEP 2: Check to see if this is only a root
658 string root = Path.GetPathRoot (path);
659 // it will return '\' for path '\', while it should return 'c:\' or so.
660 // Note: commenting this out makes the need for the (target == 1...) check in step 5
661 //if (root == path) return path;
663 // STEP 3: split the directories, this gets rid of consecutative "/"'s
664 string[] dirs = path.Split (Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
665 // STEP 4: Get rid of directories containing . and ..
668 bool isUnc = Environment.IsRunningOnWindows &&
669 root.Length > 2 && IsDsc (root[0]) && IsDsc (root[1]);
671 // Set an overwrite limit for UNC paths since '\' + server + share
672 // must not be eliminated by the '..' elimination algorithm.
673 int limit = isUnc ? 3 : 0;
675 for (int i = 0; i < dirs.Length; i++) {
676 // WIN32 path components must be trimmed
677 if (Environment.IsRunningOnWindows)
678 dirs[i] = dirs[i].TrimEnd ();
680 if (dirs[i] == "." || (i != 0 && dirs[i].Length == 0))
682 else if (dirs[i] == "..") {
683 // don't overwrite path segments below the limit
687 dirs[target++] = dirs[i];
690 // STEP 5: Combine everything.
691 if (target == 0 || (target == 1 && dirs[0] == ""))
694 string ret = String.Join (DirectorySeparatorStr, dirs, 0, target);
695 if (Environment.IsRunningOnWindows) {
696 // append leading '\' of the UNC path that was lost in STEP 3.
698 ret = Path.DirectorySeparatorStr + ret;
700 if (!SameRoot (root, ret))
705 } else if (!IsDsc (path[0]) && SameRoot (root, path)) {
706 if (ret.Length <= 2 && !ret.EndsWith (DirectorySeparatorStr)) // '\' after "c:"
707 ret += Path.DirectorySeparatorChar;
710 string current = Directory.GetCurrentDirectory ();
711 if (current.Length > 1 && current[1] == Path.VolumeSeparatorChar) {
712 // DOS local file path
713 if (ret.Length == 0 || IsDsc (ret[0]))
715 return current.Substring (0, 2) + ret;
716 } else if (IsDsc (current[current.Length - 1]) && IsDsc (ret[0]))
717 return current + ret.Substring (1);
719 return current + ret;
726 // required for FileIOPermission (and most proibably reusable elsewhere too)
727 // both path MUST be "full paths"
728 static internal bool IsPathSubsetOf (string subset, string path)
730 if (subset.Length > path.Length)
733 // check that everything up to the last separator match
734 int slast = subset.LastIndexOfAny (PathSeparatorChars);
735 if (String.Compare (subset, 0, path, 0, slast) != 0)
739 // then check if the last segment is identical
740 int plast = path.IndexOfAny (PathSeparatorChars, slast);
741 if (plast >= slast) {
742 return String.Compare (subset, slast, path, slast, path.Length - plast) == 0;
744 if (subset.Length != path.Length)
747 return String.Compare (subset, slast, path, slast, subset.Length - slast) == 0;
750 #if NET_4_0 || MOONLIGHT || MOBILE
755 static string Combine (params string [] paths)
758 throw new ArgumentNullException ("paths");
761 var ret = new StringBuilder ();
762 int pathsLen = paths.Length;
764 foreach (var s in paths) {
767 throw new ArgumentNullException ("One of the paths contains a null value", "paths");
768 if (s.IndexOfAny (InvalidPathChars) != -1)
769 throw new ArgumentException ("Illegal characters in path.");
772 if (IsPathRooted (s))
777 if (slen > 0 && pathsLen > 0) {
778 char p1end = s [slen - 1];
779 if (p1end != DirectorySeparatorChar && p1end != AltDirectorySeparatorChar && p1end != VolumeSeparatorChar)
784 ret.Append (DirectorySeparatorStr);
787 return ret.ToString ();
790 #if NET_4_0 || MOONLIGHT || MOBILE
795 static string Combine (string path1, string path2, string path3)
798 throw new ArgumentNullException ("path1");
801 throw new ArgumentNullException ("path2");
804 throw new ArgumentNullException ("path3");
806 return Combine (new string [] { path1, path2, path3 });
809 #if NET_4_0 || MOONLIGHT || MOBILE
814 static string Combine (string path1, string path2, string path3, string path4)
817 throw new ArgumentNullException ("path1");
820 throw new ArgumentNullException ("path2");
823 throw new ArgumentNullException ("path3");
826 throw new ArgumentNullException ("path4");
828 return Combine (new string [] { path1, path2, path3, path4 });
831 internal static void Validate (string path)
833 Validate (path, "path");
836 internal static void Validate (string path, string parameterName)
839 throw new ArgumentNullException (parameterName);
840 if (String.IsNullOrWhiteSpace (path))
841 throw new ArgumentException (Locale.GetText ("Path is empty"));
842 if (path.IndexOfAny (Path.InvalidPathChars) != -1)
843 throw new ArgumentException (Locale.GetText ("Path contains invalid chars"));
845 // On Moonlight (SL4+) there are some limitations in "Elevated Trust"
846 if (SecurityManager.HasElevatedPermissions) {