Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Unblock paths > MAX_PATH without extended syntax. #3001

Merged
merged 1 commit into from
Aug 28, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -14,7 +14,8 @@ internal partial class mincore

internal static bool CreateDirectory(string path, ref SECURITY_ATTRIBUTES lpSecurityAttributes)
{
path = PathInternal.AddExtendedPathPrefixForLongPaths(path);
// We always want to add for CreateDirectory to get around the legacy 248 character limitation
path = PathInternal.AddExtendedPathPrefix(path);
return CreateDirectoryPrivate(path, ref lpSecurityAttributes);
}
}
Expand Down
25 changes: 23 additions & 2 deletions src/Common/src/Interop/Windows/mincore/Interop.GetFullPathNameW.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,38 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

partial class Interop
{
partial class mincore
{
/// <summary>
/// WARNING: This overload does not implicitly handle long paths.
/// </summary>
Copy link
Member

Choose a reason for hiding this comment

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

Let's rename it then. It's strange to have two overloads so similar and yet with so different behavior.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you have any suggestions for naming? GetFullPathNameNoLongPath? This one in particular is used for the stack allocating code in PathHelper (all under MAX_PATH).

Like I mentioned, I intend to pass through these P/Invokes and normalize the naming. I want to drop the "W", use "Private" (anything better for that?), add comments on the privates to avoid leaking in other P/Invoke wrappers.

[DllImport(Libraries.CoreFile_L1, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
internal unsafe static extern int GetFullPathNameW(char* path, int numBufferChars, char* buffer, IntPtr mustBeZero);

[DllImport(Libraries.CoreFile_L1, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
internal static extern int GetFullPathNameW(string path, int numBufferChars, [Out]StringBuilder buffer, IntPtr mustBeZero);
[DllImport(Libraries.CoreFile_L1, EntryPoint = "GetFullPathNameW", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
private static extern int GetFullPathNameWPrivate(string path, int numBufferChars, [Out]StringBuilder buffer, IntPtr mustBeZero);

internal static int GetFullPathNameW(string path, int numBufferChars, [Out]StringBuilder buffer, IntPtr mustBeZero)
{
bool wasExtended = PathInternal.IsExtended(path);
if (!wasExtended)
{
path = PathInternal.AddExtendedPathPrefixForLongPaths(path);
Copy link
Member

Choose a reason for hiding this comment

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

Should we increase the size of buffer by a commensurate amount as we increased the length of path?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I'll change that. Good catch.

}
int result = GetFullPathNameWPrivate(path, buffer.Capacity, buffer, mustBeZero);

if (!wasExtended)
{
// We don't want to give back \\?\ if we possibly added it ourselves
PathInternal.RemoveExtendedPathPrefix(buffer);
}
return result;
}
}
}
25 changes: 23 additions & 2 deletions src/Common/src/Interop/Windows/mincore/Interop.GetLongPathNameW.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO;
using System.Text;
using System.Runtime.InteropServices;

partial class Interop
{
partial class mincore
{
/// <summary>
/// WARNING: This overload does not implicitly handle long paths.
/// </summary>
Copy link
Member

Choose a reason for hiding this comment

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

Ditto on using a different name. Do we expect any code other than the other overload to use these non-extending versions? Let's make them private if possible.

[DllImport(Libraries.CoreFile_L1, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
internal unsafe static extern int GetLongPathNameW(char* path, char* longPathBuffer, int bufferLength);

[DllImport(Libraries.CoreFile_L1, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
internal static extern int GetLongPathNameW(string path, [Out]StringBuilder longPathBuffer, int bufferLength);
[DllImport(Libraries.CoreFile_L1, EntryPoint = "GetLongPathNameW", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
private static extern int GetLongPathNameWPrivate(string path, [Out]StringBuilder longPathBuffer, int bufferLength);

internal static int GetLongPathNameW(string path, [Out]StringBuilder longPathBuffer, int bufferLength)
{
bool wasExtended = PathInternal.IsExtended(path);
if (!wasExtended)
{
path = PathInternal.AddExtendedPathPrefixForLongPaths(path);
Copy link
Member

Choose a reason for hiding this comment

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

In the previous review, you said we couldn't change AddExtendedPathPrefixForLongPaths to AddExtendedPathPrefixIfNecessary because it would conflict with something in this next PR. I'm not seeing it. What's the conflict?

Copy link
Member Author

Choose a reason for hiding this comment

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

CreateDirectory is the one doing it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Spoke offline. Going to use EnsureExtendedPrefix/OverMaxPath in a follow-up.

}
int result = GetLongPathNameWPrivate(path, longPathBuffer, longPathBuffer.Capacity);

if (!wasExtended)
{
// We don't want to give back \\?\ if we possibly added it ourselves
PathInternal.RemoveExtendedPathPrefix(longPathBuffer);
}
return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO;
using System.Text;
using System.Runtime.InteropServices;

Expand Down
44 changes: 24 additions & 20 deletions src/Common/src/System/IO/PathInternal.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal static partial class PathInternal
internal const string DevicePathPrefix = @"\\.\";
internal const int MaxShortPath = 260;
internal const int MaxShortDirectoryPath = 248;
internal const int MaxExtendedPath = short.MaxValue;
internal const int MaxLongPath = short.MaxValue;

internal static readonly char[] InvalidPathChars =
{
Expand All @@ -41,33 +41,37 @@ internal static partial class PathInternal
/// </summary>
internal static bool IsPathTooLong(string fullPath)
{
// We'll never know precisely what will fail as paths get changed internally in Windows and
// may grow to exceed MaxExtendedPath. We'll only try to catch ones we know will absolutely
// fail.

if (fullPath.Length < MaxLongPath - UncExtendedPathPrefix.Length)
{
// We won't push it over MaxLongPath
return false;
}

// We need to check if we have a prefix to account for one being implicitly added.
if (IsExtended(fullPath))
{
return fullPath.Length >= MaxExtendedPath;
// We won't prepend, just check
return fullPath.Length >= MaxLongPath;
}
else

if (fullPath.StartsWith(UncPathPrefix, StringComparison.Ordinal))
{
// Will need to be updated with #2581 to allow all paths to MaxExtendedPath
// minus legth of extended local or UNC prefix.
return fullPath.Length >= MaxShortPath;
return fullPath.Length + UncExtendedPrefixToInsert.Length >= MaxLongPath;
}

return fullPath.Length + ExtendedPathPrefix.Length >= MaxLongPath;
}

/// <summary>
/// Returns true if the directory is too long
/// </summary>
internal static bool IsDirectoryTooLong(string fullPath)
{
if (IsExtended(fullPath))
{
return fullPath.Length >= MaxExtendedPath;
}
else
{
// Will need to be updated with #2581 to allow all paths to MaxExtendedPath
// minus legth of extended local or UNC prefix.
return fullPath.Length >= MaxShortDirectoryPath;
}
return IsPathTooLong(fullPath);
}

/// <summary>
Expand Down Expand Up @@ -272,17 +276,17 @@ internal static bool IsPathRelative(string path)
return true;
}

if ((path[0] == '\\') || (path[0] == '/'))
if (IsDirectorySeparator(path[0]))
{
// There is no valid way to specify a relative path with two initial slashes
return !((path[1] == '\\') || (path[1] == '/'));
return !(IsDirectorySeparator(path[1]));
}

// The only way to specify a fixed path that doesn't begin with two slashes
// is the drive, colon, slash format- i.e. C:\
return !((path.Length >= 3)
&& (path[1] == ':')
&& ((path[2] == '\\') || (path[2] == '/')));
&& (path[1] == Path.VolumeSeparatorChar)
&& (IsDirectorySeparator(path[2])));
}

internal static bool IsDirectorySeparator(char c)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ public void AddExtendedPathPrefixTest(string path, string expected)
InlineData(@"\\?", false),
InlineData(@"\\", false),
InlineData(@"//", false),
InlineData(@"\a", true),
InlineData(@"/a", true),
InlineData(@"\", true),
InlineData(@"/", true),
InlineData(@"C:Path", true),
InlineData(@"C:\Path", false),
InlineData(@"\\?\C:\Path", false),
InlineData(@"Path", true),
InlineData(@"X", true)]
[PlatformSpecific(PlatformID.Windows)]
Copy link
Member

Choose a reason for hiding this comment

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

Please add an AnyUnix version of this test as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Need to implement the Unix version of the api first.

public void IsPathRelative(string path, bool expected)
{
Assert.Equal(expected, PathInternal.IsPathRelative(path));
Expand Down
35 changes: 30 additions & 5 deletions src/System.IO.FileSystem/tests/Directory/CreateDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,20 +210,44 @@ public void DirectoryWithComponentLongerThanMaxComponentAsPath_ThrowsPathTooLong

[Fact]
[PlatformSpecific(PlatformID.Windows)]
public void DirectoryLongerThanMaxPathAsPath_ThrowsPathTooLongException()
public void DirectoryLongerThanMaxPath_Succeeds()
{
var paths = IOInputs.GetPathsLongerThanMaxPath();
Assert.All(paths, (path) =>
{
DirectoryInfo result = Create(path);
Assert.True(Directory.Exists(result.FullName));
});
}

[Fact]
[PlatformSpecific(PlatformID.Windows)]
public void DirectoryLongerThanMaxLongPath_ThrowsPathTooLongException()
{
var paths = IOInputs.GetPathsLongerThanMaxLongPath();
Assert.All(paths, (path) =>
{
Assert.Throws<PathTooLongException>(() => Create(path));
});
}

[Fact]
[PlatformSpecific(PlatformID.Windows)]
public void DirectoryLongerThanMaxLongPathWithExtendedSyntax_ThrowsPathTooLongException()
{
var paths = IOInputs.GetPathsLongerThanMaxLongPath(useExtendedSyntax: true);
Assert.All(paths, (path) =>
{
Assert.Throws<PathTooLongException>(() => Create(path));
});
}


[Fact]
[PlatformSpecific(PlatformID.Windows)]
public void ExtendedDirectoryLongerThanLegacyMaxPathSucceeds()
public void ExtendedDirectoryLongerThanLegacyMaxPath_Succeeds()
{
var paths = IOInputs.GetPathsLongerThanMaxPath(useExtendedSyntax: true, includeExtendedMaxPath: false);
var paths = IOInputs.GetPathsLongerThanMaxPath(useExtendedSyntax: true);
Assert.All(paths, (path) =>
{
Assert.True(Create(path).Exists);
Expand All @@ -232,12 +256,13 @@ public void ExtendedDirectoryLongerThanLegacyMaxPathSucceeds()

[Fact]
[PlatformSpecific(PlatformID.Windows)]
public void DirectoryLongerThanMaxDirectoryAsPath_ThrowsPathTooLongException()
public void DirectoryLongerThanMaxDirectoryAsPath_Succeeds()
{
var paths = IOInputs.GetPathsLongerThanMaxDirectory();
Assert.All(paths, (path) =>
{
Assert.Throws<PathTooLongException>(() => Create(path));
var result = Create(path);
Assert.True(Directory.Exists(result.FullName));
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/System.IO.FileSystem/tests/Directory/Exists.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ public void DotDotAsPath_ReturnsTrue()
}

[Fact]
public void DirectoryLongerThanMaxPathAsPath_DoesntThrow()
public void DirectoryLongerThanMaxLongPath_DoesntThrow()
{
Assert.All((IOInputs.GetPathsLongerThanMaxPath()), (path) =>
Assert.All((IOInputs.GetPathsLongerThanMaxLongPath()), (path) =>
{
Assert.False(Exists(path), path);
});
Expand Down
22 changes: 17 additions & 5 deletions src/System.IO.FileSystem/tests/Directory/Move.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ public void IncludeSubdirectories()
}

[Fact]
public void Path_Longer_Than_MaxPath_Throws_Exception()
public void Path_Longer_Than_MaxLongPath_Throws_Exception()
{
string testDir = GetTestFilePath();
Directory.CreateDirectory(testDir);
Assert.All((IOInputs.GetPathsLongerThanMaxPath()), (path) =>
Assert.All((IOInputs.GetPathsLongerThanMaxLongPath()), (path) =>
{
Assert.Throws<PathTooLongException>(() => Move(testDir, path));
Assert.Throws<PathTooLongException>(() => Move(path, testDir));
Expand All @@ -144,14 +144,26 @@ public void Path_Longer_Than_MaxPath_Throws_Exception()

[Fact]
[PlatformSpecific(PlatformID.Windows)]
public void Path_With_Longer_Than_MaxDirectory_Throws_Exception()
public void Path_With_Longer_Than_MaxDirectory_Succeeds()
{
string testDir = GetTestFilePath();
Directory.CreateDirectory(testDir);
Assert.True(Directory.Exists(testDir), "test directory should exist");
Assert.All((IOInputs.GetPathsLongerThanMaxDirectory()), (path) =>
{
Assert.Throws<PathTooLongException>(() => Move(testDir, path));
Assert.Throws<PathTooLongException>(() => Move(path, testDir));
string baseDestinationPath = Path.GetDirectoryName(path);
if (!Directory.Exists(baseDestinationPath))
Copy link
Member

Choose a reason for hiding this comment

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

Why this check? Doesn't CreateDirectory already check?

Copy link
Member Author

Choose a reason for hiding this comment

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

For troubleshooting when tests fail. If the prereq's aren't there you can rat hole into collateral fallout.

{
Directory.CreateDirectory(baseDestinationPath);
}
Assert.True(Directory.Exists(baseDestinationPath), "base destination path should exist");

Move(testDir, path);
Assert.False(Directory.Exists(testDir), "source directory should exist");
Assert.True(Directory.Exists(path), "destination directory should exist");
Move(path, testDir);
Assert.False(Directory.Exists(path), "source directory should exist");
Assert.True(Directory.Exists(testDir), "destination directory should exist");
});
}

Expand Down
32 changes: 31 additions & 1 deletion src/System.IO.FileSystem/tests/File/Move.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,43 @@ public void FileNameWithSignificantWhitespace()
}

[Fact]
[PlatformSpecific(PlatformID.Windows)]
public void MaxPath_Windows()
{
// Create a destination path longer than the traditional Windows limit of 256 characters,
// but under the long path limitation (32K).

string testFileSource = Path.Combine(TestDirectory, GetTestFileName());
File.Create(testFileSource).Dispose();
Assert.True(File.Exists(testFileSource), "test file should exist");

Assert.All(IOInputs.GetPathsLongerThanMaxPath(), (path) =>
{
string baseDestinationPath = Path.GetDirectoryName(path);
if (!Directory.Exists(baseDestinationPath))
{
Directory.CreateDirectory(baseDestinationPath);
}
Assert.True(Directory.Exists(baseDestinationPath), "base destination path should exist");

Move(testFileSource, path);
Assert.True(File.Exists(path), "moved test file should exist");
File.Delete(testFileSource);
Assert.False(File.Exists(testFileSource), "source test file should not exist");
Move(path, testFileSource);
Assert.True(File.Exists(testFileSource), "restored test file should exist");
});
}

[Fact]
[PlatformSpecific(PlatformID.AnyUnix)]
public void LongPath()
{
//Create a destination path longer than the traditional Windows limit of 256 characters
string testFileSource = Path.Combine(TestDirectory, GetTestFileName());
File.Create(testFileSource).Dispose();

Assert.All(IOInputs.GetPathsLongerThanMaxPath(), (path) =>
Assert.All(IOInputs.GetPathsLongerThanMaxLongPath(), (path) =>
{
Assert.Throws<PathTooLongException>(() => Move(testFileSource, path));
File.Delete(testFileSource);
Expand Down
Loading