145 lines
5.1 KiB
Go
145 lines
5.1 KiB
Go
package artifacts
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
|
)
|
|
|
|
// AnalyzeEvidenceState distinguishes a verified current configured artifact
|
|
// from every form of unavailable evidence.
|
|
type AnalyzeEvidenceState string
|
|
|
|
const (
|
|
AnalyzeEvidenceCurrent AnalyzeEvidenceState = "current"
|
|
AnalyzeEvidenceNonCurrent AnalyzeEvidenceState = "non_current"
|
|
)
|
|
|
|
// AnalyzeEvidence is the read-only result of inspecting one configured
|
|
// artifact's manifest record and durable output.
|
|
type AnalyzeEvidence struct {
|
|
State AnalyzeEvidenceState
|
|
Reason string
|
|
SourceID string
|
|
Path string
|
|
ProducerRunID string
|
|
Contract *artifactmodel.ContractMetadata
|
|
Checksum string
|
|
Size int64
|
|
}
|
|
|
|
// InspectAnalyzeEvidence verifies that one configured artifact has supported,
|
|
// current manifest evidence for the exact canonical bytes on disk.
|
|
func InspectAnalyzeEvidence(
|
|
paths SessionPaths,
|
|
m *manifest.Manifest,
|
|
key string,
|
|
configured ConfiguredArtifactDefinition,
|
|
) AnalyzeEvidence {
|
|
normalizedKey := strings.TrimSpace(key)
|
|
sourceID := ConfiguredArtifactSourceID(normalizedKey)
|
|
nonCurrent := func(reason string) AnalyzeEvidence {
|
|
return AnalyzeEvidence{State: AnalyzeEvidenceNonCurrent, Reason: reason, SourceID: sourceID}
|
|
}
|
|
|
|
if m == nil {
|
|
return nonCurrent("analyze manifest evidence is absent; regenerate the artifact")
|
|
}
|
|
stageRecord := m.Stages["analyze"]
|
|
if stageRecord == nil || stageRecord.Name != "analyze" {
|
|
return nonCurrent("analyze manifest evidence is absent; regenerate the artifact")
|
|
}
|
|
if !stageRecord.HasVersionedAnalyzeState() {
|
|
return nonCurrent("analyze manifest evidence is legacy or unsupported; regenerate the artifact")
|
|
}
|
|
record, ok := stageRecord.AnalyzeArtifacts[normalizedKey]
|
|
if !ok {
|
|
return nonCurrent("configured artifact has no analyze manifest record; regenerate the artifact")
|
|
}
|
|
if record.Status != manifest.AnalyzeArtifactCurrent {
|
|
return nonCurrent(fmt.Sprintf("configured artifact manifest status is %q; regenerate the artifact", record.Status))
|
|
}
|
|
if err := manifest.ValidateAnalyzeArtifactCollection(
|
|
stageRecord.AnalyzeStateVersion,
|
|
map[string]manifest.AnalyzeArtifactRecord{normalizedKey: record},
|
|
); err != nil {
|
|
return nonCurrent("configured artifact manifest evidence is malformed; regenerate the artifact")
|
|
}
|
|
|
|
configuredPath, err := pathsafe.NormalizeRelativeDestination(strings.TrimSpace(configured.OutputPath))
|
|
if err != nil {
|
|
return nonCurrent("configured artifact output path is unsafe; correct the configuration")
|
|
}
|
|
if record.Output.LocalPath != configuredPath {
|
|
return nonCurrent("configured artifact manifest path differs from current configuration; regenerate the artifact")
|
|
}
|
|
if record.Output.SourceID != sourceID || record.Output.Kind != "scriptorium_artifact" {
|
|
return nonCurrent("configured artifact manifest identity is incompatible; regenerate the artifact")
|
|
}
|
|
|
|
file, err := fileops.OpenConfinedRegularFile(paths.Root, configuredPath)
|
|
if err != nil {
|
|
return nonCurrent("configured artifact output is missing or unsafe; regenerate the artifact")
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil || !info.Mode().IsRegular() {
|
|
return nonCurrent("configured artifact output is not a safe regular file; regenerate the artifact")
|
|
}
|
|
hash := sha256.New()
|
|
size, err := io.Copy(hash, file)
|
|
if err != nil {
|
|
return nonCurrent("configured artifact output could not be verified; regenerate the artifact")
|
|
}
|
|
if size != info.Size() || size != record.OutputSize {
|
|
return nonCurrent("configured artifact output size differs from manifest evidence; regenerate the artifact")
|
|
}
|
|
if hex.EncodeToString(hash.Sum(nil)) != record.Output.Checksum {
|
|
return nonCurrent("configured artifact output checksum differs from manifest evidence; regenerate the artifact")
|
|
}
|
|
|
|
return AnalyzeEvidence{
|
|
State: AnalyzeEvidenceCurrent,
|
|
SourceID: sourceID,
|
|
Path: filepath.Join(paths.Root, filepath.FromSlash(configuredPath)),
|
|
ProducerRunID: record.ProducerRunID,
|
|
Contract: cloneArtifactContract(record.Output.Contract),
|
|
Checksum: record.Output.Checksum,
|
|
Size: record.OutputSize,
|
|
}
|
|
}
|
|
|
|
// HydrateAnalyzeArtifacts makes configured sources available only from
|
|
// validated current manifest evidence. It never mutates manifest state.
|
|
func (c *ArtifactCatalog) HydrateAnalyzeArtifacts(
|
|
paths SessionPaths,
|
|
m *manifest.Manifest,
|
|
configured map[string]ConfiguredArtifactDefinition,
|
|
) {
|
|
if c == nil || len(configured) == 0 {
|
|
return
|
|
}
|
|
for _, entry := range c.ListConfigured() {
|
|
definition, ok := configured[entry.ConfiguredKey]
|
|
if !ok {
|
|
continue
|
|
}
|
|
evidence := InspectAnalyzeEvidence(paths, m, entry.ConfiguredKey, definition)
|
|
if evidence.State != AnalyzeEvidenceCurrent {
|
|
continue
|
|
}
|
|
_ = c.markAvailableFromAnalyzeManifest(
|
|
evidence.SourceID, evidence.Path, evidence.ProducerRunID,
|
|
evidence.Checksum, evidence.Size, evidence.Contract,
|
|
)
|
|
}
|
|
}
|