namespace MeloongCore; public static class PathUtils { #region 分隔符 /// /// 确保路径的末尾包含任意文件夹分隔符。 /// public static string AddSlashSuffix(string folder) => folder + (folder.EndsWithF(Path.DirectorySeparatorChar) || folder.EndsWithF(Path.AltDirectorySeparatorChar) ? "" : Path.DirectorySeparatorChar); /// /// 确保路径的结尾不包含文件夹分隔符。 /// public static string RemoveSlashSuffix(string folder) => folder.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); #endregion #region 长路径 /// /// 若路径较长,则尽量将其转换为短路径。 /// 结果的开头不含 \\?\,结尾不含分隔符。 /// public static string ToShortPath(string pathName) { // 快速返回 if (string.IsNullOrEmpty(pathName)) return pathName; if (!pathName.Contains(":") || pathName.Contains("://")) return pathName; if (pathName.Length <= 220) return pathName; // 开始实际处理 pathName = PathUtils.ForCompare(pathName); // 尽量保留原始文件名 string pathToKeep = ""; string pathToShorten = 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)) { // 如果路径不存在 string? parentPath = Path.GetDirectoryName(pathToShorten); if (string.IsNullOrEmpty(parentPath) || PathUtils.IsSame(parentPath, pathToShorten)) return pathName; // 已经到达根目录,全都不存在,直接返回 pathToKeep = Path.Combine(PathUtils.GetLastPart(pathToShorten), pathToKeep); pathToShorten = parentPath; } if (pathToKeep.Length >= 200) { Logger.Warn($"路径中的大部分内容都尚未建立,无法在内容不存在时缩短路径:{pathName} → {pathToShorten}", LogBehavior.None); return pathName; } // 获取短路径 Func 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))); } [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); /// /// 将完整路径转换为以 \\?\ 开头的扩展格式。 /// 这会去除路径末尾的分隔符,将 / 替换为 \。 /// public static string ToExtendedFormat(string pathName) { if (string.IsNullOrWhiteSpace(pathName)) return pathName; if (!pathName.Contains(@":")) return pathName; // 可能是相对路径,保持不变 if (pathName.StartsWithF(@"\\?\")) return pathName; // 已经是长路径 pathName = RemoveSlashSuffix(pathName).Replace('/', '\\'); // API 要求这个格式…… if (pathName.StartsWithF(@"\\")) { return $@"\\?\UNC\{pathName[2..]}"; } else { return $@"\\?\{pathName}"; } } /// /// 去除路径开头的 \\?\。 /// public static string RemoveExtendedPrefix(string pathName) { if (pathName.StartsWithF(@"\\?\UNC\")) return @"\\" + pathName.AfterLast(@"\\?\UNC\"); if (pathName.StartsWithF(@"\\?\")) return pathName.AfterLast(@"\\?\"); return pathName; } #endregion #region 路径处理 /// /// 去除路径的最后一级,并移除末尾的分隔符。 /// /// "C:\foo\bar.txt" → "C:\foo" /// "C:\foo\bar\" → "C:\foo" /// "C:\foo\bar" → "C:\foo" /// "C:\foo\" → "C:" /// "https://foo.bar/file/pack.zip?arg=1" → "https://foo.bar/file" /// public static string RemoveLastPart(string pathName) { if (pathName.Contains("://")) { // 网络路径 pathName = PathUtils.RemoveSlashSuffix(pathName.BeforeFirst("#").BeforeFirst("?")); // 去除参数 } return PathUtils.RemoveSlashSuffix(pathName).BeforeLastOfAny([@"\", "/"]); } /// /// 获取路径的最后一级,即文件名,或当前文件夹名。 /// /// "C:\foo\bar.txt" → "bar.txt" /// "C:\foo\bar\" → "bar" /// "C:\foo\bar" → "bar" /// "https://foo.bar/file/pack.zip?arg=1" → "pack.zip" /// public static string GetLastPart(string pathName) { if (pathName.Contains("://")) { // 网络路径 pathName = PathUtils.RemoveSlashSuffix(pathName.BeforeFirst("#").BeforeFirst("?")); // 去除参数 } else { // 文件路径 pathName = PathUtils.RemoveSlashSuffix(pathName); } return pathName.AfterLastOfAny([@"\", "/"]); } /// /// 获取路径或文件名中,仅不包含最后一级扩展名的部分。 /// /// "C:\foo\bar.txt" → "bar" /// "create.jar.disabled" → "create.jar" /// "https://foo.bar/file/page.xaml.vb?arg=1" → "page.xaml" /// /// public static string GetFileNameWithoutExtension(string pathName) => GetLastPart(pathName).BeforeLast("."); /// /// 获取路径或文件名中的最后一级文件扩展名,不包含 .,固定小写。 /// 如果没有 . 则返回空字符串。 /// /// "C:\FOO\BAR.TXT" → "txt" /// "create.jar.disabled" → "disabled" /// "C:\foo\bar" → "" /// "https://foo.bar/file/page.xaml.vb?arg=1" → "vb" /// /// public static string GetExtension(string pathName) { var name = GetLastPart(pathName); return name.Contains(".") ? name.AfterLast(".").Lower() : ""; } /// /// 获取路径的盘符,返回一个大写字母。 /// 若路径不包含盘符,则返回 null。 /// public static char? GetDiskName(string? pathName) { if (string.IsNullOrEmpty(pathName)) return null; pathName = PathUtils.RemoveExtendedPrefix(pathName!); if (pathName.StartsWithF(@"\\") || pathName.StartsWithF("/")) return null; return char.ToUpper(pathName[0]); } /// /// 统一路径格式,以便比较。 /// 具体而言:将短路径展开,去除前导的 \\?\,将分隔符改为 \,去除末尾的分隔符。 /// public static string ForCompare(string pathName) { try { return PathUtils.RemoveSlashSuffix(PathUtils.RemoveExtendedPrefix(Path.GetFullPath(pathName)).Replace("/", @"\")); } catch (ArgumentException ex) { throw new ArgumentException($"路径不正确:{pathName}", ex); } } /// /// 比较两个路径是否相同。null 与空字符串视为相同的空路径。 /// public static bool IsSame(string? path1, string? path2) { return PathUtils.Compare(path1, path2) == 0; } /// /// 比较两个路径,忽略大小写。 /// null 与空字符串视为相同的空路径,空路径排在非空路径之前。 /// /// /// 小于 0: 排在 之前。 /// 等于 0:两个路径相等。 /// 大于 0: 排在 之后。 /// public static int Compare(string? path1, string? path2) { if (string.IsNullOrEmpty(path1)) return string.IsNullOrEmpty(path2) ? 0 : -1; if (string.IsNullOrEmpty(path2)) return 1; return string.Compare(PathUtils.ForCompare(path1!), PathUtils.ForCompare(path2!), StringComparison.OrdinalIgnoreCase); } /// /// 将路径转换为兼容各种 Windows API 的格式。 /// 具体而言:将分隔符改为 \。添加前导的 \\?\。若为驱动器则添加末尾分隔符,否则去除分隔符。 /// public static string ForApi(string pathName) { pathName = PathUtils.ToExtendedFormat(pathName); if (pathName.EndsWithF(":")) pathName += @"\"; // 驱动器路径必须保留末尾的分隔符 if (pathName.EndsWithF(@":\")) return pathName; return PathUtils.RemoveSlashSuffix(pathName); } #endregion /// /// 判断 是否为 的父路径,或两者是否相同。 /// public static bool IsParentOf(string parentPath, string childPath) { parentPath = PathUtils.ForCompare(parentPath); childPath = PathUtils.ForCompare(childPath); return childPath.StartsWithF(parentPath + @"\", true) || PathUtils.IsSame(childPath, parentPath); } } /// /// 常用路径合集。 /// 所有路径均以分隔符结尾。 /// public static class Paths { /// /// 可执行文件所在的文件夹。 /// public static string Base => PathUtils.AddSlashSuffix(AppContext.BaseDirectory); /// /// 可执行文件所在的文件夹中,名为 的子文件夹。 /// public static string BaseThenName => PathUtils.AddSlashSuffix(Base + Main.AppName); /// /// %AppData% 文件夹。 /// public static string AppData => PathUtils.AddSlashSuffix(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); /// /// %AppData% 文件夹中,名为 的子文件夹。 /// public static string AppDataThenName => PathUtils.AddSlashSuffix(AppData + Main.AppName); }