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

Commit 72429b2

Browse files
committed
Allow extended syntax for paths
Start on unblocking extended syntax. Moves some shared code to PathInternal. Start adding tests for key scenarios. Temporarily use local libraries when testing FileSystem (until we get an updated Runtime.Extensions package).
1 parent 244f9b0 commit 72429b2

28 files changed

+443
-279
lines changed
Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,45 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.Diagnostics;
45
using System.Diagnostics.Contracts;
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
{
12+
// Therre is only one invalid path character in Unix
13+
private const char InvalidPathChar = '\0';
14+
internal static readonly char[] InvalidPathChars = { InvalidPathChar };
15+
1116
/// <summary>Returns a value indicating if the given path contains invalid characters.</summary>
1217
internal static bool HasIllegalCharacters(string path, bool checkAdditional = false)
1318
{
1419
Contract.Requires(path != null);
1520

1621
foreach (char c in path)
1722
{
18-
// Same as Path.InvalidPathChars, unrolled here for performance
19-
if (c == '\0')
23+
// Same as InvalidPathChars, unrolled here for performance
24+
if (c == InvalidPathChar)
2025
return true;
21-
22-
// checkAdditional is meant to check for additional characters
23-
// permitted in a search path but disallowed in a normal path.
24-
// In Windows this is * and ?,but Unix permits such characters
25-
// in the filename so checkAdditional is ignored.
2626
}
2727

2828
return false;
2929
}
30+
31+
internal static int GetRootLength(string path)
32+
{
33+
PathInternal.CheckInvalidPathChars(path);
34+
return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0;
35+
}
36+
37+
internal static bool IsDirectorySeparator(char c)
38+
{
39+
// The alternatie directory separator char is the same as the directory separator,
40+
// so we only need to check one.
41+
Debug.Assert(Path.DirectorySeparatorChar == Path.AltDirectorySeparatorChar);
42+
return c == Path.DirectorySeparatorChar;
43+
}
3044
}
3145
}

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

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ namespace System.IO
88
/// <summary>Contains internal path helpers that are shared between many projects.</summary>
99
internal static partial class PathInternal
1010
{
11+
internal const string LongPathPrefix = @"\\?\";
12+
internal const string UNCPathPrefix = @"\\";
13+
internal const string UNCLongPathPrefixToInsert = @"?\UNC\";
14+
internal const string UNCLongPathPrefix = @"\\?\UNC\";
15+
16+
internal static readonly char[] InvalidPathChars =
17+
{
18+
'\"', '<', '>', '|', '\0',
19+
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
20+
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
21+
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
22+
(char)31
23+
};
24+
25+
internal static readonly char[] InvalidPathCharsWithAdditionalChecks = // This is used by HasIllegalCharacters
26+
{
27+
'\"', '<', '>', '|', '\0',
28+
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
29+
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
30+
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
31+
(char)31, '*', '?'
32+
};
33+
1134
/// <summary>
1235
/// Returns a value indicating if the given path contains invalid characters (", &lt;, &gt;, |
1336
/// NUL, or any ASCII char whose integer representation is in the range of 1 through 31),
@@ -19,19 +42,67 @@ internal static bool HasIllegalCharacters(string path, bool checkAdditional = fa
1942

2043
Contract.Requires(path != null);
2144

22-
foreach (char c in path)
45+
// Question mark is a normal part of extended path syntax (\\?\)
46+
int startIndex = path.StartsWith(LongPathPrefix, StringComparison.Ordinal) ? LongPathPrefix.Length : 0;
47+
return path.IndexOfAny(checkAdditional ? InvalidPathCharsWithAdditionalChecks : InvalidPathChars, startIndex) >= 0;
48+
}
49+
50+
/// <summary>
51+
/// Gets the length of the root of the path (drive, share, etc.).
52+
/// </summary>
53+
internal static int GetRootLength(string path)
54+
{
55+
CheckInvalidPathChars(path);
56+
57+
int i = 0;
58+
int length = path.Length;
59+
int volumeSeparatorLength = 2; // Length to the colon "C:"
60+
int uncRootLength = 2; // Length to the start of the server name "\\"
61+
62+
bool extendedSyntax = path.StartsWith(LongPathPrefix, StringComparison.Ordinal);
63+
bool extendedUncSyntax = extendedSyntax && path.StartsWith(UNCLongPathPrefix, StringComparison.Ordinal);
64+
if (extendedSyntax)
65+
{
66+
// Shift the position we look for the root from to account for the extended prefix
67+
if (extendedUncSyntax)
68+
{
69+
// "C:" -> "\\?\C:"
70+
uncRootLength = UNCLongPathPrefix.Length;
71+
}
72+
else
73+
{
74+
// "\\" -> "\\?\UNC\"
75+
volumeSeparatorLength += LongPathPrefix.Length;
76+
}
77+
}
78+
79+
if ((!extendedSyntax || extendedUncSyntax) && length > 0 && IsDirectorySeparator(path[0]))
2380
{
24-
// Note: Same as Path.InvalidPathChars, unrolled here for performance
25-
if (c == '\"' || c == '<' || c == '>' || c == '|' || c < 32)
26-
return true;
27-
28-
// used when path cannot contain search strings.
29-
if (checkAdditional &&
30-
(c == '?' || c == '*'))
31-
return true;
81+
// UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")
82+
83+
i = 1; // Drive rooted (\foo) is one character
84+
if (extendedUncSyntax || (length > 1 && (IsDirectorySeparator(path[1]))))
85+
{
86+
// UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
87+
// (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
88+
i = uncRootLength;
89+
int n = 2; // Maximum separators to skip
90+
while (i < length && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
91+
}
92+
}
93+
else if (length >= volumeSeparatorLength && path[volumeSeparatorLength - 1] == Path.VolumeSeparatorChar)
94+
{
95+
// Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
96+
// If the colon is followed by a directory separator, move past it
97+
i = volumeSeparatorLength;
98+
if (length >= volumeSeparatorLength + 1 && (IsDirectorySeparator(path[volumeSeparatorLength]))) i++;
3299
}
100+
return i;
101+
}
33102

34-
return false;
103+
internal static bool IsDirectorySeparator(char c)
104+
{
105+
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
35106
}
36107
}
37108
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Diagnostics.Contracts;
5+
6+
namespace System.IO
7+
{
8+
/// <summary>Contains internal path helpers that are shared between many projects.</summary>
9+
internal static partial class PathInternal
10+
{
11+
/// <summary>
12+
/// Checks for invalid path characters in the given path.
13+
/// </summary>
14+
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
15+
/// <exception cref="System.ArgumentException">Thrown if the path has invalid characters.</exception>
16+
/// <param name="path">The path to check for invalid characters.</param>
17+
/// <param name="checkAdditional">Set to true to check for characters that are only valid in specific contexts (such as search characters on Windows).</param>
18+
internal static void CheckInvalidPathChars(string path, bool checkAdditional = false)
19+
{
20+
if (path == null)
21+
throw new ArgumentNullException("path");
22+
23+
if (PathInternal.HasIllegalCharacters(path, checkAdditional))
24+
throw new ArgumentException(SR.Argument_InvalidPathChars, "path");
25+
}
26+
}
27+
}

src/System.IO.FileSystem.DriveInfo/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@
126126
<data name="Arg_InvalidDriveChars" xml:space="preserve">
127127
<value>Illegal characters in drive name '{0}'.</value>
128128
</data>
129+
<data name="Argument_InvalidPathChars" xml:space="preserve">
130+
<value>Illegal characters in path.</value>
131+
</data>
129132
<data name="ArgumentOutOfRange_FileLengthTooBig" xml:space="preserve">
130133
<value>Specified file length was too large for the file system.</value>
131134
</data>

src/System.IO.FileSystem.DriveInfo/src/System.IO.FileSystem.DriveInfo.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
<Compile Include="$(CommonPath)\System\__HResults.cs">
3333
<Link>Common\System\__HResults.cs</Link>
3434
</Compile>
35+
<Compile Include="$(CommonPath)\System\IO\PathInternal.cs">
36+
<Link>Common\System\IO\PathInternal.cs</Link>
37+
</Compile>
3538
</ItemGroup>
3639
<ItemGroup Condition=" '$(TargetsWindows)' == 'true' ">
3740
<Compile Include="System\IO\__Error.cs" />

src/System.IO.FileSystem/src/System.IO.FileSystem.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
<Compile Include="$(CommonPath)\System\IO\StringBuilderCache.cs">
5353
<Link>Common\System\IO\StringBuilderCache.cs</Link>
5454
</Compile>
55+
<Compile Include="$(CommonPath)\System\IO\PathInternal.cs">
56+
<Link>Common\System\IO\PathInternal.cs</Link>
57+
</Compile>
5558
</ItemGroup>
5659
<!-- Windows -->
5760
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,15 +486,15 @@ public static String GetDirectoryRoot(String path)
486486
Contract.EndContractBlock();
487487

488488
String fullPath = PathHelpers.GetFullPathInternal(path);
489-
String root = fullPath.Substring(0, PathHelpers.GetRootLength(fullPath));
489+
String root = fullPath.Substring(0, PathInternal.GetRootLength(fullPath));
490490

491491
return root;
492492
}
493493

