diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/GetFiles.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/GetFiles.cs index 81bc7085a82489..279fb28a558256 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/GetFiles.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/Directory/GetFiles.cs @@ -192,5 +192,55 @@ public void DirectoryWithTrailingSeparators(string trailing) string[] files = GetEntries(root + (IsDirectoryInfo ? trailing : ""), "*", SearchOption.AllDirectories); FSAssert.EqualWhenOrdered(new string[] { rootFile, nestedFile }, files); } + + [Theory] + [MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))] + public void EnumerateFilesWithProblematicNames(string fileName) + { + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + File.Create(Path.Combine(testDir.FullName, fileName)).Dispose(); + + string[] files = GetEntries(testDir.FullName); + Assert.Single(files); + Assert.Contains(files, f => Path.GetFileName(f) == fileName); + } + + [Theory] + [MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))] + [PlatformSpecific(TestPlatforms.Windows)] + public void WindowsEnumerateFilesWithTrailingSpacePeriod(string fileName) + { + // Files with trailing spaces/periods must be created with \\?\ on Windows + // but enumeration can find them. + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string filePath = Path.Combine(testDir.FullName, fileName); + File.Create(@"\\?\" + filePath).Dispose(); + + string[] files = GetEntries(testDir.FullName); + Assert.Single(files); + Assert.Contains(files, f => Path.GetFileName(f) == 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) + { + DirectoryInfo parentDir = Directory.CreateDirectory(GetTestFilePath()); + string problematicDirPath = Path.Combine(parentDir.FullName, dirName); + Directory.CreateDirectory(@"\\?\" + problematicDirPath); + + string normalFileName = "normalfile.txt"; + string filePath = Path.Combine(problematicDirPath, normalFileName); + File.Create(filePath).Dispose(); + + string[] files = GetEntries(problematicDirPath); + Assert.Single(files); + + string returnedPath = files[0]; + Assert.True(File.Exists(returnedPath), + $"File.Exists should work on path returned by Directory.GetFiles. Path: '{returnedPath}'"); + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Copy.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Copy.cs index 7abc7ac3a286e2..99fd855ded6284 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Copy.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Copy.cs @@ -422,6 +422,43 @@ public void DestinationFileIsTruncatedWhenItsLargerThanSourceFile() Assert.Equal(content, File.ReadAllBytes(destPath)); } + + [Theory] + [MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))] + public void CopyWithProblematicNames(string fileName) + { + DirectoryInfo sourceDir = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo destDir = Directory.CreateDirectory(GetTestFilePath()); + string sourcePath = Path.Combine(sourceDir.FullName, fileName); + string destPath = Path.Combine(destDir.FullName, fileName); + + File.Create(sourcePath).Dispose(); + Copy(sourcePath, destPath); + + Assert.True(File.Exists(sourcePath)); + Assert.True(File.Exists(destPath)); + } + + [Theory] + [MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))] + [PlatformSpecific(TestPlatforms.Windows)] + public void WindowsCopyWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName) + { + // Windows path normalization strips trailing spaces/periods unless using \\?\ extended syntax. + DirectoryInfo sourceDir = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo destDir = Directory.CreateDirectory(GetTestFilePath()); + string sourcePath = Path.Combine(sourceDir.FullName, fileName); + string destPath = Path.Combine(destDir.FullName, fileName); + + // Create source with extended syntax (required for trailing spaces/periods) + File.Create(@"\\?\" + sourcePath).Dispose(); + + // Copy to destination with extended syntax (required for trailing spaces/periods) + Copy(@"\\?\" + sourcePath, @"\\?\" + destPath); + + Assert.True(File.Exists(@"\\?\" + sourcePath)); + Assert.True(File.Exists(@"\\?\" + destPath)); + } } /// diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Create.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Create.cs index 8b3daf0ca18bd3..2eb894d5881bfc 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Create.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Create.cs @@ -341,6 +341,38 @@ public void WindowsAlternateDataStream_OnExisting(string streamName) } } + [Theory] + [MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))] + public void CreateWithProblematicNames(string fileName) + { + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string filePath = Path.Combine(testDir.FullName, fileName); + using (Create(filePath)) + { + Assert.True(File.Exists(filePath)); + } + } + + [Theory] + [MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))] + [PlatformSpecific(TestPlatforms.Windows)] + public void WindowsCreateWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName) + { + // Windows path normalization strips trailing spaces/periods unless using \\?\ extended syntax. + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string filePath = Path.Combine(testDir.FullName, fileName); + string extendedPath = @"\\?\" + filePath; + + using (Create(extendedPath)) + { + Assert.True(File.Exists(extendedPath)); + } + + // Verify the file can be found via enumeration + string[] files = Directory.GetFiles(testDir.FullName); + Assert.Contains(files, f => Path.GetFileName(f) == fileName); + } + #endregion } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Delete.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Delete.cs index 59b381e4b06d65..4e1e642f74f46e 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Delete.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Delete.cs @@ -201,6 +201,35 @@ public void WindowsDeleteAlternateDataStream(string streamName) Assert.True(testFile.Exists); } + [Theory] + [MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))] + public void DeleteWithProblematicNames(string fileName) + { + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string filePath = Path.Combine(testDir.FullName, fileName); + File.Create(filePath).Dispose(); + Assert.True(File.Exists(filePath)); + Delete(filePath); + Assert.False(File.Exists(filePath)); + } + + [Theory] + [MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))] + [PlatformSpecific(TestPlatforms.Windows)] + public void WindowsDeleteWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName) + { + // Files with trailing spaces/periods require \\?\ syntax on Windows + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string filePath = Path.Combine(testDir.FullName, fileName); + string extendedPath = @"\\?\" + filePath; + + File.Create(extendedPath).Dispose(); + Assert.True(File.Exists(extendedPath)); + + Delete(extendedPath); + Assert.False(File.Exists(extendedPath)); + } + #endregion } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Exists.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Exists.cs index 12a6958dd9095e..4efca14a76d1b8 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Exists.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Exists.cs @@ -230,6 +230,33 @@ public void DirectoryWithComponentLongerThanMaxComponentAsPath_ReturnsFalse(stri Assert.False(Exists(component)); } + [Theory] + [MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))] + public void ExistsWithProblematicNames(string fileName) + { + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string filePath = Path.Combine(testDir.FullName, fileName); + File.Create(filePath).Dispose(); + Assert.True(Exists(filePath)); + } + + [Theory] + [MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))] + [PlatformSpecific(TestPlatforms.Windows)] + public void WindowsExistsWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName) + { + // Files with trailing spaces/periods require \\?\ syntax on Windows + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string filePath = Path.Combine(testDir.FullName, fileName); + string extendedPath = @"\\?\" + filePath; + + File.Create(extendedPath).Dispose(); + Assert.True(Exists(extendedPath)); + + // Without extended syntax, the trailing space/period is trimmed + Assert.False(Exists(filePath)); + } + #endregion } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Move.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Move.cs index 2ce25e9376cadb..09fa0c178bd03c 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Move.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/Move.cs @@ -394,5 +394,41 @@ public void MoveOntoExistingFileNoOverwrite() Assert.True(File.Exists(destPath)); Assert.Equal(destContents, File.ReadAllBytes(destPath)); } + + [Theory] + [MemberData(nameof(TestData.ValidFileNames), MemberType = typeof(TestData))] + public void MoveWithProblematicNames(string fileName) + { + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string srcPath = Path.Combine(testDir.FullName, fileName); + string destPath = Path.Combine(testDir.FullName, fileName + "_moved"); + + File.Create(srcPath).Dispose(); + Move(srcPath, destPath); + + Assert.False(File.Exists(srcPath)); + Assert.True(File.Exists(destPath)); + } + + [Theory] + [MemberData(nameof(TestData.WindowsTrailingProblematicFileNames), MemberType = typeof(TestData))] + [PlatformSpecific(TestPlatforms.Windows)] + public void WindowsMoveWithTrailingSpacePeriod_ViaExtendedSyntax(string fileName) + { + // Windows path normalization strips trailing spaces/periods unless using \\?\ extended syntax. + DirectoryInfo sourceDir = Directory.CreateDirectory(GetTestFilePath()); + DirectoryInfo destDir = Directory.CreateDirectory(GetTestFilePath()); + string sourcePath = Path.Combine(sourceDir.FullName, fileName); + string destPath = Path.Combine(destDir.FullName, fileName); + + // Create source with extended syntax (required for trailing spaces/periods) + File.Create(@"\\?\" + sourcePath).Dispose(); + + // Move to destination with extended syntax (required for trailing spaces/periods) + Move(@"\\?\" + sourcePath, @"\\?\" + destPath); + + Assert.False(File.Exists(@"\\?\" + sourcePath)); + Assert.True(File.Exists(@"\\?\" + destPath)); + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/TestData.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/TestData.cs index 0451d386cbfcde..f481a0a143cfac 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/TestData.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/TestData.cs @@ -91,4 +91,74 @@ public static TheoryData TrailingCharacters return data; } } + + /// + /// Filenames with problematic but valid characters that work on all platforms. + /// These test scenarios from: https://www.dwheeler.com/essays/fixing-unix-linux-filenames.html + /// + public static TheoryData ValidFileNames + { + get + { + TheoryData data = new TheoryData + { + // Leading spaces + " leading", + " leading", + " leading", + // Leading dots + ".leading", + "..leading", + "...leading", + // Dash-prefixed names + "-", + "--", + "-filename", + "--filename", + // Embedded spaces and periods + "name with spaces", + "name with multiple spaces", + "name.with.periods", + "name with spaces.txt" + }; + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // On Unix, control characters are also valid in filenames + data.Add("file\tname"); // tab + data.Add("file\rname"); // carriage return + data.Add("file\vname"); // vertical tab + data.Add("file\fname"); // form feed + // Trailing spaces and periods are also valid on Unix (but problematic on Windows) + data.Add("trailing "); + data.Add("trailing "); + data.Add("trailing."); + data.Add("trailing.."); + data.Add("trailing ."); + data.Add("trailing. "); + } + + return data; + } + } + + /// + /// Filenames with trailing spaces or periods. On Windows, these require \\?\ prefix for creation + /// but can be enumerated. Direct string-based APIs will have the trailing characters stripped. + /// + public static TheoryData WindowsTrailingProblematicFileNames + { + get + { + return new TheoryData + { + "trailing ", + "trailing ", + "trailing.", + "trailing..", + "trailing .", + "trailing. " + }; + } + } }