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

Commit 620e103

Browse files
committed
Merge pull request #2267 from stephentoub/filesystem_pathhelpers
Fix System.IO.FileSystem's PathHelpers for Unix
2 parents 61dc854 + 5945d8c commit 620e103

File tree

12 files changed

+258
-188
lines changed

12 files changed

+258
-188
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<Compile Include="Microsoft\Win32\SafeHandles\SafeFileHandle.Windows.cs" />
5757
<Compile Include="Microsoft\Win32\SafeHandles\SafeFindHandle.Windows.cs" />
5858
<Compile Include="System\IO\FileStream.Win32.cs" />
59+
<Compile Include="System\IO\PathHelpers.Windows.cs" />
5960
<Compile Include="System\IO\Win32FileStream.cs" />
6061
<Compile Include="System\IO\Win32FileStreamCompletionSource.cs" />
6162
<Compile Include="System\IO\Win32FileSystem.cs" />
@@ -214,6 +215,7 @@
214215
<Compile Include="Microsoft\Win32\SafeHandles\SafeFileHandle.Unix.cs" />
215216
<Compile Include="System\IO\FileStream.Unix.cs" />
216217
<Compile Include="System\IO\FileSystem.Current.Unix.cs" />
218+
<Compile Include="System\IO\PathHelpers.Unix.cs" />
217219
<Compile Include="System\IO\UnixFileStream.cs" />
218220
<Compile Include="System\IO\UnixFileSystem.cs" />
219221
<Compile Include="System\IO\UnixFileSystemObject.cs" />
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
namespace System.IO
5+
{
6+
internal static partial class PathHelpers
7+
{
8+
internal static int GetRootLength(string path)
9+
{
10+
CheckInvalidPathChars(path);
11+
return path.Length > 0 && IsDirectorySeparator(path[0]) ? 1 : 0;
12+
}
13+
14+
internal static void CheckSearchPattern(string searchPattern)
15+
{
16+
// ".." should not be used to move up directories. On Windows, this is more strict, and ".."
17+
// can only be used in particular places in a name, whereas on Unix it can be used anywhere.
18+
// So, throw if we find a ".." that's its own component in the path.
19+
for (int index = 0; (index = searchPattern.IndexOf("..", index, StringComparison.Ordinal)) >= 0; index += 2)
20+
{
21+
if ((index == 0 || IsDirectorySeparator(searchPattern[index - 1])) && // previous character is directory separator
22+
(index + 2 == searchPattern.Length || IsDirectorySeparator(searchPattern[index + 2]))) // next character is directory separator
23+
{
24+
throw new ArgumentException(SR.Arg_InvalidSearchPattern, "searchPattern");
25+
}
26+
}
27+
}
28+
29+
internal static bool IsDirectorySeparator(char c)
30+
{
31+
return c == Path.DirectorySeparatorChar;
32+
}
33+
34+
internal static string GetFullPathInternal(string path)
35+
{
36+
// The Windows implementation trims off whitespace from the start and end of the path,
37+
// and then based on whether that trimmed path is rooted decides to use the trimmed
38+
// or original version. On Unix, a filename can be composed of entirely whitespace
39+
// characters, so we can't legimately do such trimming. As such, we just delegate
40+
// to the real GetFullPath.
41+
return Path.GetFullPath(path);
42+
}
43+
}
44+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
namespace System.IO
5+
{
6+
internal static partial class PathHelpers
7+
{
8+
// Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space.
9+
// String.WhitespaceChars will trim more aggressively than what the underlying FS does (for ex, NTFS, FAT).
10+
internal static readonly char[] TrimEndChars = { (char)0x9, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20, (char)0x85, (char)0xA0 };
11+
internal static readonly char[] TrimStartChars = { ' ' };
12+
13+
// Gets the length of the root DirectoryInfo or whatever DirectoryInfo markers
14+
// are specified for the first part of the DirectoryInfo name.
15+
//
16+
internal static int GetRootLength(String path)
17+
{
18+
CheckInvalidPathChars(path);
19+
20+
int i = 0;
21+
int length = path.Length;
22+
23+
if (length >= 1 && (IsDirectorySeparator(path[0])))
24+
{
25+
// handles UNC names and directories off current drive's root.
26+
i = 1;
27+
if (length >= 2 && (IsDirectorySeparator(path[1])))
28+
{
29+
i = 2;
30+
int n = 2;
31+
while (i < length && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
32+
}
33+
}
34+
else if (length >= 2 && path[1] == Path.VolumeSeparatorChar)
35+
{
36+
// handles A:\foo.
37+
i = 2;
38+
if (length >= 3 && (IsDirectorySeparator(path[2]))) i++;
39+
}
40+
return i;
41+
}
42+
43+
// ".." can only be used if it is specified as a part of a valid File/Directory name. We disallow
44+
// the user being able to use it to move up directories. Here are some examples eg
45+
// Valid: a..b abc..d
46+
// Invalid: ..ab ab.. .. abc..d\abc..
47+
//
48+
internal static void CheckSearchPattern(String searchPattern)
49+
{
50+
for (int index = 0; (index = searchPattern.IndexOf("..", index, StringComparison.Ordinal)) != -1; index += 2)
51+
{
52+
// Terminal ".." or "..\". File and directory names cannot end in "..".
53+
if (index + 2 == searchPattern.Length ||
54+
IsDirectorySeparator(searchPattern[index + 2]))
55+
{
56+
throw new ArgumentException(SR.Arg_InvalidSearchPattern, "searchPattern");
57+
}
58+
}
59+
}
60+
61+
internal static bool IsDirectorySeparator(char c)
62+
{
63+
return (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar);
64+
}
65+
66+
internal static string GetFullPathInternal(string path)
67+
{
68+
if (path == null)
69+
throw new ArgumentNullException("path");
70+
71+
string pathTrimmed = path.TrimStart(TrimStartChars).TrimEnd(TrimEndChars);
72+
73+
return Path.GetFullPath(Path.IsPathRooted(pathTrimmed) ? pathTrimmed : path);
74+
}
75+
76+
// this is a lightweight version of GetDirectoryName that doesn't renormalize
77+
internal static string GetDirectoryNameInternal(string path)
78+
{
79+
string directory, file;
80+
SplitDirectoryFile(path, out directory, out file);
81+
82+
// file is null when we reach the root
83+
return (file == null) ? null : directory;
84+
}
85+
86+
internal static void SplitDirectoryFile(string path, out string directory, out string file)
87+
{
88+
directory = null;
89+
file = null;
90+
91+
// assumes a validated full path
92+
if (path != null)
93+
{
94+
int length = path.Length;
95+
int rootLength = GetRootLength(path);
96+
97+
// ignore a trailing slash
98+
if (length > rootLength && EndsInDirectorySeparator(path))
99+
length--;
100+
101+
// find the pivot index between end of string and root
102+
for (int pivot = length - 1; pivot >= rootLength; pivot--)
103+
{
104+
if (IsDirectorySeparator(path[pivot]))
105+
{
106+
directory = path.Substring(0, pivot);
107+
file = path.Substring(pivot + 1, length - pivot - 1);
108+
return;
109+
}
110+
}
111+
112+
// no pivot, return just the trimmed directory
113+
directory = path.Substring(0, length);
114+
}
115+
116+
}
117+
118+
}
119+
}
Lines changed: 3 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,19 @@
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;
5-
using System.Text;
6-
using System.Diagnostics.Contracts;
7-
84
namespace System.IO
95
{
10-
// Many of the helper methods in System.IO.Path are internal to mscorlib.
11-
//
12-
// These members are taken from src\NDP\clr\src\BCL\System\IO\Path.cs
13-
// where an appropriate public member to port to does not exist, or has
14-
// different behavior we cannot use for application compat.
15-
internal static class PathHelpers
6+
// Helper methods related to paths. Some of these are copies of
7+
// internal members of System.IO.Path from System.Runtime.Extensions.dll.
8+
internal static partial class PathHelpers
169
{
17-
// Trim trailing white spaces, tabs etc but don't be aggressive in removing everything that has UnicodeCategory of trailing space.
18-
// String.WhitespaceChars will trim more aggressively than what the underlying FS does (for ex, NTFS, FAT).
19-
internal static readonly char[] TrimEndChars = { (char)0x9, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20, (char)0x85, (char)0xA0 };
20-
internal static readonly char[] TrimStartChars = { ' ' };
21-
2210
// Array of the separator chars
2311
internal static readonly char[] DirectorySeparatorChars = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
2412

2513
// String-representation of the directory-separator character, used when appending the character to another
2614
// string so as to avoid the boxing of the character when calling String.Concat(..., object).
2715
internal static readonly string DirectorySeparatorCharAsString = Path.DirectorySeparatorChar.ToString();
2816

29-
// Gets the length of the root DirectoryInfo or whatever DirectoryInfo markers
30-
// are specified for the first part of the DirectoryInfo name.
31-
//
32-
internal static int GetRootLength(String path)
33-
{
34-
CheckInvalidPathChars(path);
35-
36-
int i = 0;
37-
int length = path.Length;
38-
39-
if (length >= 1 && (IsDirectorySeparator(path[0])))
40-
{
41-
// handles UNC names and directories off current drive's root.
42-
i = 1;
43-
if (length >= 2 && (IsDirectorySeparator(path[1])))
44-
{
45-
i = 2;
46-
int n = 2;
47-
while (i < length && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
48-
}
49-
}
50-
else if (length >= 2 && path[1] == Path.VolumeSeparatorChar)
51-
{
52-
// handles A:\foo.
53-
i = 2;
54-
if (length >= 3 && (IsDirectorySeparator(path[2]))) i++;
55-
}
56-
return i;
57-
}
58-
59-
// ".." can only be used if it is specified as a part of a valid File/Directory name. We disallow
60-
// the user being able to use it to move up directories. Here are some examples eg
61-
// Valid: a..b abc..d
62-
// Invalid: ..ab ab.. .. abc..d\abc..
63-
//
64-
internal static void CheckSearchPattern(String searchPattern)
65-
{
66-
int index = 0;
67-
while ((index = searchPattern.IndexOf("..", index, StringComparison.Ordinal)) != -1)
68-
{
69-
if (index + 2 == searchPattern.Length) // Terminal ".." . Files names cannot end in ".."
70-
throw new ArgumentException(SR.Arg_InvalidSearchPattern, "searchPattern");
71-
72-
if (IsDirectorySeparator(searchPattern[index + 2]))
73-
throw new ArgumentException(SR.Arg_InvalidSearchPattern, "searchPattern");
74-
75-
index += 2;
76-
}
77-
}
78-
7917
internal static void CheckInvalidPathChars(String path, bool checkAdditional = false)
8018
{
8119
if (path == null)
@@ -99,72 +37,9 @@ internal static void ThrowIfEmptyOrRootedPath(string path2)
9937
throw new ArgumentException(SR.Arg_Path2IsRooted, "path2");
10038
}
10139

102-
internal static bool IsDirectorySeparator(char c)
103-
{
104-
return (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar);
105-
}
106-
10740
internal static bool EndsInDirectorySeparator(String path)
10841
{
10942
return path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
11043
}
111-
112-
internal static string GetFullPathInternal(string path)
113-
{
114-
if (path == null)
115-
throw new ArgumentNullException("path");
116-
117-
string pathTrimmed = path.TrimStart(TrimStartChars).TrimEnd(TrimEndChars);
118-
119-
return Path.GetFullPath(Path.IsPathRooted(pathTrimmed) ? pathTrimmed : path);
120-
}
121-
122-
// this is a lightweight version of GetDirectoryName that doesn't renormalize
123-
internal static string GetDirectoryNameInternal(string path)
124-
{
125-
string directory, file;
126-
SplitDirectoryFile(path, out directory, out file);
127-
128-
// file is null when we reach the root
129-
return (file == null) ? null : directory;
130-
}
131-
132-
internal static void SplitDirectoryFile(string path, out string directory, out string file)
133-
{
134-
directory = null;
135-
file = null;
136-
137-
// assumes a validated full path
138-
if (path != null)
139-
{
140-
int length = path.Length;
141-
int rootLength = GetRootLength(path);
142-
143-
// ignore a trailing slash
144-
if (length > rootLength && EndsInDirectorySeparator(path))
145-
length--;
146-
147-
// find the pivot index between end of string and root
148-
for (int pivot = length - 1; pivot >= rootLength; pivot--)
149-
{
150-
if (IsDirectorySeparator(path[pivot]))
151-
{
152-
directory = path.Substring(0, pivot);
153-
file = path.Substring(pivot + 1, length - pivot - 1);
154-
return;
155-
}
156-
}
157-
158-
// no pivot, return just the trimmed directory
159-
directory = path.Substring(0, length);
160-
}
161-
162-
return;
163-
}
164-
165-
internal static StringComparison GetComparison(bool caseSensitive)
166-
{
167-
return caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
168-
}
16944
}
17045
}

src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,7 @@ internal PathPair(string userPath, string fullPath)
538538

539539
private static string NormalizeSearchPattern(string searchPattern)
540540
{
541-
searchPattern = searchPattern.TrimEnd(PathHelpers.TrimEndChars);
542-
if (searchPattern.Equals(".") || searchPattern == "*.*")
541+
if (searchPattern == "." || searchPattern == "*.*")
543542
{
544543
searchPattern = "*";
545544
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ public static void CreateDirectory_DotDotAsPath_WhenCurrentDirectoryIsRoot_DoesN
324324
#endif
325325

326326
[Fact]
327+
[PlatformSpecific(PlatformID.Windows)] // whitespace in names is significant on Unix
327328
public static void CreateDirectory_NonSignificantTrailingWhiteSpace_TreatsAsNonSignificant()
328329
{
329330
using (TemporaryDirectory directory = new TemporaryDirectory())

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public static void Exists_NonSignificantWhiteSpaceAsPath_ReturnsFalse()
9292
}
9393
}
9494

95-
95+
[PlatformSpecific(PlatformID.Windows)] // trailing whitespace is significant in filenames
9696
[Fact]
9797
public static void Exists_ExistingDirectoryWithNonSignificantTrailingWhiteSpaceAsPath_ReturnsTrue()
9898
{

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,10 @@ public void WindowsFileNameWithSpaces()
184184

185185
[Fact]
186186
[PlatformSpecific(PlatformID.AnyUnix)]
187-
[ActiveIssue(2205)]
188187
public void UnixFileNameWithSpaces()
189188
{
190189
String testDirPath = Path.Combine(TestDirectory, GetTestFileName());
190+
Directory.CreateDirectory(testDirPath);
191191
using (File.Create(Path.Combine(testDirPath, " ")))
192192
using (File.Create(Path.Combine(testDirPath, " ")))
193193
using (File.Create(Path.Combine(testDirPath, "\n")))
@@ -201,7 +201,6 @@ public void UnixFileNameWithSpaces()
201201

202202
[Fact]
203203
[PlatformSpecific(PlatformID.AnyUnix)]
204-
[ActiveIssue(2205)]
205204
public void UnixDirectoryNameWithSpaces()
206205
{
207206
String testDirPath = Path.Combine(TestDirectory, GetTestFileName());
@@ -214,6 +213,8 @@ public void UnixDirectoryNameWithSpaces()
214213
Assert.Contains(Path.Combine(testDirPath, " "), results);
215214
Assert.Contains(Path.Combine(testDirPath, " "), results);
216215
Assert.Contains(Path.Combine(testDirPath, "\n"), results);
216+
217+
testDir.Delete(true);
217218
}
218219
#endregion
219220
}

0 commit comments

Comments
 (0)