Просмотр исходного кода

fix: 当文件/文件夹没有短路径名的时候,会无法压缩短路径

LTCat 2 месяцев назад
Родитель
Сommit
8bfb4f709f
2 измененных файлов с 109 добавлено и 17 удалено
  1. 74 16
      Shared/Utils/PathUtils.cs
  2. 35 1
      Tests/Utils/PathUtilsTest.cs

+ 74 - 16
Shared/Utils/PathUtils.cs

@@ -23,18 +23,22 @@ public static class PathUtils {
     /// 若路径较长,则尽量将其转换为短路径。
     /// <para/>结果的开头不含 <c>\\?\</c>,结尾不含分隔符。
     /// </summary>
-    public static string ToShortPath(string pathName, bool keepFileName = false) {
+    public static string ToShortPath(string pathName) {
         if (string.IsNullOrEmpty(pathName)) return pathName;
         if (!pathName.Contains(":")) return pathName;
-        if (pathName.Length <= 200) return pathName;
+        if (pathName.Length <= 220) return pathName; // 快速返回
         pathName = PathUtils.ForCompare(pathName);
-        // 保留文件名
+        // 尽量保留原始文件名
         string pathToKeep = "";
         string pathToShorten = pathName;
-        if (pathName.EndsWithF(".jar", true)) keepFileName = true; // jar 文件的文件名需要保留原样,否则会导致 Forge 1.20.1 无法通过文件名识别模块名
-        if (keepFileName && FileUtils.Exists(pathName)) {
-            pathToKeep = PathUtils.GetLastPart(pathName);
-            pathToShorten = PathUtils.RemoveLastPart(pathName);
+        if (FileUtils.Exists(pathName)) {
+            var fileName = PathUtils.GetLastPart(pathName);
+            if (fileName.Length >= 180) {
+                Logger.Warn($"文件名过长,将为文件名生成短路径:{pathName}", LogBehavior.None); // 部分程序,例如 Forge 的模块加载,依赖于读取文件名进行判断,因此对文件名生成短路径是有风险的
+            } else {
+                pathToKeep = fileName;
+                pathToShorten = PathUtils.RemoveLastPart(pathName);
+            }
         }
         // 逐级向上寻找已存在的文件夹,将不存在的部分挪到 suffix,不再缩短
         while (!DirectoryUtils.Exists(pathToShorten) && !FileUtils.Exists(pathToShorten)) { // 如果路径不存在
@@ -43,16 +47,70 @@ public static class PathUtils {
             pathToKeep = Path.Combine(PathUtils.GetLastPart(pathToShorten), pathToKeep);
             pathToShorten = parentPath;
         }
-        if (pathToShorten.Length <= 10) return pathName;
-        // 实际缩短路径
-        char[] buffer = new char[260];
-        int result = GetShortPathNameW(PathUtils.ForApi(pathToShorten), buffer, buffer.Length);
-        if (result == 0) return pathName;
-        string shortPath = new(buffer, 0, result);
+        if (pathToKeep.Length >= 200) {
+            Logger.Warn($"路径中的大部分内容都尚未建立,无法在内容不存在时缩短路径:{pathName} → {pathToShorten}", LogBehavior.None);
+            return pathName;
+        }
+        // 获取短路径
+        Func<string, string> TryGetShortPath = static pathName => {
+            char[] buffer = new char[220];
+            int result = GetShortPathNameW(PathUtils.ForApi(pathName), buffer, buffer.Length);
+            return result is 0 or > 220 ? pathName : new string(buffer, 0, result);
+        };
+        var shortPath = TryGetShortPath(pathToShorten);
+        // 尝试生成新的短名称,以将长度降低到 220 以下(只会在 Windows 没有为其生成过短名称的情况下才会触发,大多数情况都不会进到这里)
+        if (Path.Combine(shortPath, pathToKeep).Length > 220) {
+            Logger.Warn($"{nameof(GetShortPathNameW)} 未能有效缩短路径,将尝试生成新的短名称:{pathToShorten} → {shortPath}", LogBehavior.None);
+            var candidates = new List<(string Path, string Name)>();
+            string? candidatePath = pathToShorten;
+            while (!string.IsNullOrEmpty(candidatePath)) {
+                string candidateName = PathUtils.GetLastPart(candidatePath);
+                if (candidateName.Length > 12 &&
+                    string.Equals(PathUtils.GetLastPart(TryGetShortPath(candidatePath)), candidateName, StringComparison.OrdinalIgnoreCase)) {
+                    candidates.Add((candidatePath, candidateName));
+                }
+                candidatePath = Path.GetDirectoryName(candidatePath);
+            }
+            foreach (var candidate in candidates.OrderByDescending(candidate => candidate.Name.Length)) {
+                using var handle = CreateFileW(PathUtils.ForApi(candidate.Path), 0x40010000, 7, IntPtr.Zero, 3, 0x02000000, IntPtr.Zero); // GENERIC_WRITE | DELETE;共享读、写、删除;OPEN_EXISTING;FILE_FLAG_BACKUP_SEMANTICS
+                if (handle.IsInvalid) continue;
+                bool created = false;
+                try {
+                    unsafe {
+                        fixed (char* originalNameBuffer = candidate.Name) {
+                            char* shortNameBuffer = stackalloc char[12]; // 8.3 名称最长 12 个 Unicode 字符
+                            var name = new UnicodeString {Length = (ushort)(candidate.Name.Length * 2), MaximumLength = (ushort)((candidate.Name.Length + 1) * 2), Buffer = originalNameBuffer};
+                            var shortName = new UnicodeString {MaximumLength = 24, Buffer = shortNameBuffer};
+                            var context = new GenerateNameContext();
+                            for (int i = 0; i < 20; i++) { // 使用 Windows 自身的 8.3 名称生成算法,并通过 context 依次生成避让冲突的候选名
+                                shortName.Length = 0;
+                                if (RtlGenerate8dot3Name(ref name, 0, ref context, ref shortName) != 0) break;
+                                string generatedName = new(shortName.Buffer, 0, shortName.Length / 2);
+                                if (SetFileShortNameW(handle, generatedName)) { created = true; break; }
+                                if (Marshal.GetLastWin32Error() != 183) break; // ERROR_ALREADY_EXISTS
+                            }
+                        }
+                    }
+                } catch (EntryPointNotFoundException) {}
+                if (!created) continue;
+                shortPath = TryGetShortPath(pathToShorten);
+                if (Path.Combine(shortPath, pathToKeep).Length <= 220) break;
+            }
+        }
         return PathUtils.RemoveSlashSuffix(PathUtils.RemoveExtendedPrefix(Path.Combine(shortPath, pathToKeep)));
     }
-    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
-    private static extern int GetShortPathNameW(string lpszLongPath, [Out] char[] buffer, int cchBuffer);
+    [StructLayout(LayoutKind.Sequential)]
+    private unsafe struct UnicodeString { public ushort Length, MaximumLength; public char* Buffer; }
+    [StructLayout(LayoutKind.Sequential)]
+    private unsafe struct GenerateNameContext { public ushort Checksum; public byte ChecksumInserted, NameLength; public fixed char NameBuffer[8]; public uint ExtensionLength; public fixed char ExtensionBuffer[4]; public uint LastIndexValue; }
+    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] 
+    private static extern int GetShortPathNameW(string lpszLongPath, [Out] char[]? buffer, int cchBuffer);
+    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] 
+    private static extern Microsoft.Win32.SafeHandles.SafeFileHandle CreateFileW(string fileName, uint desiredAccess, uint shareMode, IntPtr securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile);
+    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] 
+    private static extern bool SetFileShortNameW(Microsoft.Win32.SafeHandles.SafeFileHandle file, string shortName);
+    [DllImport("ntdll.dll")] 
+    private static extern int RtlGenerate8dot3Name(ref UnicodeString name, byte allowExtendedCharacters, ref GenerateNameContext context, ref UnicodeString name8dot3);
 
     /// <summary>
     /// 将完整路径转换为以 <c>\\?\</c> 开头的扩展格式。
@@ -210,4 +268,4 @@ public static class Paths {
     /// %AppData% 文件夹中,名为 <see cref="Main.AppName"/> 的子文件夹。
     /// </summary>
     public static string AppDataThenName => PathUtils.AddSlashSuffix(AppData + Main.AppName);
-}
+}

