Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
654d046
Initial plan
Copilot Oct 23, 2025
5e3bf96
Fix Directory.GetFiles() to not return file paths with trailing spaces
Copilot Oct 23, 2025
2a5b37c
Merge branch 'main' into copilot/fix-directory-getfiles-space-issue
jkotas Oct 23, 2025
e56dc76
Improve fix to preserve trailing separators while removing trailing s…
Copilot Oct 24, 2025
63f7b83
Revert to original simple fix and update test to match issue scenario
Copilot Oct 24, 2025
908c56c
Update src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/…
jkotas Oct 24, 2025
a9c933b
Apply suggestions from code review
jkotas Oct 24, 2025
f8ffe2a
Update src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/…
jkotas Oct 24, 2025
5457f04
Move fix to FileSystemEnumerableFactory and add OriginalRootDirectory…
Copilot Oct 24, 2025
e652606
Also trim trailing periods along with trailing spaces
Copilot Oct 24, 2025
9857edb
Update src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/…
jkotas Oct 24, 2025
5e3dbba
Update src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/…
jkotas Oct 24, 2025
dc2eeea
Delete OriginalRootDirectoryPreservesTrailingSpaces test
Copilot Oct 24, 2025
2bf80fc
Use WindowsTrailingProblematicFileNames for OriginalRootDirectory test
Copilot Oct 24, 2025
e66a19e
Make EnumerateDirectoryWithTrailingSpacePeriod Windows-specific and s…
Copilot Oct 25, 2025
89b835a
Fix OriginalRootDirectoryPreservesInput test to use trailing separato…
Copilot Oct 25, 2025
014ce4b
Skip trimming for extended path syntax (\\?\ and \\.\)
Copilot Oct 25, 2025
ac9a252
Enable EnumerateDirectoryWithTrailingSpacePeriod test on all platforms
Copilot Oct 25, 2025
5eb1705
Fix build error and make test Windows-specific
Copilot Oct 25, 2025
4e02f44
Make trailing space/period trimming Windows-specific with #if WINDOWS
Copilot Oct 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,54 @@ internal static bool NormalizeInputs(ref string directory, ref string expression
if (directory.Contains('\0'))
throw new ArgumentException(SR.Argument_NullCharInPath, directory);

#if WINDOWS
// Trim trailing whitespace and periods from directory path, but preserve directory separators.
// Windows normalizes trailing spaces and periods away when resolving paths, but if we don't
// trim them here, the returned file paths will contain trailing spaces/periods which causes
// issues with File.Exists and other file operations.
//
// Examples:
// "C:\test " → "C:\test" (remove trailing space)
// "C:\test." → "C:\test" (remove trailing period)
// "C:\test\ " → "C:\test\" (preserve separator, remove space)
// "C:\test\. " → "C:\test\" (preserve separator, remove period and space)
// "C:\test\\ " → "C:\test\\" (preserve separators, remove space)
//
// Special cases we don't trim:
// "." → "." (relative path reference)
// ".." → ".." (parent directory reference)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot How about 3 or more dots like "..."? Is that case handled correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, "..." is handled correctly. The code uses TrimEnd(' ', '.') which would trim all trailing periods and spaces, resulting in an empty string. However, we check if (trimmed.Length > 0) before applying the trim, so paths like "...", "..", and "." are not modified - they remain as-is. This preserves special directory references while still fixing the issue. Commit: 5eb1705

// " " → " " (only spaces - would result in empty)
// "\\?\C:\test." → "\\?\C:\test." (extended path syntax - no normalization)
//
// Algorithm: Trim trailing spaces/periods, but only if:
// 1. Result is non-empty
// 2. Path does not use extended syntax (\\?\ or \\.\)

// Don't trim paths using extended syntax (\\?\ or \\.\) as they explicitly disable normalization
// Check for extended path syntax on Windows
// Extended paths are paths like \\?\C:\ or \\.\device
// While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths.
// Skipping of normalization will *only* occur if back slashes ('\') are used.
ReadOnlySpan<char> path = directory.AsSpan();
const int DevicePrefixLength = 4;
bool isExtended = path.Length >= DevicePrefixLength
&& path[0] == '\\'
&& (path[1] == '\\' || path[1] == '?')
&& path[2] == '?'
&& path[3] == '\\';

if (!isExtended)
{
string trimmed = directory.TrimEnd(' ', '.');

// Only apply the trim if it results in a non-empty string
if (trimmed.Length > 0)
{
directory = trimmed;
}
}
#endif

// We always allowed breaking the passed ref directory and filter to be separated
// any way the user wanted. Looking for "C:\foo\*.cs" could be passed as "C:\" and
// "foo\*.cs" or "C:\foo" and "*.cs", for example. As such we need to combine and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,16 @@ public void WindowsEnumerateFilesWithTrailingSpacePeriod(string fileName)
}

