155 lines
5.3 KiB
Go
155 lines
5.3 KiB
Go
package artifacts
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"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,
|
|
sessionPrefix string,
|
|
pointerKey string,
|
|
validation CurrentStateValidation,
|
|
) (*CurrentState, error) {
|
|
_, pointerData, err := readCurrentStateControlObject(ctx, store, pointerKey, "current commit pointer", MaxCurrentCommitPointerBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pointer, err := DecodeCurrentCommitPointer(pointerData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := pointer.ValidateForSessionPrefix(sessionPrefix); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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)
|
|
}
|
|
commit, err := DecodeRemoteCommitManifest(commitData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := commit.ValidateForSessionPrefix(sessionPrefix); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validatePointerCommitIdentity(pointer, commit); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sessionManifest, ok := commit.Artifact(RemoteArtifactTypeSessionManifest)
|
|
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, "committed session manifest", MaxRemoteSessionManifestBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read committed session manifest %q: %w", sessionManifest.DestinationKey, err)
|
|
}
|
|
m, err := decodeCommittedManifest(ctx, manifestData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateCommitManifestIdentity(commit, m); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state := &CurrentState{
|
|
SessionPrefix: sessionPrefix,
|
|
CurrentRunIDKey: pointerKey,
|
|
CurrentPointerKey: pointerKey,
|
|
CurrentManifestKey: sessionManifest.DestinationKey,
|
|
RunID: commit.RunID,
|
|
Manifest: m,
|
|
ManifestData: append([]byte(nil), manifestData...),
|
|
Commit: commit,
|
|
Pointer: pointer,
|
|
}
|
|
if err := ValidateCurrentStateIdentity(state, validation); err != nil {
|
|
return nil, err
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func validatePointerCommitIdentity(pointer *CurrentCommitPointer, commit *RemoteCommitManifest) error {
|
|
if pointer == nil || commit == nil {
|
|
return fmt.Errorf("current commit pointer and remote commit are required")
|
|
}
|
|
if pointer.Campaign != commit.Campaign || pointer.SessionID != commit.SessionID || pointer.RunID != commit.RunID {
|
|
return fmt.Errorf("current commit pointer identity does not match selected remote commit")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateCommitManifestIdentity(commit *RemoteCommitManifest, m *manifest.Manifest) error {
|
|
if commit == nil || m == nil {
|
|
return fmt.Errorf("remote commit and committed session manifest are required")
|
|
}
|
|
if commit.Campaign != strings.TrimSpace(m.Campaign) ||
|
|
commit.SessionID != strings.TrimSpace(m.SessionID) ||
|
|
commit.RunID != strings.TrimSpace(m.RunID) {
|
|
return fmt.Errorf("selected remote commit identity does not match committed session manifest")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readVerifiedRemoteObject(
|
|
ctx context.Context,
|
|
store storage.ObjectStore,
|
|
key string,
|
|
wantSHA256 string,
|
|
wantSize int64,
|
|
wantGeneration string,
|
|
category string,
|
|
maxBytes int64,
|
|
) ([]byte, error) {
|
|
info, data, err := readCurrentStateControlObject(ctx, store, key, category, maxBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if int64(len(data)) != wantSize {
|
|
return nil, fmt.Errorf("size mismatch: got %d, want %d", len(data), wantSize)
|
|
}
|
|
if checksum := remoteObjectSHA256(data); checksum != wantSHA256 {
|
|
return nil, fmt.Errorf("checksum mismatch: got %s, want %s", checksum, wantSHA256)
|
|
}
|
|
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 {
|
|
return nil, fmt.Errorf("generation mismatch: got %q, want %q", info.ETag, wantGeneration)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
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 storage.ObjectInfo{}, nil, fmt.Errorf("read %s control object %q with %d-byte limit: %w", category, key, maxBytes, err)
|
|
}
|
|
return info, data, nil
|
|
}
|
|
|
|
func decodeCommittedManifest(ctx context.Context, data []byte) (*manifest.Manifest, error) {
|
|
m, err := (&manifest.LocalStore{}).LoadReader(ctx, bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("committed session manifest decode failed: %w", err)
|
|
}
|
|
return m, nil
|
|
}
|