2026-06-03 00:50:52 +08:00
|
|
|
namespace LanDesktopPLONDS.Installer.Services;
|
|
|
|
|
|
|
|
|
|
public static class InstallerPathGuard
|
|
|
|
|
{
|
2026-06-05 11:08:11 +08:00
|
|
|
public const string ApplicationDirectoryName = "LanMountainDesktop";
|
|
|
|
|
|
2026-06-03 00:50:52 +08:00
|
|
|
public static string GetDefaultInstallPath()
|
|
|
|
|
{
|
|
|
|
|
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
|
|
|
if (string.IsNullOrWhiteSpace(programFiles))
|
|
|
|
|
{
|
|
|
|
|
programFiles = Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
|
|
"Programs");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 11:08:11 +08:00
|
|
|
return Path.Combine(programFiles, ApplicationDirectoryName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string GetInstallPathForSelectedFolder(string selectedFolder)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(selectedFolder))
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("Selected folder is required.", nameof(selectedFolder));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fullPath = Path.GetFullPath(selectedFolder.Trim());
|
|
|
|
|
var root = Path.GetPathRoot(fullPath);
|
|
|
|
|
var trimmedPath = fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
|
var trimmedRoot = root?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
|
var basePath = string.Equals(trimmedPath, trimmedRoot, StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
? fullPath
|
|
|
|
|
: trimmedPath;
|
|
|
|
|
var selectedName = Path.GetFileName(trimmedPath);
|
|
|
|
|
var installPath = string.Equals(selectedName, ApplicationDirectoryName, StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
? trimmedPath
|
|
|
|
|
: Path.Combine(basePath, ApplicationDirectoryName);
|
|
|
|
|
|
|
|
|
|
return NormalizeInstallPath(installPath);
|
2026-06-03 00:50:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string NormalizeInstallPath(string path)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("Installation path is required.", nameof(path));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fullPath = Path.GetFullPath(path.Trim());
|
|
|
|
|
ValidateInstallPath(fullPath);
|
|
|
|
|
return fullPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void ValidateInstallPath(string path)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Installation path is required.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fullPath = Path.GetFullPath(path);
|
|
|
|
|
var root = Path.GetPathRoot(fullPath);
|
|
|
|
|
if (string.Equals(
|
|
|
|
|
fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
|
|
|
|
root?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
|
|
|
|
StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Choose a folder instead of a drive root.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var blockedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
{
|
|
|
|
|
"Windows",
|
|
|
|
|
"System32",
|
|
|
|
|
"SysWOW64",
|
|
|
|
|
"Program Files",
|
|
|
|
|
"Program Files (x86)",
|
|
|
|
|
"Users"
|
|
|
|
|
};
|
|
|
|
|
var name = Path.GetFileName(fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
|
|
|
|
if (blockedNames.Contains(name))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Choose a dedicated application folder.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void EnsureUsableInstallPath(string path, long requiredBytes)
|
|
|
|
|
{
|
|
|
|
|
var fullPath = NormalizeInstallPath(path);
|
|
|
|
|
var directory = Directory.Exists(fullPath)
|
|
|
|
|
? new DirectoryInfo(fullPath)
|
|
|
|
|
: Directory.CreateDirectory(fullPath);
|
|
|
|
|
|
|
|
|
|
var testPath = Path.Combine(directory.FullName, $".write-test-{Guid.NewGuid():N}.tmp");
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
File.WriteAllText(testPath, string.Empty);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
if (File.Exists(testPath))
|
|
|
|
|
{
|
|
|
|
|
File.Delete(testPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var drive = new DriveInfo(directory.Root.FullName);
|
|
|
|
|
if (drive.AvailableFreeSpace > 0 && drive.AvailableFreeSpace < requiredBytes)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("The selected drive does not have enough free space.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void EnsureChildPath(string parent, string child)
|
|
|
|
|
{
|
|
|
|
|
if (!IsSameOrChildPath(parent, child))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidDataException($"Path escapes the expected root: {child}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static bool IsSameOrChildPath(string parent, string child)
|
|
|
|
|
{
|
|
|
|
|
var resolvedParent = Path.GetFullPath(parent)
|
|
|
|
|
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
|
var resolvedChild = Path.GetFullPath(child);
|
|
|
|
|
return string.Equals(
|
|
|
|
|
resolvedParent,
|
|
|
|
|
resolvedChild.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
|
|
|
|
|
StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|| resolvedChild.StartsWith(resolvedParent + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|| resolvedChild.StartsWith(resolvedParent + Path.AltDirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string NormalizeRelativePath(string relativePath)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(relativePath))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidDataException("Package entry path is empty.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var normalized = relativePath
|
|
|
|
|
.Replace('\\', Path.DirectorySeparatorChar)
|
|
|
|
|
.Replace('/', Path.DirectorySeparatorChar)
|
|
|
|
|
.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
|
if (Path.IsPathRooted(normalized) || normalized.Split(Path.DirectorySeparatorChar).Contains(".."))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidDataException($"Package entry path is invalid: {relativePath}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
}
|