feat: resolve roster-bound packages through verified cache
This commit is contained in:
@@ -5,10 +5,12 @@ namespace Launcher.Next.Infrastructure;
|
||||
|
||||
public sealed class Sha256FileHasher : IFileHasher
|
||||
{ public async Task<string> Sha256Async(string path, CancellationToken cancellationToken) { await using var s = File.OpenRead(path); return Convert.ToHexString(await SHA256.HashDataAsync(s, cancellationToken)); } }
|
||||
public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalPackageStore, IPackageActivator, IRollbackManager, IPackageSynchronizer, ILastKnownGoodPackageResolver
|
||||
public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalPackageStore, IPackageActivator, IRollbackManager, IPackageSynchronizer, ILastKnownGoodPackageResolver, IPackageRequirementCacheBinder
|
||||
{
|
||||
private sealed record Pointer(string Current, string? Previous);
|
||||
private sealed record CacheBinding(string PackageId, string Product, int ProductYear, string Architecture, string Channel, string RequiredVersion, string RosterIdentity);
|
||||
private const string StoredManifestFileName = "package.manifest.json";
|
||||
private const string StoredBindingFileName = "package.binding.json";
|
||||
private string PackageRoot(string id) => Path.Combine(root, "Applications", id);
|
||||
private string PointerPath(string id) => Path.Combine(PackageRoot(id), "current.json");
|
||||
private async Task<Pointer?> ReadPointer(string id, CancellationToken ct)
|
||||
@@ -27,15 +29,21 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP
|
||||
return new(version, prior?.Current);
|
||||
}
|
||||
public async Task<string?> RollbackAsync(string id, CancellationToken ct) { var p = await ReadPointer(id, ct); return p?.Previous is null ? null : (await ActivateAsync(id, p.Previous, ct)).CurrentVersion; }
|
||||
public async Task<CachedPackageResolution?> ResolveAsync(string packageId, string product, int productYear, string architecture, bool allowOfflineFallback, CancellationToken ct)
|
||||
public async Task BindAsync(PackageRequirement requirement, string version, CancellationToken ct)
|
||||
{
|
||||
var pointer = await ReadPointer(packageId, ct);
|
||||
var root = VersionDirectory(requirement.PackageId, version);
|
||||
if (!Directory.Exists(root) || !string.Equals(requirement.RequiredVersion, version, StringComparison.Ordinal)) throw new InvalidOperationException("PACKAGE_REQUIREMENT_BINDING_INVALID");
|
||||
await File.WriteAllTextAsync(Path.Combine(root, StoredBindingFileName), JsonSerializer.Serialize(new CacheBinding(requirement.PackageId, requirement.Product, requirement.ProductYear, requirement.Architecture, requirement.Channel, requirement.RequiredVersion, requirement.RosterIdentity)), ct);
|
||||
}
|
||||
public async Task<CachedPackageResolution?> ResolveAsync(PackageRequirement requirement, CancellationToken ct)
|
||||
{
|
||||
var pointer = await ReadPointer(requirement.PackageId, ct);
|
||||
if (pointer is null) return null;
|
||||
var current = await VerifyVersionAsync(packageId, pointer.Current, product, productYear, architecture, ct);
|
||||
if (current is not null) return new(packageId, pointer.Current, current, false, "CURRENT_VERIFIED");
|
||||
if (!allowOfflineFallback || string.IsNullOrWhiteSpace(pointer.Previous) || string.Equals(pointer.Current, pointer.Previous, StringComparison.OrdinalIgnoreCase)) return null;
|
||||
var previous = await VerifyVersionAsync(packageId, pointer.Previous, product, productYear, architecture, ct);
|
||||
return previous is null ? null : new(packageId, pointer.Previous, previous, true, "PREVIOUS_VERIFIED_AFTER_CURRENT_REJECTED");
|
||||
var current = await VerifyVersionAsync(requirement, pointer.Current, ct);
|
||||
if (current is not null) return new(requirement.PackageId, pointer.Current, current, false, "CURRENT_VERIFIED");
|
||||
if (!requirement.AllowOfflineFallback || string.IsNullOrWhiteSpace(pointer.Previous) || string.Equals(pointer.Current, pointer.Previous, StringComparison.OrdinalIgnoreCase)) return null;
|
||||
var previous = await VerifyVersionAsync(requirement, pointer.Previous, ct);
|
||||
return previous is null ? null : new(requirement.PackageId, pointer.Previous, previous, true, "PREVIOUS_VERIFIED_AFTER_CURRENT_REJECTED");
|
||||
}
|
||||
public async Task<ActivationResult> SynchronizeAsync(PackageManifest manifest, string sourceRoot, CancellationToken ct, IProgress<PackageProgress>? progress = null)
|
||||
{
|
||||
@@ -47,17 +55,19 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP
|
||||
var versionDir = VersionDirectory(manifest.PackageId, manifest.Version); Directory.CreateDirectory(Path.GetDirectoryName(versionDir)!); if (Directory.Exists(versionDir)) Directory.Delete(staging, true); else Directory.Move(staging, versionDir); return await ActivateAsync(manifest.PackageId, manifest.Version, ct); }
|
||||
catch { if (Directory.Exists(staging)) Directory.Delete(staging, true); throw; }
|
||||
}
|
||||
private async Task<string?> VerifyVersionAsync(string packageId, string version, string product, int productYear, string architecture, CancellationToken ct)
|
||||
private async Task<string?> VerifyVersionAsync(PackageRequirement requirement, string version, CancellationToken ct)
|
||||
{
|
||||
string versionRoot;
|
||||
try { versionRoot = VersionDirectory(packageId, version); }
|
||||
try { versionRoot = VersionDirectory(requirement.PackageId, version); }
|
||||
catch { return null; }
|
||||
var manifestPath = Path.Combine(versionRoot, StoredManifestFileName);
|
||||
if (!Directory.Exists(versionRoot) || new DirectoryInfo(versionRoot).Attributes.HasFlag(FileAttributes.ReparsePoint) || !File.Exists(manifestPath) || new FileInfo(manifestPath).Attributes.HasFlag(FileAttributes.ReparsePoint)) return null;
|
||||
var bindingPath = Path.Combine(versionRoot, StoredBindingFileName);
|
||||
if (!Directory.Exists(versionRoot) || new DirectoryInfo(versionRoot).Attributes.HasFlag(FileAttributes.ReparsePoint) || !File.Exists(manifestPath) || !File.Exists(bindingPath) || new FileInfo(manifestPath).Attributes.HasFlag(FileAttributes.ReparsePoint) || new FileInfo(bindingPath).Attributes.HasFlag(FileAttributes.ReparsePoint)) return null;
|
||||
PackageManifest manifest;
|
||||
try { manifest = PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath, ct)); }
|
||||
CacheBinding binding;
|
||||
try { manifest = PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath, ct)); binding = JsonSerializer.Deserialize<CacheBinding>(await File.ReadAllTextAsync(bindingPath, ct))!; }
|
||||
catch { return null; }
|
||||
if (!string.Equals(manifest.PackageId, packageId, StringComparison.Ordinal) || !string.Equals(manifest.Version, version, StringComparison.Ordinal) || !string.Equals(manifest.Product, product, StringComparison.Ordinal) || manifest.ProductYear != productYear || !string.Equals(manifest.Architecture, architecture, StringComparison.OrdinalIgnoreCase) || !new PackageManifestValidator().Validate(manifest, productYear, architecture).IsValid) return null;
|
||||
if (binding is null || !Matches(requirement, binding) || !string.Equals(manifest.PackageId, requirement.PackageId, StringComparison.Ordinal) || !string.Equals(manifest.Version, version, StringComparison.Ordinal) || !string.Equals(manifest.Product, requirement.Product, StringComparison.Ordinal) || manifest.ProductYear != requirement.ProductYear || !string.Equals(manifest.Architecture, requirement.Architecture, StringComparison.OrdinalIgnoreCase) || !string.Equals(manifest.RolloutChannel, requirement.Channel, StringComparison.OrdinalIgnoreCase) || !string.Equals(manifest.Version, requirement.RequiredVersion, StringComparison.Ordinal) || !new PackageManifestValidator().Validate(manifest, requirement.ProductYear, requirement.Architecture).IsValid) return null;
|
||||
foreach (var file in manifest.Files)
|
||||
{
|
||||
var path = PackageFilePath(versionRoot, file.Path);
|
||||
@@ -78,4 +88,5 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP
|
||||
if (string.IsNullOrWhiteSpace(version) || !string.Equals(Path.GetFileName(version), version, StringComparison.Ordinal)) throw new InvalidOperationException("PACKAGE_VERSION_INVALID");
|
||||
return PackageFilePath(Path.Combine(PackageRoot(packageId), "versions"), version);
|
||||
}
|
||||
private static bool Matches(PackageRequirement requirement, CacheBinding binding) => string.Equals(requirement.PackageId, binding.PackageId, StringComparison.Ordinal) && string.Equals(requirement.Product, binding.Product, StringComparison.Ordinal) && requirement.ProductYear == binding.ProductYear && string.Equals(requirement.Architecture, binding.Architecture, StringComparison.OrdinalIgnoreCase) && string.Equals(requirement.Channel, binding.Channel, StringComparison.OrdinalIgnoreCase) && string.Equals(requirement.RequiredVersion, binding.RequiredVersion, StringComparison.Ordinal) && string.Equals(requirement.RosterIdentity, binding.RosterIdentity, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user