3 // Copyright (c) Microsoft Corporation. All rights reserved.
6 /*============================================================
10 ** <OWNER>[....]</OWNER>
13 ** Purpose: A collection of path manipulation methods.
16 ===========================================================*/
19 using System.Security.Permissions;
20 using Win32Native = Microsoft.Win32.Win32Native;
22 using System.Runtime.InteropServices;
23 using System.Security;
24 using System.Security.Cryptography;
25 using System.Runtime.CompilerServices;
26 using System.Globalization;
27 using System.Runtime.Versioning;
28 using System.Diagnostics.Contracts;
31 // Provides methods for processing directory strings in an ideally
32 // cross-platform manner. Most of the methods don't do a complete
33 // full parsing (such as examining a UNC hostname), but they will
34 // handle most string operations.
36 // File names cannot contain backslash (\), slash (/), colon (:),
37 // asterick (*), question mark (?), quote ("), less than (<;),
38 // greater than (>;), or pipe (|). The first three are used as directory
39 // separators on various platforms. Asterick and question mark are treated
40 // as wild cards. Less than, Greater than, and pipe all redirect input
41 // or output from a program to a file or some combination thereof. Quotes
44 // We are guaranteeing that Path.SeparatorChar is the correct
45 // directory separator on all platforms, and we will support
46 // Path.AltSeparatorChar as well. To write cross platform
47 // code with minimal pain, you can use slash (/) as a directory separator in
49 // Class contains only static data, no need to serialize
51 public static class Path
53 // Platform specific directory separator character. This is backslash
54 // ('\') on Windows, slash ('/') on Unix, and colon (':') on Mac.
56 public static readonly char DirectorySeparatorChar = '\\';
57 internal const string DirectorySeparatorCharAsString = "\\";
59 // Platform specific alternate directory separator character.
60 // This is backslash ('\') on Unix, and slash ('/') on Windows
63 public static readonly char AltDirectorySeparatorChar = '/';
65 // Platform specific volume separator character. This is colon (':')
66 // on Windows and MacOS, and slash ('/') on Unix. This is mostly
67 // useful for parsing paths like "c:\windows" or "MacVolume:System Folder".
69 public static readonly char VolumeSeparatorChar = ':';
71 // Platform specific invalid list of characters in a path.
72 // See the "Naming a File" MSDN conceptual docs for more details on
73 // what is valid in a file name (which is slightly different from what
74 // is legal in a path name).
75 // Note: This list is duplicated in CheckInvalidPathChars
76 [Obsolete("Please use GetInvalidPathChars or GetInvalidFileNameChars instead.")]
77 public static readonly char[] InvalidPathChars = { '\"', '<', '>', '|', '\0', (Char)1, (Char)2, (Char)3, (Char)4, (Char)5, (Char)6, (Char)7, (Char)8, (Char)9, (Char)10, (Char)11, (Char)12, (Char)13, (Char)14, (Char)15, (Char)16, (Char)17, (Char)18, (Char)19, (Char)20, (Char)21, (Char)22, (Char)23, (Char)24, (Char)25, (Char)26, (Char)27, (Char)28, (Char)29, (Char)30, (Char)31 };
79 // Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space.
80 // String.WhitespaceChars will trim aggressively than what the underlying FS does (for ex, NTFS, FAT).
81 internal static readonly char[] TrimEndChars = { (char) 0x9, (char) 0xA, (char) 0xB, (char) 0xC, (char) 0xD, (char) 0x20, (char) 0x85, (char) 0xA0};
83 private static readonly char[] RealInvalidPathChars = { '\"', '<', '>', '|', '\0', (Char)1, (Char)2, (Char)3, (Char)4, (Char)5, (Char)6, (Char)7, (Char)8, (Char)9, (Char)10, (Char)11, (Char)12, (Char)13, (Char)14, (Char)15, (Char)16, (Char)17, (Char)18, (Char)19, (Char)20, (Char)21, (Char)22, (Char)23, (Char)24, (Char)25, (Char)26, (Char)27, (Char)28, (Char)29, (Char)30, (Char)31 };
85 // This is used by HasIllegalCharacters
86 private static readonly char[] InvalidPathCharsWithAdditionalChecks = { '\"', '<', '>', '|', '\0', (Char)1, (Char)2, (Char)3, (Char)4, (Char)5, (Char)6, (Char)7, (Char)8, (Char)9, (Char)10, (Char)11, (Char)12, (Char)13, (Char)14, (Char)15, (Char)16, (Char)17, (Char)18, (Char)19, (Char)20, (Char)21, (Char)22, (Char)23, (Char)24, (Char)25, (Char)26, (Char)27, (Char)28, (Char)29, (Char)30, (Char)31, '*', '?' };
88 private static readonly char[] InvalidFileNameChars = { '\"', '<', '>', '|', '\0', (Char)1, (Char)2, (Char)3, (Char)4, (Char)5, (Char)6, (Char)7, (Char)8, (Char)9, (Char)10, (Char)11, (Char)12, (Char)13, (Char)14, (Char)15, (Char)16, (Char)17, (Char)18, (Char)19, (Char)20, (Char)21, (Char)22, (Char)23, (Char)24, (Char)25, (Char)26, (Char)27, (Char)28, (Char)29, (Char)30, (Char)31, ':', '*', '?', '\\', '/' };
90 public static readonly char PathSeparator = ';';
93 // Make this public sometime.
94 // The max total path is 260, and the max individual component length is 255.
95 // For example, D:\<256 char file name> isn't legal, even though it's under 260 chars.
96 internal static readonly int MaxPath = 260;
97 private static readonly int MaxDirectoryLength = 255;
99 // Windows API definitions
100 internal const int MAX_PATH = 260; // From WinDef.h
101 internal const int MAX_DIRECTORY_PATH = 248; // cannot create directories greater than 248 characters
103 // Changes the extension of a file path. The path parameter
104 // specifies a file path, and the extension parameter
105 // specifies a file extension (with a leading period, such as
108 // The function returns a file path with the same root, directory, and base
109 // name parts as path, but with the file extension changed to
110 // the specified extension. If path is null, the function
111 // returns null. If path does not contain a file extension,
112 // the new file extension is appended to the path. If extension
113 // is null, any exsiting extension is removed from path.
115 public static String ChangeExtension(String path, String extension) {
117 CheckInvalidPathChars(path);
120 for (int i = path.Length; --i >= 0;) {
123 s = path.Substring(0, i);
126 if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) break;
128 if (extension != null && path.Length != 0) {
129 if (extension.Length == 0 || extension[0] != '.') {
140 // Returns the directory path of a file path. This method effectively
141 // removes the last element of the given file path, i.e. it returns a
142 // string consisting of all characters up to but not including the last
143 // backslash ("\") in the file path. The returned value is null if the file
144 // path is null or if the file path denotes a root (such as "\", "C:", or
145 // "\\server\share").
147 [ResourceExposure(ResourceScope.None)]
148 [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
149 public static String GetDirectoryName(String path) {
151 CheckInvalidPathChars(path);
153 #if FEATURE_LEGACYNETCF
154 if (!CompatibilitySwitches.IsAppEarlierThanWindowsPhone8) {
157 string normalizedPath = NormalizePath(path, false);
159 // If there are no permissions for PathDiscovery to this path, we should NOT expand the short paths
160 // as this would leak information about paths to which the user would not have access to.
165 // If we were passed in a path with \\?\ we need to remove it as FileIOPermission does not like it.
166 string tempPath = Path.RemoveLongPathPrefix(path);
168 // FileIOPermission cannot handle paths that contain ? or *
169 // So we only pass to FileIOPermission the text up to them.
171 while (pos < tempPath.Length && (tempPath[pos] != '?' && tempPath[pos] != '*'))
174 // GetFullPath will Demand that we have the PathDiscovery FileIOPermission and thus throw
175 // SecurityException if we don't.
176 // While we don't use the result of this call we are using it as a consistent way of
177 // doing the security checks.
179 Path.GetFullPath(tempPath.Substring(0, pos));
181 catch (SecurityException) {
182 // If the user did not have permissions to the path, make sure that we don't leak expanded short paths
183 // Only re-normalize if the original path had a ~ in it.
184 if (path.IndexOf("~", StringComparison.Ordinal) != -1)
186 normalizedPath = NormalizePath(path, /*fullCheck*/ false, /*expandShortPaths*/ false);
189 catch (PathTooLongException) { }
190 catch (NotSupportedException) { } // Security can throw this on "c:\foo:"
191 catch (IOException) { }
192 catch (ArgumentException) { } // The normalizePath with fullCheck will throw this for file: and http:
195 path = normalizedPath;
197 #if FEATURE_LEGACYNETCF
201 int root = GetRootLength(path);
205 if (i == root) return null;
206 while (i > root && path[--i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar);
207 String dir = path.Substring(0, i);
208 #if FEATURE_LEGACYNETCF
209 if (CompatibilitySwitches.IsAppEarlierThanWindowsPhone8) {
210 if (dir.Length >= MAX_PATH - 1)
211 throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
220 // Gets the length of the root DirectoryInfo or whatever DirectoryInfo markers
221 // are specified for the first part of the DirectoryInfo name.
223 internal static int GetRootLength(String path) {
224 CheckInvalidPathChars(path);
227 int length = path.Length;
229 if (length >= 1 && (IsDirectorySeparator(path[0]))) {
230 // handles UNC names and directories off current drive's root.
232 if (length >= 2 && (IsDirectorySeparator(path[1]))) {
235 while (i < length && ((path[i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar) || --n > 0)) i++;
238 else if (length >= 2 && path[1] == VolumeSeparatorChar) {
241 if (length >= 3 && (IsDirectorySeparator(path[2]))) i++;
246 internal static bool IsDirectorySeparator(char c) {
247 return (c==DirectorySeparatorChar || c == AltDirectorySeparatorChar);
251 public static char[] GetInvalidPathChars()
253 return (char[]) RealInvalidPathChars.Clone();
256 public static char[] GetInvalidFileNameChars()
258 return (char[]) InvalidFileNameChars.Clone();
261 // Returns the extension of the given path. The returned value includes the
262 // period (".") character of the extension except when you have a terminal period when you get String.Empty, such as ".exe" or
263 // ".cpp". The returned value is null if the given path is
264 // null or if the given path does not include an extension.
267 public static String GetExtension(String path) {
271 CheckInvalidPathChars(path);
272 int length = path.Length;
273 for (int i = length; --i >= 0;) {
278 return path.Substring(i, length - i);
282 if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar)
288 // Expands the given path to a fully qualified path. The resulting string
289 // consists of a drive letter, a colon, and a root relative path. This
290 // function does not verify that the resulting path
291 // refers to an existing file or directory on the associated volume.
293 [System.Security.SecuritySafeCritical]
294 [ResourceExposure(ResourceScope.Machine)]
295 [ResourceConsumption(ResourceScope.Machine)]
296 public static String GetFullPath(String path) {
297 String fullPath = GetFullPathInternal(path);
299 FileSecurityState state = new FileSecurityState(FileSecurityStateAccess.PathDiscovery, path, fullPath);
302 FileIOPermission.QuickDemand(FileIOPermissionAccess.PathDiscovery, fullPath, false, false);
307 [System.Security.SecurityCritical]
308 [ResourceExposure(ResourceScope.Machine)]
309 [ResourceConsumption(ResourceScope.Machine)]
310 internal static String UnsafeGetFullPath(String path)
312 String fullPath = GetFullPathInternal(path);
314 FileIOPermission.QuickDemand(FileIOPermissionAccess.PathDiscovery, fullPath, false, false);
319 // This method is package access to let us quickly get a string name
320 // while avoiding a security check. This also serves a slightly
321 // different purpose - when we open a file, we need to resolve the
322 // path into a fully qualified, non-relative path name. This
323 // method does that, finding the current drive &; directory. But
324 // as long as we don't return this info to the user, we're good. However,
325 // the public GetFullPath does need to do a security check.
326 [ResourceExposure(ResourceScope.Machine)]
327 [ResourceConsumption(ResourceScope.Machine)]
328 internal static String GetFullPathInternal(String path) {
330 throw new ArgumentNullException("path");
331 Contract.EndContractBlock();
333 String newPath = NormalizePath(path, true);
338 [System.Security.SecuritySafeCritical] // auto-generated
339 [ResourceExposure(ResourceScope.Machine)]
340 [ResourceConsumption(ResourceScope.Machine)]
341 internal unsafe static String NormalizePath(String path, bool fullCheck) {
342 return NormalizePath(path, fullCheck, MaxPath);
345 [System.Security.SecuritySafeCritical] // auto-generated
346 [ResourceExposure(ResourceScope.Machine)]
347 [ResourceConsumption(ResourceScope.Machine)]
348 internal unsafe static String NormalizePath(String path, bool fullCheck, bool expandShortPaths)
350 return NormalizePath(path, fullCheck, MaxPath, expandShortPaths);
353 [System.Security.SecuritySafeCritical] // auto-generated
354 [ResourceExposure(ResourceScope.Machine)]
355 [ResourceConsumption(ResourceScope.Machine)]
356 internal unsafe static String NormalizePath(String path, bool fullCheck, int maxPathLength) {
357 return NormalizePath(path, fullCheck, maxPathLength, true);
360 [System.Security.SecurityCritical] // auto-generated
361 [ResourceExposure(ResourceScope.Machine)]
362 [ResourceConsumption(ResourceScope.Machine)]
363 internal unsafe static String NormalizePath(String path, bool fullCheck, int maxPathLength, bool expandShortPaths) {
365 Contract.Requires(path != null, "path can't be null");
366 // If we're doing a full path check, trim whitespace and look for
367 // illegal path characters.
369 // Trim whitespace off the end of the string.
370 // Win32 normalization trims only U+0020.
371 path = path.TrimEnd(TrimEndChars);
373 // Look for illegal path characters.
374 CheckInvalidPathChars(path);
378 // We prefer to allocate on the stack for workingset/perf gain. If the
379 // starting path is less than MaxPath then we can stackalloc; otherwise we'll
380 // use a StringBuilder (PathHelper does this under the hood). The latter may
381 // happen in 2 cases:
382 // 1. Starting path is greater than MaxPath but it normalizes down to MaxPath.
383 // This is relevant for paths containing escape sequences. In this case, we
384 // attempt to normalize down to MaxPath, but the caller pays a perf penalty
385 // since StringBuilder is used.
386 // 2. IsolatedStorage, which supports paths longer than MaxPath (value given
388 PathHelper newBuffer;
389 if (path.Length + 1 <= MaxPath) {
390 char* m_arrayPtr = stackalloc char[MaxPath];
391 newBuffer = new PathHelper(m_arrayPtr, MaxPath);
393 newBuffer = new PathHelper(path.Length + Path.MaxPath, maxPathLength);
398 bool fixupDirectorySeparator = false;
399 // Number of significant chars other than potentially suppressible
400 // dots and spaces since the last directory or volume separator char
401 uint numSigChars = 0;
402 int lastSigChar = -1; // Index of last significant character.
403 // Whether this segment of the path (not the complete path) started
404 // with a volume separator char. Reject "c:...".
405 bool startedWithVolumeSeparator = false;
406 bool firstSegment = true;
407 int lastDirectorySeparatorPos = 0;
409 bool mightBeShortFileName = false;
411 // LEGACY: This code is here for backwards compatibility reasons. It
412 // ensures that \\foo.cs\bar.cs stays \\foo.cs\bar.cs instead of being
413 // turned into \foo.cs\bar.cs.
414 if (path.Length > 0 && (path[0] == DirectorySeparatorChar || path[0] == AltDirectorySeparatorChar)) {
415 newBuffer.Append('\\');
420 // Normalize the string, stripping out redundant dots, spaces, and
422 while (index < path.Length) {
423 char currentChar = path[index];
425 // We handle both directory separators and dots specially. For
426 // directory separators, we consume consecutive appearances.
427 // For dots, we consume all dots beyond the second in
428 // succession. All other characters are added as is. In
429 // addition we consume all spaces after the last other char
430 // in a directory name up until the directory separator.
432 if (currentChar == DirectorySeparatorChar || currentChar == AltDirectorySeparatorChar) {
433 // If we have a path like "123.../foo", remove the trailing dots.
434 // However, if we found "c:\temp\..\bar" or "c:\temp\...\bar", don't.
435 // Also remove trailing spaces from both files & directory names.
436 // This was agreed on with the OS team to fix undeletable directory
437 // names ending in spaces.
439 // If we saw a '\' as the previous last significant character and
440 // are simply going to write out dots, suppress them.
441 // If we only contain dots and slashes though, only allow
442 // a string like [dot]+ [space]*. Ignore everything else.
443 // Legal: "\.. \", "\...\", "\. \"
444 // Illegal: "\.. .\", "\. .\", "\ .\"
445 if (numSigChars == 0) {
446 // Dot and space handling
448 // Look for ".[space]*" or "..[space]*"
449 int start = lastSigChar + 1;
450 if (path[start] != '.')
451 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
453 // Only allow "[dot]+[space]*", and normalize the
454 // legal ones to "." or ".."
457 if (startedWithVolumeSeparator && numDots > 2)
459 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
461 if (path[start + 1] == '.') {
462 // Search for a space in the middle of the
464 for(int i=start + 2; i < start + numDots; i++) {
466 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
473 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
479 newBuffer.Append('.');
482 newBuffer.Append('.');
483 fixupDirectorySeparator = false;
485 // Continue in this case, potentially writing out '\'.
488 if (numSpaces > 0 && firstSegment) {
489 // Handle strings like " \\server\share".
490 if (index + 1 < path.Length &&
491 (path[index + 1] == DirectorySeparatorChar || path[index + 1] == AltDirectorySeparatorChar))
493 newBuffer.Append(DirectorySeparatorChar);
498 numSpaces = 0; // Suppress trailing spaces
500 if (!fixupDirectorySeparator) {
501 fixupDirectorySeparator = true;
502 newBuffer.Append(DirectorySeparatorChar);
506 startedWithVolumeSeparator = false;
507 firstSegment = false;
509 // For short file names, we must try to expand each of them as
510 // soon as possible. We need to allow people to specify a file
511 // name that doesn't exist using a path with short file names
512 // in it, such as this for a temp file we're trying to create:
513 // C:\DOCUME~1\USERNA~1.RED\LOCALS~1\Temp\bg3ylpzp
514 // We could try doing this afterwards piece by piece, but it's
515 // probably a lot simpler to do it here.
516 if (mightBeShortFileName) {
517 newBuffer.TryExpandShortFileName();
518 mightBeShortFileName = false;
521 int thisPos = newBuffer.Length - 1;
522 if (thisPos - lastDirectorySeparatorPos > MaxDirectoryLength)
524 throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
526 lastDirectorySeparatorPos = thisPos;
527 } // if (Found directory separator)
528 else if (currentChar == '.') {
529 // Reduce only multiple .'s only after slash to 2 dots. For
530 // instance a...b is a valid file name.
532 // Don't flush out non-terminal spaces here, because they may in
533 // the end not be significant. Turn "c:\ . .\foo" -> "c:\foo"
534 // which is the conclusion of removing trailing dots & spaces,
535 // as well as folding multiple '\' characters.
537 else if (currentChar == ' ') {
540 else { // Normal character logic
541 if (currentChar == '~' && expandShortPaths)
542 mightBeShortFileName = true;
544 fixupDirectorySeparator = false;
546 // To reject strings like "C:...\foo" and "C :\foo"
547 if (firstSegment && currentChar == VolumeSeparatorChar) {
548 // Only accept "C:", not "c :" or ":"
549 // Get a drive letter or ' ' if index is 0.
550 char driveLetter = (index > 0) ? path[index-1] : ' ';
551 bool validPath = ((numDots == 0) && (numSigChars >= 1) && (driveLetter != ' '));
553 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
555 startedWithVolumeSeparator = true;
556 // We need special logic to make " c:" work, we should not fix paths like " foo::$DATA"
557 if (numSigChars > 1) { // Common case, simply do nothing
558 int spaceCount = 0; // How many spaces did we write out, numSpaces has already been reset.
559 while((spaceCount < newBuffer.Length) && newBuffer[spaceCount] == ' ')
561 if (numSigChars - spaceCount == 1) {
562 //Safe to update stack ptr directly
563 newBuffer.Length = 0;
564 newBuffer.Append(driveLetter); // Overwrite spaces, we need a special case to not break " foo" as a relative path.
571 numSigChars += 1 + numDots + numSpaces;
574 // Copy any spaces & dots since the last significant character
575 // to here. Note we only counted the number of dots & spaces,
576 // and don't know what order they're in. Hence the copy.
577 if (numDots > 0 || numSpaces > 0) {
578 int numCharsToCopy = (lastSigChar >= 0) ? index - lastSigChar - 1 : index;
579 if (numCharsToCopy > 0) {
580 for (int i=0; i<numCharsToCopy; i++) {
581 newBuffer.Append(path[lastSigChar + 1 + i]);
588 newBuffer.Append(currentChar);
595 if (newBuffer.Length - 1 - lastDirectorySeparatorPos > MaxDirectoryLength)
597 throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
600 // Drop any trailing dots and spaces from file & directory names, EXCEPT
601 // we MUST make sure that "C:\foo\.." is correctly handled.
602 // Also handle "C:\foo\." -> "C:\foo", while "C:\." -> "C:\"
603 if (numSigChars == 0) {
605 // Look for ".[space]*" or "..[space]*"
606 int start = lastSigChar + 1;
607 if (path[start] != '.')
608 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
610 // Only allow "[dot]+[space]*", and normalize the
611 // legal ones to "." or ".."
614 if (startedWithVolumeSeparator && numDots > 2)
615 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
617 if (path[start + 1] == '.') {
618 // Search for a space in the middle of the
620 for(int i=start + 2; i < start + numDots; i++) {
622 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
629 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
635 newBuffer.Append('.');
638 newBuffer.Append('.');
640 } // if (numSigChars == 0)
642 // If we ended up eating all the characters, bail out.
643 if (newBuffer.Length == 0)
644 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegal"));
646 // Disallow URL's here. Some of our other Win32 API calls will reject
647 // them later, so we might be better off rejecting them here.
648 // Note we've probably turned them into "file:\D:\foo.tmp" by now.
649 // But for compatibility, ensure that callers that aren't doing a
650 // full check aren't rejected here.
652 if ( newBuffer.OrdinalStartsWith("http:", false) ||
653 newBuffer.OrdinalStartsWith("file:", false))
655 throw new ArgumentException(Environment.GetResourceString("Argument_PathUriFormatNotSupported"));
659 // If the last part of the path (file or directory name) had a tilde,
661 if (mightBeShortFileName) {
662 newBuffer.TryExpandShortFileName();
665 // Call the Win32 API to do the final canonicalization step.
669 // NOTE: Win32 GetFullPathName requires the input buffer to be big enough to fit the initial
670 // path which is a concat of CWD and the relative path, this can be of an arbitrary
671 // size and could be > MAX_PATH (which becomes an artificial limit at this point),
672 // even though the final normalized path after fixing up the relative path syntax
673 // might be well within the MAX_PATH restriction. For ex,
674 // "c:\SomeReallyLongDirName(thinkGreaterThan_MAXPATH)\..\foo.txt" which actually requires a
675 // buffer well with in the MAX_PATH as the normalized path is just "c:\foo.txt"
676 // This buffer requirement seems wrong, it could be a bug or a perf optimization
677 // like returning required buffer length quickly or avoid stratch buffer etc.
678 // Either way we need to workaround it here...
680 // Ideally we would get the required buffer length first by calling GetFullPathName
681 // once without the buffer and use that in the later call but this doesn't always work
682 // due to Win32 GetFullPathName bug. For instance, in Win2k, when the path we are trying to
683 // fully qualify is a single letter name (such as "a", "1", ",") GetFullPathName
684 // fails to return the right buffer size (i.e, resulting in insufficient buffer).
685 // To workaround this bug we will start with MAX_PATH buffer and grow it once if the
686 // return value is > MAX_PATH.
688 result = newBuffer.GetFullPathName();
690 // If we called GetFullPathName with something like "foo" and our
691 // command window was in short file name mode (ie, by running edlin or
692 // DOS versions of grep, etc), we might have gotten back a short file
693 // name. So, check to see if we need to expand it.
694 mightBeShortFileName = false;
695 for(int i=0; i < newBuffer.Length && !mightBeShortFileName; i++) {
696 if (newBuffer[i] == '~' && expandShortPaths)
697 mightBeShortFileName = true;
700 if (mightBeShortFileName) {
701 bool r = newBuffer.TryExpandShortFileName();
702 // Consider how the path "Doesn'tExist" would expand. If
703 // we add in the current directory, it too will need to be
704 // fully expanded, which doesn't happen if we use a file
705 // name that doesn't exist.
709 for (int i = newBuffer.Length - 1; i >= 0; i--) {
710 if (newBuffer[i] == DirectorySeparatorChar) {
716 if (lastSlash >= 0) {
718 // This bounds check is for safe memcpy but we should never get this far
719 if (newBuffer.Length >= maxPathLength)
720 throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
722 int lenSavedName = newBuffer.Length - lastSlash - 1;
723 Contract.Assert(lastSlash < newBuffer.Length, "path unexpectedly ended in a '\'");
725 newBuffer.Fixup(lenSavedName, lastSlash);
732 /* Throw an ArgumentException for paths like \\, \\server, \\server\
733 This check can only be properly done after normalizing, so
734 \\foo\.. will be properly rejected. Also, reject \\?\GLOBALROOT\
735 (an internal kernel path) because it provides aliases for drives. */
736 if (newBuffer.Length > 1 && newBuffer[0] == '\\' && newBuffer[1] == '\\') {
738 while (startIndex < result) {
739 if (newBuffer[startIndex] == '\\') {
747 if (startIndex == result)
748 throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC"));
750 // Check for \\?\Globalroot, an internal mechanism to the kernel
751 // that provides aliases for drives and other undocumented stuff.
752 // The kernel team won't even describe the full set of what
753 // is available here - we don't want managed apps mucking
754 // with this for security reasons.
755 if ( newBuffer.OrdinalStartsWith("\\\\?\\globalroot", true))
756 throw new ArgumentException(Environment.GetResourceString("Arg_PathGlobalRoot"));
760 // Check our result and form the managed string as necessary.
761 if (newBuffer.Length >= maxPathLength)
762 throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
765 int errorCode = Marshal.GetLastWin32Error();
767 errorCode = Win32Native.ERROR_BAD_PATHNAME;
768 __Error.WinIOError(errorCode, path);
769 return null; // Unreachable - silence a compiler error.
772 String returnVal = newBuffer.ToString();
773 if (String.Equals(returnVal, path, StringComparison.Ordinal))
780 internal const int MaxLongPath = 32000;
782 private const string LongPathPrefix = @"\\?\";
783 private const string UNCPathPrefix = @"\\";
784 private const string UNCLongPathPrefixToInsert = @"?\UNC\";
785 private const string UNCLongPathPrefix = @"\\?\UNC\";
787 internal unsafe static bool HasLongPathPrefix(String path)
789 return path.StartsWith(LongPathPrefix, StringComparison.Ordinal);
792 internal unsafe static String AddLongPathPrefix(String path)
794 if (path.StartsWith(LongPathPrefix, StringComparison.Ordinal))
797 if (path.StartsWith(UNCPathPrefix, StringComparison.Ordinal))
798 return path.Insert(2, UNCLongPathPrefixToInsert); // Given \\server\share in longpath becomes \\?\UNC\server\share => UNCLongPathPrefix + path.SubString(2); => The actual command simply reduces the operation cost.
800 return LongPathPrefix + path;
803 internal unsafe static String RemoveLongPathPrefix(String path)
805 if (!path.StartsWith(LongPathPrefix, StringComparison.Ordinal))
808 if (path.StartsWith(UNCLongPathPrefix, StringComparison.OrdinalIgnoreCase))
809 return path.Remove(2, 6); // Given \\?\UNC\server\share we return \\server\share => @'\\' + path.SubString(UNCLongPathPrefix.Length) => The actual command simply reduces the operation cost.
811 return path.Substring(4);
814 internal unsafe static StringBuilder RemoveLongPathPrefix(StringBuilder pathSB)
816 string path = pathSB.ToString();
817 if (!path.StartsWith(LongPathPrefix, StringComparison.Ordinal))
820 if (path.StartsWith(UNCLongPathPrefix, StringComparison.OrdinalIgnoreCase))
821 return pathSB.Remove(2, 6); // Given \\?\UNC\server\share we return \\server\share => @'\\' + path.SubString(UNCLongPathPrefix.Length) => The actual command simply reduces the operation cost.
823 return pathSB.Remove(0, 4);
826 // Returns the name and extension parts of the given path. The resulting
827 // string contains the characters of path that follow the last
828 // backslash ("\"), slash ("/"), or colon (":") character in
829 // path. The resulting string is the entire path if path
830 // contains no backslash after removing trailing slashes, slash, or colon characters. The resulting
831 // string is null if path is null.
834 public static String GetFileName(String path) {
836 CheckInvalidPathChars(path);
838 int length = path.Length;
839 for (int i = length; --i >= 0;) {
841 if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar)
842 return path.Substring(i + 1, length - i - 1);
850 public static String GetFileNameWithoutExtension(String path) {
851 path = GetFileName(path);
855 if ((i=path.LastIndexOf('.')) == -1)
856 return path; // No path extension found
858 return path.Substring(0,i);
865 // Returns the root portion of the given path. The resulting string
866 // consists of those rightmost characters of the path that constitute the
867 // root of the path. Possible patterns for the resulting string are: An
868 // empty string (a relative path on the current drive), "\" (an absolute
869 // path on the current drive), "X:" (a relative path on a given drive,
870 // where X is the drive letter), "X:\" (an absolute path on a given drive),
871 // and "\\server\share" (a UNC path for a given server and share name).
872 // The resulting string is null if path is null.
875 [ResourceExposure(ResourceScope.Machine)]
876 [ResourceConsumption(ResourceScope.Machine)]
877 public static String GetPathRoot(String path) {
878 if (path == null) return null;
879 path = NormalizePath(path, false);
880 return path.Substring(0, GetRootLength(path));
883 [System.Security.SecuritySafeCritical]
884 [ResourceExposure(ResourceScope.Machine)]
885 [ResourceConsumption(ResourceScope.Machine)]
886 public static String GetTempPath()
889 new EnvironmentPermission(PermissionState.Unrestricted).Demand();
891 StringBuilder sb = new StringBuilder(MAX_PATH);
892 uint r = Win32Native.GetTempPath(MAX_PATH, sb);
893 String path = sb.ToString();
894 if (r==0) __Error.WinIOError();
895 path = GetFullPathInternal(path);
897 FileSecurityState state = new FileSecurityState(FileSecurityStateAccess.Write, String.Empty, path);
903 internal static bool IsRelative(string path)
905 Contract.Assert(path != null, "path can't be null");
906 if ((path.Length >= 3 && path[1] == VolumeSeparatorChar && path[2] == DirectorySeparatorChar &&
907 ((path[0] >= 'a' && path[0] <= 'z') || (path[0] >= 'A' && path[0] <= 'Z'))) ||
908 (path.Length >= 2 && path[0] == '\\' && path[1] == '\\'))
915 // Returns a cryptographically strong random 8.3 string that can be
916 // used as either a folder name or a file name.
917 public static String GetRandomFileName()
919 // 5 bytes == 40 bits == 40/5 == 8 chars in our encoding
920 // This gives us exactly 8 chars. We want to avoid the 8.3 short name issue
921 byte[] key = new byte[10];
923 // RNGCryptoServiceProvider is disposable in post-Orcas desktop mscorlibs, but not in CoreCLR's
924 // mscorlib, so we need to do a manual using block for it.
925 RNGCryptoServiceProvider rng = null;
928 rng = new RNGCryptoServiceProvider();
931 // rndCharArray is expected to be 16 chars
932 char[] rndCharArray = Path.ToBase32StringSuitableForDirName(key).ToCharArray();
933 rndCharArray[8] = '.';
934 return new String(rndCharArray, 0, 12);
945 // Returns a unique temporary file name, and creates a 0-byte file by that
947 [System.Security.SecuritySafeCritical]
948 [ResourceExposure(ResourceScope.AppDomain)]
949 [ResourceConsumption(ResourceScope.AppDomain)]
950 public static String GetTempFileName()
952 return InternalGetTempFileName(true);
955 [System.Security.SecurityCritical]
956 [ResourceExposure(ResourceScope.AppDomain)]
957 [ResourceConsumption(ResourceScope.AppDomain)]
958 internal static String UnsafeGetTempFileName()
960 return InternalGetTempFileName(false);
963 [System.Security.SecurityCritical]
964 [ResourceExposure(ResourceScope.AppDomain)]
965 [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
966 private static String InternalGetTempFileName(bool checkHost)
968 String path = GetTempPath();
970 // Since this can write to the temp directory and theoretically
971 // cause a denial of service attack, demand FileIOPermission to
977 FileSecurityState state = new FileSecurityState(FileSecurityStateAccess.Write, String.Empty, path);
981 new FileIOPermission(FileIOPermissionAccess.Write, path).Demand();
983 StringBuilder sb = new StringBuilder(MAX_PATH);
984 uint r = Win32Native.GetTempFileName(path, "tmp", 0, sb);
985 if (r==0) __Error.WinIOError();
986 return sb.ToString();
989 // Tests if a path includes a file extension. The result is
990 // true if the characters that follow the last directory
991 // separator ('\\' or '/') or volume separator (':') in the path include
992 // a period (".") other than a terminal period. The result is false otherwise.
995 public static bool HasExtension(String path) {
997 CheckInvalidPathChars(path);
999 for (int i = path.Length; --i >= 0;) {
1002 if ( i != path.Length - 1)
1007 if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) break;
1014 // Tests if the given path contains a root. A path is considered rooted
1015 // if it starts with a backslash ("\") or a drive letter and a colon (":").
1018 public static bool IsPathRooted(String path) {
1020 CheckInvalidPathChars(path);
1022 int length = path.Length;
1023 if ((length >= 1 && (path[0] == DirectorySeparatorChar || path[0] == AltDirectorySeparatorChar)) || (length >= 2 && path[1] == VolumeSeparatorChar))
1029 public static String Combine(String path1, String path2) {
1030 if (path1==null || path2==null)
1031 throw new ArgumentNullException((path1==null) ? "path1" : "path2");
1032 Contract.EndContractBlock();
1033 CheckInvalidPathChars(path1);
1034 CheckInvalidPathChars(path2);
1036 return CombineNoChecks(path1, path2);
1039 public static String Combine(String path1, String path2, String path3) {
1040 if (path1 == null || path2 == null || path3 == null)
1041 throw new ArgumentNullException((path1 == null) ? "path1" : (path2 == null) ? "path2" : "path3");
1042 Contract.EndContractBlock();
1043 CheckInvalidPathChars(path1);
1044 CheckInvalidPathChars(path2);
1045 CheckInvalidPathChars(path3);
1047 return CombineNoChecks(CombineNoChecks(path1, path2), path3);
1050 public static String Combine(String path1, String path2, String path3, String path4) {
1051 if (path1 == null || path2 == null || path3 == null || path4 == null)
1052 throw new ArgumentNullException((path1 == null) ? "path1" : (path2 == null) ? "path2" : (path3 == null) ? "path3" : "path4");
1053 Contract.EndContractBlock();
1054 CheckInvalidPathChars(path1);
1055 CheckInvalidPathChars(path2);
1056 CheckInvalidPathChars(path3);
1057 CheckInvalidPathChars(path4);
1059 return CombineNoChecks(CombineNoChecks(CombineNoChecks(path1, path2), path3), path4);
1062 public static String Combine(params String[] paths) {
1063 if (paths == null) {
1064 throw new ArgumentNullException("paths");
1066 Contract.EndContractBlock();
1069 int firstComponent = 0;
1071 // We have two passes, the first calcuates how large a buffer to allocate and does some precondition
1072 // checks on the paths passed in. The second actually does the combination.
1074 for (int i = 0; i < paths.Length; i++) {
1075 if (paths[i] == null) {
1076 throw new ArgumentNullException("paths");
1079 if (paths[i].Length == 0) {
1083 CheckInvalidPathChars(paths[i]);
1085 if (Path.IsPathRooted(paths[i])) {
1087 finalSize = paths[i].Length;
1089 finalSize += paths[i].Length;
1092 char ch = paths[i][paths[i].Length - 1];
1093 if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar)
1097 StringBuilder finalPath = StringBuilderCache.Acquire(finalSize);
1099 for (int i = firstComponent; i < paths.Length; i++) {
1100 if (paths[i].Length == 0) {
1104 if (finalPath.Length == 0) {
1105 finalPath.Append(paths[i]);
1107 char ch = finalPath[finalPath.Length - 1];
1108 if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar) {
1109 finalPath.Append(DirectorySeparatorChar);
1112 finalPath.Append(paths[i]);
1116 return StringBuilderCache.GetStringAndRelease(finalPath);
1119 private static String CombineNoChecks(String path1, String path2) {
1120 if (path2.Length == 0)
1123 if (path1.Length == 0)
1126 if (IsPathRooted(path2))
1129 char ch = path1[path1.Length - 1];
1130 if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar)
1131 return path1 + DirectorySeparatorCharAsString + path2;
1132 return path1 + path2;
1135 private static readonly Char[] s_Base32Char = {
1136 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
1137 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
1138 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
1139 'y', 'z', '0', '1', '2', '3', '4', '5'};
1141 internal static String ToBase32StringSuitableForDirName(byte[] buff)
1143 // This routine is optimised to be used with buffs of length 20
1144 Contract.Assert(((buff.Length % 5) == 0), "Unexpected hash length");
1146 StringBuilder sb = StringBuilderCache.Acquire();
1147 byte b0, b1, b2, b3, b4;
1153 // Create l chars using the last 5 bits of each byte.
1154 // Consume 3 MSB bits 5 bytes at a time.
1158 b0 = (i < l) ? buff[i++] : (byte)0;
1159 b1 = (i < l) ? buff[i++] : (byte)0;
1160 b2 = (i < l) ? buff[i++] : (byte)0;
1161 b3 = (i < l) ? buff[i++] : (byte)0;
1162 b4 = (i < l) ? buff[i++] : (byte)0;
1164 // Consume the 5 Least significant bits of each byte
1165 sb.Append(s_Base32Char[b0 & 0x1F]);
1166 sb.Append(s_Base32Char[b1 & 0x1F]);
1167 sb.Append(s_Base32Char[b2 & 0x1F]);
1168 sb.Append(s_Base32Char[b3 & 0x1F]);
1169 sb.Append(s_Base32Char[b4 & 0x1F]);
1171 // Consume 3 MSB of b0, b1, MSB bits 6, 7 of b3, b4
1172 sb.Append(s_Base32Char[(
1173 ((b0 & 0xE0) >> 5) |
1174 ((b3 & 0x60) >> 2))]);
1176 sb.Append(s_Base32Char[(
1177 ((b1 & 0xE0) >> 5) |
1178 ((b4 & 0x60) >> 2))]);
1180 // Consume 3 MSB bits of b2, 1 MSB bit of b3, b4
1184 Contract.Assert(((b2 & 0xF8) == 0), "Unexpected set bits");
1186 if ((b3 & 0x80) != 0)
1188 if ((b4 & 0x80) != 0)
1191 sb.Append(s_Base32Char[b2]);
1195 return StringBuilderCache.GetStringAndRelease(sb);
1198 // ".." can only be used if it is specified as a part of a valid File/Directory name. We disallow
1199 // the user being able to use it to move up directories. Here are some examples eg
1200 // Valid: a..b abc..d
1201 // Invalid: ..ab ab.. .. abc..d\abc..
1203 internal static void CheckSearchPattern(String searchPattern)
1206 while ((index = searchPattern.IndexOf("..", StringComparison.Ordinal)) != -1) {
1208 if (index + 2 == searchPattern.Length) // Terminal ".." . Files names cannot end in ".."
1209 throw new ArgumentException(Environment.GetResourceString("Arg_InvalidSearchPattern"));
1211 if ((searchPattern[index+2] == DirectorySeparatorChar)
1212 || (searchPattern[index+2] == AltDirectorySeparatorChar))
1213 throw new ArgumentException(Environment.GetResourceString("Arg_InvalidSearchPattern"));
1215 searchPattern = searchPattern.Substring(index + 2);
1220 internal static bool HasIllegalCharacters(String path, bool checkAdditional)
1222 Contract.Requires(path != null);
1224 if (checkAdditional)
1226 return path.IndexOfAny(InvalidPathCharsWithAdditionalChecks) >= 0;
1229 return path.IndexOfAny(RealInvalidPathChars) >= 0;
1232 internal static void CheckInvalidPathChars(String path, bool checkAdditional = false)
1235 throw new ArgumentNullException("path");
1237 if (Path.HasIllegalCharacters(path, checkAdditional))
1238 throw new ArgumentException(Environment.GetResourceString("Argument_InvalidPathChars"));
1242 internal static String InternalCombine(String path1, String path2) {
1243 if (path1==null || path2==null)
1244 throw new ArgumentNullException((path1==null) ? "path1" : "path2");
1245 Contract.EndContractBlock();
1246 CheckInvalidPathChars(path1);
1247 CheckInvalidPathChars(path2);
1249 if (path2.Length == 0)
1250 throw new ArgumentException(Environment.GetResourceString("Argument_PathEmpty"), "path2");
1251 if (IsPathRooted(path2))
1252 throw new ArgumentException(Environment.GetResourceString("Arg_Path2IsRooted"), "path2");
1253 int i = path1.Length;
1254 if (i == 0) return path2;
1255 char ch = path1[i - 1];
1256 if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar)
1257 return path1 + DirectorySeparatorCharAsString + path2;
1258 return path1 + path2;