diff --git a/schema/loader.go b/schema/loader.go new file mode 100644 index 000000000..c6bde0048 --- /dev/null +++ b/schema/loader.go @@ -0,0 +1,126 @@ +// 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" + "io/ioutil" + "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 ioutil.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{} { // nolint: golint + 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) { // nolint: golint + return gojsonreference.NewJsonReference(l.JsonSource().(string)) +} + +func (l *fsLoader) LoaderFactory() gojsonschema.JSONLoaderFactory { + return l.factory +} diff --git a/schema/schema.go b/schema/schema.go index 6a317f139..6fc32f093 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -35,13 +35,37 @@ var ( // having the OCI JSON schema files in root "/". fs = _escFS(false) - // specs maps OCI schema media types to schema files. + // 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/", + } + + // 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: "content-descriptor.json", - ValidatorMediaTypeLayoutHeader: "image-layout-schema.json", - ValidatorMediaTypeManifest: "image-manifest-schema.json", - ValidatorMediaTypeImageIndex: "image-index-schema.json", - ValidatorMediaTypeImageConfig: "config-schema.json", + 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", } ) diff --git a/schema/validator.go b/schema/validator.go index e9f6d4378..029217c3b 100644 --- a/schema/validator.go +++ b/schema/validator.go @@ -67,7 +67,7 @@ func (v Validator) Validate(src io.Reader) error { } } - sl := gojsonschema.NewReferenceLoaderFileSystem("file:///"+specs[v], fs) + sl := newFSLoaderFactory(schemaNamespaces, fs).New(specs[v]) ml := gojsonschema.NewStringLoader(string(buf)) result, err := gojsonschema.Validate(sl, ml)