+ 35 - 1
Tests/Utils/PathUtilsTest.cs

@@ -1,6 +1,40 @@
 namespace MeloongCore.Tests;
 public class PathUtilsTests : TestBase {
 
+    #region 长路径
+
+    [Test]
+    public async Task 长路径_ToShortPath_输出超过260字符() {
+        string root = Path.Combine(Path.GetTempPath(), $"{nameof(PathUtilsTests)}-{Guid.NewGuid():N}");
+        string path = root;
+        for (int i = 0; i < 40; i++) path = Path.Combine(path, $"D{i:000000}");
+        try {
+            DirectoryUtils.Create(path);
+            string result = PathUtils.ToShortPath(path);
+            await Assert.That(result.Length > 260).IsTrue();
+            await Assert.That(DirectoryUtils.Exists(result)).IsTrue();
+        } finally {
+            DirectoryUtils.Delete(root);
+        }
+    }
+
+    [Test]
+    public async Task 长路径_ToShortPath_创建短文件名() {
+        string root = Path.Combine(Path.GetTempPath(), $"{nameof(PathUtilsTests)}-{Guid.NewGuid():N}");
+        string path = Path.Combine(root, new string('A', 220), "versions", "test");
+        try {
+            DirectoryUtils.Create(path);
+            string result = PathUtils.ToShortPath(path);
+            await Assert.That(result.Length <= 200).IsTrue();
+            await Assert.That(result.Length < path.Length).IsTrue();
+            await Assert.That(DirectoryUtils.Exists(result)).IsTrue();
+        } finally {
+            DirectoryUtils.Delete(root);
+        }
+    }
+
+    #endregion
+
     #region 路径处理
 
     [Test]
@@ -101,4 +135,4 @@ public class PathUtilsTests : TestBase {
 
     #endregion
 
-}
+}