1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package schema
16
17 import (
18 "bytes"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "io"
23 "regexp"
24
25 digest "github.com/opencontainers/go-digest"
26 v1 "github.com/opencontainers/image-spec/specs-go/v1"
27 "github.com/xeipuuv/gojsonschema"
28 )
29
30
31
32 type Validator string
33
34 type validateFunc func(r io.Reader) error
35
36 var mapValidate = map[Validator]validateFunc{
37 ValidatorMediaTypeImageConfig: validateConfig,
38 ValidatorMediaTypeDescriptor: validateDescriptor,
39 ValidatorMediaTypeImageIndex: validateIndex,
40 ValidatorMediaTypeManifest: validateManifest,
41 }
42
43
44 type ValidationError struct {
45 Errs []error
46 }
47
48 func (e ValidationError) Error() string {
49 return fmt.Sprintf("%v", e.Errs)
50 }
51
52
53 func (v Validator) Validate(src io.Reader) error {
54 buf, err := io.ReadAll(src)
55 if err != nil {
56 return fmt.Errorf("unable to read the document file: %w", err)
57 }
58
59 if f, ok := mapValidate[v]; ok {
60 if f == nil {
61 return fmt.Errorf("internal error: mapValidate[%q] is nil", v)
62 }
63 err = f(bytes.NewReader(buf))
64 if err != nil {
65 return err
66 }
67 }
68
69 sl := newFSLoaderFactory(schemaNamespaces, FileSystem()).New(specs[v])
70 ml := gojsonschema.NewStringLoader(string(buf))
71
72 result, err := gojsonschema.Validate(sl, ml)
73 if err != nil {
74 return fmt.Errorf("schema %s: unable to validate: %w", v,
75 WrapSyntaxError(bytes.NewReader(buf), err))
76 }
77
78 if result.Valid() {
79 return nil
80 }
81
82 errs := make([]error, 0, len(result.Errors()))
83 for _, desc := range result.Errors() {
84 errs = append(errs, fmt.Errorf("%s", desc))
85 }
86
87 return ValidationError{
88 Errs: errs,
89 }
90 }
91
92 type unimplemented string
93
94 func (v unimplemented) Validate(_ io.Reader) error {
95 return fmt.Errorf("%s: unimplemented", v)
96 }
97
98 func validateManifest(r io.Reader) error {
99 header := v1.Manifest{}
100
101 buf, err := io.ReadAll(r)
102 if err != nil {
103 return fmt.Errorf("error reading the io stream: %w", err)
104 }
105
106 err = json.Unmarshal(buf, &header)
107 if err != nil {
108 return fmt.Errorf("manifest format mismatch: %w", err)
109 }
110
111 if header.Config.MediaType != string(v1.MediaTypeImageConfig) {
112 fmt.Printf("warning: config %s has an unknown media type: %s\n", header.Config.Digest, header.Config.MediaType)
113 }
114
115 for _, layer := range header.Layers {
116 if layer.MediaType != string(v1.MediaTypeImageLayer) &&
117 layer.MediaType != string(v1.MediaTypeImageLayerGzip) &&
118 layer.MediaType != string(v1.MediaTypeImageLayerZstd) &&
119 layer.MediaType != string(v1.MediaTypeImageLayerNonDistributable) &&
120 layer.MediaType != string(v1.MediaTypeImageLayerNonDistributableGzip) &&
121 layer.MediaType != string(v1.MediaTypeImageLayerNonDistributableZstd) {
122 fmt.Printf("warning: layer %s has an unknown media type: %s\n", layer.Digest, layer.MediaType)
123 }
124 }
125 return nil
126 }
127
128 func validateDescriptor(r io.Reader) error {
129 header := v1.Descriptor{}
130
131 buf, err := io.ReadAll(r)
132 if err != nil {
133 return fmt.Errorf("error reading the io stream: %w", err)
134 }
135
136 err = json.Unmarshal(buf, &header)
137 if err != nil {
138 return fmt.Errorf("descriptor format mismatch: %w", err)
139 }
140
141 err = header.Digest.Validate()
142 if errors.Is(err, digest.ErrDigestUnsupported) {
143
144 fmt.Printf("warning: unsupported digest: %q: %v\n", header.Digest, err)
145 return nil
146 }
147 return err
148 }
149
150 func validateIndex(r io.Reader) error {
151 header := v1.Index{}
152
153 buf, err := io.ReadAll(r)
154 if err != nil {
155 return fmt.Errorf("error reading the io stream: %w", err)
156 }
157
158 err = json.Unmarshal(buf, &header)
159 if err != nil {
160 return fmt.Errorf("index format mismatch: %w", err)
161 }
162
163 for _, manifest := range header.Manifests {
164 if manifest.MediaType != string(v1.MediaTypeImageManifest) {
165 fmt.Printf("warning: manifest %s has an unknown media type: %s\n", manifest.Digest, manifest.MediaType)
166 }
167 if manifest.Platform != nil {
168 checkPlatform(manifest.Platform.OS, manifest.Platform.Architecture)
169 checkArchitecture(manifest.Platform.Architecture, manifest.Platform.Variant)
170 }
171
172 }
173
174 return nil
175 }
176
177 func validateConfig(r io.Reader) error {
178 header := v1.Image{}
179
180 buf, err := io.ReadAll(r)
181 if err != nil {
182 return fmt.Errorf("error reading the io stream: %w", err)
183 }
184
185 err = json.Unmarshal(buf, &header)
186 if err != nil {
187 return fmt.Errorf("config format mismatch: %w", err)
188 }
189
190 checkPlatform(header.OS, header.Architecture)
191 checkArchitecture(header.Architecture, header.Variant)
192
193 envRegexp := regexp.MustCompile(`^[^=]+=.*$`)
194 for _, e := range header.Config.Env {
195 if !envRegexp.MatchString(e) {
196 return fmt.Errorf("unexpected env: %q", e)
197 }
198 }
199
200 return nil
201 }
202
203 func checkArchitecture(Architecture string, Variant string) {
204 validCombins := map[string][]string{
205 "arm": {"", "v6", "v7", "v8"},
206 "arm64": {"", "v8"},
207 "386": {""},
208 "amd64": {""},
209 "ppc64": {""},
210 "ppc64le": {""},
211 "mips64": {""},
212 "mips64le": {""},
213 "s390x": {""},
214 "riscv64": {""},
215 }
216 for arch, variants := range validCombins {
217 if arch == Architecture {
218 for _, variant := range variants {
219 if variant == Variant {
220 return
221 }
222 }
223 fmt.Printf("warning: combination of architecture %q and variant %q is not valid.\n", Architecture, Variant)
224 }
225 }
226 fmt.Printf("warning: architecture %q is not supported yet.\n", Architecture)
227 }
228
229 func checkPlatform(OS string, Architecture string) {
230 validCombins := map[string][]string{
231 "android": {"arm"},
232 "darwin": {"386", "amd64", "arm", "arm64"},
233 "dragonfly": {"amd64"},
234 "freebsd": {"386", "amd64", "arm"},
235 "linux": {"386", "amd64", "arm", "arm64", "ppc64", "ppc64le", "mips64", "mips64le", "s390x", "riscv64"},
236 "netbsd": {"386", "amd64", "arm"},
237 "openbsd": {"386", "amd64", "arm"},
238 "plan9": {"386", "amd64"},
239 "solaris": {"amd64"},
240 "windows": {"386", "amd64"}}
241 for os, archs := range validCombins {
242 if os == OS {
243 for _, arch := range archs {
244 if arch == Architecture {
245 return
246 }
247 }
248 fmt.Printf("warning: combination of os %q and architecture %q is invalid.\n", OS, Architecture)
249 }
250 }
251 fmt.Printf("warning: operating system %q of the bundle is not supported yet.\n", OS)
252 }
253
View as plain text