Bound remote control object reads

This commit is contained in:
2026-08-11 03:43:18 +00:00
parent 2545faef6c
commit 8ef6e99d69
18 changed files with 535 additions and 227 deletions

View File

@@ -1,15 +1,24 @@
package artifacts
import (
"bytes"
"context"
"fmt"
"os"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
const (
// MaxCurrentCommitPointerBytes bounds the mutable commit-selection record.
MaxCurrentCommitPointerBytes int64 = 64 << 10
// MaxRemoteCommitManifestBytes bounds an immutable commit manifest.
MaxRemoteCommitManifestBytes int64 = 4 << 20
// MaxRemoteSessionManifestBytes bounds the selected session manifest.
MaxRemoteSessionManifestBytes int64 = 8 << 20
)
func loadCommittedCurrentState(
ctx context.Context,
store storage.ObjectStore,
@@ -17,9 +26,9 @@ func loadCommittedCurrentState(
pointerKey string,
validation CurrentStateValidation,
) (*CurrentState, error) {
pointerData, err := downloadRemoteObject(ctx, store, pointerKey, "narratio-current-commit-pointer-*.json")
_, pointerData, err := readCurrentStateControlObject(ctx, store, pointerKey, "current commit pointer", MaxCurrentCommitPointerBytes)
if err != nil {
return nil, fmt.Errorf("download current commit pointer %q: %w", pointerKey, err)
return nil, err
}
pointer, err := DecodeCurrentCommitPointer(pointerData)
if err != nil {
@@ -29,7 +38,7 @@ func loadCommittedCurrentState(
return nil, err
}
commitData, err := readVerifiedRemoteObject(ctx, store, pointer.CommitKey, pointer.CommitSHA256, pointer.CommitSize, pointer.CommitGeneration, "narratio-remote-commit-*.json")
commitData, err := readVerifiedRemoteObject(ctx, store, pointer.CommitKey, pointer.CommitSHA256, pointer.CommitSize, pointer.CommitGeneration, "remote commit manifest", MaxRemoteCommitManifestBytes)
if err != nil {
return nil, fmt.Errorf("read selected remote commit %q: %w", pointer.CommitKey, err)
}
@@ -48,7 +57,7 @@ func loadCommittedCurrentState(
if !ok {
return nil, fmt.Errorf("remote commit must declare exactly one session manifest artifact")
}
manifestData, err := readVerifiedRemoteObject(ctx, store, sessionManifest.DestinationKey, sessionManifest.SHA256, sessionManifest.Size, sessionManifest.Generation, "narratio-remote-session-manifest-*.json")
manifestData, err := readVerifiedRemoteObject(ctx, store, sessionManifest.DestinationKey, sessionManifest.SHA256, sessionManifest.Size, sessionManifest.Generation, "committed session manifest", MaxRemoteSessionManifestBytes)
if err != nil {
return nil, fmt.Errorf("read committed session manifest %q: %w", sessionManifest.DestinationKey, err)
}
@@ -106,9 +115,10 @@ func readVerifiedRemoteObject(
wantSHA256 string,
wantSize int64,
wantGeneration string,
tempPattern string,
category string,
maxBytes int64,
) ([]byte, error) {
data, err := downloadRemoteObject(ctx, store, key, tempPattern)
info, data, err := readCurrentStateControlObject(ctx, store, key, category, maxBytes)
if err != nil {
return nil, err
}
@@ -118,11 +128,7 @@ func readVerifiedRemoteObject(
if checksum := remoteObjectSHA256(data); checksum != wantSHA256 {
return nil, fmt.Errorf("checksum mismatch: got %s, want %s", checksum, wantSHA256)
}
info, err := remoteObjectInfo(ctx, store, key)
if err != nil {
return nil, err
}
if info.Size != wantSize {
if info.Size > 0 && info.Size != wantSize {
return nil, fmt.Errorf("storage size mismatch: got %d, want %d", info.Size, wantSize)
}
if strings.TrimSpace(info.ETag) != wantGeneration {
@@ -131,56 +137,16 @@ func readVerifiedRemoteObject(
return data, nil
}
func downloadRemoteObject(ctx context.Context, store storage.ObjectStore, key, tempPattern string) ([]byte, error) {
localPath, err := storage.DownloadObjectToTemp(ctx, store, key, tempPattern)
func readCurrentStateControlObject(ctx context.Context, store storage.ObjectStore, key, category string, maxBytes int64) (storage.ObjectInfo, []byte, error) {
info, data, err := storage.ReadObjectBounded(ctx, store, key, maxBytes)
if err != nil {
return nil, err
return storage.ObjectInfo{}, nil, fmt.Errorf("read %s control object %q with %d-byte limit: %w", category, key, maxBytes, err)
}
defer func() { _ = os.Remove(localPath) }()
data, err := os.ReadFile(localPath)
if err != nil {
return nil, fmt.Errorf("read downloaded object %q: %w", key, err)
}
return data, nil
}
func remoteObjectInfo(ctx context.Context, store storage.ObjectStore, key string) (storage.ObjectInfo, error) {
objects, err := store.List(ctx, key)
if err != nil {
return storage.ObjectInfo{}, fmt.Errorf("list remote object %q: %w", key, err)
}
var found *storage.ObjectInfo
for _, object := range objects {
if object.Key != key {
continue
}
if found != nil {
return storage.ObjectInfo{}, fmt.Errorf("remote object %q is ambiguous", key)
}
copy := object
found = &copy
}
if found == nil {
return storage.ObjectInfo{}, fmt.Errorf("remote object %q is missing", key)
}
return *found, nil
return info, data, nil
}
func decodeCommittedManifest(ctx context.Context, data []byte) (*manifest.Manifest, error) {
file, err := os.CreateTemp("", "narratio-committed-session-manifest-*.json")
if err != nil {
return nil, fmt.Errorf("create committed manifest file: %w", err)
}
path := file.Name()
defer func() { _ = os.Remove(path) }()
if _, err := file.Write(data); err != nil {
_ = file.Close()
return nil, fmt.Errorf("write committed manifest file: %w", err)
}
if err := file.Close(); err != nil {
return nil, fmt.Errorf("close committed manifest file: %w", err)
}
m, err := (&manifest.LocalStore{}).Load(ctx, path)
m, err := (&manifest.LocalStore{}).LoadReader(ctx, bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("committed session manifest decode failed: %w", err)
}