Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 0721434

Browse files
committed
Allow extended syntax for paths
Additional work unblocking extended syntax. Fixes a few issues, adds a number of tests, normalizes naming a bit more and moves/adds helpers to shared code. One nice side effect of this change is that you can now delete directories that were undeletable before (if you had subdirectories that had trailing spaces as one example).
1 parent 14e98f7 commit 0721434

File tree

14 files changed

+238
-114
lines changed

14 files changed

+238
-114
lines changed

src/Common/src/System/IO/PathInternal.Windows.cs

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Diagnostics.Contracts;
5+
using System.Text;
56

67
namespace System.IO
78
{
89
/// <summary>Contains internal path helpers that are shared between many projects.</summary>
910
internal static partial class PathInternal
1011
{
11-
internal const string LongPathPrefix = @"\\?\";
12-
internal const string UNCPathPrefix = @"\\";
13-
internal const string UNCLongPathPrefixToInsert = @"?\UNC\";
14-
internal const string UNCLongPathPrefix = @"\\?\UNC\";
12+
internal const string ExtendedPathPrefix = @"\\?\";
13+
internal const string UncPathPrefix = @"\\";
14+
internal const string UncExtendedPrefixToInsert = @"?\UNC\";
15+
internal const string UncExtendedPathPrefix = @"\\?\UNC\";
1516

1617
internal static readonly char[] InvalidPathChars =
1718
{
@@ -31,6 +32,95 @@ internal static partial class PathInternal
3132
(char)31, '*', '?'
3233
};
3334

35+
/// <summary>
36+
/// Adds the extended path prefix (\\?\) if not already present.
37+
/// </summary>
38+
internal static string AddExtendedPathPrefix(string path)
39+
{
40+
if (IsExtended(path))
41+
return path;
42+
43+
// Given \\server\share in longpath becomes \\?\UNC\server\share
44+
if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
45+
return path.Insert(2, PathInternal.UncExtendedPrefixToInsert);
46+
47+
return PathInternal.ExtendedPathPrefix + path;
48+
}
49+
50+
/// <summary>
51+
/// Removes the extended path prefix (\\?\) if present.
52+
/// </summary>
53+
internal static string RemoveExtendedPathPrefix(string path)
54+
{
55+
if (!IsExtended(path))
56+
return path;
57+
58+
// Given \\?\UNC\server\share we return \\server\share
59+
if (IsExtendedUnc(path))
60+
return path.Remove(2, 6);
61+
62+
return path.Substring(4);
63+
}
64+
65+
/// <summary>
66+
/// Removes the extended path prefix (\\?\) if present.
67+
/// </summary>
68+
internal static StringBuilder RemoveExtendedPathPrefix(StringBuilder path)
69+
{
70+
if (!IsExtended(path))
71+
return path;
72+
73+
// Given \\?\UNC\server\share we return \\server\share
74+
if (IsExtendedUnc(path))
75+
return path.Remove(2, 6);
76+
77+
return path.Remove(0, 4);
78+
}
79+
80+
/// <summary>
81+
/// Returns true if the path uses the extended syntax (\\?\)
82+
/// </summary>
83+
internal static bool IsExtended(string path)
84+
{
85+
return path != null && path.StartsWith(ExtendedPathPrefix, StringComparison.Ordinal);
86+
}
87+
88+
/// <summary>
89+
/// Returns true if the path uses the extended syntax (\\?\)
90+
/// </summary>
91+
internal static bool IsExtended(StringBuilder path)
92+
{
93+
return path != null && path.StartsWithOrdinal(ExtendedPathPrefix);
94+
}
95+
96+
/// <summary>
97+
/// Returns true if the path uses the extended UNC syntax (\\?\UNC\)
98+
/// </summary>
99+
internal static bool IsExtendedUnc(string path)
100+
{
101+
return path != null && path.StartsWith(UncExtendedPathPrefix, StringComparison.Ordinal);
102+
}
103+
104+
/// <summary>
105+
/// Returns true if the path uses the extended UNC syntax (\\?\UNC\)
106+
/// </summary>
107+
internal static bool IsExtendedUnc(StringBuilder path)
108+
{
109+
return path != null && path.StartsWithOrdinal(UncExtendedPathPrefix);
110+
}
111+
112+
private static bool StartsWithOrdinal(this StringBuilder builder, string value)
113+
{
114+
if (value == null || builder.Length < value.Length)
115+
return false;
116+
117+
for (int i = 0; i < value.Length; i++)
118+
{
119+
if (builder[i] != value[i]) return false;
120+
}
121+
return true;
122+
}
123+
34124
/// <summary>
35125
/// Returns a value indicating if the given path contains invalid characters (", &lt;, &gt;, |
36126
/// NUL, or any ASCII char whose integer representation is in the range of 1 through 31),
@@ -43,7 +133,7 @@ internal static bool HasIllegalCharacters(string path, bool checkAdditional = fa
43133
Contract.Requires(path != null);
44134

45135
// Question mark is a normal part of extended path syntax (\\?\)
46-
int startIndex = path.StartsWith(LongPathPrefix, StringComparison.Ordinal) ? LongPathPrefix.Length : 0;
136+
int startIndex = PathInternal.IsExtended(path) ? ExtendedPathPrefix.Length : 0;
47137
return path.IndexOfAny(checkAdditional ? InvalidPathCharsWithAdditionalChecks : InvalidPathChars, startIndex) >= 0;
48138
}
49139

@@ -59,20 +149,20 @@ internal static int GetRootLength(string path)
59149
int volumeSeparatorLength = 2; // Length to the colon "C:"
60150
int uncRootLength = 2; // Length to the start of the server name "\\"
61151

62-
bool extendedSyntax = path.StartsWith(LongPathPrefix, StringComparison.Ordinal);
63-
bool extendedUncSyntax = extendedSyntax && path.StartsWith(UNCLongPathPrefix, StringComparison.Ordinal);
152+
bool extendedSyntax = IsExtended(path);
153+
bool extendedUncSyntax = IsExtendedUnc(path);
64154
if (extendedSyntax)
65155
{
66156
// Shift the position we look for the root from to account for the extended prefix
67157
if (extendedUncSyntax)
68158
{
69-
// "C:" -> "\\?\C:"
70-
uncRootLength = UNCLongPathPrefix.Length;
159+
// "\\" -> "\\?\UNC\"
160+
uncRootLength = UncExtendedPathPrefix.Length;
71161
}
72162
else
73163
{
74-
// "\\" -> "\\?\UNC\"
75-
volumeSeparatorLength += LongPathPrefix.Length;
164+
// "C:" -> "\\?\C:"
165+
volumeSeparatorLength += ExtendedPathPrefix.Length;
76166
}
77167
}
78168

src/System.IO.FileSystem/src/System/IO/PathHelpers.Windows.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ internal static bool ShouldReviseDirectoryPathToCurrent(string path)
2222
// Valid: a..b abc..d
2323
// Invalid: ..ab ab.. .. abc..d\abc..
2424
//
25-
internal static void CheckSearchPattern(String searchPattern)
25+
internal static void CheckSearchPattern(string searchPattern)
2626
{
2727
for (int index = 0; (index = searchPattern.IndexOf("..", index, StringComparison.Ordinal)) != -1; index += 2)
2828
{
@@ -40,9 +40,16 @@ internal static string GetFullPathInternal(string path)
4040
if (path == null)
4141
throw new ArgumentNullException("path");
4242

43-
string pathTrimmed = path.TrimStart(TrimStartChars).TrimEnd(TrimEndChars);
44-
45-
return Path.GetFullPath(Path.IsPathRooted(pathTrimmed) ? pathTrimmed : path);
43+
if (PathInternal.IsExtended(path))
44+
{
45+
// Don't want to trim extended paths
46+
return Path.GetFullPath(path);
47+
}
48+
else
49+
{
50+
string pathTrimmed = path.TrimStart(TrimStartChars).TrimEnd(TrimEndChars);
51+
return Path.GetFullPath(Path.IsPathRooted(pathTrimmed) ? pathTrimmed : path);
52+
}
4653
}
4754

4855
// this is a lightweight version of GetDirectoryName that doesn't renormalize

src/System.IO.FileSystem/src/System/IO/Win32FileSystem.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -486,11 +486,13 @@ public override void RemoveDirectory(string fullPath, bool recursive)
486486
if (((FileAttributes)data.fileAttributes & FileAttributes.ReparsePoint) != 0)
487487
recursive = false;
488488

489-
RemoveDirectoryHelper(fullPath, recursive, true);
489+
// We want extended syntax so we can delete "extended" subdirectories and files
490+
// (most notably ones with trailing whitespace or periods)
491+
RemoveDirectoryHelper(PathInternal.AddExtendedPathPrefix(fullPath), recursive, true);
490492
}
491493

492494
[System.Security.SecurityCritical] // auto-generated
493-
private static void RemoveDirectoryHelper(String fullPath, bool recursive, bool throwOnTopLevelDirectoryNotFound)
495+
private static void RemoveDirectoryHelper(string fullPath, bool recursive, bool throwOnTopLevelDirectoryNotFound)
494496
{
495497
bool r;
496498
int errorCode;
@@ -531,7 +533,7 @@ private static void RemoveDirectoryHelper(String fullPath, bool recursive, bool
531533
bool shouldRecurse = (0 == (data.dwFileAttributes & (int)FileAttributes.ReparsePoint));
532534
if (shouldRecurse)
533535
{
534-
String newFullPath = Path.Combine(fullPath, data.cFileName);
536+
string newFullPath = Path.Combine(fullPath, data.cFileName);
535537
try
536538
{
537539
RemoveDirectoryHelper(newFullPath, recursive, false);

src/System.IO.FileSystem/src/System/IO/WinRTFileSystem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ public override FileStreamBase Open(string fullPath, FileMode mode, FileAccess a
477477
private async Task<FileStreamBase> OpenAsync(string fullPath, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, FileStream parent)
478478
{
479479
// When trying to open the root directory, we need to throw an Access Denied
480-
if (PathHelpers.GetRootLength(fullPath) == fullPath.Length)
480+
if (PathInternal.GetRootLength(fullPath) == fullPath.Length)
481481
throw Win32Marshal.GetExceptionForWin32Error(Interop.mincore.Errors.ERROR_ACCESS_DENIED, fullPath);
482482

483483
// Win32 CreateFile returns ERROR_PATH_NOT_FOUND when given a path that ends with '\'

src/System.IO.FileSystem/tests/Directory/CreateDirectory.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ public void WindowsExtendedSyntaxWhiteSpace()
303303
{
304304
string extendedPath = Path.Combine(@"\\?\" + directory.Path, path);
305305
Directory.CreateDirectory(extendedPath);
306-
Assert.True(Directory.Exists(extendedPath));
306+
Assert.True(Directory.Exists(extendedPath), extendedPath);
307307
}
308308
}
309309
}
@@ -348,6 +348,20 @@ public void PathWithReservedDeviceNameAsPath_ThrowsDirectoryNotFoundException()
348348
});
349349
}
350350

351+
[Fact]
352+
[PlatformSpecific(PlatformID.Windows)] // device name prefixes
353+
public void PathWithReservedDeviceNameAsExtendedPath()
354+
{
355+
var paths = IOInputs.GetReservedDeviceNames();
356+
using (TemporaryDirectory directory = new TemporaryDirectory())
357+
{
358+
Assert.All(paths, (path) =>
359+
{
360+
Assert.True(Create(@"\\?\" + Path.Combine(directory.Path, path)).Exists, path);
361+
});
362+
}
363+
}
364+
351365
[Fact]
352366
[PlatformSpecific(PlatformID.Windows)] // UNC shares
353367
public void UncPathWithoutShareNameAsPath_ThrowsArgumentException()

src/System.IO.FileSystem/tests/Directory/Delete.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ public void ShouldThrowIOExceptionDeletingCurrentDirectory()
9090

9191
#region PlatformSpecific
9292

93+
[Fact]
94+
[PlatformSpecific(PlatformID.Windows)]
95+
public void WindowsExtendedDirectoryWithSubdirectories()
96+
{
97+
DirectoryInfo testDir = Directory.CreateDirectory(@"\\?\" + GetTestFilePath());
98+
testDir.CreateSubdirectory(GetTestFileName());
99+
Assert.Throws<IOException>(() => Delete(testDir.FullName));
100+
Assert.True(testDir.Exists);
101+
}
102+
93103
[Fact]
94104
[PlatformSpecific(PlatformID.Windows)]
95105
public void WindowsDeleteReadOnlyDirectory()
@@ -101,6 +111,17 @@ public void WindowsDeleteReadOnlyDirectory()
101111
testDir.Attributes = FileAttributes.Normal;
102112
}
103113

114+
[Fact]
115+
[PlatformSpecific(PlatformID.Windows)]
116+
public void WindowsDeleteExtendedReadOnlyDirectory()
117+
{
118+
DirectoryInfo testDir = Directory.CreateDirectory(@"\\?\" + GetTestFilePath());
119+
testDir.Attributes = FileAttributes.ReadOnly;
120+
Assert.Throws<IOException>(() => Delete(testDir.FullName));
121+
Assert.True(testDir.Exists);
122+
testDir.Attributes = FileAttributes.Normal;
123+
}
124+
104125
[Fact]
105126
[PlatformSpecific(PlatformID.AnyUnix)]
106127
public void UnixDeleteReadOnlyDirectory()
@@ -121,6 +142,16 @@ public void WindowsShouldBeAbleToDeleteHiddenDirectory()
121142
Assert.False(testDir.Exists);
122143
}
123144

145+
[Fact]
146+
[PlatformSpecific(PlatformID.Windows)]
147+
public void WindowsShouldBeAbleToDeleteExtendedHiddenDirectory()
148+
{
149+
DirectoryInfo testDir = Directory.CreateDirectory(@"\\?\" + GetTestFilePath());
150+
testDir.Attributes = FileAttributes.Hidden;
151+
Delete(testDir.FullName);
152+
Assert.False(testDir.Exists);
153+
}
154+
124155
[Fact]
125156
[PlatformSpecific(PlatformID.AnyUnix)]
126157
public void UnixShouldBeAbleToDeleteHiddenDirectory()

src/System.IO.FileSystem/tests/Directory/Exists.cs

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,42 @@ public void DirectoryLongerThanMaxPathAsPath_DoesntThrow()
113113

114114
#region PlatformSpecific
115115

116+
[Fact]
117+
[PlatformSpecific(PlatformID.Windows)]
118+
public void ValidExtendedPathExists_ReturnsTrue()
119+
{
120+
Assert.All((IOInputs.GetValidPathComponentNames()), (component) =>
121+
{
122+
string path = @"\\?\" + Path.Combine(TestDirectory, "extended", component);
123+
DirectoryInfo testDir = Directory.CreateDirectory(path);
124+
Assert.True(Exists(path));
125+
});
126+
}
127+
128+
[Fact]
129+
[PlatformSpecific(PlatformID.Windows)]
130+
public void ExtendedPathAlreadyExistsAsFile()
131+
{
132+
string path = @"\\?\" + GetTestFilePath();
133+
File.Create(path).Dispose();
134+
135+
Assert.False(Exists(IOServices.RemoveTrailingSlash(path)));
136+
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
137+
Assert.False(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
138+
}
139+
140+
[Fact]
141+
[PlatformSpecific(PlatformID.Windows)]
142+
public void ExtendedPathAlreadyExistsAsDirectory()
143+
{
144+
string path = @"\\?\" + GetTestFilePath();
145+
DirectoryInfo testDir = Directory.CreateDirectory(path);
146+
147+
Assert.True(Exists(IOServices.RemoveTrailingSlash(path)));
148+
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.RemoveTrailingSlash(path))));
149+
Assert.True(Exists(IOServices.RemoveTrailingSlash(IOServices.AddTrailingSlashIfNeeded(path))));
150+
}
151+
116152
[Fact]
117153
[PlatformSpecific(PlatformID.Windows)]
118154
public void DirectoryLongerThanMaxDirectoryAsPath_DoesntThrow()
@@ -125,7 +161,7 @@ public void DirectoryLongerThanMaxDirectoryAsPath_DoesntThrow()
125161

126162
[Fact]
127163
[PlatformSpecific(PlatformID.Windows)] // Unix equivalent tested already in CreateDirectory
128-
public void WindowsNonSignificantWhiteSpaceAsPath_ReturnsFalse()
164+
public void WindowsWhiteSpaceAsPath_ReturnsFalse()
129165
{
130166
// Checks that errors aren't thrown when calling Exists() on impossible paths
131167
Assert.All(IOInputs.GetWhiteSpace(), (component) =>
@@ -136,7 +172,7 @@ public void WindowsNonSignificantWhiteSpaceAsPath_ReturnsFalse()
136172

137173
[Fact]
138174
[PlatformSpecific(PlatformID.Windows | PlatformID.OSX)]
139-
public void DoesCaseInsensitiveInvariantComparions()
175+
public void DoesCaseInsensitiveInvariantComparisons()
140176
{
141177
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
142178
Assert.True(Exists(testDir.FullName));
@@ -146,7 +182,7 @@ public void DoesCaseInsensitiveInvariantComparions()
146182

147183
[Fact]
148184
[PlatformSpecific(PlatformID.Linux | PlatformID.FreeBSD)]
149-
public void DoesCaseSensitiveComparions()
185+
public void DoesCaseSensitiveComparisons()
150186
{
151187
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
152188
Assert.True(Exists(testDir.FullName));
@@ -155,13 +191,23 @@ public void DoesCaseSensitiveComparions()
155191
}
156192

157193
[Fact]
158-
[PlatformSpecific(PlatformID.Windows)] // In Windows, trailing whitespace in a path is trimmed
159-
public void TrimTrailingWhitespacePath()
194+
[PlatformSpecific(PlatformID.Windows)] // In Windows, trailing whitespace in a path is trimmed appropriately
195+
public void TrailingWhitespaceExistence()
160196
{
161197
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
162198
Assert.All(IOInputs.GetWhiteSpace(), (component) =>
163199
{
164-
Assert.True(Exists(testDir.FullName + component)); // string concat in case Path.Combine() trims whitespace before Exists gets to it
200+
string path = testDir.FullName + component;
201+
Assert.True(Exists(path), path); // string concat in case Path.Combine() trims whitespace before Exists gets to it
202+
Assert.False(Exists(@"\\?\" + path), path);
203+
});
204+
205+
Assert.All(IOInputs.GetSimpleWhiteSpace(), (component) =>
206+
{
207+
string path = GetTestFilePath("Extended") + component;
208+
testDir = Directory.CreateDirectory(@"\\?\" + path);
209+
Assert.False(Exists(path), path);
210+
Assert.True(Exists(testDir.FullName));
165211
});
166212
}
167213

0 commit comments

Comments
 (0)