Keep official clients in charge of sync while Explorer can group locations, persist UI prefs, and list zip/rar/7z members without extracting them. Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
5.4 KiB
C#
215 lines
5.4 KiB
C#
namespace Explorer.Domain;
|
|
|
|
public static class PathRules
|
|
{
|
|
public const string ExtendedPrefix = @"\\?\";
|
|
public const string ExtendedUncPrefix = @"\\?\UNC\";
|
|
|
|
public static string ToExtended(string path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (path.StartsWith(ExtendedPrefix, StringComparison.Ordinal))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
if (path.StartsWith(@"\\", StringComparison.Ordinal))
|
|
{
|
|
return ExtendedUncPrefix + path[2..];
|
|
}
|
|
|
|
return ExtendedPrefix + path;
|
|
}
|
|
|
|
public static string FromExtended(string path)
|
|
{
|
|
if (path.StartsWith(ExtendedUncPrefix, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return @"\\" + path[ExtendedUncPrefix.Length..];
|
|
}
|
|
|
|
if (path.StartsWith(ExtendedPrefix, StringComparison.Ordinal))
|
|
{
|
|
return path[ExtendedPrefix.Length..];
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
public static bool IsUnc(string path)
|
|
{
|
|
var p = FromExtended(path);
|
|
return p.StartsWith(@"\\", StringComparison.Ordinal);
|
|
}
|
|
|
|
public static string NormalizeDirectorySeparators(string path)
|
|
=> path.Replace('/', '\\');
|
|
|
|
public static string Combine(string root, string relative)
|
|
{
|
|
root = FromExtended(root).TrimEnd('\\');
|
|
relative = relative.Replace('/', '\\').TrimStart('\\');
|
|
if (string.IsNullOrEmpty(relative))
|
|
{
|
|
return EnsureDirectoryTrailingSlashIfRoot(root);
|
|
}
|
|
|
|
return root + "\\" + relative;
|
|
}
|
|
|
|
public static string MakeRelative(string root, string fullPath)
|
|
{
|
|
var r = FromExtended(root).TrimEnd('\\');
|
|
var f = FromExtended(fullPath).TrimEnd('\\');
|
|
if (f.Equals(r, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
if (f.StartsWith(r + "\\", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return f[(r.Length + 1)..];
|
|
}
|
|
|
|
return f;
|
|
}
|
|
|
|
public static string CanonicalUncRoot(string path)
|
|
{
|
|
var p = FromExtended(path).TrimEnd('\\');
|
|
if (!p.StartsWith(@"\\", StringComparison.Ordinal))
|
|
{
|
|
return p;
|
|
}
|
|
|
|
var parts = p[2..].Split('\\', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length < 2)
|
|
{
|
|
return @"\\" + p[2..].ToLowerInvariant();
|
|
}
|
|
|
|
return @"\\" + parts[0].ToLowerInvariant() + "\\" + parts[1];
|
|
}
|
|
|
|
public static string Parent(string path)
|
|
{
|
|
var p = FromExtended(path).TrimEnd('\\');
|
|
if (p.Length >= 2 && p[1] == ':' && p.Length <= 3)
|
|
{
|
|
return p.Length == 2 ? p + "\\" : p + (p.EndsWith('\\') ? "" : "\\");
|
|
}
|
|
|
|
if (IsUnc(p))
|
|
{
|
|
var root = CanonicalUncRoot(p);
|
|
if (p.Equals(root, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return root;
|
|
}
|
|
}
|
|
|
|
var idx = p.LastIndexOf('\\');
|
|
if (idx <= 0)
|
|
{
|
|
return p;
|
|
}
|
|
|
|
if (idx == 2 && p[1] == ':')
|
|
{
|
|
return p[..3];
|
|
}
|
|
|
|
return p[..idx];
|
|
}
|
|
|
|
public static bool IsDriveRoot(string path)
|
|
{
|
|
var p = FromExtended(path).TrimEnd('\\');
|
|
return p.Length == 2 && p[1] == ':';
|
|
}
|
|
|
|
public static string EnsureDirectoryTrailingSlashIfRoot(string path)
|
|
{
|
|
var p = FromExtended(path);
|
|
if (p.Length == 2 && p[1] == ':')
|
|
{
|
|
return p + "\\";
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
public static string GetFileName(string path)
|
|
{
|
|
var p = FromExtended(path).TrimEnd('\\');
|
|
var idx = p.LastIndexOf('\\');
|
|
return idx < 0 ? p : p[(idx + 1)..];
|
|
}
|
|
|
|
public static string RelativeParent(string pathRel)
|
|
{
|
|
if (string.IsNullOrEmpty(pathRel))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var n = NormalizeDirectorySeparators(pathRel).TrimEnd('\\');
|
|
var idx = n.LastIndexOf('\\');
|
|
return idx < 0 ? string.Empty : n[..idx];
|
|
}
|
|
|
|
public static string JoinDisplay(string? root, string pathRel)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(root))
|
|
{
|
|
return pathRel;
|
|
}
|
|
|
|
return string.IsNullOrWhiteSpace(pathRel) ? EnsureDirectoryTrailingSlashIfRoot(root) : Combine(root, pathRel);
|
|
}
|
|
|
|
public static string ShortenDisplay(string path, int maxChars = 64)
|
|
{
|
|
var p = FromExtended(path);
|
|
if (p.Length <= maxChars)
|
|
{
|
|
return p;
|
|
}
|
|
|
|
var unc = IsUnc(p);
|
|
var parts = p.TrimEnd('\\').Split('\\', StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length <= 2)
|
|
{
|
|
return p;
|
|
}
|
|
|
|
string prefix;
|
|
var start = 0;
|
|
if (unc)
|
|
{
|
|
prefix = @"\\" + parts[0] + "\\" + parts[1];
|
|
start = 2;
|
|
}
|
|
else
|
|
{
|
|
prefix = parts.Length > 2 ? parts[0] + "\\" + parts[1] : parts[0];
|
|
start = parts.Length > 2 ? 2 : 1;
|
|
}
|
|
|
|
var leaf = parts[^1];
|
|
var parent = parts.Length - 1 > start ? parts[^2] + "\\" + leaf : leaf;
|
|
var candidate = prefix + @"\…\" + parent;
|
|
if (candidate.Length <= maxChars)
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
candidate = prefix + @"\…\" + leaf;
|
|
return candidate.Length <= maxChars ? candidate : prefix + @"\…";
|
|
}
|
|
}
|