494494
internal static String InternalGetDirectoryRoot(String path)
495495
{
496496
if (path == null) return null;
497-
return path.Substring(0, PathHelpers.GetRootLength(path));
497+
return path.Substring(0, PathInternal.GetRootLength(path));
498498
}
499499

500500
/*===============================CurrentDirectory===============================

src/System.IO.FileSystem/src/System/IO/DirectoryInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public DirectoryInfo(String path)
2525
[System.Security.SecuritySafeCritical]
2626
internal DirectoryInfo(String fullPath, IFileSystemObject fileSystemObject) : base(fileSystemObject)
2727
{
28-
Debug.Assert(PathHelpers.GetRootLength(fullPath) > 0, "fullPath must be fully qualified!");
28+
Debug.Assert(PathInternal.GetRootLength(fullPath) > 0, "fullPath must be fully qualified!");
2929

3030
// Fast path when we know a DirectoryInfo exists.
3131
OriginalPath = Path.GetFileName(fullPath);

src/System.IO.FileSystem/src/System/IO/File.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public static bool Exists(String path)
195195
// Otherwise, FillAttributeInfo removes it and we may return a false positive.
196196
// GetFullPath should never return null
197197
Debug.Assert(path != null, "File.Exists: GetFullPath returned null");
198-
if (path.Length > 0 && PathHelpers.IsDirectorySeparator(path[path.Length - 1]))
198+
if (path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]))
199199
{
200200
return false;
201201
}

src/System.IO.FileSystem/src/System/IO/FileStream.Win32.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,6 @@ static partial void ValidatePath(string fullPath, string paramName)
2424
// Prevent access to your disk drives as raw block devices.
2525
if (fullPath.StartsWith("\\\\.\\", StringComparison.Ordinal))
2626
throw new ArgumentException(SR.Arg_DevicesNotSupported, paramName);
27-
28-
// Check for additional invalid characters. Most invalid characters were checked above
29-
// in our call to Path.GetFullPath(path);
30-
if (fullPath.IndexOfAny(s_additionalInvalidChars) != -1)
31-
throw new ArgumentException(SR.Argument_InvalidPathChars, paramName);
32-
33-
if (fullPath.IndexOf(':', 2) != -1)
34-
throw new NotSupportedException(SR.Argument_PathFormatNotSupported);
3527
}
3628

3729
private static readonly char[] s_additionalInvalidChars = new[] { '?', '*' };

0 commit comments

Comments
 (0)