Skip to content

Commit ef756da

Browse files
Copilotstephentoubjkotasjozkee
authored
Add comprehensive tests for problematic filename characters in System.IO (#120639)
Fixes #25009 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> Co-authored-by: Jan Kotas <jkotas@microsoft.com> Co-authored-by: David Cantú <dacantu@microsoft.com>
1 parent 25cae04 commit ef756da

File tree

7 files changed

+281
-0
lines changed

7 files changed

+281
-0
lines changed

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/GetFiles.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,5 +192,55 @@ public void DirectoryWithTrailingSeparators(string trailing)
192192
string[] files = GetEntries(root + (IsDirectoryInfo ? trailing : ""), "*", SearchOption.AllDirectories);
193193
FSAssert.EqualWhenOrdered(new string[] { rootFile, nestedFile }, files);
194194
}
195+
196+
[Theory]
197+
[MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))]
198+
public void EnumerateFilesWithProblematicNames(string fileName)
199+
{
200+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
201+
File.Create(Path.Combine(testDir.FullName, fileName)).Dispose();
202+
203+
string[] files = GetEntries(testDir.FullName);
204+
Assert.Single(files);
205+
Assert.Contains(files, f => Path.GetFileName(f) == fileName);
206+
}
207+
208+
[Theory]
209+
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
210+
[PlatformSpecific(TestPlatforms.Windows)]
211+
public void WindowsEnumerateFilesWithTrailingSpacePeriod(string fileName)
212+
{
213+
// Files with trailing spaces/periods must be created with \\?\ on Windows
214+
// but enumeration can find them.
215+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
216+
string filePath = Path.Combine(testDir.FullName, fileName);
217+
File.Create(@"\\?\" + filePath).Dispose();
218+
219+
string[] files = GetEntries(testDir.FullName);
220+
Assert.Single(files);
221+
Assert.Contains(files, f => Path.GetFileName(f) == fileName);
222+
}
223+
224+
[Theory]
225+
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
226+
[PlatformSpecific(TestPlatforms.Windows)]
227+
[ActiveIssue("https://github.com/dotnet/runtime/issues/113120")]
228+
public void WindowsEnumerateDirectoryWithTrailingSpacePeriod(string dirName)
229+
{
230+
DirectoryInfo parentDir = Directory.CreateDirectory(GetTestFilePath());
231+
string problematicDirPath = Path.Combine(parentDir.FullName, dirName);
232+
Directory.CreateDirectory(@"\\?\" + problematicDirPath);
233+
234+
string normalFileName = "normalfile.txt";
235+
string filePath = Path.Combine(problematicDirPath, normalFileName);
236+
File.Create(filePath).Dispose();
237+
238+
string[] files = GetEntries(problematicDirPath);
239+
Assert.Single(files);
240+
241+
string returnedPath = files[0];
242+
Assert.True(File.Exists(returnedPath),
243+
$"File.Exists should work on path returned by Directory.GetFiles. Path: '{returnedPath}'");
244+
}
195245
}
196246
}

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Copy.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,43 @@ public void DestinationFileIsTruncatedWhenItsLargerThanSourceFile()
422422

