summaryrefslogtreecommitdiffstats
path: root/pkg/v1/validate/layer.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/v1/validate/layer.go')
-rw-r--r--pkg/v1/validate/layer.go191
1 files changed, 191 insertions, 0 deletions
diff --git a/pkg/v1/validate/layer.go b/pkg/v1/validate/layer.go
new file mode 100644
index 0000000..fdd8f38
--- /dev/null
+++ b/pkg/v1/validate/layer.go
@@ -0,0 +1,191 @@
+// Copyright 2019 Google LLC All Rights Reserved.
+//
+// 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 validate
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "crypto"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/partial"
+)
+
+// Layer validates that the values return by its methods are consistent with the
+// contents returned by Compressed and Uncompressed.
+func Layer(layer v1.Layer, opt ...Option) error {
+ o := makeOptions(opt...)
+ if o.fast {
+ ok, err := partial.Exists(layer)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return fmt.Errorf("layer does not exist")
+ }
+ return nil
+ }
+
+ cl, err := computeLayer(layer)
+ if err != nil {
+ return err
+ }
+
+ errs := []string{}
+
+ digest, err := layer.Digest()
+ if err != nil {
+ return err
+ }
+ diffid, err := layer.DiffID()
+ if err != nil {
+ return err
+ }
+ size, err := layer.Size()
+ if err != nil {
+ return err
+ }
+
+ if digest != cl.digest {
+ errs = append(errs, fmt.Sprintf("mismatched digest: Digest()=%s, SHA256(Compressed())=%s", digest, cl.digest))
+ }
+
+ if diffid != cl.diffid {
+ errs = append(errs, fmt.Sprintf("mismatched diffid: DiffID()=%s, SHA256(Gunzip(Compressed()))=%s", diffid, cl.diffid))
+ }
+
+ if diffid != cl.uncompressedDiffid {
+ errs = append(errs, fmt.Sprintf("mismatched diffid: DiffID()=%s, SHA256(Uncompressed())=%s", diffid, cl.uncompressedDiffid))
+ }
+
+ if size != cl.size {
+ errs = append(errs, fmt.Sprintf("mismatched size: Size()=%d, len(Compressed())=%d", size, cl.size))
+ }
+
+ if len(errs) != 0 {
+ return errors.New(strings.Join(errs, "\n"))
+ }
+
+ return nil
+}
+
+type computedLayer struct {
+ // Calculated from Compressed stream.
+ digest v1.Hash
+ size int64
+ diffid v1.Hash
+
+ // Calculated from Uncompressed stream.
+ uncompressedDiffid v1.Hash
+ uncompressedSize int64
+}
+
+func computeLayer(layer v1.Layer) (*computedLayer, error) {
+ compressed, err := layer.Compressed()
+ if err != nil {
+ return nil, err
+ }
+
+ // Keep track of compressed digest.
+ digester := crypto.SHA256.New()
+ // Everything read from compressed is written to digester to compute digest.
+ hashCompressed := io.TeeReader(compressed, digester)
+
+ // Call io.Copy to write from the layer Reader through to the tarReader on
+ // the other side of the pipe.
+ pr, pw := io.Pipe()
+ var size int64
+ go func() {
+ n, err := io.Copy(pw, hashCompressed)
+ if err != nil {
+ pw.CloseWithError(err)
+ return
+ }
+ size = n
+
+ // Now close the compressed reader, to flush the gzip stream
+ // and calculate digest/diffID/size. This will cause pr to
+ // return EOF which will cause readers of the Compressed stream
+ // to finish reading.
+ pw.CloseWithError(compressed.Close())
+ }()
+
+ // Read the bytes through gzip.Reader to compute the DiffID.
+ uncompressed, err := gzip.NewReader(pr)
+ if err != nil {
+ return nil, err
+ }
+ diffider := crypto.SHA256.New()
+ hashUncompressed := io.TeeReader(uncompressed, diffider)
+
+ // Ensure there aren't duplicate file paths.
+ tarReader := tar.NewReader(hashUncompressed)
+ files := make(map[string]struct{})
+ for {
+ hdr, err := tarReader.Next()
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := files[hdr.Name]; ok {
+ return nil, fmt.Errorf("duplicate file path: %s", hdr.Name)
+ }
+ files[hdr.Name] = struct{}{}
+ }
+
+ // Discard any trailing padding that the tar.Reader doesn't consume.
+ if _, err := io.Copy(io.Discard, hashUncompressed); err != nil {
+ return nil, err
+ }
+
+ if err := uncompressed.Close(); err != nil {
+ return nil, err
+ }
+
+ digest := v1.Hash{
+ Algorithm: "sha256",
+ Hex: hex.EncodeToString(digester.Sum(make([]byte, 0, digester.Size()))),
+ }
+
+ diffid := v1.Hash{
+ Algorithm: "sha256",
+ Hex: hex.EncodeToString(diffider.Sum(make([]byte, 0, diffider.Size()))),
+ }
+
+ ur, err := layer.Uncompressed()
+ if err != nil {
+ return nil, err
+ }
+ defer ur.Close()
+ udiffid, usize, err := v1.SHA256(ur)
+ if err != nil {
+ return nil, err
+ }
+
+ return &computedLayer{
+ digest: digest,
+ diffid: diffid,
+ size: size,
+ uncompressedDiffid: udiffid,
+ uncompressedSize: usize,
+ }, nil
+}