[Theory]
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
[PlatformSpecific(TestPlatforms.Windows)]
[ActiveIssue("https://github.com/dotnet/runtime/issues/113120")]
public void WindowsEnumerateDirectoryWithTrailingSpacePeriod(string dirName)
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
public void EnumerateDirectoryWithTrailingSpacePeriod(string dirName)
{
DirectoryInfo parentDir = Directory.CreateDirectory(GetTestFilePath());
string problematicDirPath = Path.Combine(parentDir.FullName, dirName);
Directory.CreateDirectory(@"\\?\" + problematicDirPath);
Directory.CreateDirectory(problematicDirPath);

string normalFileName = "normalfile.txt";
string filePath = Path.Combine(problematicDirPath, normalFileName);
string filePath = Path.Combine(Path.GetFullPath(problematicDirPath), normalFileName);
File.Create(filePath).Dispose();

string[] files = GetEntries(problematicDirPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ protected override bool ShouldRecurseIntoEntry(ref FileSystemEntry entry)
}
}

private class OriginalRootDirectoryEnumerator : FileSystemEnumerator<string>
{
public string CapturedOriginalRootDirectory { get; private set; }

public OriginalRootDirectoryEnumerator(string directory, EnumerationOptions options)
: base(directory, options)
{
}

protected override bool ShouldIncludeEntry(ref FileSystemEntry entry) => true;

protected override string TransformEntry(ref FileSystemEntry entry)
{
CapturedOriginalRootDirectory = new string(entry.OriginalRootDirectory);
return entry.ToFullPath();
}
}

[Fact]
[SkipOnPlatform(TestPlatforms.Android, "Test could not work on android since accessing '/' isn't allowed.")]
public void CanRecurseFromRoot()
Expand All @@ -55,5 +73,43 @@ public void CanRecurseFromRoot()
Assert.NotNull(recursed.LastDirectory);
}
}

[Theory]
[InlineData("/")]
[InlineData("//")]
[InlineData("///")]
public void OriginalRootDirectoryPreservesInput(string trailingSeparators)
{
// OriginalRootDirectory should preserve the exact input path provided by the user,
// including trailing directory separators. This is important for backward compatibility with
// code that relies on the exact format of the original path when using FileSystemEnumerator directly.
// Note: This tests direct FileSystemEnumerator usage, not Directory.GetFiles which goes through
// NormalizeInputs and trims trailing spaces/periods.

DirectoryInfo testDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
try
{
// Create a test file
string testFile = Path.Combine(testDir.FullName, "test.txt");
File.WriteAllText(testFile, "test");

string pathWithTrailingSeparators = testDir.FullName + trailingSeparators;

using (var enumerator = new OriginalRootDirectoryEnumerator(
pathWithTrailingSeparators,
new EnumerationOptions { RecurseSubdirectories = false }))
{
if (enumerator.MoveNext())
{
// OriginalRootDirectory should match the input path exactly
Assert.Equal(pathWithTrailingSeparators, enumerator.CapturedOriginalRootDirectory);
}
}
}
finally
{
testDir.Delete(true);
}
}
}
}
Loading