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

Commit 800e7b7

Browse files
committed
Merge pull request #2453 from JeremyKuhne/MoreExtendedPath
Allow extended syntax for paths
2 parents 5bdeb5b + 0721434 commit 800e7b7

File tree

13 files changed

+237
-113
lines changed

13 files changed

+237
-113
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/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)