Przeglądaj źródła

fix: 驱动器根路径兼容

龙腾猫跃 5 miesięcy temu
rodzic
commit
d4af33f5be

+ 1 - 1
Shared/General/Logger.cs

@@ -314,7 +314,7 @@ public class FileLogger : BaseLogger, IDisposable {
             Logger.Warn(ex, "整理日志文件失败");
         }
         // 新建 writer
-        writer = new StreamWriter(PathUtils.WithLongPath(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")), append: true) { AutoFlush = true };
+        writer = new StreamWriter(PathUtils.ForApi(Path.Combine(logFolder, $"{fileNamePrefix}1{fileNameSuffix}")), append: true) { AutoFlush = true };
     }
 
     /// <inheritdoc/>

+ 2 - 2
Shared/General/MsDelta.cs

@@ -20,7 +20,7 @@ public static class MsDelta {
         // fileTypeSet:https://learn.microsoft.com/en-us/previous-versions/bb417345(v=msdn.10)?redirectedfrom=MSDN#file-type-sets (15L: DELTA_FILE_TYPE_SET_EXECUTABLES)
         // 131072L: IgnoreFileSizeLimit, 32u: Crc32
         if (!CreateDelta(15L, 131072L, 0L, 
-            PathUtils.WithLongPath(oldFilePath), PathUtils.WithLongPath(newFilePath), null, null, new DeltaInput(), IntPtr.Zero, 32u, PathUtils.WithLongPath(deltaFilePath)))
+            PathUtils.ForApi(oldFilePath), PathUtils.ForApi(newFilePath), null, null, new DeltaInput(), IntPtr.Zero, 32u, PathUtils.ForApi(deltaFilePath)))
             throw new Win32Exception();
     }
 
@@ -35,7 +35,7 @@ public static class MsDelta {
         DirectoryUtils.Create(PathUtils.RemoveLastPart(newFilePath));
         Logger.Info($"正在应用补丁:原始文件 {oldFilePath} + 补丁文件 {deltaFilePath} → 输出至 {newFilePath}");
         // 1L: AllowLegacy
-        if (!ApplyDelta(1L, PathUtils.WithLongPath(oldFilePath), PathUtils.WithLongPath(deltaFilePath), PathUtils.WithLongPath(newFilePath)))
+        if (!ApplyDelta(1L, PathUtils.ForApi(oldFilePath), PathUtils.ForApi(deltaFilePath), PathUtils.ForApi(newFilePath)))
             throw new Win32Exception();
     }
 

+ 13 - 14
Shared/Utils/DirectoryUtils.cs

