1
0
mirror of https://github.com/opencontainers/image-spec.git synced 2025-04-18 03:24:01 +03:00

Switch jsonschema validation libraries

Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
Brandon Mitchell 2024-05-26 17:09:21 -04:00
parent dd33f727e2
commit 4bbdd7f035
No known key found for this signature in database
GPG Key ID: 6E0FF28C767A8BEE
8 changed files with 135 additions and 275 deletions

10
go.mod
View File

@ -5,13 +5,5 @@ go 1.18
require (
github.com/opencontainers/go-digest v1.0.0
github.com/russross/blackfriday v1.6.0
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415
github.com/xeipuuv/gojsonschema v1.2.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
)

21
go.sum
View File

@ -1,23 +1,6 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=

View File

@ -4,7 +4,7 @@
"mediaType": {
"id": "https://opencontainers.org/schema/image/descriptor/mediaType",
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$"
"pattern": "^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}$"
},
"digest": {
"description": "the cryptographic checksum digest of the object, in the pattern '<algorithm>:<encoded>'",

View File

@ -23,6 +23,8 @@ import (
// A SyntaxError is a description of a JSON syntax error
// including line, column and offset in the JSON file.
//
// Deprecated: SyntaxError is no longer returned from Validator.
type SyntaxError struct {
msg string
Line, Col int
@ -34,6 +36,8 @@ func (e *SyntaxError) Error() string { return e.msg }
// WrapSyntaxError checks whether the given error is a *json.SyntaxError
// and converts it into a *schema.SyntaxError containing line/col information using the given reader.
// If the given error is not a *json.SyntaxError it is returned unchanged.
//
// Deprecated: WrapSyntaxError is no longer returned by Validator.
func WrapSyntaxError(r io.Reader, err error) error {
var serr *json.SyntaxError
if errors.As(err, &serr) {

View File

@ -1,125 +0,0 @@
// Copyright 2018 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package schema
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/xeipuuv/gojsonreference"
"github.com/xeipuuv/gojsonschema"
)
// fsLoaderFactory implements gojsonschema.JSONLoaderFactory by reading files under the specified namespaces from the root of fs.
type fsLoaderFactory struct {
namespaces []string
fs http.FileSystem
}
// newFSLoaderFactory returns a fsLoaderFactory reading files under the specified namespaces from the root of fs.
func newFSLoaderFactory(namespaces []string, fs http.FileSystem) *fsLoaderFactory {
return &fsLoaderFactory{
namespaces: namespaces,
fs: fs,
}
}
func (factory *fsLoaderFactory) New(source string) gojsonschema.JSONLoader {
return &fsLoader{
factory: factory,
source: source,
}
}
// refContents returns the contents of ref, if available in fsLoaderFactory.
func (factory *fsLoaderFactory) refContents(ref gojsonreference.JsonReference) ([]byte, error) {
refStr := ref.String()
path := ""
for _, ns := range factory.namespaces {
if strings.HasPrefix(refStr, ns) {
path = "/" + strings.TrimPrefix(refStr, ns)
break
}
}
if path == "" {
return nil, fmt.Errorf("schema reference %#v unexpectedly not available in fsLoaderFactory with namespaces %#v", path, factory.namespaces)
}
f, err := factory.fs.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
// fsLoader implements gojsonschema.JSONLoader by reading the document named by source from a fsLoaderFactory.
type fsLoader struct {
factory *fsLoaderFactory
source string
}
// JsonSource implements gojsonschema.JSONLoader.JsonSource. The "Json" capitalization needs to be maintained to conform to the interface.
func (l *fsLoader) JsonSource() interface{} { // revive:disable-line:var-naming
return l.source
}
func (l *fsLoader) LoadJSON() (interface{}, error) {
// Based on gojsonschema.jsonReferenceLoader.LoadJSON.
reference, err := gojsonreference.NewJsonReference(l.source)
if err != nil {
return nil, err
}
refToURL := reference
refToURL.GetUrl().Fragment = ""
body, err := l.factory.refContents(refToURL)
if err != nil {
return nil, err
}
return decodeJSONUsingNumber(bytes.NewReader(body))
}
// decodeJSONUsingNumber returns JSON parsed from an io.Reader
func decodeJSONUsingNumber(r io.Reader) (interface{}, error) {
// Copied from gojsonschema.
var document interface{}
decoder := json.NewDecoder(r)
decoder.UseNumber()
err := decoder.Decode(&document)
if err != nil {
return nil, err
}
return document, nil
}
// JsonReference implements gojsonschema.JSONLoader.JsonReference. The "Json" capitalization needs to be maintained to conform to the interface.
func (l *fsLoader) JsonReference() (gojsonreference.JsonReference, error) { // revive:disable-line:var-naming
return gojsonreference.NewJsonReference(l.JsonSource().(string))
}
func (l *fsLoader) LoaderFactory() gojsonschema.JSONLoaderFactory {
return l.factory
}

View File

@ -23,56 +23,62 @@ import (
// Media types for the OCI image formats
const (
ValidatorMediaTypeDescriptor Validator = v1.MediaTypeDescriptor
ValidatorMediaTypeLayoutHeader Validator = v1.MediaTypeLayoutHeader
ValidatorMediaTypeManifest Validator = v1.MediaTypeImageManifest
ValidatorMediaTypeImageIndex Validator = v1.MediaTypeImageIndex
ValidatorMediaTypeImageConfig Validator = v1.MediaTypeImageConfig
ValidatorMediaTypeImageLayer unimplemented = v1.MediaTypeImageLayer
ValidatorMediaTypeDescriptor Validator = v1.MediaTypeDescriptor
ValidatorMediaTypeLayoutHeader Validator = v1.MediaTypeLayoutHeader
ValidatorMediaTypeManifest Validator = v1.MediaTypeImageManifest
ValidatorMediaTypeImageIndex Validator = v1.MediaTypeImageIndex
ValidatorMediaTypeImageConfig Validator = v1.MediaTypeImageConfig
ValidatorMediaTypeImageLayer Validator = v1.MediaTypeImageLayer
)
var (
// fs stores the embedded http.FileSystem
// having the OCI JSON schema files in root "/".
// specFS stores the embedded http.FileSystem having the OCI JSON schema files in root "/".
//go:embed *.json
fs embed.FS
specFS embed.FS
// schemaNamespaces is a set of URI prefixes which are treated as containing the schema files of fs.
// This is necessary because *.json schema files in this directory use "id" and "$ref" attributes which evaluate to such URIs, e.g.
// ./image-manifest-schema.json URI contains
// "id": "https://opencontainers.org/schema/image/manifest",
// and
// "$ref": "content-descriptor.json"
// which evaluates as a link to https://opencontainers.org/schema/image/content-descriptor.json .
//
// To support such links without accessing the network (and trying to load content which is not hosted at these URIs),
// fsLoaderFactory accepts any URI starting with one of the schemaNamespaces below,
// and uses _escFS to load them from the root of its in-memory filesystem tree.
//
// (Note that this must contain subdirectories before its parent directories for fsLoaderFactory.refContents to work.)
schemaNamespaces = []string{
"https://opencontainers.org/schema/image/descriptor/",
"https://opencontainers.org/schema/image/index/",
"https://opencontainers.org/schema/image/manifest/",
"https://opencontainers.org/schema/image/",
"https://opencontainers.org/schema/descriptor/",
"https://opencontainers.org/schema/",
// specsOrig maps OCI schema media types to schema files.
specs = map[Validator]string{
ValidatorMediaTypeDescriptor: "content-descriptor.json",
ValidatorMediaTypeLayoutHeader: "image-layout-schema.json",
ValidatorMediaTypeManifest: "image-manifest-schema.json",
ValidatorMediaTypeImageIndex: "image-index-schema.json",
ValidatorMediaTypeImageConfig: "config-schema.json",
}
// specs maps OCI schema media types to schema URIs.
// These URIs are expected to be used only by fsLoaderFactory (which trims schemaNamespaces defined above)
// and should never cause a network access.
specs = map[Validator]string{
ValidatorMediaTypeDescriptor: "https://opencontainers.org/schema/content-descriptor.json",
ValidatorMediaTypeLayoutHeader: "https://opencontainers.org/schema/image/image-layout-schema.json",
ValidatorMediaTypeManifest: "https://opencontainers.org/schema/image/image-manifest-schema.json",
ValidatorMediaTypeImageIndex: "https://opencontainers.org/schema/image/image-index-schema.json",
ValidatorMediaTypeImageConfig: "https://opencontainers.org/schema/image/config-schema.json",
// specURLs lists the various URLs a given spec may be known by.
// This is generated from the "id" value in each spec and relative ref values they contain.
specURLs = map[string][]string{
"config-schema.json": {
"https://opencontainers.org/schema/image/config",
},
"content-descriptor.json": {
"https://opencontainers.org/schema/descriptor",
"https://opencontainers.org/schema/image/content-descriptor.json",
},
"defs-descriptor.json": {
"https://opencontainers.org/schema/image/descriptor/mediaType",
"https://opencontainers.org/schema/defs-descriptor.json",
"https://opencontainers.org/schema/image/defs-descriptor.json",
},
"defs.json": {
"https://opencontainers.org/schema/defs.json",
"https://opencontainers.org/schema/image/defs.json",
"https://opencontainers.org/schema/image/descriptor/defs.json",
},
"image-index-schema.json": {
"https://opencontainers.org/schema/image/index",
},
"image-layout-schema.json": {
"https://opencontainers.org/schema/image/layout",
},
"image-manifest-schema.json": {
"https://opencontainers.org/schema/image/manifest",
},
}
)
// FileSystem returns an in-memory filesystem including the schema files.
// The schema files are located at the root directory.
func FileSystem() http.FileSystem {
return http.FS(fs)
return http.FS(specFS)
}

View File

@ -106,27 +106,11 @@ func validate(t *testing.T, name string) {
err = schema.Validator(example.Mediatype).Validate(strings.NewReader(example.Body))
if err == nil {
printFields(t, "ok", example.Mediatype, example.Title)
t.Log(example.Body, "---")
continue
}
var errs []error
var verr schema.ValidationError
if errors.As(err, &verr) {
errs = verr.Errs
} else {
printFields(t, "error", example.Mediatype, example.Title, err)
t.Error(err)
t.Log(example.Body, "---")
continue
}
for _, err := range errs {
printFields(t, "invalid", example.Mediatype, example.Title)
t.Error(err)
fmt.Println(example.Body, "---")
continue
}
t.Log(example.Body, "---")
}
}

View File

@ -20,90 +20,121 @@ import (
"errors"
"fmt"
"io"
"os"
"regexp"
digest "github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/xeipuuv/gojsonschema"
"github.com/santhosh-tekuri/jsonschema/v5"
)
// Validator wraps a media type string identifier
// and implements validation against a JSON schema.
// Validator wraps a media type string identifier and implements validation against a JSON schema.
type Validator string
type validateFunc func(r io.Reader) error
var mapValidate = map[Validator]validateFunc{
ValidatorMediaTypeImageConfig: validateConfig,
ValidatorMediaTypeDescriptor: validateDescriptor,
ValidatorMediaTypeImageIndex: validateIndex,
ValidatorMediaTypeManifest: validateManifest,
}
// ValidationError contains all the errors that happened during validation.
//
// Deprecated: this is no longer used by [Validator].
type ValidationError struct {
Errs []error
}
// Error returns the error message.
//
// Deprecated: this is no longer used by [Validator].
func (e ValidationError) Error() string {
return fmt.Sprintf("%v", e.Errs)
}
// Validate validates the given reader against the schema of the wrapped media type.
func (v Validator) Validate(src io.Reader) error {
buf, err := io.ReadAll(src)
if err != nil {
return fmt.Errorf("unable to read the document file: %w", err)
}
if f, ok := mapValidate[v]; ok {
if f == nil {
return fmt.Errorf("internal error: mapValidate[%q] is nil", v)
// run the media type specific validation
if fn, ok := validateByMediaType[v]; ok {
if fn == nil {
return fmt.Errorf("internal error: mapValidate is nil for %s", string(v))
}
err = f(bytes.NewReader(buf))
// buffer the src so the media type validation and the schema validation can both read it
buf, err := io.ReadAll(src)
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
src = bytes.NewReader(buf)
err = fn(buf)
if err != nil {
return err
}
}
sl := newFSLoaderFactory(schemaNamespaces, FileSystem()).New(specs[v])
ml := gojsonschema.NewStringLoader(string(buf))
// json schema validation
return v.validateSchema(src)
}
result, err := gojsonschema.Validate(sl, ml)
func (v Validator) validateSchema(src io.Reader) error {
if _, ok := specs[v]; !ok {
return fmt.Errorf("no validator available for %s", string(v))
}
c := jsonschema.NewCompiler()
// load the schema files from the embedded FS
dir, err := specFS.ReadDir(".")
if err != nil {
return fmt.Errorf("schema %s: unable to validate: %w", v,
WrapSyntaxError(bytes.NewReader(buf), err))
return fmt.Errorf("spec embedded directory could not be loaded: %w", err)
}
for _, file := range dir {
if file.IsDir() {
continue
}
specBuf, err := specFS.ReadFile(file.Name())
if err != nil {
return fmt.Errorf("could not read spec file %s: %w", file.Name(), err)
}
err = c.AddResource(file.Name(), bytes.NewReader(specBuf))
if err != nil {
return fmt.Errorf("failed to add spec file %s: %w", file.Name(), err)
}
if len(specURLs[file.Name()]) == 0 {
fmt.Fprintf(os.Stderr, "warning: spec file has no aliases: %s", file.Name())
}
for _, specURL := range specURLs[file.Name()] {
err = c.AddResource(specURL, bytes.NewReader(specBuf))
if err != nil {
return fmt.Errorf("failed to add spec file %s as url %s: %w", file.Name(), specURL, err)
}
}
}
if result.Valid() {
return nil
// compile based on the type of validator
schema, err := c.Compile(specs[v])
if err != nil {
return fmt.Errorf("failed to compile schema %s: %w", string(v), err)
}
errs := make([]error, 0, len(result.Errors()))
for _, desc := range result.Errors() {
errs = append(errs, fmt.Errorf("%s", desc))
// read in the user input and validate
var input interface{}
err = json.NewDecoder(src).Decode(&input)
if err != nil {
return fmt.Errorf("unable to parse json to validate: %w", err)
}
return ValidationError{
Errs: errs,
err = schema.Validate(input)
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
return nil
}
type unimplemented string
type validateFunc func([]byte) error
func (v unimplemented) Validate(_ io.Reader) error {
return fmt.Errorf("%s: unimplemented", v)
var validateByMediaType = map[Validator]validateFunc{
ValidatorMediaTypeImageConfig: validateConfig,
ValidatorMediaTypeDescriptor: validateDescriptor,
ValidatorMediaTypeImageIndex: validateIndex,
ValidatorMediaTypeManifest: validateManifest,
}
func validateManifest(r io.Reader) error {
func validateManifest(buf []byte) error {
header := v1.Manifest{}
buf, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("error reading the io stream: %w", err)
}
err = json.Unmarshal(buf, &header)
err := json.Unmarshal(buf, &header)
if err != nil {
return fmt.Errorf("manifest format mismatch: %w", err)
}
@ -125,15 +156,10 @@ func validateManifest(r io.Reader) error {
return nil
}
func validateDescriptor(r io.Reader) error {
func validateDescriptor(buf []byte) error {
header := v1.Descriptor{}
buf, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("error reading the io stream: %w", err)
}
err = json.Unmarshal(buf, &header)
err := json.Unmarshal(buf, &header)
if err != nil {
return fmt.Errorf("descriptor format mismatch: %w", err)
}
@ -147,15 +173,10 @@ func validateDescriptor(r io.Reader) error {
return err
}
func validateIndex(r io.Reader) error {
func validateIndex(buf []byte) error {
header := v1.Index{}
buf, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("error reading the io stream: %w", err)
}
err = json.Unmarshal(buf, &header)
err := json.Unmarshal(buf, &header)
if err != nil {
return fmt.Errorf("index format mismatch: %w", err)
}
@ -174,15 +195,10 @@ func validateIndex(r io.Reader) error {
return nil
}
func validateConfig(r io.Reader) error {
func validateConfig(buf []byte) error {
header := v1.Image{}
buf, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("error reading the io stream: %w", err)
}
err = json.Unmarshal(buf, &header)
err := json.Unmarshal(buf, &header)
if err != nil {
return fmt.Errorf("config format mismatch: %w", err)
}