gittuf/internal/cache/cache.go
Paulo Gomes 4a53c1b214
refactor: standardize on githash.Hash
Signed-off-by: Paulo Gomes <paulo@entire.io>
2026-08-03 21:27:52 +01:00

235 lines
6.1 KiB
Go

// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0
package cache
import (
"encoding/json"
"errors"
"log/slog"
"github.com/gittuf/gittuf/internal/attestations"
"github.com/gittuf/gittuf/pkg/githash"
"github.com/gittuf/gittuf/pkg/gitinterface"
"github.com/gittuf/gittuf/pkg/gitstore"
"github.com/gittuf/gittuf/pkg/rsl"
)
const (
Ref = "refs/local/gittuf/persistent-cache"
persistentTreeEntryName = "persistentCache"
policyRef = "refs/gittuf/policy" // this is copied from internal/policy to avoid an import cycle
)
var (
ErrNoPersistentCache = errors.New("persistent cache not found")
ErrEntryNotNumbered = errors.New("one or more entries are not numbered")
)
type Persistent struct {
// PolicyEntries is a list of index values for entries pertaining to the
// policy ref. The list is ordered by each entry's Number.
PolicyEntries []RSLEntryIndex `json:"policyEntries"`
// AttestationEntries is a list of index values for entries pertaining to
// the attestations ref. The list is ordered by each entry's Number.
AttestationEntries []RSLEntryIndex `json:"attestationEntries"`
// AddedAttestationsBeforeNumber tracks the number up to which
// attestations have been searched for and added to
// attestationsEntryNumbers. We need to track this for attestations in
// particular because attestations are optional in gittuf repositories,
// meaning attestationsEntryNumbers may be empty which would trigger a
// full search.
AddedAttestationsBeforeNumber uint64 `json:"addedAttestationsBeforeNumber"`
// LastVerifiedEntryForRef is a map that indicates the last verified RSL
// entry for a ref.
LastVerifiedEntryForRef map[string]RSLEntryIndex `json:"lastVerifiedEntryForRef"`
}
func (p *Persistent) Commit(repo gitstore.Storer) error {
if len(p.PolicyEntries) == 0 && len(p.AttestationEntries) == 0 && p.AddedAttestationsBeforeNumber == 0 && len(p.LastVerifiedEntryForRef) == 0 {
// nothing to do
return nil
}
contents, err := json.Marshal(p)
if err != nil {
return err
}
blobID, err := repo.WriteBlob(contents)
if err != nil {
return err
}
treeID, err := repo.WriteTree([]gitstore.TreeEntry{{Path: persistentTreeEntryName, ID: blobID, Kind: gitstore.KindBlob}})
if err != nil {
return err
}
currentCommitID, _ := repo.GetReference(Ref) //nolint:errcheck
if !currentCommitID.IsZero() {
currentTreeID, err := repo.GetCommitTreeID(currentCommitID)
if err == nil && treeID.Equal(currentTreeID) {
// no change in cache contents, noop
return nil
}
}
_, err = repo.Commit(treeID, Ref, "Set persistent cache\n", false)
return err
}
// PopulatePersistentCache scans the repository's RSL and generates a persistent
// local-only cache of policy and attestation entries. This makes subsequent
// verifications faster.
func PopulatePersistentCache(repo gitstore.Storer) error {
persistent := &Persistent{
PolicyEntries: []RSLEntryIndex{},
AttestationEntries: []RSLEntryIndex{},
}
iterator, err := rsl.GetLatestEntry(repo)
if err != nil {
return err
}
if iterator.GetNumber() == 0 {
return ErrEntryNotNumbered
}
persistent.AddedAttestationsBeforeNumber = iterator.GetNumber()
for {
if iterator, isReferenceEntry := iterator.(*rsl.ReferenceEntry); isReferenceEntry {
switch iterator.RefName {
case policyRef:
persistent.InsertPolicyEntryNumber(iterator.GetNumber(), iterator.GetID())
case attestations.Ref:
persistent.InsertAttestationEntryNumber(iterator.GetNumber(), iterator.GetID())
}
}
iterator, err = rsl.GetParentForEntry(repo, iterator)
if err != nil {
if errors.Is(err, rsl.ErrRSLEntryNotFound) {
break
}
return err
}
if iterator.GetNumber() == 0 {
return ErrEntryNotNumbered
}
}
return persistent.Commit(repo)
}
// LoadPersistentCache loads the persistent cache from the tip of the local ref.
// If an instance has already been loaded and a pointer has been stored in
// memory, that instance is returned.
func LoadPersistentCache(repo gitstore.Storer) (*Persistent, error) {
slog.Debug("Loading persistent cache from disk...")
commitID, err := repo.GetReference(Ref)
if err != nil {
if errors.Is(err, gitinterface.ErrReferenceNotFound) {
// Persistent cache doesn't exist
slog.Debug("Persistent cache does not exist")
return nil, ErrNoPersistentCache
}
return nil, err
}
treeID, err := repo.GetCommitTreeID(commitID)
if err != nil {
return nil, err
}
allFiles, err := repo.GetAllFilesInTree(treeID)
if err != nil {
return nil, err
}
blobID, has := allFiles[persistentTreeEntryName]
if !has {
// Persistent cache doesn't seem to exist? This maybe warrants
// an error but we may have more than one file here in future?
slog.Debug("Persistent cache does not exist")
return nil, ErrNoPersistentCache
}
blob, err := repo.ReadBlob(blobID)
if err != nil {
return nil, err
}
persistentCache := &Persistent{}
if err := json.Unmarshal(blob, &persistentCache); err != nil {
return nil, err
}
slog.Debug("Loaded persistent cache")
return persistentCache, nil
}
// DeletePersistentCache deletes the local persistent cache ref.
func DeletePersistentCache(repo gitstore.Storer) error {
ref, err := repo.GetReference(Ref)
if err != nil {
if errors.Is(err, gitinterface.ErrReferenceNotFound) {
return ErrNoPersistentCache
}
return err
}
if ref.IsZero() {
return ErrNoPersistentCache
}
err = repo.DeleteReference(Ref)
if err != nil {
return err
}
return nil
}
// RSLEntryIndex is essentially a tuple that maps RSL entry IDs to numbers. This
// may be expanded in future to include more information as needed.
type RSLEntryIndex struct {
EntryID string `json:"entryID"`
EntryNumber uint64 `json:"entryNumber"`
}
func (r *RSLEntryIndex) GetEntryID() githash.Hash {
hash, _ := gitinterface.NewHash(r.EntryID)
// TODO: error?
return hash
}
func (r *RSLEntryIndex) GetEntryNumber() uint64 {
return r.EntryNumber
}
func binarySearch(a, b RSLEntryIndex) int {
if a.GetEntryNumber() == b.GetEntryNumber() {
// Exact match
return 0
}
if a.GetEntryNumber() < b.GetEntryNumber() {
// Precedes
return -1
}
// Succeeds
return 1
}