@@ -8,21 +8,20 @@ public static class DirectoryUtils {
     public static void Create(string path) {
         if (DirectoryUtils.Exists(path)) return;
         Logger.Trace($"新建文件夹:{path}");
-        path = PathUtils.WithLongPath(path);
-        Directory.CreateDirectory(path);
+        Directory.CreateDirectory(PathUtils.ForApi(path));
     }
 
     /// <summary>
     /// 判断文件夹是否存在。
     /// </summary>
     public static bool Exists(string path) 
-        => Directory.Exists(PathUtils.WithLongPath(path));
+        => Directory.Exists(PathUtils.ForApi(path));
 
     /// <summary>
     /// 获取 <see cref="DirectoryInfo"/> 对象。
     /// </summary>
     public static DirectoryInfo GetInfo(string path) 
-        => new(PathUtils.WithLongPath(path));
+        => new(PathUtils.ForApi(path));
 
     /// <summary>
     /// 返回指定路径下的所有文件,不以 \\?\ 开头。
@@ -30,7 +29,7 @@ public static class DirectoryUtils {
     /// </summary>
     public static IEnumerable<string> GetFiles(string path, bool topDirectoryOnly = false, string searchPattern = "*") {
         if (!DirectoryUtils.Exists(path)) return [];
-        return Directory.EnumerateFiles(PathUtils.WithLongPath(path), searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories).
+        return Directory.EnumerateFiles(PathUtils.ForApi(path), searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories).
             Select(PathUtils.WithoutLongPath);
     }
 
@@ -40,7 +39,7 @@ public static class DirectoryUtils {
     /// </summary>
     public static IEnumerable<string> GetDirectories(string path, bool topDirectoryOnly = false, string searchPattern = "*") {
         if (!DirectoryUtils.Exists(path)) return [];
-        return Directory.EnumerateDirectories(PathUtils.WithLongPath(path), searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories).
+        return Directory.EnumerateDirectories(PathUtils.ForApi(path), searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories).
             Select(PathUtils.WithoutLongPath);
     }
 
@@ -50,7 +49,7 @@ public static class DirectoryUtils {
     /// </summary>
     public static bool IsEmpty(string path) {
         if (!DirectoryUtils.Exists(path)) return true;
-        return !Directory.EnumerateFileSystemEntries(PathUtils.WithLongPath(path)).Any();
+        return !Directory.EnumerateFileSystemEntries(PathUtils.ForApi(path)).Any();
     }
 
     /// <summary>
@@ -63,8 +62,8 @@ public static class DirectoryUtils {
             progressHandler?.Invoke(1);
             return;
         }
-        sourceFolder = PathUtils.Normalize(sourceFolder, true);
-        destFolder = PathUtils.Normalize(destFolder, true);
+        sourceFolder = PathUtils.ForCompare(sourceFolder);
+        destFolder = PathUtils.ForCompare(destFolder);
         int doneCount = 0;
         Lazy<int> totalCount = new(() => DirectoryUtils.GetFiles(sourceFolder).Count());
         Logger.Trace($"复制文件夹:{sourceFolder} → {destFolder}");
@@ -82,7 +81,7 @@ public static class DirectoryUtils {
         if (!DirectoryUtils.Exists(sourceFolder)) return;
         DirectoryUtils.Delete(destFolder); // Move 要求不存在对应文件夹
         Logger.Trace($"剪切文件夹:{sourceFolder} → {destFolder}");
-        Directory.Move(PathUtils.WithLongPath(sourceFolder), PathUtils.WithLongPath(destFolder));
+        Directory.Move(PathUtils.ForApi(sourceFolder), PathUtils.ForApi(destFolder));
     }
 
     /// <summary>
@@ -92,8 +91,8 @@ public static class DirectoryUtils {
         // 安全检查
         if (!DirectoryUtils.Exists(folder)) return;
         Logger.Trace($"{(toRecycleBin ? "将文件夹删除到回收站" : "删除文件夹")}:{folder}");
-        folder = PathUtils.Normalize(folder, true);
-        if (folder == Path.GetPathRoot(folder))
+        folder = PathUtils.ForCompare(folder);
+        if (folder == PathUtils.ForCompare(Path.GetPathRoot(folder)))
             throw new UnauthorizedAccessException($"不应删除磁盘根目录:{folder}");
         if (criticalFolders.Value.Any(f => f.StartsWithF(folder, ignoreCase: true)))
             throw new UnauthorizedAccessException($"不应删除的文件夹:{folder}");
@@ -104,7 +103,7 @@ public static class DirectoryUtils {
             DeleteInternal(folder); // 永久删除
             static void DeleteInternal(string folder) {
                 try {
-                    folder = PathUtils.WithLongPath(folder);
+                    folder = PathUtils.ForApi(folder);
                     foreach (string filePath in DirectoryUtils.GetFiles(folder, true)) FileUtils.Delete(filePath); // 删除文件
                     foreach (string str in DirectoryUtils.GetDirectories(folder, true)) DeleteInternal(str); // 递归删除子文件夹
                     ResilientUtils.RetryOn<IOException>(() => Directory.Delete(folder, true)); // 最终删除文件夹
@@ -125,6 +124,6 @@ public static class DirectoryUtils {
         Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
         Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
         Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads") // 下载文件夹没有 SpecialFolder 枚举
-    }.Select(f => PathUtils.Normalize(f, true))));
+    }.Select(f => PathUtils.ForCompare(f))));
 
 }

+ 17 - 20
Shared/Utils/FileUtils.cs

@@ -9,13 +9,13 @@ public static class FileUtils {
     /// 打开指定文件的只读 <see cref="FileStream"/>。
     /// </summary>
     public static FileStream ReadAsStream(string filePath)
-        => new(PathUtils.WithLongPath(filePath), FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+        => new(PathUtils.ForApi(filePath), FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
 
     /// <summary>
     /// 读取文件中的所有内容。
     /// </summary>
     public static byte[] ReadAsBytes(string filePath) {
-        using Stream fs = ReadAsStream(filePath); // 不能使用 File.ReadAllBytes,它不指定 FileShare.ReadWrite,会在文件被占用时抛出异常
+        using Stream fs = FileUtils.ReadAsStream(filePath); // 不能使用 File.ReadAllBytes,它不指定 FileShare.ReadWrite,会在文件被占用时抛出异常
         using MemoryStream ms = new();
         fs.CopyTo(ms);
         return ms.ToArray();
@@ -75,7 +75,7 @@ public static class FileUtils {
         DirectoryUtils.Create(PathUtils.RemoveLastPart(filePath));
         Logger.Trace($"写入文件:{filePath}({content.Length} 字节)");
         ResilientUtils.RetryOn<IOException>(() 
-            => File.WriteAllBytes(PathUtils.WithLongPath(filePath), content));
+            => File.WriteAllBytes(PathUtils.ForApi(filePath), content));
     }
 
     /// <summary>
@@ -97,7 +97,7 @@ public static class FileUtils {
         DirectoryUtils.Create(PathUtils.RemoveLastPart(filePath));
         Logger.Trace($"创建文件流:{filePath}");
         return ResilientUtils.RetryOn<IOException, FileStream>(()
-            => new FileStream(PathUtils.WithLongPath(filePath), FileMode.Create));
+            => new FileStream(PathUtils.ForApi(filePath), FileMode.Create));
     }
 
     #endregion
@@ -118,7 +118,7 @@ public static class FileUtils {
         DirectoryUtils.Create(PathUtils.RemoveLastPart(destFilePath));
         Logger.Trace($"复制文件:{sourceFilePath} → {destFilePath}");
         ResilientUtils.RetryOn<IOException>(()
-            => File.Copy(PathUtils.WithLongPath(sourceFilePath), PathUtils.WithLongPath(destFilePath), true));
+            => File.Copy(PathUtils.ForApi(sourceFilePath), PathUtils.ForApi(destFilePath), true));
     }
 
     /// <summary>
@@ -131,7 +131,7 @@ public static class FileUtils {
         FileUtils.Delete(destFilePath);
         Logger.Trace($"剪切文件:{sourceFilePath} → {destFilePath}");
         ResilientUtils.RetryOn<IOException>(()
-            => File.Move(PathUtils.WithLongPath(sourceFilePath), PathUtils.WithLongPath(destFilePath)));
+            => File.Move(PathUtils.ForApi(sourceFilePath), PathUtils.ForApi(destFilePath)));
     }
 
     #endregion
@@ -148,7 +148,7 @@ public static class FileUtils {
             DeleteToRecycleBin(filePath);
         } else {
             ResilientUtils.RetryOn<IOException>(()
-                => File.Delete(PathUtils.WithLongPath(filePath)));
+                => File.Delete(PathUtils.ForApi(filePath)));
         }
     }
 
