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()
{