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 fullName, bool keepFileName = false) {
if (string.IsNullOrEmpty(fullName)) return fullName;
if (!fullName.Contains(@":\")) return fullName;
if (fullName.Length <= 200) return fullName;
fullName = PathUtils.ForCompare(fullName);
// 保留文件名
string pathToKeep = "";
string pathToShorten = fullName;
if (fullName.EndsWithF(".jar", true)) keepFileName = true; // jar 文件的文件名需要保留原样,否则会导致 Forge 1.20.1 无法通过文件名识别模块名
if (keepFileName && FileUtils.Exists(fullName)) {
pathToKeep = PathUtils.GetLastPart(fullName);
pathToShorten = PathUtils.RemoveLastPart(fullName);
}
// 逐级向上寻找已存在的文件夹,将不存在的部分挪到 suffix,不再缩短
while (!DirectoryUtils.Exists(pathToShorten) && !FileUtils.Exists(pathToShorten)) { // 如果路径不存在
string parentPath = Path.GetDirectoryName(pathToShorten);
if (string.IsNullOrEmpty(parentPath) || parentPath == pathToShorten) return fullName; // 已经到达根目录,全都不存在,直接返回
pathToKeep = Path.Combine(PathUtils.GetLastPart(pathToShorten), pathToKeep);
pathToShorten = parentPath;
}
if (pathToShorten.Length <= 10) return fullName;
// 实际缩短路径
char[] buffer = new char[260];
int result = GetShortPathNameW(PathUtils.ForApi(pathToShorten), buffer, buffer.Length);
if (result == 0) return fullName;
string shortPath = new(buffer, 0, result);
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);
///
/// 将完整路径转换为以 \\?\ 开头的扩展格式。
/// 这会去除路径末尾的分隔符,将 / 替换为 \。
///
public static string ToExtendedFormat(string path) {
if (string.IsNullOrWhiteSpace(path)) return path;
if (path.StartsWithF(@"\\?\")) return path; // 已经是长路径
path = RemoveSlashSuffix(path).Replace('/', '\\'); // API 要求这个格式……
if (path.StartsWithF(@"\\")) {
return $@"\\?\UNC\{path[2..]}";
} else {
return $@"\\?\{path}";
}
}
///
/// 去除路径开头的 \\?\。
///
public static string RemoveExtendedPrefix(string path) {
if (path.StartsWithF(@"\\?\UNC\")) return @"\\" + path.AfterLast(@"\\?\UNC\");
if (path.StartsWithF(@"\\?\")) return path.AfterLast(@"\\?\");
return path;
}
#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 path) {
if (path!.Contains("://")) { // 网络路径
path = PathUtils.RemoveSlashSuffix(path.BeforeFirst("#").BeforeFirst("?")); // 去除参数
}
return PathUtils.RemoveSlashSuffix(path!).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 path) {
if (path!.Contains("://")) { // 网络路径
path = PathUtils.RemoveSlashSuffix(path.BeforeFirst("#").BeforeFirst("?")); // 去除参数
} else { // 文件路径
path = PathUtils.RemoveSlashSuffix(path);
}
return path.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 path)
=> GetLastPart(path).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 path) {
var name = GetLastPart(path);
return name.Contains(".") ? name.AfterLast(".").Lower() : "";
}
///
/// 获取路径的盘符,返回一个大写字母。
/// 若路径不包含盘符,则返回 null。
///
public static char? GetDiskName(string? path) {
if (string.IsNullOrEmpty(path)) return null;
path = PathUtils.RemoveExtendedPrefix(path!);
if (path.StartsWithF(@"\\") || path.StartsWithF("/")) return null;
return char.ToUpper(path[0]);
}
///
/// 统一路径格式,以便比较。
/// 具体而言:将短路径展开,去除前导的 \\?\,将分隔符改为 \,去除末尾的分隔符。
///
public static string ForCompare(string path)
=> PathUtils.RemoveSlashSuffix(PathUtils.RemoveExtendedPrefix(Path.GetFullPath(path)).Replace("/", @"\"));
///
/// 将路径转换为兼容各种 Windows API 的格式。
/// 具体而言:将分隔符改为 \。添加前导的 \\?\。若为驱动器则添加末尾分隔符,否则去除分隔符。
///
public static string ForApi(string path) {
path = PathUtils.ToExtendedFormat(path);
if (path.EndsWithF(":")) path += @"\"; // 驱动器路径必须保留末尾的分隔符
if (path.EndsWithF(@":\")) return path;
return PathUtils.RemoveSlashSuffix(path);
}
#endregion
#region 常用路径
///
/// 程序可执行文件的所在文件夹,以 \ 结尾。
///
public static string CurrentFolder => PathUtils.AddSlashSuffix(AppDomain.CurrentDomain.BaseDirectory);
#endregion
}