diff --git a/src/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs b/src/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs index 43aa61403..4bff826d3 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/WindowsNameTransform.cs @@ -1,7 +1,6 @@ using ICSharpCode.SharpZipLib.Core; using System; using System.IO; -using System.Runtime.InteropServices; using System.Text; namespace ICSharpCode.SharpZipLib.Zip @@ -141,6 +140,10 @@ public string TransformFile(string name) pathBase += Path.DirectorySeparatorChar; } + // https://github.com/dotnet/runtime/issues/78834 + // For older Windows versions, Path.GetFullPath will alter paths with reserved names to escape them + // e.g. Path.GetFullPath("COM3") gives "\\.\COM3" + // This is not the case for newer versions of Windows, so we need to check for this ourselves if (!_allowParentTraversal && !Path.GetFullPath(name).StartsWith(pathBase, StringComparison.InvariantCultureIgnoreCase)) { throw new InvalidNameException("Parent traversal in paths is not allowed"); @@ -228,6 +231,11 @@ public static string MakeValidName(string name, char replacement) name = builder.ToString(); } + if (IsReservedName(name)) + { + throw new InvalidNameException($"\"{name}\" is a Windows reserved filename. See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions for more information."); + } + // Check for names greater than MaxPath characters. // TODO: Were is CLR version of MaxPath defined? Can't find it in Environment. if (name.Length > MaxPath) @@ -262,5 +270,33 @@ public char Replacement _replacementChar = value; } } + + + /// + /// Checks if the given name is a reserved name in Windows. + /// + /// The name to check. + /// True if the name is reserved; otherwise, false. + /// + /// Microsoft changed the OS behavior for legacy DOS device names in recent versions of Windows. + /// In older versions of Windows, the file extension would be considered for checking if the path is a legacy DOS device name: + /// + /// Path.GetFullPath("COM3") gives \\.\COM3 + /// Path.GetFullPath("COM3.txt") gives \\.\COM3.txt + /// Path.GetFullPath("C:\COM3") gives \\.\COM3 + /// + /// In newer versions of Windows, the file extension is ignored for checking if the path is a legacy DOS device name: + /// + /// Path.GetFullPath("COM3") gives \\.\COM3 + /// Path.GetFullPath("COM3.txt") gives COM3.txt + /// Path.GetFullPath("C:\COM3") gives C:\COM3 + /// + /// Therefore, we can detect if the path is a legacy DOS device name by checking if the full path starts with \\.\ or \\?\. + /// + internal static bool IsReservedName(string name) + { + var fullPathName = Path.GetFullPath(name); + return fullPathName.StartsWith(@"\\.\", StringComparison.Ordinal) || fullPathName.StartsWith(@"\\?\", StringComparison.Ordinal); + } } } diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/WindowsNameTransformHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/WindowsNameTransformHandling.cs index 8e6941251..e42a55047 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/WindowsNameTransformHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/WindowsNameTransformHandling.cs @@ -54,6 +54,17 @@ public void NameTooLong() } } + [Test] + public void TransformDirectory_NameIsReservedName_ThrowsInvalidNameException() + { + var wnt = new WindowsNameTransform(); + const string Reserved = "COM1"; + var e = Assert.Throws(() => wnt.TransformDirectory(Reserved)); + + // This second assert is to differentiate between the reserved name exception and the parent traversal in paths not allowed exception + Assert.That(e.Message, Does.Contain("https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions")); + } + [Test] public void LengthBoundaryOk() {