diff --git a/cmd/cvetool/scan.go b/cmd/cvetool/scan.go index 7bf0f83..fd0d83a 100644 --- a/cmd/cvetool/scan.go +++ b/cmd/cvetool/scan.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/quay/claircore" "github.com/quay/claircore/enricher/cvss" "github.com/quay/claircore/indexer" "github.com/quay/claircore/libindex" @@ -127,8 +128,8 @@ func scan(c *cli.Context) error { ) var ( - img image.Image - fa indexer.FetchArena + mf *claircore.Manifest + fa indexer.FetchArena ) switch { case imgRef != "": @@ -138,24 +139,32 @@ func scan(c *cli.Context) error { if err != nil { return fmt.Errorf("error setting DOCKER_CONFIG env var") } - img = image.NewDockerRemoteImage(ctx, imgRef) + mf, err = image.ManifestFromRemote(ctx, imgRef) + if err != nil { + return fmt.Errorf("error getting image information: %v", err) + } case imgPath != "": fa = &LocalFetchArena{} var err error - img, err = image.NewDockerLocalImage(ctx, imgPath, os.TempDir()) + mf, err = image.ManifestFromLocal(ctx, imgPath) if err != nil { return fmt.Errorf("error getting image information: %v", err) } case rootPath != "": fa = &LocalFetchArena{} var err error - img, err = image.NewFileSystemImage(ctx, rootPath) + mf, err = image.ManifestFromFilesystem(ctx, rootPath) if err != nil { return fmt.Errorf("error getting filesystem information: %v", err) } default: return fmt.Errorf("no --image-path ($IMAGE_PATH), --image-ref ($IMAGE_REF) or --root-path ($ROOT_PATH) set") } + defer func() { + for _, l := range mf.Layers { + l.Close() + } + }() switch { case dbPath != "": @@ -204,11 +213,6 @@ func scan(c *cli.Context) error { return fmt.Errorf("error creating Libvuln: %v", err) } - mf, err := img.GetManifest(ctx) - if err != nil { - return fmt.Errorf("error creating manifest: %v", err) - } - indexerOpts := &libindex.Options{ Store: datastore.NewLocalIndexerStore(), Locker: NewLocalLockSource(), diff --git a/go.mod b/go.mod index 95b5466..d6d294d 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/remind101/migrate v0.0.0-20170729031349-52c1edff7319 github.com/rs/zerolog v1.35.1 github.com/urfave/cli/v2 v2.27.7 + golang.org/x/tools v0.44.0 modernc.org/sqlite v1.49.1 ) @@ -70,7 +71,6 @@ require ( golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.44.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gotest.tools/v3 v3.5.2 // indirect modernc.org/libc v1.72.0 // indirect diff --git a/image/docker.go b/image/docker.go deleted file mode 100644 index d3e26ff..0000000 --- a/image/docker.go +++ /dev/null @@ -1,172 +0,0 @@ -package image - -import ( - "archive/tar" - "context" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/quay/claircore" - "github.com/quay/zlog" -) - -var _ Image = (*dockerLocalImage)(nil) - -type Image interface { - GetManifest(context.Context) (*claircore.Manifest, error) -} - -type imageInfo struct { - Config string `json:"Config"` - Layers []string `json:"Layers"` -} - -type dockerLocalImage struct { - imageDigest string - layerPaths []string -} - -func NewDockerLocalImage(ctx context.Context, exportDir string, importDir string) (*dockerLocalImage, error) { - f, err := os.Open(exportDir) - if err != nil { - return nil, fmt.Errorf("unable to open tar: %w", err) - } - - di := &dockerLocalImage{} - m := &imageInfo{} - - tr := tar.NewReader(f) - hdr, err := tr.Next() - for ; err == nil; hdr, err = tr.Next() { - dir, fn := filepath.Split(hdr.Name) - - if strings.HasSuffix(fn, ".tar") { - layerFilePath := "" - - if fn == "layer.tar" { - if hdr.Linkname == "" && hdr.Size > 0 { - sha := filepath.Base(dir) - layerFilePath = filepath.Join(importDir, "sha256:"+sha) - } else { - continue - } - } else { - sha := strings.TrimSuffix(fn, filepath.Ext(fn)) - layerFilePath = filepath.Join(importDir, "sha256:"+sha) - } - - zlog.Debug(ctx).Str("layerFilePath", layerFilePath).Msg("found .tar file") - - layerFile, err := os.OpenFile(layerFilePath, os.O_CREATE|os.O_RDWR, os.FileMode(0600)) - if err != nil { - return nil, err - } - _, err = io.Copy(layerFile, tr) - if err != nil { - return nil, err - } - di.layerPaths = append(di.layerPaths, layerFile.Name()) - layerFile.Close() - } - - if fn == "manifest.json" { - _m := []*imageInfo{} - b, err := io.ReadAll(tr) - if err != nil { - return nil, err - } - err = json.Unmarshal(b, &_m) - if err != nil { - return nil, err - } - m = _m[0] - digest := strings.TrimSuffix(m.Config, filepath.Ext(m.Config)) - zlog.Debug(ctx).Str("digest", digest) - di.imageDigest = "sha256:" + digest - } - } - - var sortedPaths []string - zlog.Debug(ctx).Any("m.Layers", m.Layers) - zlog.Debug(ctx).Any("di.layerPaths", di.layerPaths) - - for _, p := range m.Layers { - zlog.Debug(ctx).Str("p", p) - for _, l := range di.layerPaths { - zlog.Debug(ctx).Str("p", p).Str("l", l).Msg("lps") - if filepath.Dir(p) == strings.TrimPrefix(filepath.Base(l), "sha256:") { - sortedPaths = append(sortedPaths, l) - } - if strings.TrimSuffix(p, filepath.Ext(p)) == strings.TrimPrefix(filepath.Base(l), "sha256:") { - sortedPaths = append(sortedPaths, l) - } - } - } - zlog.Debug(ctx).Any("sortedPaths", sortedPaths).Msg("layers") - di.layerPaths = sortedPaths - return di, nil -} - -func (i *dockerLocalImage) getLayers(ctx context.Context) ([]*claircore.Layer, error) { - if len(i.layerPaths) == 0 { - return nil, nil - } - layers := []*claircore.Layer{} - for _, layerStr := range i.layerPaths { - _, d := filepath.Split(layerStr) - - desc := &claircore.LayerDescription{ - Digest: d, - URI: layerStr, - MediaType: "application/vnd.oci.image.layer.v1.tar", - } - - l := &claircore.Layer{} - f, err := os.OpenFile(layerStr, os.O_RDONLY, os.FileMode(0600)) - if err != nil { - zlog.Error(ctx).Err(err) - } - err = l.Init(ctx, desc, f) - if err != nil { - zlog.Error(ctx).Err(err) - } - - layers = append(layers, l) - - l.Close() - } - return layers, nil -} - -func (i *dockerLocalImage) GetManifest(ctx context.Context) (*claircore.Manifest, error) { - digest, err := claircore.ParseDigest(i.imageDigest) - if err != nil { - return nil, err - } - - layers, err := i.getLayers(ctx) - if err != nil { - return nil, err - } - - return &claircore.Manifest{ - Hash: digest, - Layers: layers, - }, nil -} - -type dockerRemoteImage struct { - ref string -} - -func NewDockerRemoteImage(ctx context.Context, imgRef string) *dockerRemoteImage { - return &dockerRemoteImage{ref: imgRef} -} - -func (i *dockerRemoteImage) GetManifest(ctx context.Context) (*claircore.Manifest, error) { - return Inspect(ctx, i.ref) -} diff --git a/image/docker_test.go b/image/docker_test.go deleted file mode 100644 index 920de13..0000000 --- a/image/docker_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package image - -import ( - "context" - "fmt" - "os" - "testing" -) - -func setup(resultsDir string) { - os.Mkdir(resultsDir, 0700) -} - -func teardown(resultsDir string) { - os.RemoveAll(resultsDir) -} - -func TestFromExported(t *testing.T) { - resultsDir := "testdata/results" - setup(resultsDir) - defer teardown(resultsDir) - ctx := context.TODO() - di, err := NewDockerLocalImage(ctx, "testdata/algo", resultsDir) - if err != nil { - t.Fatalf("got error %v", err) - } - fmt.Println(di) -} diff --git a/image/filesystem.go b/image/filesystem.go index 8e19d16..d5df221 100644 --- a/image/filesystem.go +++ b/image/filesystem.go @@ -8,54 +8,27 @@ import ( "github.com/quay/claircore" ) -type fileSystemImage struct { - imageDigest string - layerPaths []string - rootDir string -} - -func NewFileSystemImage(ctx context.Context, rootDir string) (*fileSystemImage, error) { - fsi := &fileSystemImage{} - fsi.rootDir = rootDir - return fsi, nil -} - -func (i *fileSystemImage) getLayers(ctx context.Context) ([]*claircore.Layer, error) { - layers := []*claircore.Layer{} +func ManifestFromFilesystem(ctx context.Context, rootDir string) (*claircore.Manifest, error) { + digest, err := claircore.ParseDigest(fmt.Sprintf("sha256:%s", strings.Repeat("0", 64))) + if err != nil { + return nil, err + } desc := &claircore.LayerDescription{ Digest: fmt.Sprintf("sha256:%s", strings.Repeat("1", 64)), - URI: "file://" + i.rootDir, + URI: "file://" + rootDir, MediaType: "application/vnd.claircore.filesystem", } l := &claircore.Layer{} - err := l.Init(ctx, desc, nil) - + err = l.Init(ctx, desc, nil) if err != nil { return nil, err } - l.Close() - layers = append(layers, l) - - return layers, nil -} - -func (i *fileSystemImage) GetManifest(ctx context.Context) (*claircore.Manifest, error) { - digest, err := claircore.ParseDigest(fmt.Sprintf("sha256:%s", strings.Repeat("0", 64))) - if err != nil { - return nil, err - } - - layers, err := i.getLayers(ctx) - if err != nil { - return nil, err - } - return &claircore.Manifest{ Hash: digest, - Layers: layers, + Layers: []*claircore.Layer{l}, }, nil } diff --git a/image/inspect.go b/image/inspect.go deleted file mode 100644 index 4e5351a..0000000 --- a/image/inspect.go +++ /dev/null @@ -1,130 +0,0 @@ -// This is lifted from Clairctl - -package image - -import ( - "context" - "net/http" - "net/url" - "path" - "strings" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/remote/transport" - - "github.com/quay/claircore" - "github.com/quay/zlog" -) - -const ( - userAgent = `clair-action/1` -) - -func rt(ctx context.Context, ref string) (http.RoundTripper, error) { - r, err := name.ParseReference(ref) - if err != nil { - return nil, err - } - repo := r.Context() - - auth, err := authn.DefaultKeychain.Resolve(repo) - if err != nil { - return nil, err - } - rt := http.DefaultTransport - rt = transport.NewUserAgent(rt, userAgent) - rt = transport.NewRetry(rt) - rt, err = transport.NewWithContext(ctx, repo.Registry, auth, rt, []string{repo.Scope(transport.PullScope)}) - if err != nil { - return nil, err - } - return rt, nil -} - -func Inspect(ctx context.Context, r string) (*claircore.Manifest, error) { - rt, err := rt(ctx, r) - if err != nil { - return nil, err - } - - ref, err := name.ParseReference(r) - if err != nil { - return nil, err - } - desc, err := remote.Get(ref, remote.WithTransport(rt)) - if err != nil { - return nil, err - } - img, err := desc.Image() - if err != nil { - return nil, err - } - dig, err := img.Digest() - if err != nil { - return nil, err - } - ccd, err := claircore.ParseDigest(dig.String()) - if err != nil { - return nil, err - } - out := claircore.Manifest{Hash: ccd} - zlog.Debug(ctx). - Str("ref", r). - Stringer("digest", ccd). - Msg("found manifest") - - ls, err := img.Layers() - if err != nil { - return nil, err - } - zlog.Debug(ctx). - Str("ref", r). - Int("count", len(ls)). - Msg("found layers") - - repo := ref.Context() - rURL := url.URL{ - Scheme: repo.Scheme(), - Host: repo.RegistryStr(), - } - c := http.Client{ - Transport: rt, - } - - for _, l := range ls { - d, err := l.Digest() - if err != nil { - return nil, err - } - ccd, err := claircore.ParseDigest(d.String()) - if err != nil { - return nil, err - } - u, err := rURL.Parse(path.Join("/", "v2", strings.TrimPrefix(repo.RepositoryStr(), repo.RegistryStr()), "blobs", d.String())) - if err != nil { - return nil, err - } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) - if err != nil { - return nil, err - } - req.Header.Add("Range", "bytes=0-0") - res, err := c.Do(req) - if err != nil { - return nil, err - } - res.Body.Close() - - res.Request.Header.Del("User-Agent") - res.Request.Header.Del("Range") - out.Layers = append(out.Layers, &claircore.Layer{ - Hash: ccd, - URI: res.Request.URL.String(), - Headers: res.Request.Header, - }) - } - - return &out, nil -} diff --git a/image/manifest.go b/image/manifest.go new file mode 100644 index 0000000..602ab55 --- /dev/null +++ b/image/manifest.go @@ -0,0 +1,293 @@ +// This is lifted from Clairctl + +package image + +import ( + "archive/tar" + "compress/gzip" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + + "github.com/quay/claircore" + "github.com/quay/claircore/pkg/tarfs" + "github.com/quay/zlog" +) + +const ( + userAgent = `cvetool/1` +) + +func rt(ctx context.Context, ref string) (http.RoundTripper, error) { + r, err := name.ParseReference(ref) + if err != nil { + return nil, err + } + repo := r.Context() + + auth, err := authn.DefaultKeychain.Resolve(repo) + if err != nil { + return nil, err + } + rt := http.DefaultTransport + rt = transport.NewUserAgent(rt, userAgent) + rt = transport.NewRetry(rt) + rt, err = transport.NewWithContext(ctx, repo.Registry, auth, rt, []string{repo.Scope(transport.PullScope)}) + if err != nil { + return nil, err + } + return rt, nil +} + +func ManifestFromRemote(ctx context.Context, r string) (*claircore.Manifest, error) { + rt, err := rt(ctx, r) + if err != nil { + return nil, err + } + + ref, err := name.ParseReference(r) + if err != nil { + return nil, err + } + desc, err := remote.Get(ref, remote.WithTransport(rt)) + if err != nil { + return nil, err + } + img, err := desc.Image() + if err != nil { + return nil, err + } + dig, err := img.Digest() + if err != nil { + return nil, err + } + ccd, err := claircore.ParseDigest(dig.String()) + if err != nil { + return nil, err + } + out := claircore.Manifest{Hash: ccd} + zlog.Debug(ctx). + Str("ref", r). + Stringer("digest", ccd). + Msg("found manifest") + + ls, err := img.Layers() + if err != nil { + return nil, err + } + zlog.Debug(ctx). + Str("ref", r). + Int("count", len(ls)). + Msg("found layers") + + repo := ref.Context() + rURL := url.URL{ + Scheme: repo.Scheme(), + Host: repo.RegistryStr(), + } + c := http.Client{ + Transport: rt, + } + + for _, l := range ls { + d, err := l.Digest() + if err != nil { + return nil, err + } + ccd, err := claircore.ParseDigest(d.String()) + if err != nil { + return nil, err + } + u, err := rURL.Parse(path.Join("/", "v2", strings.TrimPrefix(repo.RepositoryStr(), repo.RegistryStr()), "blobs", d.String())) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + req.Header.Add("Range", "bytes=0-0") + res, err := c.Do(req) + if err != nil { + return nil, err + } + res.Body.Close() + + res.Request.Header.Del("User-Agent") + res.Request.Header.Del("Range") + out.Layers = append(out.Layers, &claircore.Layer{ + Hash: ccd, + URI: res.Request.URL.String(), + Headers: res.Request.Header, + }) + } + + return &out, nil +} + +type indexFile struct { + Manifests []manifestInfo `json:"manifests"` +} + +type manifestInfo struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size int64 `json:"size"` +} + +type manifestFile struct { + Layers []layerInfo `json:"layers"` +} + +type layerInfo struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size int64 `json:"size"` +} + +func ManifestFromLocal(ctx context.Context, exportDir string) (*claircore.Manifest, error) { + f, err := os.Open(exportDir) + if err != nil { + return nil, fmt.Errorf("unable to open tar: %w", err) + } + defer f.Close() + + out := &claircore.Manifest{} + m := &manifestFile{} + i := &indexFile{} + fs, err := tarfs.New(f) + if err != nil { + return nil, fmt.Errorf("unable to create tarfs: %w", err) + } + index, err := fs.Open("index.json") + if err != nil { + return nil, fmt.Errorf("unable to open index.json: %w", err) + } + defer index.Close() + b, err := io.ReadAll(index) + if err != nil { + return nil, fmt.Errorf("unable to read index.json: %w", err) + } + err = json.Unmarshal(b, &i) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal index.json: %w", err) + } + manifestDigest := "" + for _, m := range i.Manifests { + if m.MediaType == "application/vnd.oci.image.manifest.v1+json" { + manifestDigest = m.Digest + break + } + } + if manifestDigest == "" { + return nil, fmt.Errorf("manifest digest not found") + } + md, err := claircore.ParseDigest(manifestDigest) + if err != nil { + return nil, fmt.Errorf("unable to parse manifest digest: %w", err) + } + out.Hash = md + + mdb := make([]byte, hex.EncodedLen(len(md.Checksum()))) + hex.Encode(mdb, md.Checksum()) + manifestPath := filepath.Join("blobs", md.Algorithm(), string(mdb)) + manifest, err := fs.Open(manifestPath) + if err != nil { + return nil, fmt.Errorf("unable to open manifest: %w", err) + } + defer manifest.Close() + + b, err = io.ReadAll(manifest) + if err != nil { + return nil, fmt.Errorf("unable to read manifest: %w", err) + } + err = json.Unmarshal(b, &m) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal manifest: %w", err) + } + + // We have to revert to tar.NewReader() because tarfs.New() doesn't support + // seeking. + f.Seek(0, io.SeekStart) + tr := tar.NewReader(f) + out.Layers = make([]*claircore.Layer, len(m.Layers)) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("unable to read layer: %w", err) + } + start, _ := f.Seek(0, io.SeekCurrent) + for i, l := range m.Layers { + ld, err := claircore.ParseDigest(l.Digest) + if err != nil { + return nil, fmt.Errorf("unable to parse layer digest: %w", err) + } + + ldb := make([]byte, hex.EncodedLen(len(ld.Checksum()))) + hex.Encode(ldb, ld.Checksum()) + if hdr.Name == filepath.Join("blobs", ld.Algorithm(), string(ldb)) { + ra := io.NewSectionReader(f, start, hdr.Size) + var rAt io.ReaderAt + switch l.MediaType { + case "application/vnd.oci.image.layer.v1.tar+gzip", "application/vnd.docker.image.rootfs.diff.tar.gzip": + gr, err := gzip.NewReader(ra) + if err != nil { + return nil, fmt.Errorf("unable to create gzip reader: %w", err) + } + tmp, err := os.CreateTemp("", "layer-*.tar") + if err != nil { + return nil, fmt.Errorf("unable to create temp file: %w", err) + } + if _, err := io.Copy(tmp, gr); err != nil { + return nil, fmt.Errorf("unable to decompress layer: %w", err) + } + if err := gr.Close(); err != nil { + return nil, fmt.Errorf("unable to close gzip reader: %w", err) + } + if _, err := tmp.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to rewind temp file: %w", err) + } + os.Remove(tmp.Name()) + rAt = tmp + case "application/vnd.oci.image.layer.v1.tar", "application/vnd.docker.image.rootfs.diff.tar": + // uncompressed tar, use section directly + rAt = ra + default: + return nil, fmt.Errorf("unsupported layer media type: %s", l.MediaType) + } + layer := &claircore.Layer{Hash: ld} + err = layer.Init(context.Background(), &claircore.LayerDescription{ + Digest: ld.String(), + MediaType: l.MediaType, + }, rAt) + if err != nil { + return nil, fmt.Errorf("unable to initialize layer: %w", err) + } + out.Layers[i] = layer + } + } + } + for i, l := range out.Layers { + if l == nil { + return nil, fmt.Errorf("layer %d (%s) not found in tar", i, m.Layers[i].Digest) + } + } + return out, nil + +} diff --git a/image/manifest_test.go b/image/manifest_test.go new file mode 100644 index 0000000..4395e87 --- /dev/null +++ b/image/manifest_test.go @@ -0,0 +1,115 @@ +package image + +import ( + "archive/tar" + "bytes" + "context" + "encoding/base64" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "golang.org/x/tools/txtar" +) + +// writeTarFromTxtar converts a txtar-like archive into a tar file on disk. +func writeTarFromTxtar(t *testing.T, txtarPath string) string { + t.Helper() + b, err := os.ReadFile(txtarPath) + if err != nil { + t.Fatalf("read txtar: %v", err) + } + ar := txtar.Parse(b) + + tmpTar := filepath.Join(t.TempDir(), "image-save.tar") + tf, err := os.Create(tmpTar) + if err != nil { + t.Fatalf("create tar: %v", err) + } + defer tf.Close() + tw := tar.NewWriter(tf) + for _, fe := range ar.Files { + if fe.Name == "" { + t.Fatalf("empty file name in txtar") + } + name := fe.Name + data := fe.Data + if strings.HasSuffix(name, ".b64") { + decoded, err := base64.StdEncoding.DecodeString(string(data)) + if err != nil { + t.Fatalf("base64 decode %s: %v", name, err) + } + data = decoded + name = strings.TrimSuffix(name, ".b64") + } + + h := &tar.Header{ + Name: name, + Mode: 0600, + Size: int64(len(data)), + } + if err := tw.WriteHeader(h); err != nil { + t.Fatalf("write header %s: %v", name, err) + } + if _, err := io.Copy(tw, bytes.NewReader(data)); err != nil { + t.Fatalf("write contents %s: %v", name, err) + } + } + if err := tw.Close(); err != nil { + t.Fatalf("close tar: %v", err) + } + return tmpTar +} + +func TestLocalManifest(t *testing.T) { + t.Parallel() + tests := []struct { + name string + txtarRelPath string + wantManifestHash string + wantLayerHash string + }{ + { + name: "docker save", + txtarRelPath: "testdata/docker_save.txtar", + wantManifestHash: "sha256:c9b978d8d0fa53a27117f46b2e17ce906a9de863df82d7709e73868a4932f750", + wantLayerHash: "sha256:e7328e803158cca63d8efdbe1caefb1b51654de77e5fa8691079ad06db1abf75", + }, + { + name: "podman save", + txtarRelPath: "testdata/podman_save.txtar", + wantManifestHash: "sha256:869d3637f2f9b10c265e4bab4b0eccfe8770520e6e903e7dd8acf33b4987bfc1", + wantLayerHash: "sha256:3c6d585e6a72780f0632d16bb8bfd98dfc35b403a11f5cd61925ec31643a76d3", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + exportTar := writeTarFromTxtar(t, tt.txtarRelPath) + ctx := context.Background() + m, err := ManifestFromLocal(ctx, exportTar) + if err != nil { + t.Fatalf("InspectLocal error: %v", err) + } + if m.Hash.String() != tt.wantManifestHash { + t.Fatalf("manifest hash = %s, want %s", m.Hash.String(), tt.wantManifestHash) + } + if len(m.Layers) == 0 { + t.Fatalf("no layers parsed") + } + found := false + for _, l := range m.Layers { + if l.Hash.String() == tt.wantLayerHash { + found = true + break + } + } + if !found { + t.Fatalf("expected layer hash %s not found in layers", tt.wantLayerHash) + } + }) + } +} diff --git a/image/testdata/docker_save.txtar b/image/testdata/docker_save.txtar new file mode 100644 index 0000000..c271d91 --- /dev/null +++ b/image/testdata/docker_save.txtar @@ -0,0 +1,35 @@ +-- index.json -- +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:c9b978d8d0fa53a27117f46b2e17ce906a9de863df82d7709e73868a4932f750", + "size": 403, + "annotations": { + "io.containerd.image.name": "registry.access.redhat.com/ubi9:latest", + "org.opencontainers.image.ref.name": "latest" + } + } + ] +} +-- blobs/sha256/c9b978d8d0fa53a27117f46b2e17ce906a9de863df82d7709e73868a4932f750 -- +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:not_used", + "size": 6421 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar", + "size": 220809728, + "digest": "sha256:e7328e803158cca63d8efdbe1caefb1b51654de77e5fa8691079ad06db1abf75" + } + ] +} +-- blobs/sha256/e7328e803158cca63d8efdbe1caefb1b51654de77e5fa8691079ad06db1abf75.b64 -- +Li8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA3NTUAMDAwMTc1MAAwMDAxNzUwADAwMDAwMDAwMDAwADE1MTExMzU3MDI1ADAxMDY3NQAgNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhciAgAGNyb3p6eQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY3Jvenp5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuL2FsZ28udHh0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAwMDY0NAAwMDAxNzUwADAwMDE3NTAAMDAwMDAwMDAwMDUAMTUxMTEzNTY3NjAAMDEyMzYwACAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHVzdGFyICAAY3Jvenp5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjcm96enkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGFsZ28KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== diff --git a/image/testdata/podman_save.txtar b/image/testdata/podman_save.txtar new file mode 100644 index 0000000..2ae57f5 --- /dev/null +++ b/image/testdata/podman_save.txtar @@ -0,0 +1,33 @@ +-- index.json -- +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:869d3637f2f9b10c265e4bab4b0eccfe8770520e6e903e7dd8acf33b4987bfc1", + "size": 1040, + "annotations": { + "org.opencontainers.image.ref.name": "registry.redhat.io/ubi8/nodejs-10@sha256:4bf163fdb2499b9fc965c26d58c5b7c94381cf3b0f2f018d056a8894097507e3" + } + } + ] +} +-- blobs/sha256/869d3637f2f9b10c265e4bab4b0eccfe8770520e6e903e7dd8acf33b4987bfc1 -- +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:107d07b9bdecdfbe5fbda13d7c8d63be3e3b13ce53bcd3ec54029c41df582fb1", + "size": 4091 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:3c6d585e6a72780f0632d16bb8bfd98dfc35b403a11f5cd61925ec31643a76d3", + "size": 75995687 + } + ] +} +-- blobs/sha256/3c6d585e6a72780f0632d16bb8bfd98dfc35b403a11f5cd61925ec31643a76d3.b64 -- +H4sIAAAAAAAAA+3RQQrCMBCF4Vl7ipwgzbSd5DzFhRuhUCNoT2+yKLpRQQgi/t/mQWYgA8930lwokllNTRYecyNqqjpYCn3Z0xCTibP2p4mcT3lanJP9Mq/r9fneu/mP8t10PMw+X3K7P2rBcRxf9G9b/zHF8q79UMKFdifd/Xn/tf3dt48AAAAAAAAAAAAAAAAA8JEbVuueDAAoAAA=