diff --git a/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs b/src/TestableIO.System.IO.Abstractions.TestingHelpers/PathVerifier.cs index b56aa674c..2c426637c 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; @@ -15,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. /// @@ -64,6 +69,12 @@ public void IsLegalAbsoluteOrRelative(string path, string paramName) private static bool IsValidUseOfVolumeSeparatorChar(string path) { + if (XFS.IsWindowsPlatform() && path.StartsWith(WINDOWS_EXTENDED_LENGTH_PATH_PREFIX)) + { + // Skip over the `\\?\` prefix if there is one. + path = path.Substring(WINDOWS_EXTENDED_LENGTH_PATH_PREFIX.Length); + } + var lastVolSepIndex = path.LastIndexOf(Path.VolumeSeparatorChar); return lastVolSepIndex == -1 || lastVolSepIndex == 1 && char.IsLetter(path[0]); } @@ -97,6 +108,14 @@ 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. + if (XFS.IsWindowsPlatform() && path.StartsWith(WINDOWS_EXTENDED_LENGTH_PATH_PREFIX)) + { + path = path.Substring(WINDOWS_EXTENDED_LENGTH_PATH_PREFIX.Length); + } + return path.IndexOfAny(invalidPathChars.Concat(AdditionalInvalidPathChars).ToArray()) >= 0; } @@ -164,4 +183,4 @@ public bool TryNormalizeDriveName(string name, out string result) result = name; return true; } -} \ No newline at end of file +} diff --git a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs index fae8b65a3..79b531edc 100644 --- a/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs +++ b/tests/TestableIO.System.IO.Abstractions.TestingHelpers.Tests/MockDirectoryTests.cs @@ -747,6 +747,25 @@ 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 + 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 [Test] public async Task MockDirectory_CreateDirectory_ShouldIgnoreExistingDirectoryRegardlessOfTrailingSlash() @@ -2219,4 +2238,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 +}