181 lines
6.1 KiB
Go
181 lines
6.1 KiB
Go
package artifacts
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
)
|
|
|
|
const (
|
|
extractStageName = "extract"
|
|
extractionLaneKind = "notarius_lane"
|
|
extractionIndexKind = "notarius_index"
|
|
extractionMetadataRun = "narratio_run_id"
|
|
extractionMetadataRoot = "bundle_root"
|
|
)
|
|
|
|
type hydratedExtraction struct {
|
|
sourceID string
|
|
path string
|
|
}
|
|
|
|
// HydrateExtractionArtifacts marks extraction sources available only when the current
|
|
// manifest contains one complete, internally consistent, succeeded extraction bundle.
|
|
// Invalid, stale, incomplete, or unsafe records leave every extraction source unavailable.
|
|
func (c *ArtifactCatalog) HydrateExtractionArtifacts(
|
|
paths SessionPaths,
|
|
m *manifest.Manifest,
|
|
configured map[string]ExtractionArtifactDefinition,
|
|
) {
|
|
if c == nil || m == nil || len(configured) == 0 {
|
|
return
|
|
}
|
|
record := m.Stages[extractStageName]
|
|
if record == nil || record.Name != extractStageName || record.Status != manifest.StatusSucceeded {
|
|
return
|
|
}
|
|
producerRunID := extractionMetadataString(record.Metadata, extractionMetadataRun)
|
|
if !safeExtractionPathSegment(producerRunID) {
|
|
return
|
|
}
|
|
bundleRoot := filepath.Clean(filepath.Join(paths.ArtifactsDir, "notarius", producerRunID))
|
|
if !filepath.IsAbs(bundleRoot) || extractionMetadataString(record.Metadata, extractionMetadataRoot) != bundleRoot {
|
|
return
|
|
}
|
|
if !safeExistingExtractionDirectory(paths.Root, bundleRoot) {
|
|
return
|
|
}
|
|
receiptRunID, receiptPipelineID := extractionReceiptIdentity(record.Metadata)
|
|
if receiptRunID == "" || receiptPipelineID == "" {
|
|
return
|
|
}
|
|
|
|
expected := make(map[string]ExtractionArtifactDefinition, len(configured))
|
|
for key, definition := range configured {
|
|
sourceID, ok := c.SourceIDForExtractionKey(key)
|
|
if !ok {
|
|
return
|
|
}
|
|
expected[sourceID] = definition
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(expected))
|
|
hydrated := make([]hydratedExtraction, 0, len(expected))
|
|
indexSeen := false
|
|
for _, output := range record.Outputs {
|
|
if strings.TrimSpace(output.ProducerRunID) != producerRunID {
|
|
return
|
|
}
|
|
if output.SourceID == "" {
|
|
if indexSeen || output.Kind != extractionIndexKind || filepath.Clean(output.LocalPath) != filepath.Join(bundleRoot, "index.json") ||
|
|
!validExtractionPayload(bundleRoot, output.LocalPath, output.Checksum) {
|
|
return
|
|
}
|
|
indexSeen = true
|
|
continue
|
|
}
|
|
|
|
definition, ok := expected[output.SourceID]
|
|
if !ok || output.Kind != extractionLaneKind {
|
|
return
|
|
}
|
|
if _, duplicate := seen[output.SourceID]; duplicate {
|
|
return
|
|
}
|
|
if !compatibleCatalogExtractionContract(output.Contract, definition) ||
|
|
!compatibleCatalogExtractionProvenance(output.ExternalProvenance, receiptRunID, receiptPipelineID, definition) ||
|
|
!validExtractionPayload(bundleRoot, output.LocalPath, output.Checksum) {
|
|
return
|
|
}
|
|
seen[output.SourceID] = struct{}{}
|
|
hydrated = append(hydrated, hydratedExtraction{sourceID: output.SourceID, path: output.LocalPath})
|
|
}
|
|
if !indexSeen || len(seen) != len(expected) || len(record.Outputs) != len(expected)+1 {
|
|
return
|
|
}
|
|
for _, item := range hydrated {
|
|
_ = c.markAvailableFromExtractManifest(item.sourceID, item.path, producerRunID)
|
|
}
|
|
}
|
|
|
|
func compatibleCatalogExtractionContract(got *artifactmodel.ContractMetadata, want ExtractionArtifactDefinition) bool {
|
|
return got != nil && got.MediaType == want.MediaType && got.SchemaID == want.SchemaID &&
|
|
got.SchemaVersion == want.SchemaVersion && (want.ModuleKey == "" || got.ModuleKey == want.ModuleKey)
|
|
}
|
|
|
|
func compatibleCatalogExtractionProvenance(
|
|
got *artifactmodel.ExternalProvenance,
|
|
runID, pipelineID string,
|
|
want ExtractionArtifactDefinition,
|
|
) bool {
|
|
return got != nil && got.System == "notarius" && got.RunID == runID && got.PipelineID == pipelineID &&
|
|
pipelineID == strings.TrimSpace(want.PipelineID) && got.ArtifactID == want.LaneID
|
|
}
|
|
|
|
func validExtractionPayload(bundleRoot, path, checksum string) bool {
|
|
if !filepath.IsAbs(path) || !pathWithinExtractionRoot(bundleRoot, path) || strings.TrimSpace(checksum) == "" {
|
|
return false
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
|
return false
|
|
}
|
|
if !safeExtractionComponents(bundleRoot, path) {
|
|
return false
|
|
}
|
|
actual, err := SHA256File(path)
|
|
if err != nil || actual != checksum {
|
|
return false
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
return err == nil && json.Valid(body)
|
|
}
|
|
|
|
func safeExistingExtractionDirectory(sessionRoot, bundleRoot string) bool {
|
|
if !pathWithinExtractionRoot(sessionRoot, bundleRoot) || !safeExtractionComponents(sessionRoot, bundleRoot) {
|
|
return false
|
|
}
|
|
info, err := os.Lstat(bundleRoot)
|
|
return err == nil && info.IsDir() && info.Mode()&os.ModeSymlink == 0
|
|
}
|
|
|
|
func safeExtractionComponents(root, target string) bool {
|
|
relative, err := filepath.Rel(filepath.Clean(root), filepath.Clean(target))
|
|
if err != nil || relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
|
return false
|
|
}
|
|
current := filepath.Clean(root)
|
|
for _, part := range strings.Split(relative, string(filepath.Separator)) {
|
|
current = filepath.Join(current, part)
|
|
info, err := os.Lstat(current)
|
|
if err != nil || info.Mode()&os.ModeSymlink != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func pathWithinExtractionRoot(root, target string) bool {
|
|
relative, err := filepath.Rel(filepath.Clean(root), filepath.Clean(target))
|
|
return err == nil && relative != "." && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
|
|
}
|
|
|
|
func safeExtractionPathSegment(value string) bool {
|
|
return value != "" && value != "." && value != ".." && filepath.Base(value) == value &&
|
|
!strings.ContainsAny(value, `/\\`)
|
|
}
|
|
|
|
func extractionMetadataString(metadata map[string]any, key string) string {
|
|
value, _ := metadata[key].(string)
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func extractionReceiptIdentity(metadata map[string]any) (string, string) {
|
|
receipt, _ := metadata["receipt"].(map[string]any)
|
|
return extractionMetadataString(receipt, "run_id"), extractionMetadataString(receipt, "pipeline_id")
|
|
}
|