From 87630ba56c208c7af97df2a1158beea0564974ca Mon Sep 17 00:00:00 2001 From: Mostyn Bramley-Moore Date: Wed, 2 Jul 2025 01:01:57 +0200 Subject: [PATCH 1/5] Test that MockDirectory.CreateDirectory supports windows extended-length paths Windows paths can have a `\\?\` prefix, the '?' character is not always invalid. Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry Refers to #1304 --- .../MockDirectoryTests.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs index fae8b65a3..d6f4876b8 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs @@ -747,6 +747,20 @@ public async Task MockDirectory_CreateDirectory_ShouldThrowIfIllegalCharacterInP await That(createDelegate).Throws(); } + [Test] + [WindowsOnly(WindowsSpecifics.UNCPaths)] + public async Task MockDirectory_CreateDirectory_ShouldSupportExtendedLengthPaths() + { + // Arrange + var fileSystem = new MockFileSystem(); + + // Act + fileSystem.Directory.CreateDirectory(XFS.Path(@"\\?\c:\bar")); + + // Assert + await That(fileSystem.Directory.Exists(XFS.Path(@"\\?\c:\bar"))).IsTrue(); + } + // Issue #210 [Test] public async Task MockDirectory_CreateDirectory_ShouldIgnoreExistingDirectoryRegardlessOfTrailingSlash() @@ -2219,4 +2233,4 @@ public static void MockDirectory_Move_ShouldNotThrowException_InWindows_When_Sou // Act & Assert Assert.DoesNotThrow(() => mockFs.Directory.Move(src, dest)); } -} \ No newline at end of file +} From b9d59a2579bf533ec7099a799f0bf16d91e37238 Mon Sep 17 00:00:00 2001 From: Mostyn Bramley-Moore Date: Fri, 4 Jul 2025 13:59:43 +0200 Subject: [PATCH 2/5] Make MockDirectory_CreateDirectory_ShouldSupportExtendedLengthPaths pass Fixes #1304 --- .../PathVerifier.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs index b56aa674c..93ad644e0 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; namespace System.IO.Abstractions.TestingHelpers; @@ -97,6 +98,16 @@ public bool HasIllegalCharacters(string path, bool checkAdditional) if (checkAdditional) { + // AdditionalInvalidPathChars includes '?', but this character is allowed in extended-length + // windows path prefixes (`\\?\`). If we're dealing with such a path, check for invalid + // characters after the prefix. + // Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry + const string EXTENDED_LENGTH_PATH_PREFIX = @"\\?\"; + if (XFS.IsWindowsPlatform() && path.StartsWith(EXTENDED_LENGTH_PATH_PREFIX)) + { + path = path.Substring(EXTENDED_LENGTH_PATH_PREFIX.Length); + } + return path.IndexOfAny(invalidPathChars.Concat(AdditionalInvalidPathChars).ToArray()) >= 0; } @@ -164,4 +175,4 @@ public bool TryNormalizeDriveName(string name, out string result) result = name; return true; } -} \ No newline at end of file +} From 264f1e020225068e5c94b5a2fc87df64c9dc704d Mon Sep 17 00:00:00 2001 From: Mostyn Bramley-Moore Date: Sun, 6 Jul 2025 19:14:48 +0200 Subject: [PATCH 3/5] Also test that MockDirectory.CreateDirectory with an extended path returns a DirectoryInfo with an extended path --- .../MockDirectoryTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs index d6f4876b8..2e41cc79a 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs @@ -755,10 +755,11 @@ public async Task MockDirectory_CreateDirectory_ShouldSupportExtendedLengthPaths var fileSystem = new MockFileSystem(); // Act - fileSystem.Directory.CreateDirectory(XFS.Path(@"\\?\c:\bar")); + var directoryInfo = fileSystem.Directory.CreateDirectory(XFS.Path(@"\\?\c:\bar")); // Assert await That(fileSystem.Directory.Exists(XFS.Path(@"\\?\c:\bar"))).IsTrue(); + await That(directoryInfo.FullName).IsEqualTo(@"\\?\c:\bar"); } // Issue #210 From 0918171f91fb85fd3ea9c7e2a7e52096ee4d1c0f Mon Sep 17 00:00:00 2001 From: Mostyn Bramley-Moore Date: Sun, 6 Jul 2025 20:06:08 +0200 Subject: [PATCH 4/5] Make MockFileSystem's File.WriteAllText, File.ReadAllText and Directory.GetFiles support extended length paths too --- .../PathVerifier.cs | 6 ++++++ .../MockDirectoryTests.cs | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs index 93ad644e0..0b6b8e584 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs @@ -65,6 +65,12 @@ public void IsLegalAbsoluteOrRelative(string path, string paramName) private static bool IsValidUseOfVolumeSeparatorChar(string path) { + const string EXTENDED_LENGTH_PATH_PREFIX = @"\\?\"; + if (path.StartsWith(EXTENDED_LENGTH_PATH_PREFIX)) + { + path = path.Substring(EXTENDED_LENGTH_PATH_PREFIX.Length); + } + var lastVolSepIndex = path.LastIndexOf(Path.VolumeSeparatorChar); return lastVolSepIndex == -1 || lastVolSepIndex == 1 && char.IsLetter(path[0]); } diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs index 2e41cc79a..79b531edc 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs @@ -756,10 +756,14 @@ public async Task MockDirectory_CreateDirectory_ShouldSupportExtendedLengthPaths // Act var directoryInfo = fileSystem.Directory.CreateDirectory(XFS.Path(@"\\?\c:\bar")); + fileSystem.File.WriteAllText(@"\\?\c:\bar\grok.txt", "hello world\n"); // Assert await That(fileSystem.Directory.Exists(XFS.Path(@"\\?\c:\bar"))).IsTrue(); await That(directoryInfo.FullName).IsEqualTo(@"\\?\c:\bar"); + await That(fileSystem.File.ReadAllText(@"\\?\c:\bar\grok.txt")).IsEqualTo("hello world\n"); + await That(fileSystem.Directory.GetFiles(@"\\?\c:\bar")).HasSingle() + .Which.IsEqualTo(@"\\?\c:\bar\grok.txt"); } // Issue #210 From 01e274a0a7bbb5ef3c40b8555269624f537fc02e Mon Sep 17 00:00:00 2001 From: Mostyn Bramley-Moore Date: Tue, 8 Jul 2025 10:55:26 +0200 Subject: [PATCH 5/5] Refactor a little --- .../PathVerifier.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs index 0b6b8e584..2c426637c 100644 --- a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs +++ b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs @@ -16,6 +16,10 @@ public class PathVerifier private static readonly char[] AdditionalInvalidPathChars = { '*', '?' }; private readonly IMockFileDataAccessor _mockFileDataAccessor; + // Windows supports extended-length paths with a `\\?\` prefix, to work around low path length limits. + // Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry + private const string WINDOWS_EXTENDED_LENGTH_PATH_PREFIX = @"\\?\"; + /// /// Creates a new verifier instance. /// @@ -65,10 +69,10 @@ public void IsLegalAbsoluteOrRelative(string path, string paramName) private static bool IsValidUseOfVolumeSeparatorChar(string path) { - const string EXTENDED_LENGTH_PATH_PREFIX = @"\\?\"; - if (path.StartsWith(EXTENDED_LENGTH_PATH_PREFIX)) + if (XFS.IsWindowsPlatform() && path.StartsWith(WINDOWS_EXTENDED_LENGTH_PATH_PREFIX)) { - path = path.Substring(EXTENDED_LENGTH_PATH_PREFIX.Length); + // Skip over the `\\?\` prefix if there is one. + path = path.Substring(WINDOWS_EXTENDED_LENGTH_PATH_PREFIX.Length); } var lastVolSepIndex = path.LastIndexOf(Path.VolumeSeparatorChar); @@ -107,11 +111,9 @@ public bool HasIllegalCharacters(string path, bool checkAdditional) // AdditionalInvalidPathChars includes '?', but this character is allowed in extended-length // windows path prefixes (`\\?\`). If we're dealing with such a path, check for invalid // characters after the prefix. - // Ref: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry - const string EXTENDED_LENGTH_PATH_PREFIX = @"\\?\"; - if (XFS.IsWindowsPlatform() && path.StartsWith(EXTENDED_LENGTH_PATH_PREFIX)) + if (XFS.IsWindowsPlatform() && path.StartsWith(WINDOWS_EXTENDED_LENGTH_PATH_PREFIX)) { - path = path.Substring(EXTENDED_LENGTH_PATH_PREFIX.Length); + path = path.Substring(WINDOWS_EXTENDED_LENGTH_PATH_PREFIX.Length); } return path.IndexOfAny(invalidPathChars.Concat(AdditionalInvalidPathChars).ToArray()) >= 0;