feat: bind activation receipts to immutable content
This commit is contained in:
@@ -9,7 +9,7 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP
|
||||
{
|
||||
// Machine-process local serialization only; package roots must remain local.
|
||||
private static readonly SemaphoreSlim PointerGate = new(1, 1);
|
||||
private sealed record Pointer(string Current, string? Previous, int SchemaVersion = 1, long Generation = 0, string? ActivationOperationId = null, DateTimeOffset? ActivatedAtUtc = null);
|
||||
private sealed record Pointer(string Current, string? Previous, int SchemaVersion = 1, long Generation = 0, string? ActivationOperationId = null, DateTimeOffset? ActivatedAtUtc = null, string? ManifestSha256 = null, string? PayloadSetSha256 = null);
|
||||
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";
|
||||
@@ -29,19 +29,19 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP
|
||||
await PointerGate.WaitAsync(ct); try
|
||||
{
|
||||
var dir = VersionDirectory(id, version); if (!Directory.Exists(dir)) throw new DirectoryNotFoundException(dir);
|
||||
Directory.CreateDirectory(PackageRoot(id)); var prior = await ReadPointer(id, ct); var operationId=Guid.NewGuid().ToString("N");
|
||||
var temp = PointerPath(id) + "." + operationId + ".tmp"; await using (var stream=new FileStream(temp,FileMode.CreateNew,FileAccess.Write,FileShare.None)) { await JsonSerializer.SerializeAsync(stream,new Pointer(version, prior?.Current,1,(prior?.Generation??0)+1,operationId,DateTimeOffset.UtcNow),cancellationToken:ct); await stream.FlushAsync(ct); }
|
||||
Directory.CreateDirectory(PackageRoot(id)); var prior = await ReadPointer(id, ct); var operationId=Guid.NewGuid().ToString("N"); var content=await ContentFingerprintAsync(id,version,ct);
|
||||
var temp = PointerPath(id) + "." + operationId + ".tmp"; await using (var stream=new FileStream(temp,FileMode.CreateNew,FileAccess.Write,FileShare.None)) { await JsonSerializer.SerializeAsync(stream,new Pointer(version, prior?.Current,1,(prior?.Generation??0)+1,operationId,DateTimeOffset.UtcNow,content.Manifest,content.PayloadSet),cancellationToken:ct); await stream.FlushAsync(ct); }
|
||||
File.Move(temp, PointerPath(id), true); var verified=await ReadPointer(id,ct); if (verified?.Current!=version || verified.ActivationOperationId!=operationId) throw new InvalidOperationException("POINTER_REVALIDATION_FAILED");
|
||||
return new(version, prior?.Current);
|
||||
} finally { PointerGate.Release(); }
|
||||
}
|
||||
public async Task<PackageActivationResult> ActivateImmutableAsync(ImmutableLocalPackage package, PackageRequirement requirement, CancellationToken ct)
|
||||
{
|
||||
if (!string.Equals(package.PackageId, requirement.PackageId, StringComparison.Ordinal) || !string.Equals(package.Version, requirement.RequiredVersion, StringComparison.Ordinal)) return new("Blocked", null);
|
||||
if (!string.Equals(package.PackageId, requirement.PackageId, StringComparison.Ordinal) || !string.Equals(package.Version, requirement.RequiredVersion, StringComparison.Ordinal) || !await MatchesImmutableManifestAsync(package,requirement,ct)) return new("Blocked", null);
|
||||
var prior = await ReadPointer(package.PackageId, ct);
|
||||
if (prior?.Current == package.Version) return new("AlreadyActiveExact", new(prior.ActivationOperationId ?? Guid.NewGuid().ToString("N"), package.PackageId, package.Version, prior.Previous, prior.ActivatedAtUtc ?? DateTimeOffset.UtcNow, prior.Generation, PackageRoot(package.PackageId)));
|
||||
if (prior?.Current == package.Version) return new("AlreadyActiveExact", new(prior.ActivationOperationId ?? Guid.NewGuid().ToString("N"), package.PackageId, package.Version, prior.Previous, prior.ActivatedAtUtc ?? DateTimeOffset.UtcNow, prior.Generation, PackageRoot(package.PackageId),1,prior.ManifestSha256,prior.PayloadSetSha256,requirement.RosterIdentity));
|
||||
var activated = await ActivateAsync(package.PackageId, package.Version, ct);
|
||||
var after=await ReadPointer(package.PackageId,ct); return new("Activated", new(after!.ActivationOperationId!, package.PackageId, activated.CurrentVersion, activated.PreviousVersion, after.ActivatedAtUtc ?? DateTimeOffset.UtcNow, after.Generation, PackageRoot(package.PackageId)));
|
||||
var after=await ReadPointer(package.PackageId,ct); return new("Activated", new(after!.ActivationOperationId!, package.PackageId, activated.CurrentVersion, activated.PreviousVersion, after.ActivatedAtUtc ?? DateTimeOffset.UtcNow, after.Generation, PackageRoot(package.PackageId),1,after.ManifestSha256,after.PayloadSetSha256,requirement.RosterIdentity));
|
||||
}
|
||||
public async Task<PackageRollbackResult> RollbackAsync(PackageActivationReceipt receipt, CancellationToken ct)
|
||||
{
|
||||
@@ -114,6 +114,10 @@ public sealed class LocalPackageStore(string root, IFileHasher hasher) : ILocalP
|
||||
}
|
||||
return versionRoot;
|
||||
}
|
||||
private async Task<(string Manifest,string PayloadSet)> ContentFingerprintAsync(string id,string version,CancellationToken ct)
|
||||
{ var dir=VersionDirectory(id,version); var manifestPath=Path.Combine(dir,StoredManifestFileName); var manifest=PackageManifestJson.Parse(await File.ReadAllTextAsync(manifestPath,ct)); var manifestHash=await hasher.Sha256Async(manifestPath,ct); var payload=string.Join("|",manifest.Files.OrderBy(x=>x.Path,StringComparer.Ordinal).Select(x=>$"{x.Path}:{x.Length}:{x.Sha256}")); return (manifestHash,Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(payload)))); }
|
||||
private async Task<bool> MatchesImmutableManifestAsync(ImmutableLocalPackage package,PackageRequirement requirement,CancellationToken ct)
|
||||
{ try { if (!package.LocalPackageRoot.Equals(VersionDirectory(package.PackageId,package.Version),StringComparison.OrdinalIgnoreCase)) return false; var manifest=PackageManifestJson.Parse(await File.ReadAllTextAsync(Path.Combine(package.LocalPackageRoot,StoredManifestFileName),ct)); if (!string.Equals(manifest.PackageId,requirement.PackageId,StringComparison.Ordinal)||!string.Equals(manifest.Version,requirement.RequiredVersion,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)) return false; foreach(var f in manifest.Files){var p=PackageFilePath(package.LocalPackageRoot,f.Path); var i=new FileInfo(p); if(!i.Exists||i.Length!=f.Length||!string.Equals(await hasher.Sha256Async(p,ct),f.Sha256,StringComparison.OrdinalIgnoreCase)) return false;} return true; } catch { return false; } }
|
||||
private static string PackageFilePath(string packageRoot, string relativePath)
|
||||
{
|
||||
var root = Path.GetFullPath(packageRoot).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
|
||||
Reference in New Issue
Block a user