Files
narratio/internal/adapters/notarius/subprocess.go

761 lines
29 KiB
Go

package notarius
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
)
const (
maxReceiptBytes = 1 << 20
maxIndexBytes = 4 << 20
maxSummaryBytes = 4 << 20
canonicalIndexFile = "index.json"
canonicalManifestFile = "manifest.json"
canonicalRejectedFile = "rejected.json"
canonicalWarningsFile = "warnings.json"
canonicalDiagnosticsFile = "diagnostics.json"
warningsSchemaVersion = "notarius.warnings.v2"
diagnosticsSchemaVersion = "notarius.diagnostics.v1"
maxWarningGroups = 128
maxDiagnosticGroups = 256
maxFindingSamples = 3
)
type subprocessRun func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error)
// SubprocessRunner invokes Notarius through its public CLI.
type SubprocessRunner struct {
run subprocessRun
}
// NewSubprocessRunner constructs a production Notarius subprocess runner.
func NewSubprocessRunner() *SubprocessRunner {
return &SubprocessRunner{run: subprocess.Run}
}
// Run executes a complete Notarius pipeline and discovers its published bundle.
func (r *SubprocessRunner) Run(ctx context.Context, req RunRequest) (RunResult, error) {
if r == nil || r.run == nil {
return RunResult{}, fmt.Errorf("notarius subprocess runner is nil")
}
if err := validateRunRequest(req); err != nil {
return RunResult{}, err
}
args := []string{
"run", req.PipelineID,
"--config", req.ConfigPath,
"--input", req.InputPath,
"--output-dir", req.OutputRoot,
"--json",
}
processResult, err := r.run(ctx, subprocess.RunRequest{
Executable: req.Binary,
Args: args,
WorkingDir: req.WorkingDirectory,
Timeout: req.Timeout,
DiagnosticOwner: "notarius",
StdoutLogPath: req.ReceiptPath,
StderrLogPath: req.LogPath,
})
baseResult := RunResult{
ReceiptPath: req.ReceiptPath,
LogPath: req.LogPath,
ExitCode: processResult.ExitCode,
Duration: processResult.Duration,
}
if err != nil {
return baseResult, fmt.Errorf("run notarius pipeline %q: %w", req.PipelineID, err)
}
receipt, err := loadReceipt(req.ReceiptPath, req.PipelineID)
if err != nil {
return baseResult, err
}
bundleRoot, err := validateBundleRoot(req.OutputRoot, receipt.OutputDirectory)
if err != nil {
return baseResult, err
}
indexPath, err := resolveRegularFile(bundleRoot, receipt.IndexFile)
if err != nil {
return baseResult, fmt.Errorf("resolve receipt index file: %w", err)
}
index, err := loadIndex(bundleRoot, indexPath)
if err != nil {
return baseResult, err
}
rejections, err := loadRejections(index.RejectedPath)
if err != nil {
return baseResult, err
}
warnings, err := loadWarnings(index.WarningsPath)
if err != nil {
return baseResult, err
}
diagnostics, diagnosticOccurrences, diagnosticsTruncated, err := loadDiagnostics(index.DiagnosticsPath)
if err != nil {
return baseResult, err
}
if receipt.NormalizedOutputCount != len(index.Lanes) || receipt.RejectedOutputCount != len(rejections) ||
receipt.WarningGroupCount != len(warnings) || receipt.DiagnosticGroupCount != len(diagnostics) {
return baseResult, fmt.Errorf("notarius receipt counts do not match published bundle")
}
warningOccurrences, err := sumWarningOccurrences(warnings)
if err != nil {
return baseResult, err
}
if receipt.WarningOccurrenceCount != warningOccurrences ||
receipt.DiagnosticOccurrenceCount != diagnosticOccurrences ||
receipt.DiagnosticsTruncated != diagnosticsTruncated {
return baseResult, fmt.Errorf("notarius receipt occurrence counts do not match published bundle")
}
baseResult.Receipt = receipt
baseResult.Index = index
baseResult.BundleRoot = bundleRoot
baseResult.Rejections = rejections
baseResult.Warnings = warnings
baseResult.Diagnostics = diagnostics
return baseResult, nil
}
func validateRunRequest(req RunRequest) error {
if strings.TrimSpace(req.Binary) == "" {
return fmt.Errorf("notarius binary is required")
}
if strings.TrimSpace(req.PipelineID) == "" {
return fmt.Errorf("notarius pipeline id is required")
}
if req.Timeout <= 0 {
return fmt.Errorf("notarius timeout must be positive")
}
for label, path := range map[string]string{
"config": req.ConfigPath,
"input": req.InputPath,
"output root": req.OutputRoot,
"working directory": req.WorkingDirectory,
"receipt": req.ReceiptPath,
"log": req.LogPath,
} {
if strings.TrimSpace(path) == "" {
return fmt.Errorf("notarius %s path is required", label)
}
if !filepath.IsAbs(path) {
return fmt.Errorf("notarius %s path must be absolute", label)
}
}
if filepath.Clean(req.ReceiptPath) == filepath.Clean(req.LogPath) {
return fmt.Errorf("notarius receipt and log paths must be different")
}
if err := requireRegularFile(req.ConfigPath); err != nil {
return fmt.Errorf("validate notarius config path: %w", err)
}
if err := requireRegularFile(req.InputPath); err != nil {
return fmt.Errorf("validate notarius input path: %w", err)
}
if err := requireDirectory(req.OutputRoot); err != nil {
return fmt.Errorf("validate notarius output root: %w", err)
}
if err := requireDirectory(req.WorkingDirectory); err != nil {
return fmt.Errorf("validate notarius working directory: %w", err)
}
if err := validateLogDestination(req.ReceiptPath); err != nil {
return fmt.Errorf("validate notarius receipt path: %w", err)
}
if err := validateLogDestination(req.LogPath); err != nil {
return fmt.Errorf("validate notarius log path: %w", err)
}
return nil
}
type receiptDocument struct {
SchemaVersion string `json:"schema_version"`
RunID string `json:"run_id"`
PipelineID string `json:"pipeline_id"`
OutputDirectory string `json:"output_directory"`
IndexFile string `json:"index_file"`
NormalizedOutputCount *int `json:"normalized_output_count"`
RejectedOutputCount *int `json:"rejected_output_count"`
WarningGroupCount *int `json:"warning_group_count"`
WarningOccurrenceCount *int `json:"warning_occurrence_count"`
DiagnosticGroupCount *int `json:"diagnostic_group_count"`
DiagnosticOccurrenceCount *int `json:"diagnostic_occurrence_count"`
DiagnosticsTruncated *bool `json:"diagnostics_truncated"`
ValidationStatus string `json:"validation_status"`
ValidationSummaries []validationSummaryDocument `json:"validation_summaries"`
DebugDirectory string `json:"debug_directory"`
}
type validationSummaryDocument struct {
Stage string `json:"stage"`
StepID string `json:"step_id"`
LaneID string `json:"lane_id"`
ModuleKey string `json:"module_key"`
ChunkID string `json:"chunk_id"`
Status string `json:"status"`
RejectingValidators []string `json:"rejecting_validators"`
ReasonCodes []string `json:"reason_codes"`
IncompleteValidators []string `json:"incomplete_validators"`
ProducerAttemptCount *int `json:"producer_attempt_count"`
TerminalAction string `json:"terminal_action"`
}
func validValidationStatus(value string) bool {
switch value {
case "approved", "rejected", "incomplete":
return true
default:
return false
}
}
func validateValidationSummaries(documents []validationSummaryDocument) ([]ValidationSummary, error) {
summaries := make([]ValidationSummary, 0, len(documents))
for _, document := range documents {
if document.Status != "complete" && document.Status != "rejected" && document.Status != "incomplete" {
return nil, fmt.Errorf("notarius validation summary status %q is invalid", document.Status)
}
if document.ProducerAttemptCount == nil || *document.ProducerAttemptCount <= 0 || !validTerminalAction(document.TerminalAction) {
return nil, fmt.Errorf("notarius validation summary is missing required fields")
}
summaries = append(summaries, ValidationSummary{
Stage: document.Stage, StepID: document.StepID, LaneID: document.LaneID,
ModuleKey: document.ModuleKey, ChunkID: document.ChunkID, Status: document.Status,
RejectingValidators: append([]string(nil), document.RejectingValidators...),
ReasonCodes: append([]string(nil), document.ReasonCodes...),
IncompleteValidators: append([]string(nil), document.IncompleteValidators...),
ProducerAttemptCount: *document.ProducerAttemptCount, TerminalAction: document.TerminalAction,
})
}
return summaries, nil
}
func validTerminalAction(value string) bool {
switch value {
case "accepted", "reject_output", "warn_continue", "fail_run":
return true
default:
return false
}
}
func loadReceipt(path, pipelineID string) (Receipt, error) {
var document receiptDocument
if err := decodeBoundedJSON(path, maxReceiptBytes, &document); err != nil {
return Receipt{}, fmt.Errorf("decode notarius receipt: %w", err)
}
if document.SchemaVersion != ReceiptSchemaVersion {
return Receipt{}, fmt.Errorf("unsupported notarius receipt schema version %q", document.SchemaVersion)
}
if strings.TrimSpace(document.RunID) == "" || strings.TrimSpace(document.PipelineID) == "" ||
strings.TrimSpace(document.OutputDirectory) == "" || strings.TrimSpace(document.ValidationStatus) == "" ||
document.NormalizedOutputCount == nil ||
document.RejectedOutputCount == nil || document.WarningGroupCount == nil ||
document.WarningOccurrenceCount == nil || document.DiagnosticGroupCount == nil ||
document.DiagnosticOccurrenceCount == nil || document.DiagnosticsTruncated == nil {
return Receipt{}, fmt.Errorf("notarius receipt is missing required fields")
}
if document.IndexFile != canonicalIndexFile {
return Receipt{}, fmt.Errorf("notarius receipt index_file %q is incompatible; want %q", document.IndexFile, canonicalIndexFile)
}
if document.PipelineID != pipelineID {
return Receipt{}, fmt.Errorf("notarius receipt pipeline id %q does not match requested pipeline %q", document.PipelineID, pipelineID)
}
if *document.NormalizedOutputCount < 0 || *document.RejectedOutputCount < 0 ||
*document.WarningGroupCount < 0 || *document.WarningOccurrenceCount < 0 ||
*document.DiagnosticGroupCount < 0 || *document.DiagnosticOccurrenceCount < 0 {
return Receipt{}, fmt.Errorf("notarius receipt counts must be non-negative")
}
if !validValidationStatus(document.ValidationStatus) {
return Receipt{}, fmt.Errorf("notarius receipt validation_status %q is invalid", document.ValidationStatus)
}
validationSummaries, err := validateValidationSummaries(document.ValidationSummaries)
if err != nil {
return Receipt{}, err
}
if !filepath.IsAbs(document.OutputDirectory) {
return Receipt{}, fmt.Errorf("notarius receipt output directory must be absolute")
}
if document.DebugDirectory != "" && !filepath.IsAbs(document.DebugDirectory) {
return Receipt{}, fmt.Errorf("notarius receipt debug directory must be absolute when present")
}
return Receipt{
SchemaVersion: document.SchemaVersion,
RunID: document.RunID,
PipelineID: document.PipelineID,
OutputDirectory: filepath.Clean(document.OutputDirectory),
IndexFile: document.IndexFile,
NormalizedOutputCount: *document.NormalizedOutputCount,
RejectedOutputCount: *document.RejectedOutputCount,
WarningGroupCount: *document.WarningGroupCount,
WarningOccurrenceCount: *document.WarningOccurrenceCount,
DiagnosticGroupCount: *document.DiagnosticGroupCount,
DiagnosticOccurrenceCount: *document.DiagnosticOccurrenceCount,
DiagnosticsTruncated: *document.DiagnosticsTruncated,
ValidationStatus: document.ValidationStatus,
ValidationSummaries: validationSummaries,
DebugDirectory: document.DebugDirectory,
}, nil
}
type indexDocument struct {
ManifestFile string `json:"manifest_file"`
OutputFiles *[]laneDocument `json:"output_files"`
RejectedFile string `json:"rejected_file"`
WarningsFile string `json:"warnings_file"`
DiagnosticsFile string `json:"diagnostics_file"`
ChunkMap *pipelineDocument `json:"chunk_map"`
EvidenceContext *pipelineDocument `json:"evidence_context"`
}
type laneDocument struct {
LaneID string `json:"lane_id"`
File string `json:"file"`
MediaType string `json:"media_type"`
ModuleKey string `json:"module_key"`
SchemaID string `json:"schema_id"`
SchemaName string `json:"schema_name"`
SchemaVersion string `json:"schema_version"`
}
type pipelineDocument struct {
ArtifactKind string `json:"artifact_kind"`
File string `json:"file"`
MediaType string `json:"media_type"`
SchemaID string `json:"schema_id"`
SchemaName string `json:"schema_name"`
SchemaVersion string `json:"schema_version"`
}
func loadIndex(bundleRoot, indexPath string) (Index, error) {
var document indexDocument
if err := decodeBoundedJSON(indexPath, maxIndexBytes, &document); err != nil {
return Index{}, fmt.Errorf("decode notarius index: %w", err)
}
for _, field := range []struct {
name string
got string
want string
}{
{name: "manifest_file", got: document.ManifestFile, want: canonicalManifestFile},
{name: "rejected_file", got: document.RejectedFile, want: canonicalRejectedFile},
{name: "warnings_file", got: document.WarningsFile, want: canonicalWarningsFile},
{name: "diagnostics_file", got: document.DiagnosticsFile, want: canonicalDiagnosticsFile},
} {
if field.got != field.want {
return Index{}, fmt.Errorf("notarius index %s %q is incompatible; want %q", field.name, field.got, field.want)
}
}
if document.OutputFiles == nil {
return Index{}, fmt.Errorf("notarius index is missing required output_files")
}
index := Index{
Path: indexPath,
ManifestFile: document.ManifestFile,
RejectedFile: document.RejectedFile,
WarningsFile: document.WarningsFile,
DiagnosticsFile: document.DiagnosticsFile,
}
var err error
if index.ManifestPath, err = resolveRegularFile(bundleRoot, index.ManifestFile); err != nil {
return Index{}, fmt.Errorf("resolve notarius manifest file: %w", err)
}
if index.RejectedPath, err = resolveRegularFile(bundleRoot, index.RejectedFile); err != nil {
return Index{}, fmt.Errorf("resolve notarius rejection file: %w", err)
}
if index.WarningsPath, err = resolveRegularFile(bundleRoot, index.WarningsFile); err != nil {
return Index{}, fmt.Errorf("resolve notarius warning file: %w", err)
}
if index.DiagnosticsPath, err = resolveRegularFile(bundleRoot, index.DiagnosticsFile); err != nil {
return Index{}, fmt.Errorf("resolve notarius diagnostics file: %w", err)
}
seenLanes := make(map[string]struct{}, len(*document.OutputFiles))
for _, lane := range *document.OutputFiles {
if strings.TrimSpace(lane.LaneID) == "" || strings.TrimSpace(lane.File) == "" {
return Index{}, fmt.Errorf("notarius lane descriptors require lane_id and file")
}
if _, exists := seenLanes[lane.LaneID]; exists {
return Index{}, fmt.Errorf("notarius index contains duplicate lane id %q", lane.LaneID)
}
seenLanes[lane.LaneID] = struct{}{}
path, err := resolveRegularFile(bundleRoot, lane.File)
if err != nil {
return Index{}, fmt.Errorf("resolve notarius lane %q file: %w", lane.LaneID, err)
}
index.Lanes = append(index.Lanes, LaneDescriptor{
LaneID: lane.LaneID, File: lane.File, Path: path, MediaType: lane.MediaType,
ModuleKey: lane.ModuleKey, SchemaID: lane.SchemaID, SchemaName: lane.SchemaName,
SchemaVersion: lane.SchemaVersion,
})
}
if document.ChunkMap != nil {
index.ChunkMap, err = resolvePipelineDescriptor(bundleRoot, "chunk_map", *document.ChunkMap)
if err != nil {
return Index{}, err
}
}
if document.EvidenceContext != nil {
index.EvidenceContext, err = resolvePipelineDescriptor(bundleRoot, "evidence_context", *document.EvidenceContext)
if err != nil {
return Index{}, err
}
}
return index, nil
}
func resolvePipelineDescriptor(bundleRoot, label string, document pipelineDocument) (*PipelineDescriptor, error) {
if strings.TrimSpace(document.ArtifactKind) == "" || strings.TrimSpace(document.File) == "" ||
strings.TrimSpace(document.MediaType) == "" || strings.TrimSpace(document.SchemaID) == "" ||
strings.TrimSpace(document.SchemaName) == "" || strings.TrimSpace(document.SchemaVersion) == "" {
return nil, fmt.Errorf("notarius %s descriptor is missing required fields", label)
}
path, err := resolveRegularFile(bundleRoot, document.File)
if err != nil {
return nil, fmt.Errorf("resolve notarius %s file: %w", label, err)
}
return &PipelineDescriptor{
ArtifactKind: document.ArtifactKind, File: document.File, Path: path,
MediaType: document.MediaType, SchemaID: document.SchemaID,
SchemaName: document.SchemaName, SchemaVersion: document.SchemaVersion,
}, nil
}
type rejectionDocument struct {
Rejected *[]struct {
Stage string `json:"stage"`
StepID string `json:"step_id"`
LaneID string `json:"lane_id"`
ModuleKey string `json:"module_key"`
ChunkID string `json:"chunk_id"`
ValidatorName string `json:"validator_name"`
ReasonCode string `json:"reason_code"`
Message string `json:"message"`
} `json:"rejected"`
}
func loadRejections(path string) ([]RejectionSummary, error) {
var document rejectionDocument
if err := decodeBoundedJSON(path, maxSummaryBytes, &document); err != nil {
return nil, fmt.Errorf("decode notarius rejections: %w", err)
}
if document.Rejected == nil {
return nil, fmt.Errorf("notarius rejection document is missing rejected array")
}
summaries := make([]RejectionSummary, 0, len(*document.Rejected))
for _, item := range *document.Rejected {
if strings.TrimSpace(item.Stage) == "" || strings.TrimSpace(item.Message) == "" {
return nil, fmt.Errorf("notarius rejection entries require stage and message")
}
summaries = append(summaries, RejectionSummary{
Stage: item.Stage, StepID: item.StepID, LaneID: item.LaneID,
ModuleKey: item.ModuleKey, ChunkID: item.ChunkID,
ValidatorName: item.ValidatorName, ReasonCode: item.ReasonCode,
})
}
return summaries, nil
}
type findingGroupDocument struct {
Disposition string `json:"disposition"`
Category string `json:"category"`
ReasonCode string `json:"reason_code"`
Origin diagnosticOriginDocument `json:"origin"`
OccurrenceCount *int `json:"occurrence_count"`
Samples *[]struct {
Scope string `json:"scope"`
Message string `json:"message"`
ChunkID string `json:"chunk_id"`
ChunkIndex *int `json:"chunk_index"`
} `json:"samples"`
OmittedSampleCount *int `json:"omitted_sample_count"`
}
type diagnosticOriginDocument struct {
Stage string `json:"stage"`
StepID string `json:"step_id"`
LaneID string `json:"lane_id"`
ModuleKey string `json:"module_key"`
ValidatorKey string `json:"validator_key"`
}
type warningDocument struct {
SchemaVersion string `json:"schema_version"`
GroupCount *int `json:"group_count"`
OccurrenceCount *int `json:"occurrence_count"`
Groups *[]findingGroupDocument `json:"groups"`
}
func loadWarnings(path string) ([]WarningSummary, error) {
var document warningDocument
if err := decodeBoundedJSON(path, maxSummaryBytes, &document); err != nil {
return nil, fmt.Errorf("decode notarius warnings: %w", err)
}
if document.SchemaVersion != warningsSchemaVersion || document.GroupCount == nil ||
document.OccurrenceCount == nil || document.Groups == nil {
return nil, fmt.Errorf("notarius warning document is missing or incompatible required fields")
}
if *document.GroupCount < 0 || *document.GroupCount > maxWarningGroups || *document.OccurrenceCount < 0 ||
*document.GroupCount != len(*document.Groups) {
return nil, fmt.Errorf("notarius warning document counts are inconsistent")
}
summaries := make([]WarningSummary, 0, len(*document.Groups))
occurrences := 0
for _, group := range *document.Groups {
if err := validateFindingGroup(group); err != nil {
return nil, fmt.Errorf("notarius warning group: %w", err)
}
if group.Disposition != "warning" {
return nil, fmt.Errorf("notarius warning group disposition %q is invalid", group.Disposition)
}
if *group.OccurrenceCount > int(^uint(0)>>1)-occurrences {
return nil, fmt.Errorf("notarius warning occurrence count overflows")
}
occurrences += *group.OccurrenceCount
summaries = append(summaries, WarningSummary{
Disposition: group.Disposition, Category: group.Category, ReasonCode: group.ReasonCode,
Origin: diagnosticOrigin(group.Origin), OccurrenceCount: *group.OccurrenceCount,
})
}
if occurrences != *document.OccurrenceCount {
return nil, fmt.Errorf("notarius warning document occurrence count is inconsistent")
}
return summaries, nil
}
type diagnosticDocument struct {
SchemaVersion string `json:"schema_version"`
GroupCount *int `json:"group_count"`
OccurrenceCount *int `json:"occurrence_count"`
Truncated *bool `json:"truncated"`
UnrepresentedOccurrenceCount *int `json:"unrepresented_occurrence_count"`
Groups *[]findingGroupDocument `json:"groups"`
}
func loadDiagnostics(path string) ([]DiagnosticSummary, int, bool, error) {
var document diagnosticDocument
if err := decodeBoundedJSON(path, maxSummaryBytes, &document); err != nil {
return nil, 0, false, fmt.Errorf("decode notarius diagnostics: %w", err)
}
if document.SchemaVersion != diagnosticsSchemaVersion || document.GroupCount == nil ||
document.OccurrenceCount == nil || document.Truncated == nil ||
document.UnrepresentedOccurrenceCount == nil || document.Groups == nil {
return nil, 0, false, fmt.Errorf("notarius diagnostics document is missing or incompatible required fields")
}
if *document.GroupCount < 0 || *document.GroupCount > maxDiagnosticGroups || *document.OccurrenceCount < 0 ||
*document.UnrepresentedOccurrenceCount < 0 || *document.GroupCount != len(*document.Groups) {
return nil, 0, false, fmt.Errorf("notarius diagnostics document counts are inconsistent")
}
if !*document.Truncated && *document.UnrepresentedOccurrenceCount != 0 {
return nil, 0, false, fmt.Errorf("notarius diagnostics document has unrepresented occurrences without truncation")
}
summaries := make([]DiagnosticSummary, 0, len(*document.Groups))
representedOccurrences := 0
for _, group := range *document.Groups {
if err := validateFindingGroup(group); err != nil {
return nil, 0, false, fmt.Errorf("notarius diagnostic group: %w", err)
}
if group.Disposition != "advisory" && group.Disposition != "observation" {
return nil, 0, false, fmt.Errorf("notarius diagnostic group disposition %q is invalid", group.Disposition)
}
if *group.OccurrenceCount > int(^uint(0)>>1)-representedOccurrences {
return nil, 0, false, fmt.Errorf("notarius diagnostic occurrence count overflows")
}
representedOccurrences += *group.OccurrenceCount
summaries = append(summaries, DiagnosticSummary{
Disposition: group.Disposition, Category: group.Category, ReasonCode: group.ReasonCode,
Origin: diagnosticOrigin(group.Origin), OccurrenceCount: *group.OccurrenceCount,
})
}
if *document.UnrepresentedOccurrenceCount > int(^uint(0)>>1)-representedOccurrences ||
representedOccurrences+*document.UnrepresentedOccurrenceCount != *document.OccurrenceCount {
return nil, 0, false, fmt.Errorf("notarius diagnostics document occurrence count is inconsistent")
}
return summaries, *document.OccurrenceCount, *document.Truncated, nil
}
func validateFindingGroup(group findingGroupDocument) error {
if strings.TrimSpace(group.Disposition) == "" || strings.TrimSpace(group.Category) == "" ||
strings.TrimSpace(group.ReasonCode) == "" || !validDiagnosticOriginStage(group.Origin.Stage) ||
!validDiagnosticCategory(group.Disposition, group.Category) ||
group.OccurrenceCount == nil || *group.OccurrenceCount <= 0 || group.Samples == nil ||
group.OmittedSampleCount == nil || *group.OmittedSampleCount < 0 {
return fmt.Errorf("missing required fields")
}
if len(*group.Samples) == 0 || len(*group.Samples) > maxFindingSamples ||
*group.OmittedSampleCount != *group.OccurrenceCount-len(*group.Samples) {
return fmt.Errorf("sample counts are inconsistent")
}
for _, sample := range *group.Samples {
if strings.TrimSpace(sample.Scope) == "" || strings.TrimSpace(sample.Message) == "" ||
(sample.ChunkIndex != nil && *sample.ChunkIndex < 0) {
return fmt.Errorf("samples require scope and message")
}
}
return nil
}
func validDiagnosticCategory(disposition, category string) bool {
switch disposition {
case "warning":
return category == "configuration" || category == "degradation" ||
category == "validation_incomplete" || category == "fallback"
case "advisory":
return category == "data_quality"
case "observation":
return category == "normalization"
default:
return false
}
}
func validDiagnosticOriginStage(stage string) bool {
switch stage {
case "references", "chunk", "extract", "merge", "normalize":
return true
default:
return false
}
}
func diagnosticOrigin(document diagnosticOriginDocument) DiagnosticOrigin {
return DiagnosticOrigin{
Stage: document.Stage, StepID: document.StepID, LaneID: document.LaneID,
ModuleKey: document.ModuleKey, ValidatorKey: document.ValidatorKey,
}
}
func sumWarningOccurrences(values []WarningSummary) (int, error) {
total := 0
for _, value := range values {
if value.OccurrenceCount > int(^uint(0)>>1)-total {
return 0, fmt.Errorf("notarius warning occurrence count overflows")
}
total += value.OccurrenceCount
}
return total, nil
}
func decodeBoundedJSON(path string, limit int64, destination any) error {
data, err := fileops.ReadRegularFile(path, limit)
if err != nil {
return fmt.Errorf("notarius JSON result exceeds or cannot be read within %d-byte limit: %w", limit, err)
}
if err := json.Unmarshal(data, destination); err != nil {
return err
}
return nil
}
func validateBundleRoot(outputRoot, bundleRoot string) (string, error) {
root := filepath.Clean(outputRoot)
bundle := filepath.Clean(bundleRoot)
relative, err := filepath.Rel(root, bundle)
if err != nil {
return "", fmt.Errorf("compare notarius output paths: %w", err)
}
if relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
return "", fmt.Errorf("notarius output directory %q is not beneath output root %q", bundleRoot, outputRoot)
}
if err := requireDirectoryTree(root, relative); err != nil {
return "", fmt.Errorf("validate notarius output directory: %w", err)
}
return bundle, nil
}
func resolveRegularFile(root, logicalPath string) (string, error) {
resolved, err := pathsafe.JoinSlashRelativeUnderRoot(root, logicalPath)
if err != nil {
return "", err
}
relative, err := filepath.Rel(root, resolved)
if err != nil {
return "", err
}
if err := requireRegularFileTree(root, relative); err != nil {
return "", err
}
return resolved, nil
}
func requireDirectoryTree(root, relative string) error {
if err := requireDirectory(root); err != nil {
return err
}
current := root
for _, component := range strings.Split(relative, string(filepath.Separator)) {
current = filepath.Join(current, component)
if err := requireDirectory(current); err != nil {
return err
}
}
return nil
}
func requireRegularFileTree(root, relative string) error {
components := strings.Split(relative, string(filepath.Separator))
if len(components) == 0 {
return fmt.Errorf("regular file path is required")
}
if err := requireDirectory(root); err != nil {
return err
}
current := root
for _, component := range components[:len(components)-1] {
current = filepath.Join(current, component)
if err := requireDirectory(current); err != nil {
return err
}
}
return requireRegularFile(filepath.Join(current, components[len(components)-1]))
}
func requireDirectory(path string) error {
info, err := os.Lstat(path)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return fmt.Errorf("path %q must be a directory without symlinks", path)
}
return nil
}
func requireRegularFile(path string) error {
info, err := os.Lstat(path)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return fmt.Errorf("path %q must be a regular file without symlinks", path)
}
return nil
}
func validateLogDestination(path string) error {
if err := requireDirectory(filepath.Dir(path)); err != nil {
return err
}
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return fmt.Errorf("path %q must be absent or a regular file without symlinks", path)
}
return nil
}