package evidencecontext import ( "bytes" "embed" "encoding/json" "fmt" "io" "strings" "sync" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "github.com/santhosh-tekuri/jsonschema/v6" ) //go:embed assets/schemas/source_evidence_context.v1.json var schemaAssets embed.FS var ( loadSchemaOnce sync.Once loadedSchema []byte compiledSchema *jsonschema.Schema loadSchemaErr error ) // Codec owns strict serialization for the durable evidence-context contract. type Codec struct{} func New() *Codec { return &Codec{} } func (c *Codec) Kind() contracts.ArtifactKind { return ArtifactKind } func (c *Codec) Schema() contracts.ArtifactSchema { raw, err := c.schemaBytes() if err != nil { return contracts.ArtifactSchema{} } return contracts.ArtifactSchema{ID: SchemaID, Name: SchemaName, Version: SchemaVersion, JSONSchema: raw} } func (c *Codec) MediaType() string { return MediaType } // Serialize builds and encodes the framework-owned serialized artifact. func Serialize(request BuildRequest) (contracts.SerializedArtifact, error) { value, err := Build(request) if err != nil { return contracts.SerializedArtifact{}, err } codec := New() content, err := codec.Encode(value) if err != nil { return contracts.SerializedArtifact{}, err } return contracts.SerializedArtifact{Kind: ArtifactKind, Schema: codec.Schema(), MediaType: MediaType, Content: content}, nil } func (c *Codec) Encode(value Document) ([]byte, error) { if _, err := c.schemaBytes(); err != nil { return nil, err } canonical, err := canonicalize(value) if err != nil { return nil, fmt.Errorf("encode evidence context: %w", err) } content, err := json.Marshal(canonical) if err != nil { return nil, fmt.Errorf("encode evidence context: %w", err) } if err := validateSchemaInstance(content); err != nil { return nil, fmt.Errorf("encode evidence context: %w", err) } return content, nil } func (c *Codec) Decode(content []byte) (Document, error) { if _, err := c.schemaBytes(); err != nil { return nil, err } if err := validateSchemaInstance(content); err != nil { return nil, fmt.Errorf("decode evidence context: %w", err) } decoder := json.NewDecoder(bytes.NewReader(content)) decoder.DisallowUnknownFields() var value Document if err := decoder.Decode(&value); err != nil { return nil, fmt.Errorf("decode evidence context: %w", err) } var trailing any if err := decoder.Decode(&trailing); err != io.EOF { return nil, fmt.Errorf("decode evidence context: multiple JSON values") } canonical, err := canonicalizeOwned(value) if err != nil { return nil, fmt.Errorf("decode evidence context: %w", err) } return canonical, nil } func (c *Codec) schemaBytes() ([]byte, error) { loadSchemaOnce.Do(loadAndCompileSchema) if loadSchemaErr != nil { return nil, loadSchemaErr } return append([]byte(nil), loadedSchema...), nil } func loadAndCompileSchema() { raw, err := schemaAssets.ReadFile("assets/schemas/source_evidence_context.v1.json") if err != nil { loadSchemaErr = fmt.Errorf("read source evidence context schema: %w", err) return } var identity struct { ID string `json:"$id"` Title string `json:"title"` Type string `json:"type"` } if err := json.Unmarshal(raw, &identity); err != nil { loadSchemaErr = fmt.Errorf("decode source evidence context schema: %w", err) return } if identity.ID != SchemaID || identity.Title != SchemaName || identity.Type != "array" { loadSchemaErr = fmt.Errorf("source evidence context schema identity is invalid") return } schemaDocument, err := jsonschema.UnmarshalJSON(bytes.NewReader(raw)) if err != nil { loadSchemaErr = fmt.Errorf("parse source evidence context schema: %w", err) return } compiler := jsonschema.NewCompiler() if err := compiler.AddResource("source-evidence-context-schema.json", schemaDocument); err != nil { loadSchemaErr = fmt.Errorf("load source evidence context schema: %w", err) return } compiled, err := compiler.Compile("source-evidence-context-schema.json") if err != nil { loadSchemaErr = fmt.Errorf("compile source evidence context schema: %w", err) return } loadedSchema = append([]byte(nil), raw...) compiledSchema = compiled } func validateSchemaInstance(content []byte) error { instance, err := jsonschema.UnmarshalJSON(bytes.NewReader(content)) if err != nil { return fmt.Errorf("payload is not valid JSON: %w", err) } if err := compiledSchema.Validate(instance); err != nil { return fmt.Errorf("payload does not conform to source evidence context schema: %w", err) } return nil } func canonicalize(value Document) (Document, error) { owned, err := clone(value) if err != nil { return nil, err } return canonicalizeOwned(owned) } func canonicalizeOwned(value Document) (Document, error) { if value == nil { return nil, fmt.Errorf("document must be a JSON array") } seenUnitIDs := make(map[int]struct{}, len(value)) sourceID := "" for unitIndex := range value { unit := value[unitIndex] if unit.ID <= 0 || strings.TrimSpace(unit.Kind) == "" || strings.TrimSpace(unit.Text) == "" { return nil, fmt.Errorf("units[%d] has invalid required fields", unitIndex) } if err := validateUnitRef(unit, unitIndex); err != nil { return nil, err } if sourceID == "" { sourceID = unit.Ref.SourceID } else if unit.Ref.SourceID != sourceID { return nil, fmt.Errorf("units[%d].ref.source_id must match units[0].ref.source_id", unitIndex) } if _, exists := seenUnitIDs[unit.ID]; exists { return nil, fmt.Errorf("units contains duplicate unit id %d", unit.ID) } seenUnitIDs[unit.ID] = struct{}{} } return value, nil } func validateUnitRef(unit source.SourceUnit, unitIndex int) error { prefix := fmt.Sprintf("units[%d].ref", unitIndex) if strings.TrimSpace(unit.Ref.SourceID) == "" || strings.TrimSpace(unit.Ref.SourceID) != unit.Ref.SourceID { return fmt.Errorf("%s.source_id must be a non-empty trimmed string", prefix) } if unit.Ref.StartUnitID != unit.ID || unit.Ref.EndUnitID != unit.ID { return fmt.Errorf("%s must identify unit id %d", prefix, unit.ID) } return nil } func clone(value Document) (Document, error) { if value == nil { return nil, nil } cloned := make(Document, len(value)) for unitIndex, unit := range value { owned, err := cloneSourceUnit(unit) if err != nil { return nil, fmt.Errorf("clone units[%d]: %w", unitIndex, err) } cloned[unitIndex] = owned } return cloned, nil } func cloneSourceUnit(unit source.SourceUnit) (source.SourceUnit, error) { metadata, err := source.CloneMetadata(unit.Metadata) if err != nil { return source.SourceUnit{}, fmt.Errorf("clone metadata: %w", err) } unit.Metadata = metadata return unit, nil }