Harden chunk map export and retire completed plans
This commit is contained in:
@@ -8,9 +8,11 @@ import (
|
||||
"io"
|
||||
"regexp"
|
||||
"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_chunk_map.v1.json
|
||||
@@ -18,6 +20,13 @@ var schemaAssets embed.FS
|
||||
|
||||
var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
|
||||
var (
|
||||
loadSchemaOnce sync.Once
|
||||
loadedSchema []byte
|
||||
compiledSchema *jsonschema.Schema
|
||||
loadSchemaErr error
|
||||
)
|
||||
|
||||
// Codec owns strict serialization for the durable chunk-map contract.
|
||||
type Codec struct{}
|
||||
|
||||
@@ -123,7 +132,7 @@ func (c *Codec) Encode(value ChunkMap) ([]byte, error) {
|
||||
if _, err := c.schemaBytes(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canonical, err := canonicalize(value)
|
||||
canonical, err := canonicalize(clone(value))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode source chunk map: %w", err)
|
||||
}
|
||||
@@ -131,6 +140,9 @@ func (c *Codec) Encode(value ChunkMap) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode source chunk map: %w", err)
|
||||
}
|
||||
if err := validateSchemaInstance(content); err != nil {
|
||||
return nil, fmt.Errorf("encode source chunk map: %w", err)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
@@ -138,6 +150,9 @@ func (c *Codec) Decode(content []byte) (ChunkMap, error) {
|
||||
if _, err := c.schemaBytes(); err != nil {
|
||||
return ChunkMap{}, err
|
||||
}
|
||||
if err := validateSchemaInstance(content); err != nil {
|
||||
return ChunkMap{}, fmt.Errorf("decode source chunk map: %w", err)
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
var value ChunkMap
|
||||
@@ -156,23 +171,61 @@ func (c *Codec) Decode(content []byte) (ChunkMap, error) {
|
||||
}
|
||||
|
||||
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_chunk_map.v1.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read source chunk map schema: %w", err)
|
||||
loadSchemaErr = fmt.Errorf("read source chunk map schema: %w", err)
|
||||
return
|
||||
}
|
||||
var schema struct {
|
||||
var identity struct {
|
||||
ID string `json:"$id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Required []string `json:"required"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &schema); err != nil {
|
||||
return nil, fmt.Errorf("decode source chunk map schema: %w", err)
|
||||
if err := json.Unmarshal(raw, &identity); err != nil {
|
||||
loadSchemaErr = fmt.Errorf("decode source chunk map schema: %w", err)
|
||||
return
|
||||
}
|
||||
if schema.ID != SchemaID || schema.Title != SchemaName || schema.Type != "object" || !hasRequiredFields(schema.Required) {
|
||||
return nil, fmt.Errorf("source chunk map schema identity or required fields are invalid")
|
||||
if identity.ID != SchemaID || identity.Title != SchemaName || identity.Type != "object" || !hasRequiredFields(identity.Required) {
|
||||
loadSchemaErr = fmt.Errorf("source chunk map schema identity or required fields are invalid")
|
||||
return
|
||||
}
|
||||
return append([]byte(nil), raw...), nil
|
||||
schemaDocument, err := jsonschema.UnmarshalJSON(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
loadSchemaErr = fmt.Errorf("parse source chunk map schema: %w", err)
|
||||
return
|
||||
}
|
||||
compiler := jsonschema.NewCompiler()
|
||||
if err := compiler.AddResource("source-chunk-map-schema.json", schemaDocument); err != nil {
|
||||
loadSchemaErr = fmt.Errorf("load source chunk map schema: %w", err)
|
||||
return
|
||||
}
|
||||
compiled, err := compiler.Compile("source-chunk-map-schema.json")
|
||||
if err != nil {
|
||||
loadSchemaErr = fmt.Errorf("compile source chunk map 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 chunk map schema: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasRequiredFields(required []string) bool {
|
||||
|
||||
Reference in New Issue
Block a user