423423
Assert.Equal(content, File.ReadAllBytes(destPath));
424424
}
425+
426+
[Theory]
427+
[MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))]
428+
public void CopyWithProblematicNames(string fileName)
429+
{
430+
DirectoryInfo sourceDir = Directory.CreateDirectory(GetTestFilePath());
431+
DirectoryInfo destDir = Directory.CreateDirectory(GetTestFilePath());
432+
string sourcePath = Path.Combine(sourceDir.FullName, fileName);
433+
string destPath = Path.Combine(destDir.FullName, fileName);
434+
435+
File.Create(sourcePath).Dispose();
436+
Copy(sourcePath, destPath);
437+
438+
Assert.True(File.Exists(sourcePath));
439+
Assert.True(File.Exists(destPath));
440+
}
441+
442+
[Theory]
443+
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
444+
[PlatformSpecific(TestPlatforms.Windows)]
445+
public void WindowsCopyWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName)
446+
{
447+
// Windows path normalization strips trailing spaces/periods unless using \\?\ extended syntax.
448+
DirectoryInfo sourceDir = Directory.CreateDirectory(GetTestFilePath());
449+
DirectoryInfo destDir = Directory.CreateDirectory(GetTestFilePath());
450+
string sourcePath = Path.Combine(sourceDir.FullName, fileName);
451+
string destPath = Path.Combine(destDir.FullName, fileName);
452+
453+
// Create source with extended syntax (required for trailing spaces/periods)
454+
File.Create(@"\\?\" + sourcePath).Dispose();
455+
456+
// Copy to destination with extended syntax (required for trailing spaces/periods)
457+
Copy(@"\\?\" + sourcePath, @"\\?\" + destPath);
458+
459+
Assert.True(File.Exists(@"\\?\" + sourcePath));
460+
Assert.True(File.Exists(@"\\?\" + destPath));
461+
}
425462
}
426463

427464
/// <summary>

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Create.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,38 @@ public void WindowsAlternateDataStream_OnExisting(string streamName)
341341
}
342342
}
343343

344+
[Theory]
345+
[MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))]
346+
public void CreateWithProblematicNames(string fileName)
347+
{
348+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
349+
string filePath = Path.Combine(testDir.FullName, fileName);
350+
using (Create(filePath))
351+
{
352+
Assert.True(File.Exists(filePath));
353+
}
354+
}
355+
356+
[Theory]
357+
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
358+
[PlatformSpecific(TestPlatforms.Windows)]
359+
public void WindowsCreateWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName)
360+
{
361+
// Windows path normalization strips trailing spaces/periods unless using \\?\ extended syntax.
362+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
363+
string filePath = Path.Combine(testDir.FullName, fileName);
364+
string extendedPath = @"\\?\" + filePath;
365+
366+
using (Create(extendedPath))
367+
{
368+
Assert.True(File.Exists(extendedPath));
369+
}
370+
371+
// Verify the file can be found via enumeration
372+
string[] files = Directory.GetFiles(testDir.FullName);
373+
Assert.Contains(files, f => Path.GetFileName(f) == fileName);
374+
}
375+
344376
#endregion
345377
}
346378

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Delete.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,35 @@ public void WindowsDeleteAlternateDataStream(string streamName)
201201
Assert.True(testFile.Exists);
202202
}
203203

204+
[Theory]
205+
[MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))]
206+
public void DeleteWithProblematicNames(string fileName)
207+
{
208+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
209+
string filePath = Path.Combine(testDir.FullName, fileName);
210+
File.Create(filePath).Dispose();
211+
Assert.True(File.Exists(filePath));
212+
Delete(filePath);
213+
Assert.False(File.Exists(filePath));
214+
}
215+
216+
[Theory]
217+
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
218+
[PlatformSpecific(TestPlatforms.Windows)]
219+
public void WindowsDeleteWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName)
220+
{
221+
// Files with trailing spaces/periods require \\?\ syntax on Windows
222+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
223+
string filePath = Path.Combine(testDir.FullName, fileName);
224+
string extendedPath = @"\\?\" + filePath;
225+
226+
File.Create(extendedPath).Dispose();
227+
Assert.True(File.Exists(extendedPath));
228+
229+
Delete(extendedPath);
230+
Assert.False(File.Exists(extendedPath));
231+
}
232+
204233
#endregion
205234
}
206235
}

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Exists.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,33 @@ public void DirectoryWithComponentLongerThanMaxComponentAsPath_ReturnsFalse(stri
230230
Assert.False(Exists(component));
231231
}
232232