@@ -234,7 +234,7 @@ public static class FileUtils {
     public static ZipArchive OpenZip(string zipFilePath) {
         ZipArchive TryOpen(Encoding encoding) {
             Logger.Trace($"尝试以 {encoding.EncodingName} 编码打开压缩包:{zipFilePath}");
-            var result = ZipFile.Open(PathUtils.WithLongPath(zipFilePath), ZipArchiveMode.Read, encoding);
+            var result = ZipFile.Open(PathUtils.ForApi(zipFilePath), ZipArchiveMode.Read, encoding);
             try {
                 _ = result.Entries; // 如果编码有误,会在这里抛出 DecoderFallbackException;如果文件异常,会在这里抛出 InvalidDataException
                 return result;
@@ -260,7 +260,7 @@ public static class FileUtils {
     /// </summary>
     /// <param name="progressHandler">参数为已完成的总比例(0~1)。</param>
     public static void ExtractToDirectory(string compressionFile, string outputDirectory, Action<double>? progressHandler = null) {
-        compressionFile = PathUtils.WithLongPath(compressionFile);
+        compressionFile = PathUtils.ForApi(compressionFile);
         DirectoryUtils.Create(outputDirectory);
         // 解压 gz(gz 不需要考虑编码)
         if (compressionFile.EndsWithF(".gz", true)) {
@@ -282,8 +282,8 @@ public static class FileUtils {
             if (totalCount > 0) progressHandler?.Invoke((double) doneCount / totalCount);
             if (string.IsNullOrEmpty(entry.Name)) continue; // 跳过文件夹条目(ZipArchive 会将文件夹也作为一个 entry,但它们的 Name 为空)
             // ZipSlip 修复
-            string outputFilePath = PathUtils.WithLongPath(Path.GetFullPath(Path.Combine(outputDirectory, entry.FullName)));
-            if (!outputFilePath.StartsWithF(PathUtils.WithSeparator(PathUtils.WithLongPath(Path.GetFullPath(outputDirectory))), ignoreCase:true))
+            string outputFilePath = PathUtils.ForCompare(Path.Combine(outputDirectory, entry.FullName));
+            if (!outputFilePath.StartsWithF(PathUtils.WithSeparator(PathUtils.ForCompare(outputDirectory)), ignoreCase:true))
                 throw new ZipSlipException($"Zip 文件项 {entry.FullName} 的路径在压缩包之外,这可能导致安全问题");
             // 实际的解压
             using var entryStream = entry.Open();
@@ -297,13 +297,11 @@ public static class FileUtils {
     /// 会自动创建文件夹。会覆盖已有文件。
     /// </summary>
     public static void CreateZipFromDirectory(string outputFullPath, string sourceDirectory) {
-        outputFullPath = PathUtils.WithLongPath(outputFullPath);
-        sourceDirectory = PathUtils.WithLongPath(sourceDirectory);
         DirectoryUtils.Create(PathUtils.RemoveLastPart(outputFullPath));
         FileUtils.Delete(outputFullPath);
         Logger.Trace($"将文件夹中的内容压缩为 zip 文件:{sourceDirectory} → {outputFullPath}");
         ResilientUtils.RetryOn<IOException>(()
-            => ZipFile.CreateFromDirectory(sourceDirectory, outputFullPath));
+            => ZipFile.CreateFromDirectory(PathUtils.ForApi(sourceDirectory), PathUtils.ForApi(outputFullPath)));
     }
 
     /// <summary>
@@ -317,7 +315,7 @@ public static class FileUtils {
             if (sources.ContainsKey(fileName)) throw new ArgumentException($"尝试将多个同文件名的文件放进压缩包中({fileName})", nameof(sourceFiles));
             sources.Add(fileName, source);
         }
-        CreateZipFromFiles(outputFullPath, sources);
+        FileUtils.CreateZipFromFiles(outputFullPath, sources);
     }
 
     /// <summary>
@@ -326,13 +324,12 @@ public static class FileUtils {
     /// </summary>
     /// <param name="sources">键为 zip 文件下的路径,值为文件的本地路径。</param>
     public static void CreateZipFromFiles(string outputFullPath, IDictionary<string, string> sources) {
-        outputFullPath = PathUtils.WithLongPath(outputFullPath);
         DirectoryUtils.Create(PathUtils.RemoveLastPart(outputFullPath));
         FileUtils.Delete(outputFullPath);
         using var archive = ResilientUtils.RetryOn<IOException, ZipArchive>(()
-            => ZipFile.Open(outputFullPath, ZipArchiveMode.Create));
+            => ZipFile.Open(PathUtils.ForApi(outputFullPath), ZipArchiveMode.Create));
         Logger.Trace($"创建 zip 文件:{sources.Count} 个文件 → {outputFullPath}\n{sources.Select(p => $"- {p.Value} → {p.Key}").Join('\n')}");
-        foreach (var pair in sources) archive.CreateEntryFromFile(PathUtils.WithLongPath(pair.Value), pair.Key.Replace('\\', '/'), CompressionLevel.Optimal);
+        foreach (var pair in sources) archive.CreateEntryFromFile(PathUtils.ForApi(pair.Value), pair.Key.Replace('\\', '/'), CompressionLevel.Optimal);
     }
 
     #endregion
@@ -341,12 +338,12 @@ public static class FileUtils {
     /// 确定指定的文件是否存在。
     /// </summary>
     public static bool Exists(string filePath) 
-        => File.Exists(PathUtils.WithLongPath(filePath));
+        => File.Exists(PathUtils.ForApi(filePath));
 
     /// <summary>
     /// 获取 <see cref="FileInfo"/> 对象。
     /// </summary>
     public static FileInfo GetInfo(string path) 
-        => new(PathUtils.WithLongPath(path));
+        => new(PathUtils.ForApi(path));
 
 }

+ 18 - 8
Shared/Utils/PathUtils.cs

@@ -21,12 +21,12 @@ public static class PathUtils {
 
     /// <summary>
     /// 若路径较长,则尽量将其转换为短路径。
-    /// 若输入的是文件夹路径,不保证其结尾是否有文件夹分隔符。
+    /// <para/>结果的开头不含 <c>\\?\</c>、结尾不含分隔符。
     /// </summary>
     public static string ToShortPath(string fullName, bool keepFileName = false) {
         if (string.IsNullOrEmpty(fullName)) return fullName;
+        fullName = PathUtils.ForCompare(fullName);
         if (!fullName.Contains(":")) return fullName;
-        fullName = PathUtils.Normalize(fullName, false);
         if (fullName.Length <= 200) return fullName;
 
         // 保留文件名
@@ -49,10 +49,10 @@ public static class PathUtils {
 
         // 缩短路径
         char[] buffer = new char[260];
-        int result = GetShortPathNameW(PathUtils.WithLongPath(pathToShorten), buffer, buffer.Length);
+        int result = GetShortPathNameW(PathUtils.ForApi(pathToShorten), buffer, buffer.Length);
         if (result == 0) return fullName;
         string shortPath = new(buffer, 0, result);
-        return PathUtils.WithoutLongPath(Path.Combine(shortPath, pathToKeep));
+        return PathUtils.WithoutSeparator(PathUtils.WithoutLongPath(Path.Combine(shortPath, pathToKeep)));
     }
     [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
     private static extern int GetShortPathNameW(string lpszLongPath, [Out] char[] buffer, int cchBuffer);
@@ -147,11 +147,21 @@ public static class PathUtils {
     }
 
     /// <summary>
-    /// 将路径转换为标准格式:将短路径展开,将分隔符改为 \,去除前导的 <c>\\?\</c>,统一末尾是否包含分隔符。
+    /// 统一路径格式,以便比较。
+    /// <para/>具体而言:将短路径展开,去除前导的 <c>\\?\</c>,将分隔符改为 \,去除末尾的分隔符。
     /// </summary>
-    public static string Normalize(string path, bool withSeparator) {
-        path = PathUtils.WithoutLongPath(Path.GetFullPath(path)).Replace("/", @"\");
-        return withSeparator ? PathUtils.WithSeparator(path) : PathUtils.WithoutSeparator(path);
+    public static string ForCompare(string path) 
+        => PathUtils.WithoutSeparator(PathUtils.WithoutLongPath(Path.GetFullPath(path)).Replace("/", @"\"));
+
+    /// <summary>
+    /// 将路径转换为兼容各种 Windows API 的格式。
+    /// <para/>具体而言:将分隔符改为 \。添加前导的 <c>\\?\</c>。若为驱动器则添加末尾分隔符,否则去除分隔符。
+    /// </summary>
+    public static string ForApi(string path) {
+        path = PathUtils.WithLongPath(path);
+        if (path.EndsWithF(":")) path += @"\"; // 驱动器路径必须保留末尾的分隔符
+        if (path.EndsWithF(@":\")) return path;
+        return PathUtils.WithoutSeparator(path);
     }
 
     #endregion