40 lines
2.0 KiB
C#
40 lines
2.0 KiB
C#
using Launcher.Next.Core;
|
|
|
|
namespace Launcher.Next.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// Reads a catalog descriptor only. The returned source is later copied into
|
|
/// local immutable staging by <see cref="IPackageSynchronizer"/>; it is never
|
|
/// an active launch root.
|
|
/// </summary>
|
|
public sealed class FileSystemCatalogPackageAcquirer(string catalogRoot) : IPackageCatalogPackageAcquirer
|
|
{
|
|
private const int MaxFiles = 256;
|
|
private const long MaxBytes = 512L * 1024 * 1024;
|
|
|
|
public async Task<CatalogPackage?> AcquireAsync(PackageRequirement requirement, CancellationToken cancellationToken)
|
|
{
|
|
var root = Path.GetFullPath(catalogRoot);
|
|
var candidate = Path.GetFullPath(Path.Combine(root, requirement.PackageId, requirement.RequiredVersion));
|
|
if (!Directory.Exists(root) || IsReparsePoint(root) || !IsWithin(root, candidate) || !Directory.Exists(candidate) || IsReparsePoint(candidate))
|
|
return null;
|
|
|
|
var manifestPath = Path.Combine(candidate, "package.manifest.json");
|
|
if (!File.Exists(manifestPath) || IsReparsePoint(manifestPath))
|
|
return null;
|
|
|
|
var manifest = PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath, cancellationToken));
|
|
if (manifest.Files.Count > MaxFiles || manifest.Files.Sum(file => file.Length) > MaxBytes ||
|
|
manifest.Files.Any(file => string.IsNullOrWhiteSpace(file.Path) || Path.IsPathRooted(file.Path) ||
|
|
file.Path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Any(segment => segment is ".." or ".")))
|
|
return null;
|
|
return new CatalogPackage(manifest, candidate);
|
|
}
|
|
|
|
private static bool IsWithin(string root, string candidate) =>
|
|
candidate.StartsWith(Path.TrimEndingDirectorySeparator(root) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
|
|
|
|
private static bool IsReparsePoint(string path) =>
|
|
(File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0;
|
|
}
|