233+
[Theory]
234+
[MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))]
235+
public void ExistsWithProblematicNames(string fileName)
236+
{
237+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
238+
string filePath = Path.Combine(testDir.FullName, fileName);
239+
File.Create(filePath).Dispose();
240+
Assert.True(Exists(filePath));
241+
}
242+
243+
[Theory]
244+
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
245+
[PlatformSpecific(TestPlatforms.Windows)]
246+
public void WindowsExistsWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName)
247+
{
248+
// Files with trailing spaces/periods require \\?\ syntax on Windows
249+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
250+
string filePath = Path.Combine(testDir.FullName, fileName);
251+
string extendedPath = @"\\?\" + filePath;
252+
253+
File.Create(extendedPath).Dispose();
254+
Assert.True(Exists(extendedPath));
255+
256+
// Without extended syntax, the trailing space/period is trimmed
257+
Assert.False(Exists(filePath));
258+
}
259+
233260
#endregion
234261
}
235262

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Move.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,5 +394,41 @@ public void MoveOntoExistingFileNoOverwrite()
394394
Assert.True(File.Exists(destPath));
395395
Assert.Equal(destContents, File.ReadAllBytes(destPath));
396396
}
397+
398+
[Theory]
399+
[MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))]
400+
public void MoveWithProblematicNames(string fileName)
401+
{
402+
DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath());
403+
string srcPath = Path.Combine(testDir.FullName, fileName);
404+
string destPath = Path.Combine(testDir.FullName, fileName + "_moved");
405+
406+
File.Create(srcPath).Dispose();
407+
Move(srcPath, destPath);
408+
409+
Assert.False(File.Exists(srcPath));
410+
Assert.True(File.Exists(destPath));
411+
}
412+
413+
[Theory]
414+
[MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))]
415+
[PlatformSpecific(TestPlatforms.Windows)]
416+
public void WindowsMoveWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName)
417+
{
418+
// Windows path normalization strips trailing spaces/periods unless using \\?\ extended syntax.
419+
DirectoryInfo sourceDir = Directory.CreateDirectory(GetTestFilePath());
420+
DirectoryInfo destDir = Directory.CreateDirectory(GetTestFilePath());
421+
string sourcePath = Path.Combine(sourceDir.FullName, fileName);
422+
string destPath = Path.Combine(destDir.FullName, fileName);
423+
424+
// Create source with extended syntax (required for trailing spaces/periods)
425+
File.Create(@"\\?\" + sourcePath).Dispose();
426+
427+
// Move to destination with extended syntax (required for trailing spaces/periods)
428+
Move(@"\\?\" + sourcePath, @"\\?\" + destPath);
429+
430+
Assert.False(File.Exists(@"\\?\" + sourcePath));
431+
Assert.True(File.Exists(@"\\?\" + destPath));
432+
}
397433
}
398434
}

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/TestData.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,74 @@ public static TheoryData<char> TrailingCharacters
9191
return data;
9292
}
9393
}
94+
95+
/// <summary>
96+
/// Filenames with problematic but valid characters that work on all platforms.
97+
/// These test scenarios from: https://www.dwheeler.com/essays/fixing-unix-linux-filenames.html
98+
/// </summary>
99+
public static TheoryData<string> ValidFileNames
100+
{
101+
get
102+
{
103+
TheoryData<string> data = new TheoryData<string>
104+
{
105+
// Leading spaces
106+
" leading",
107+
" leading",
108+
" leading",
109+
// Leading dots
110+
".leading",
111+
"..leading",
112+
"...leading",
113+
// Dash-prefixed names
114+
"-",
115+
"--",
116+
"-filename",
117+
"--filename",
118+
// Embedded spaces and periods
119+
"name with spaces",
120+
"name with multiple spaces",
121+
"name.with.periods",
122+
"name with spaces.txt"
123+
};
124+
125+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
126+
{
127+
// On Unix, control characters are also valid in filenames
128+
data.Add("file\tname"); // tab
129+
data.Add("file\rname"); // carriage return
130+
data.Add("file\vname"); // vertical tab
131+
data.Add("file\fname"); // form feed
132+
// Trailing spaces and periods are also valid on Unix (but problematic on Windows)
133+
data.Add("trailing ");
134+
data.Add("trailing ");
135+
data.Add("trailing.");
136+
data.Add("trailing..");
137+
data.Add("trailing .");
138+
data.Add("trailing. ");
139+
}
140+
141+
return data;
142+
}
143+
}
144+
145+
/// <summary>
146+
/// Filenames with trailing spaces or periods. On Windows, these require \\?\ prefix for creation
147+
/// but can be enumerated. Direct string-based APIs will have the trailing characters stripped.
148+
/// </summary>
149+
public static TheoryData<string> WindowsTrailingProblematicFileNames
150+
{
151+
get
152+
{
153+
return new TheoryData<string>
154+
{
155+
"trailing ",
156+
"trailing ",
157+
"trailing.",
158+
"trailing..",
159+
"trailing .",
160+
"trailing. "
161+
};
162+
}
163+
}
94164
}

0 commit comments

Comments
 (0)