-
Notifications
You must be signed in to change notification settings - Fork 49
Support for long paths in run command #398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
211cccc
added long path support
zateutsch ffdc9f9
Merge branch 'main' into zt/391-long-paths
nmetulev ae5ece8
Merge branch 'main' into zt/391-long-paths
nmetulev 5adc42a
Update src/winapp-CLI/WinApp.Cli/Services/PackageRegistrationService.cs
zateutsch 462aac7
Update src/winapp-CLI/WinApp.Cli/Helpers/LongPathHelper.cs
zateutsch 99adb19
Add GetShortPathOrThrow to fail fast when 8.3 shortening doesn't redu…
Copilot e122bf0
Merge branch 'main' into zt/391-long-paths
nmetulev 9ff0579
Merge branch 'main' into zt/391-long-paths
zateutsch ca0bc07
Address review feedback: fix GetShortPath boundary/UNC/trailing-sep, …
Copilot 570c775
Merge branch 'main' into zt/391-long-paths
nmetulev 020252a
Apply suggestion from @zateutsch
zateutsch 4103a4f
Apply suggestion from @zateutsch
zateutsch 8e8db11
Apply suggestion from @zateutsch
zateutsch File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| // Copyright (c) Microsoft Corporation and Contributors. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using WinApp.Cli.Helpers; | ||
|
|
||
| namespace WinApp.Cli.Tests; | ||
|
|
||
| [TestClass] | ||
| public class LongPathHelperTests | ||
| { | ||
| private const int MaxPath = 260; | ||
|
|
||
| // C:\ = 3 chars, .txt = 4 chars | ||
| private static readonly string PrefixC = "C:" + Path.DirectorySeparatorChar; | ||
| private const string SuffixTxt = ".txt"; | ||
|
|
||
| /// <summary>Creates a local path of exactly <paramref name="targetLength"/> characters.</summary> | ||
| private static string MakeLocalPath(int targetLength) | ||
| { | ||
| var aCount = targetLength - PrefixC.Length - SuffixTxt.Length; | ||
| return PrefixC + new string('a', aCount) + SuffixTxt; | ||
| } | ||
|
|
||
| #region EnsureExtendedLengthPrefix tests | ||
|
|
||
| [TestMethod] | ||
| public void EnsureExtendedLengthPrefix_ShortPath_ReturnsUnchanged() | ||
| { | ||
| var path = @"C:\short\path\file.txt"; | ||
| Assert.AreEqual(path, LongPathHelper.EnsureExtendedLengthPrefix(path)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void EnsureExtendedLengthPrefix_ExactlyMaxPath_ReturnsUnchanged() | ||
| { | ||
| // A path of exactly MaxPath (260) chars should not get the extended prefix | ||
| var path = MakeLocalPath(MaxPath); | ||
| Assert.AreEqual(MaxPath, path.Length, "Test path should be exactly MaxPath characters"); | ||
| Assert.AreEqual(path, LongPathHelper.EnsureExtendedLengthPrefix(path)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void EnsureExtendedLengthPrefix_LongLocalPath_AddsPrefix() | ||
| { | ||
| var path = MakeLocalPath(MaxPath + 1); | ||
| Assert.IsTrue(path.Length > MaxPath); | ||
| var result = LongPathHelper.EnsureExtendedLengthPrefix(path); | ||
| Assert.IsTrue(result.StartsWith(@"\\?\", StringComparison.Ordinal)); | ||
| Assert.IsTrue(result.Contains(path), "Original path should be embedded in result"); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void EnsureExtendedLengthPrefix_AlreadyPrefixed_ReturnsUnchanged() | ||
| { | ||
| var path = @"\\?\" + new string('a', MaxPath) + ".txt"; | ||
| Assert.AreEqual(path, LongPathHelper.EnsureExtendedLengthPrefix(path)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void EnsureExtendedLengthPrefix_LongUncPath_AddsUncPrefix() | ||
| { | ||
| var path = @"\\server\share\" + new string('a', MaxPath); | ||
| Assert.IsTrue(path.Length > MaxPath); | ||
| var result = LongPathHelper.EnsureExtendedLengthPrefix(path); | ||
| Assert.IsTrue(result.StartsWith(@"\\?\UNC\", StringComparison.Ordinal), | ||
| @"UNC paths should use the \\?\UNC\ prefix form"); | ||
| // \\server\share\... -> \\?\UNC\server\share\... | ||
| Assert.IsTrue(result.Contains(@"server\share\"), "Server and share should be preserved"); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void EnsureExtendedLengthPrefix_ShortUncPath_ReturnsUnchanged() | ||
| { | ||
| var path = @"\\server\share\file.txt"; | ||
| Assert.AreEqual(path, LongPathHelper.EnsureExtendedLengthPrefix(path)); | ||
| } | ||
|
|
||
| #endregion | ||
|
|
||
| #region ValidatePathLength tests | ||
|
|
||
| [TestMethod] | ||
| public void ValidatePathLength_ShortPath_DoesNotThrow() | ||
| { | ||
| var path = @"C:\short\path\file.txt"; | ||
| LongPathHelper.ValidatePathLength(path); // Should not throw | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ValidatePathLength_ExactlyMaxPath_DoesNotThrow() | ||
| { | ||
| var path = MakeLocalPath(MaxPath); | ||
| Assert.AreEqual(MaxPath, path.Length, "Test path should be exactly MaxPath characters"); | ||
| LongPathHelper.ValidatePathLength(path); // Should not throw at exactly MaxPath | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ValidatePathLength_LongPath_WhenLongPathsDisabled_ThrowsWithActionableMessage() | ||
| { | ||
| var path = MakeLocalPath(MaxPath + 1); | ||
| Assert.IsTrue(path.Length > MaxPath); | ||
|
|
||
| if (!LongPathHelper.IsSystemLongPathEnabled()) | ||
| { | ||
| var ex = Assert.ThrowsExactly<InvalidOperationException>(() => LongPathHelper.ValidatePathLength(path)); | ||
| Assert.IsTrue(ex.Message.Contains("MAX_PATH"), "Error message should mention MAX_PATH"); | ||
| Assert.IsTrue(ex.Message.Contains("LongPathsEnabled") || ex.Message.Contains("reg add"), | ||
| "Error message should include actionable guidance"); | ||
| } | ||
| else | ||
| { | ||
| // Long paths enabled on this machine -- method should not throw | ||
| LongPathHelper.ValidatePathLength(path); | ||
| } | ||
| } | ||
|
|
||
| #endregion | ||
|
|
||
| #region GetShortPath tests | ||
|
|
||
| [TestMethod] | ||
| public void GetShortPath_ShortPath_ReturnsUnchanged() | ||
| { | ||
| var path = @"C:\short\path\file.txt"; | ||
| Assert.AreEqual(path, LongPathHelper.GetShortPath(path)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void GetShortPath_ExactlyMaxPath_ReturnsUnchanged() | ||
| { | ||
| // Paths at or below MaxPath should be returned as-is without calling GetShortPathName | ||
| var path = MakeLocalPath(MaxPath); | ||
| Assert.AreEqual(MaxPath, path.Length, "Test path should be exactly MaxPath characters"); | ||
| Assert.AreEqual(path, LongPathHelper.GetShortPath(path)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void GetShortPath_PathWithTrailingSeparator_PreservesTrailingSeparator() | ||
| { | ||
| // A directory path ending with the platform separator must still end with a separator | ||
| // after GetShortPath processes it (whether or not GetShortPathName succeeds). | ||
| var sep = Path.DirectorySeparatorChar; | ||
| var path = PrefixC + new string('a', MaxPath) + sep; | ||
| Assert.IsTrue(Path.EndsInDirectorySeparator(path), "Test path should end with directory separator"); | ||
| Assert.IsTrue(path.Length > MaxPath, "Test path must exceed MaxPath"); | ||
|
|
||
| var result = LongPathHelper.GetShortPath(path); | ||
|
|
||
| Assert.IsTrue(Path.EndsInDirectorySeparator(result), | ||
| "GetShortPath must preserve the trailing directory separator"); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void GetShortPath_DirectoryPathWithoutTrailingSeparator_DoesNotAddSeparator() | ||
| { | ||
| var path = @"C:\short\directory"; | ||
| var result = LongPathHelper.GetShortPath(path); | ||
| Assert.IsFalse(Path.EndsInDirectorySeparator(result), | ||
| "GetShortPath should not add a trailing separator when input has none"); | ||
| } | ||
|
|
||
| #endregion | ||
|
|
||
| #region GetShortPathOrThrow tests | ||
|
|
||
| [TestMethod] | ||
| public void GetShortPathOrThrow_ShortPath_ReturnsUnchanged() | ||
| { | ||
| var path = @"C:\short\path\file.txt"; | ||
| Assert.AreEqual(path, LongPathHelper.GetShortPathOrThrow(path)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void GetShortPathOrThrow_ExactlyMaxPath_ReturnsUnchanged() | ||
| { | ||
| var path = MakeLocalPath(MaxPath); | ||
| Assert.AreEqual(MaxPath, path.Length, "Test path should be exactly MaxPath characters"); | ||
| Assert.AreEqual(path, LongPathHelper.GetShortPathOrThrow(path)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void GetShortPathOrThrow_LongPathThatCannotBeShortened_Throws() | ||
| { | ||
| // A path with a non-existent deeply nested directory cannot be shortened by GetShortPathName. | ||
| // GetShortPath returns the original (still-long) path, so GetShortPathOrThrow must throw. | ||
| var path = MakeLocalPath(MaxPath + 1); | ||
| Assert.IsTrue(path.Length > MaxPath, "Test path must exceed MaxPath"); | ||
|
|
||
| var ex = Assert.ThrowsExactly<InvalidOperationException>(() => LongPathHelper.GetShortPathOrThrow(path)); | ||
| Assert.IsTrue(ex.Message.Contains("too long") || ex.Message.Contains("MAX_PATH") || ex.Message.Contains("short"), | ||
| "Error message should describe that the path is too long and cannot be shortened to a usable length"); | ||
| } | ||
|
|
||
| #endregion | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.