Repository: containers/image
Pull request #991 (2020-07-25 21:01:52)

Support reading any/all images from docker-archive: tarballs
- Add support for choosing an image to read from a `docker-archive:`, either by reference (using the existing syntax) or by index (`docker-archive:/some/path:@0`).
- Add `docker/archive.Reader` that allows listing all images in a `docker-archive`-formatted file, and reading any/all of them, only copying/extracting the archive once.

See individual commit messages for details.

mtrmac: @vrothberg This should now be code-complete (full transport tests, leading to fixes in reference parsing), but I still need to rebase to move some changes across commits. And it remains mostly untested, hoping Podman’s tests will handle the basics.
mtrmac: @vrothberg Ready for review (but still basically untested).
vrothberg: Thanks, I'll test https://github.com/containers/image/pull/998 and will comment there.
rhatdan: Needs a rebase
mtrmac: Rebased, now includes all of #998.
vrothberg: @rhatdan ... merge me :)
mtrmac: It wouldn’t be unprecedented… but I kind of think that the caller should know that in the first place. IIRC the only really ambiguous case is `podman pull docker-archive:$path`, where I’d argue “treat the transport:name syntax always as a single image” is closest to a consistent semantics.

> Could we add means to figure out if a given reference refers to a file? Ultimately, if it's a file then we have to create a Reader and query for the reference list.
> 
> Currently, I don't find any other way than checking if `(ref).StringWithinTransport()` is on disk or not.

Well, if you want an ugly hack, the syntax is documented to split path:rest on a `:`. But I’d recommend against that.
mtrmac: Added another commit, “Add docker/archive.NewReaderForReference”. DO NOT MERGE until it is verified it is useful for Podman.
mtrmac: Also added `Reader.ManifestTagsForReference` to accommodate Podman’s handling of unqualified short names in the archives (which are only created by projectatomic/docker AFAIK). Ugh.
rhatdan: Need @mtrmac to remove the "DO NOT Merge:" from the commit message.
mtrmac: OK, rebased and “DO NOT MERGE” dropped (although I haven’t had time to fully review the Podman PR)
rhatdan: LGTM
Merge once it passes the tests.

Patch:
diff --git a/docker/archive/dest.go b/docker/archive/dest.go
index 1ead9c57..e874e02e 100644
--- a/docker/archive/dest.go
+++ b/docker/archive/dest.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/containers/image/v5/docker/internal/tarfile"
 	"github.com/containers/image/v5/types"
+	"github.com/pkg/errors"
 )
 
 type archiveImageDestination struct {
@@ -16,6 +17,10 @@ type archiveImageDestination struct {
 }
 
 func newImageDestination(sys *types.SystemContext, ref archiveReference) (types.ImageDestination, error) {
+	if ref.sourceIndex != -1 {
+		return nil, errors.Errorf("Destination reference must not contain a manifest index @%d", ref.sourceIndex)
+	}
+
 	var archive *tarfile.Writer
 	var writer io.Closer
 	if ref.archiveWriter != nil {
@@ -30,7 +35,7 @@ func newImageDestination(sys *types.SystemContext, ref archiveReference) (types.
 		archive = tarfile.NewWriter(fh)
 		writer = fh
 	}
-	tarDest := tarfile.NewDestination(sys, archive, ref.destinationRef)
+	tarDest := tarfile.NewDestination(sys, archive, ref.ref)
 	if sys != nil && sys.DockerArchiveAdditionalTags != nil {
 		tarDest.AddRepoTags(sys.DockerArchiveAdditionalTags)
 	}
diff --git a/docker/archive/reader.go b/docker/archive/reader.go
new file mode 100644
index 00000000..c7bb311b
--- /dev/null
+++ b/docker/archive/reader.go
@@ -0,0 +1,120 @@
+package archive
+
+import (
+	"github.com/containers/image/v5/docker/internal/tarfile"
+	"github.com/containers/image/v5/docker/reference"
+	"github.com/containers/image/v5/transports"
+	"github.com/containers/image/v5/types"
+	"github.com/pkg/errors"
+)
+
+// Reader manages a single Docker archive, allows listing its contents and accessing
+// individual images with less overhead than creating image references individually
+// (because the archive is, if necessary, copied or decompressed only once).
+type Reader struct {
+	path    string // The original, user-specified path; not the maintained temporary file, if any
+	archive *tarfile.Reader
+}
+
+// NewReader returns a Reader for path.
+// The caller should call .Close() on the returned object.
+func NewReader(sys *types.SystemContext, path string) (*Reader, error) {
+	archive, err := tarfile.NewReaderFromFile(sys, path)
+	if err != nil {
+		return nil, err
+	}
+	return &Reader{
+		path:    path,
+		archive: archive,
+	}, nil
+}
+
+// Close deletes temporary files associated with the Reader, if any.
+func (r *Reader) Close() error {
+	return r.archive.Close()
+}
+
+// NewReaderForReference creates a Reader from a Reader-independent imageReference, which must be from docker/archive.Transport,
+// and a variant of imageReference that points at the same image within the reader.
+// The caller should call .Close() on the returned Reader.
+func NewReaderForReference(sys *types.SystemContext, ref types.ImageReference) (*Reader, types.ImageReference, error) {
+	standalone, ok := ref.(archiveReference)
+	if !ok {
+		return nil, nil, errors.Errorf("Internal error: NewReaderForReference called for a non-docker/archive ImageReference %s", transports.ImageName(ref))
+	}
+	if standalone.archiveReader != nil {
+		return nil, nil, errors.Errorf("Internal error: NewReaderForReference called for a reader-bound reference %s", standalone.StringWithinTransport())
+	}
+	reader, err := NewReader(sys, standalone.path)
+	if err != nil {
+		return nil, nil, err
+	}
+	succeeded := false
+	defer func() {
+		if !succeeded {
+			reader.Close()
+		}
+	}()
+	readerRef, err := newReference(standalone.path, standalone.ref, standalone.sourceIndex, reader.archive, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+	succeeded = true
+	return reader, readerRef, nil
+}
+
+// List returns the a set of references for images in the Reader,
+// grouped by the image the references point to.
+// The references are valid only until the Reader is closed.
+func (r *Reader) List() ([][]types.ImageReference, error) {
+	res := [][]types.ImageReference{}
+	for imageIndex, image := range r.archive.Manifest {
+		refs := []types.ImageReference{}
+		for _, tag := range image.RepoTags {
+			parsedTag, err := reference.ParseNormalizedNamed(tag)
+			if err != nil {
+				return nil, errors.Wrapf(err, "Invalid tag %#v in manifest item @%d", tag, imageIndex)
+			}
+			nt, ok := parsedTag.(reference.NamedTagged)
+			if !ok {
+				return nil, errors.Errorf("Invalid tag %s (%s): does not contain a tag", tag, parsedTag.String())
+			}
+			ref, err := newReference(r.path, nt, -1, r.archive, nil)
+			if err != nil {
+				return nil, errors.Wrapf(err, "Error creating a reference for tag %#v in manifest item @%d", tag, imageIndex)
+			}
+			refs = append(refs, ref)
+		}
+		if len(refs) == 0 {
+			ref, err := newReference(r.path, nil, imageIndex, r.archive, nil)
+			if err != nil {
+				return nil, errors.Wrapf(err, "Error creating a reference for manifest item @%d", imageIndex)
+			}
+			refs = append(refs, ref)
+		}
+		res = append(res, refs)
+	}
+	return res, nil
+}
+
+// ManifestTagsForReference returns the set of tags “matching” ref in reader, as strings
+// (i.e. exposing the short names before normalization).
+// The function reports an error if ref does not identify a single image.
+// If ref contains a NamedTagged reference, only a single tag “matching” ref is returned;
+// If ref contains a source index, or neither a NamedTagged nor a source index, all tags
+// matching the image are returned.
+// Almost all users should use List() or ImageReference.DockerReference() instead.
+func (r *Reader) ManifestTagsForReference(ref types.ImageReference) ([]string, error) {
+	archiveRef, ok := ref.(archiveReference)
+	if !ok {
+		return nil, errors.Errorf("Internal error: ManifestTagsForReference called for a non-docker/archive ImageReference %s", transports.ImageName(ref))
+	}
+	manifestItem, tagIndex, err := r.archive.ChooseManifestItem(archiveRef.ref, archiveRef.sourceIndex)
+	if err != nil {
+		return nil, err
+	}
+	if tagIndex != -1 {
+		return []string{manifestItem.RepoTags[tagIndex]}, nil
+	}
+	return manifestItem.RepoTags, nil
+}
diff --git a/docker/archive/src.go b/docker/archive/src.go
index 6a628508..7acca210 100644
--- a/docker/archive/src.go
+++ b/docker/archive/src.go
@@ -3,9 +3,8 @@ package archive
 import (
 	"context"
 
-	"github.com/containers/image/v5/docker/tarfile"
+	"github.com/containers/image/v5/docker/internal/tarfile"
 	"github.com/containers/image/v5/types"
-	"github.com/sirupsen/logrus"
 )
 
 type archiveImageSource struct {
@@ -16,13 +15,20 @@ type archiveImageSource struct {
 // newImageSource returns a types.ImageSource for the specified image reference.
 // The caller must call .Close() on the returned ImageSource.
 func newImageSource(ctx context.Context, sys *types.SystemContext, ref archiveReference) (types.ImageSource, error) {
-	if ref.destinationRef != nil {
-		logrus.Warnf("docker-archive: references are not supported for sources (ignoring)")
-	}
-	src, err := tarfile.NewSourceFromFileWithContext(sys, ref.path)
-	if err != nil {
-		return nil, err
+	var archive *tarfile.Reader
+	var closeArchive bool
+	if ref.archiveReader != nil {
+		archive = ref.archiveReader
+		closeArchive = false
+	} else {
+		a, err := tarfile.NewReaderFromFile(sys, ref.path)
+		if err != nil {
+			return nil, err
+		}
+		archive = a
+		closeArchive = true
 	}
+	src := tarfile.NewSource(archive, closeArchive, ref.ref, ref.sourceIndex)
 	return &archiveImageSource{
 		Source: src,
 		ref:    ref,
diff --git a/docker/archive/transport.go b/docker/archive/transport.go
index 08c07770..ff9e2748 100644
--- a/docker/archive/transport.go
+++ b/docker/archive/transport.go
@@ -3,6 +3,7 @@ package archive
 import (
 	"context"
 	"fmt"
+	"strconv"
 	"strings"
 
 	"github.com/containers/image/v5/docker/internal/tarfile"
@@ -43,9 +44,14 @@ func (t archiveTransport) ValidatePolicyConfigurationScope(scope string) error {
 // archiveReference is an ImageReference for Docker images.
 type archiveReference struct {
 	path string
-	// only used for destinations,
-	// archiveReference.destinationRef is optional and can be nil for destinations as well.
-	destinationRef reference.NamedTagged
+	// May be nil to read the only image in an archive, or to create an untagged image.
+	ref reference.NamedTagged
+	// If not -1, a zero-based index of the image in the manifest. Valid only for sources.
+	// Must not be set if ref is set.
+	sourceIndex int
+	// If not nil, must have been created from path (but archiveReader.path may point at a temporary
+	// file, not necesarily path precisely).
+	archiveReader *tarfile.Reader
 	// If not nil, must have been created for path
 	archiveWriter *tarfile.Writer
 }
@@ -58,44 +64,69 @@ func ParseReference(refString string) (types.ImageReference, error) {
 
 	parts := strings.SplitN(refString, ":", 2)
 	path := parts[0]
-	var destinationRef reference.NamedTagged
+	var nt reference.NamedTagged
+	sourceIndex := -1
 
-	// A :tag was specified, which is only necessary for destinations.
 	if len(parts) == 2 {
-		ref, err := reference.ParseNormalizedNamed(parts[1])
-		if err != nil {
-			return nil, errors.Wrapf(err, "docker-archive parsing reference")
+		// A :tag or :@index was specified.
+		if len(parts[1]) > 0 && parts[1][0] == '@' {
+			i, err := strconv.Atoi(parts[1][1:])
+			if err != nil {
+				return nil, errors.Wrapf(err, "Invalid source index %s", parts[1])
+			}
+			if i < 0 {
+				return nil, errors.Errorf("Invalid source index @%d: must not be negative", i)
+			}
+			sourceIndex = i
+		} else {
+			ref, err := reference.ParseNormalizedNamed(parts[1])
+			if err != nil {
+				return nil, errors.Wrapf(err, "docker-archive parsing reference")
+			}
+			ref = reference.TagNameOnly(ref)
+			refTagged, isTagged := ref.(reference.NamedTagged)
+			if !isTagged { // If ref contains a digest, TagNameOnly does not change it
+				return nil, errors.Errorf("reference does not include a tag: %s", ref.String())
+			}
+			nt = refTagged
 		}
-		ref = reference.TagNameOnly(ref)
-		refTagged, isTagged := ref.(reference.NamedTagged)
-		if !isTagged {
-			// Really shouldn't be hit...
-			return nil, errors.Errorf("internal error: reference is not tagged even after reference.TagNameOnly: %s", refString)
-		}
-		destinationRef = refTagged
 	}
 
-	return NewReference(path, destinationRef)
+	return newReference(path, nt, sourceIndex, nil, nil)
+}
+
+// NewReference returns a Docker archive reference for a path and an optional reference.
+func NewReference(path string, ref reference.NamedTagged) (types.ImageReference, error) {
+	return newReference(path, ref, -1, nil, nil)
 }
 
-// NewReference returns a Docker archive reference for a path and an optional destination reference.
-func NewReference(path string, destinationRef reference.NamedTagged) (types.ImageReference, error) {
-	return newReference(path, destinationRef, nil)
+// NewIndexReference returns a Docker archive reference for a path and a zero-based source manifest index.
+func NewIndexReference(path string, sourceIndex int) (types.ImageReference, error) {
+	return newReference(path, nil, sourceIndex, nil, nil)
 }
 
 // newReference returns a docker archive reference for a path, an optional reference or sourceIndex,
-// and optionally a tarfile.Writer matching path.
-func newReference(path string, destinationRef reference.NamedTagged, archiveWriter *tarfile.Writer) (types.ImageReference, error) {
+// and optionally a tarfile.Reader and/or a tarfile.Writer matching path.
+func newReference(path string, ref reference.NamedTagged, sourceIndex int,
+	archiveReader *tarfile.Reader, archiveWriter *tarfile.Writer) (types.ImageReference, error) {
 	if strings.Contains(path, ":") {
 		return nil, errors.Errorf("Invalid docker-archive: reference: colon in path %q is not supported", path)
 	}
-	if _, isDigest := destinationRef.(reference.Canonical); isDigest {
-		return nil, errors.Errorf("docker-archive doesn't support digest references: %s", destinationRef.String())
+	if ref != nil && sourceIndex != -1 {
+		return nil, errors.Errorf("Invalid docker-archive: reference: cannot use both a tag and a source index")
+	}
+	if _, isDigest := ref.(reference.Canonical); isDigest {
+		return nil, errors.Errorf("docker-archive doesn't support digest references: %s", ref.String())
+	}
+	if sourceIndex != -1 && sourceIndex < 0 {
+		return nil, errors.Errorf("Invalid docker-archive: reference: index @%d must not be negative", sourceIndex)
 	}
 	return archiveReference{
-		path:           path,
-		destinationRef: destinationRef,
-		archiveWriter:  archiveWriter,
+		path:          path,
+		ref:           ref,
+		sourceIndex:   sourceIndex,
+		archiveReader: archiveReader,
+		archiveWriter: archiveWriter,
 	}, nil
 }
 
@@ -109,17 +140,21 @@ func (ref archiveReference) Transport() types.ImageTransport {
 // e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa.
 // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix.
 func (ref archiveReference) StringWithinTransport() string {
-	if ref.destinationRef == nil {
+	switch {
+	case ref.ref != nil:
+		return fmt.Sprintf("%s:%s", ref.path, ref.ref.String())
+	case ref.sourceIndex != -1:
+		return fmt.Sprintf("%s:@%d", ref.path, ref.sourceIndex)
+	default:
 		return ref.path
 	}
-	return fmt.Sprintf("%s:%s", ref.path, ref.destinationRef.String())
 }
 
 // DockerReference returns a Docker reference associated with this reference
 // (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent,
 // not e.g. after redirect or alias processing), or nil if unknown/not applicable.
 func (ref archiveReference) DockerReference() reference.Named {
-	return ref.destinationRef
+	return ref.ref
 }
 
 // PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup.
diff --git a/docker/archive/writer.go b/docker/archive/writer.go
index b54c5ddd..afac2aae 100644
--- a/docker/archive/writer.go
+++ b/docker/archive/writer.go
@@ -47,7 +47,7 @@ func (w *Writer) Close() error {
 // NewReference returns an ImageReference that allows adding an image to Writer,
 // with an optional reference.
 func (w *Writer) NewReference(destinationRef reference.NamedTagged) (types.ImageReference, error) {
-	return newReference(w.path, destinationRef, w.archive)
+	return newReference(w.path, destinationRef, -1, nil, w.archive)
 }
 
 // openArchiveForWriting opens path for writing a tar archive,
diff --git a/docker/daemon/daemon_src.go b/docker/daemon/daemon_src.go
index 1827f811..74a67881 100644
--- a/docker/daemon/daemon_src.go
+++ b/docker/daemon/daemon_src.go
@@ -3,7 +3,7 @@ package daemon
 import (
 	"context"
 
-	"github.com/containers/image/v5/docker/tarfile"
+	"github.com/containers/image/v5/docker/internal/tarfile"
 	"github.com/containers/image/v5/types"
 	"github.com/pkg/errors"
 )
@@ -35,10 +35,11 @@ func newImageSource(ctx context.Context, sys *types.SystemContext, ref daemonRef
 	}
 	defer inputStream.Close()
 
-	src, err := tarfile.NewSourceFromStreamWithSystemContext(sys, inputStream)
+	archive, err := tarfile.NewReaderFromStream(sys, inputStream)
 	if err != nil {
 		return nil, err
 	}
+	src := tarfile.NewSource(archive, true, nil, -1)
 	return &daemonImageSource{
 		ref:    ref,
 		Source: src,
diff --git a/docker/internal/tarfile/reader.go b/docker/internal/tarfile/reader.go
new file mode 100644
index 00000000..83de0c52
--- /dev/null
+++ b/docker/internal/tarfile/reader.go
@@ -0,0 +1,269 @@
+package tarfile
+
+import (
+	"archive/tar"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+
+	"github.com/containers/image/v5/docker/reference"
+	"github.com/containers/image/v5/internal/iolimits"
+	"github.com/containers/image/v5/internal/tmpdir"
+	"github.com/containers/image/v5/pkg/compression"
+	"github.com/containers/image/v5/types"
+	"github.com/pkg/errors"
+)
+
+// Reader is a ((docker save)-formatted) tar archive that allows random access to any component.
+type Reader struct {
+	// None of the fields below are modified after the archive is created, until .Close();
+	// this allows concurrent readers of the same archive.
+	path          string         // "" if the archive has already been closed.
+	removeOnClose bool           // Remove file on close if true
+	Manifest      []ManifestItem // Guaranteed to exist after the archive is created.
+}
+
+// NewReaderFromFile returns a Reader for the specified path.
+// The caller should call .Close() on the returned archive when done.
+func NewReaderFromFile(sys *types.SystemContext, path string) (*Reader, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, "error opening file %q", path)
+	}
+	defer file.Close()
+
+	// If the file is already not compressed we can just return the file itself
+	// as a source. Otherwise we pass the stream to NewReaderFromStream.
+	stream, isCompressed, err := compression.AutoDecompress(file)
+	if err != nil {
+		return nil, errors.Wrapf(err, "Error detecting compression for file %q", path)
+	}
+	defer stream.Close()
+	if !isCompressed {
+		return newReader(path, false)
+	}
+	return NewReaderFromStream(sys, stream)
+}
+
+// NewReaderFromStream returns a Reader for the specified inputStream,
+// which can be either compressed or uncompressed. The caller can close the
+// inputStream immediately after NewReaderFromFile returns.
+// The caller should call .Close() on the returned archive when done.
+func NewReaderFromStream(sys *types.SystemContext, inputStream io.Reader) (*Reader, error) {
+	// Save inputStream to a temporary file
+	tarCopyFile, err := ioutil.TempFile(tmpdir.TemporaryDirectoryForBigFiles(sys), "docker-tar")
+	if err != nil {
+		return nil, errors.Wrap(err, "error creating temporary file")
+	}
+	defer tarCopyFile.Close()
+
+	succeeded := false
+	defer func() {
+		if !succeeded {
+			os.Remove(tarCopyFile.Name())
+		}
+	}()
+
+	// In order to be compatible with docker-load, we need to support
+	// auto-decompression (it's also a nice quality-of-life thing to avoid
+	// giving users really confusing "invalid tar header" errors).
+	uncompressedStream, _, err := compression.AutoDecompress(inputStream)
+	if err != nil {
+		return nil, errors.Wrap(err, "Error auto-decompressing input")
+	}
+	defer uncompressedStream.Close()
+
+	// Copy the plain archive to the temporary file.
+	//
+	// TODO: This can take quite some time, and should ideally be cancellable
+	//       using a context.Context.
+	if _, err := io.Copy(tarCopyFile, uncompressedStream); err != nil {
+		return nil, errors.Wrapf(err, "error copying contents to temporary file %q", tarCopyFile.Name())
+	}
+	succeeded = true
+
+	return newReader(tarCopyFile.Name(), true)
+}
+
+// newReader creates a Reader for the specified path and removeOnClose flag.
+// The caller should call .Close() on the returned archive when done.
+func newReader(path string, removeOnClose bool) (*Reader, error) {
+	// This is a valid enough archive, except Manifest is not yet filled.
+	r := Reader{
+		path:          path,
+		removeOnClose: removeOnClose,
+	}
+	succeeded := false
+	defer func() {
+		if !succeeded {
+			r.Close()
+		}
+	}()
+
+	// We initialize Manifest immediately when constructing the Reader instead
+	// of later on-demand because every caller will need the data, and because doing it now
+	// removes the need to synchronize the access/creation of the data if the archive is later
+	// used from multiple goroutines to access different images.
+
+	// FIXME? Do we need to deal with the legacy format?
+	bytes, err := r.readTarComponent(manifestFileName, iolimits.MaxTarFileManifestSize)
+	if err != nil {
+		return nil, err
+	}
+	if err := json.Unmarshal(bytes, &r.Manifest); err != nil {
+		return nil, errors.Wrap(err, "Error decoding tar manifest.json")
+	}
+
+	succeeded = true
+	return &r, nil
+}
+
+// Close removes resources associated with an initialized Reader, if any.
+func (r *Reader) Close() error {
+	path := r.path
+	r.path = "" // Mark the archive as closed
+	if r.removeOnClose {
+		return os.Remove(path)
+	}
+	return nil
+}
+
+// ChooseManifestItem selects a manifest item from r.Manifest matching (ref, sourceIndex), one or
+// both of which should be (nil, -1).
+// On success, it returns the manifest item and an index of the matching tag, if a tag was used
+// for matching; the index is -1 if a tag was not used.
+func (r *Reader) ChooseManifestItem(ref reference.NamedTagged, sourceIndex int) (*ManifestItem, int, error) {
+	switch {
+	case ref != nil && sourceIndex != -1:
+		return nil, -1, errors.Errorf("Internal error: Cannot have both ref %s and source index @%d",
+			ref.String(), sourceIndex)
+
+	case ref != nil:
+		refString := ref.String()
+		for i := range r.Manifest {
+			for tagIndex, tag := range r.Manifest[i].RepoTags {
+				parsedTag, err := reference.ParseNormalizedNamed(tag)
+				if err != nil {
+					return nil, -1, errors.Wrapf(err, "Invalid tag %#v in manifest.json item @%d", tag, i)
+				}
+				if parsedTag.String() == refString {
+					return &r.Manifest[i], tagIndex, nil
+				}
+			}
+		}
+		return nil, -1, errors.Errorf("Tag %#v not found", refString)
+
+	case sourceIndex != -1:
+		if sourceIndex >= len(r.Manifest) {
+			return nil, -1, errors.Errorf("Invalid source index @%d, only %d manifest items available",
+				sourceIndex, len(r.Manifest))
+		}
+		return &r.Manifest[sourceIndex], -1, nil
+
+	default:
+		if len(r.Manifest) != 1 {
+			return nil, -1, errors.Errorf("Unexpected tar manifest.json: expected 1 item, got %d", len(r.Manifest))
+		}
+		return &r.Manifest[0], -1, nil
+	}
+}
+
+// tarReadCloser is a way to close the backing file of a tar.Reader when the user no longer needs the tar component.
+type tarReadCloser struct {
+	*tar.Reader
+	backingFile *os.File
+}
+
+func (t *tarReadCloser) Close() error {
+	return t.backingFile.Close()
+}
+
+// openTarComponent returns a ReadCloser for the specific file within the archive.
+// This is linear scan; we assume that the tar file will have a fairly small amount of files (~layers),
+// and that filesystem caching will make the repeated seeking over the (uncompressed) tarPath cheap enough.
+// It is safe to call this method from multiple goroutines simultaneously.
+// The caller should call .Close() on the returned stream.
+func (r *Reader) openTarComponent(componentPath string) (io.ReadCloser, error) {
+	// This is only a sanity check; if anyone did concurrently close ra, this access is technically
+	// racy against the write in .Close().
+	if r.path == "" {
+		return nil, errors.New("Internal error: trying to read an already closed tarfile.Reader")
+	}
+
+	f, err := os.Open(r.path)
+	if err != nil {
+		return nil, err
+	}
+	succeeded := false
+	defer func() {
+		if !succeeded {
+			f.Close()
+		}
+	}()
+
+	tarReader, header, err := findTarComponent(f, componentPath)
+	if err != nil {
+		return nil, err
+	}
+	if header == nil {
+		return nil, os.ErrNotExist
+	}
+	if header.FileInfo().Mode()&os.ModeType == os.ModeSymlink { // FIXME: untested
+		// We follow only one symlink; so no loops are possible.
+		if _, err := f.Seek(0, io.SeekStart); err != nil {
+			return nil, err
+		}
+		// The new path could easily point "outside" the archive, but we only compare it to existing tar headers without extracting the archive,
+		// so we don't care.
+		tarReader, header, err = findTarComponent(f, path.Join(path.Dir(componentPath), header.Linkname))
+		if err != nil {
+			return nil, err
+		}
+		if header == nil {
+			return nil, os.ErrNotExist
+		}
+	}
+
+	if !header.FileInfo().Mode().IsRegular() {
+		return nil, errors.Errorf("Error reading tar archive component %s: not a regular file", header.Name)
+	}
+	succeeded = true
+	return &tarReadCloser{Reader: tarReader, backingFile: f}, nil
+}
+
+// findTarComponent returns a header and a reader matching componentPath within inputFile,
+// or (nil, nil, nil) if not found.
+func findTarComponent(inputFile io.Reader, componentPath string) (*tar.Reader, *tar.Header, error) {
+	t := tar.NewReader(inputFile)
+	componentPath = path.Clean(componentPath)
+	for {
+		h, err := t.Next()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, nil, err
+		}
+		if path.Clean(h.Name) == componentPath {
+			return t, h, nil
+		}
+	}
+	return nil, nil, nil
+}
+
+// readTarComponent returns full contents of componentPath.
+// It is safe to call this method from multiple goroutines simultaneously.
+func (r *Reader) readTarComponent(path string, limit int) ([]byte, error) {
+	file, err := r.openTarComponent(path)
+	if err != nil {
+		return nil, errors.Wrapf(err, "Error loading tar component %s", path)
+	}
+	defer file.Close()
+	bytes, err := iolimits.ReadAtMost(file, limit)
+	if err != nil {
+		return nil, err
+	}
+	return bytes, nil
+}
diff --git a/docker/internal/tarfile/src.go b/docker/internal/tarfile/src.go
new file mode 100644
index 00000000..0db9a72b
--- /dev/null
+++ b/docker/internal/tarfile/src.go
@@ -0,0 +1,331 @@
+package tarfile
+
+import (
+	"archive/tar"
+	"bytes"
+	"context"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"os"
+	"path"
+	"sync"
+
+	"github.com/containers/image/v5/docker/reference"
+	"github.com/containers/image/v5/internal/iolimits"
+	"github.com/containers/image/v5/manifest"
+	"github.com/containers/image/v5/pkg/compression"
+	"github.com/containers/image/v5/types"
+	digest "github.com/opencontainers/go-digest"
+	"github.com/pkg/errors"
+)
+
+// Source is a partial implementation of types.ImageSource for reading from tarPath.
+type Source struct {
+	archive      *Reader
+	closeArchive bool // .Close() the archive when the source is closed.
+	// If ref is nil and sourceIndex is -1, indicates the only image in the archive.
+	ref         reference.NamedTagged // May be nil
+	sourceIndex int                   // May be -1
+	// The following data is only available after ensureCachedDataIsPresent() succeeds
+	tarManifest       *ManifestItem // nil if not available yet.
+	configBytes       []byte
+	configDigest      digest.Digest
+	orderedDiffIDList []digest.Digest
+	knownLayers       map[digest.Digest]*layerInfo
+	// Other state
+	generatedManifest []byte    // Private cache for GetManifest(), nil if not set yet.
+	cacheDataLock     sync.Once // Private state for ensureCachedDataIsPresent to make it concurrency-safe
+	cacheDataResult   error     // Private state for ensureCachedDataIsPresent
+}
+
+type layerInfo struct {
+	path string
+	size int64
+}
+
+// NewSource returns a tarfile.Source for an image in the specified archive matching ref
+// and sourceIndex (or the only image if they are (nil, -1)).
+// The archive will be closed if closeArchive
+func NewSource(archive *Reader, closeArchive bool, ref reference.NamedTagged, sourceIndex int) *Source {
+	return &Source{
+		archive:      archive,
+		closeArchive: closeArchive,
+		ref:          ref,
+		sourceIndex:  sourceIndex,
+	}
+}
+
+// ensureCachedDataIsPresent loads data necessary for any of the public accessors.
+// It is safe to call this from multi-threaded code.
+func (s *Source) ensureCachedDataIsPresent() error {
+	s.cacheDataLock.Do(func() {
+		s.cacheDataResult = s.ensureCachedDataIsPresentPrivate()
+	})
+	return s.cacheDataResult
+}
+
+// ensureCachedDataIsPresentPrivate is a private implementation detail of ensureCachedDataIsPresent.
+// Call ensureCachedDataIsPresent instead.
+func (s *Source) ensureCachedDataIsPresentPrivate() error {
+	tarManifest, _, err := s.archive.ChooseManifestItem(s.ref, s.sourceIndex)
+	if err != nil {
+		return err
+	}
+
+	// Read and parse config.
+	configBytes, err := s.archive.readTarComponent(tarManifest.Config, iolimits.MaxConfigBodySize)
+	if err != nil {
+		return err
+	}
+	var parsedConfig manifest.Schema2Image // There's a lot of info there, but we only really care about layer DiffIDs.
+	if err := json.Unmarshal(configBytes, &parsedConfig); err != nil {
+		return errors.Wrapf(err, "Error decoding tar config %s", tarManifest.Config)
+	}
+	if parsedConfig.RootFS == nil {
+		return errors.Errorf("Invalid image config (rootFS is not set): %s", tarManifest.Config)
+	}
+
+	knownLayers, err := s.prepareLayerData(tarManifest, &parsedConfig)
+	if err != nil {
+		return err
+	}
+
+	// Success; commit.
+	s.tarManifest = tarManifest
+	s.configBytes = configBytes
+	s.configDigest = digest.FromBytes(configBytes)
+	s.orderedDiffIDList = parsedConfig.RootFS.DiffIDs
+	s.knownLayers = knownLayers
+	return nil
+}
+
+// Close removes resources associated with an initialized Source, if any.
+func (s *Source) Close() error {
+	if s.closeArchive {
+		return s.archive.Close()
+	}
+	return nil
+}
+
+// TarManifest returns contents of manifest.json
+func (s *Source) TarManifest() []ManifestItem {
+	return s.archive.Manifest
+}
+
+func (s *Source) prepareLayerData(tarManifest *ManifestItem, parsedConfig *manifest.Schema2Image) (map[digest.Digest]*layerInfo, error) {
+	// Collect layer data available in manifest and config.
+	if len(tarManifest.Layers) != len(parsedConfig.RootFS.DiffIDs) {
+		return nil, errors.Errorf("Inconsistent layer count: %d in manifest, %d in config", len(tarManifest.Layers), len(parsedConfig.RootFS.DiffIDs))
+	}
+	knownLayers := map[digest.Digest]*layerInfo{}
+	unknownLayerSizes := map[string]*layerInfo{} // Points into knownLayers, a "to do list" of items with unknown sizes.
+	for i, diffID := range parsedConfig.RootFS.DiffIDs {
+		if _, ok := knownLayers[diffID]; ok {
+			// Apparently it really can happen that a single image contains the same layer diff more than once.
+			// In that case, the diffID validation ensures that both layers truly are the same, and it should not matter
+			// which of the tarManifest.Layers paths is used; (docker save) actually makes the duplicates symlinks to the original.
+			continue
+		}
+		layerPath := path.Clean(tarManifest.Layers[i])
+		if _, ok := unknownLayerSizes[layerPath]; ok {
+			return nil, errors.Errorf("Layer tarfile %s used for two different DiffID values", layerPath)
+		}
+		li := &layerInfo{ // A new element in each iteration
+			path: layerPath,
+			size: -1,
+		}
+		knownLayers[diffID] = li
+		unknownLayerSizes[layerPath] = li
+	}
+
+	// Scan the tar file to collect layer sizes.
+	file, err := os.Open(s.archive.path)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+	t := tar.NewReader(file)
+	for {
+		h, err := t.Next()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+		layerPath := path.Clean(h.Name)
+		// FIXME: Cache this data across images in Reader.
+		if li, ok := unknownLayerSizes[layerPath]; ok {
+			// Since GetBlob will decompress layers that are compressed we need
+			// to do the decompression here as well, otherwise we will
+			// incorrectly report the size. Pretty critical, since tools like
+			// umoci always compress layer blobs. Obviously we only bother with
+			// the slower method of checking if it's compressed.
+			uncompressedStream, isCompressed, err := compression.AutoDecompress(t)
+			if err != nil {
+				return nil, errors.Wrapf(err, "Error auto-decompressing %s to determine its size", layerPath)
+			}
+			defer uncompressedStream.Close()
+
+			uncompressedSize := h.Size
+			if isCompressed {
+				uncompressedSize, err = io.Copy(ioutil.Discard, uncompressedStream)
+				if err != nil {
+					return nil, errors.Wrapf(err, "Error reading %s to find its size", layerPath)
+				}
+			}
+			li.size = uncompressedSize
+			delete(unknownLayerSizes, layerPath)
+		}
+	}
+	if len(unknownLayerSizes) != 0 {
+		return nil, errors.Errorf("Some layer tarfiles are missing in the tarball") // This could do with a better error reporting, if this ever happened in practice.
+	}
+
+	return knownLayers, nil
+}
+
+// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).
+// It may use a remote (= slow) service.
+// If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list);
+// this never happens if the primary manifest is not a manifest list (e.g. if the source never returns manifest lists).
+// This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil,
+// as the primary manifest can not be a list, so there can be no secondary instances.
+func (s *Source) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) {
+	if instanceDigest != nil {
+		// How did we even get here? GetManifest(ctx, nil) has returned a manifest.DockerV2Schema2MediaType.
+		return nil, "", errors.New(`Manifest lists are not supported by "docker-daemon:"`)
+	}
+	if s.generatedManifest == nil {
+		if err := s.ensureCachedDataIsPresent(); err != nil {
+			return nil, "", err
+		}
+		m := manifest.Schema2{
+			SchemaVersion: 2,
+			MediaType:     manifest.DockerV2Schema2MediaType,
+			ConfigDescriptor: manifest.Schema2Descriptor{
+				MediaType: manifest.DockerV2Schema2ConfigMediaType,
+				Size:      int64(len(s.configBytes)),
+				Digest:    s.configDigest,
+			},
+			LayersDescriptors: []manifest.Schema2Descriptor{},
+		}
+		for _, diffID := range s.orderedDiffIDList {
+			li, ok := s.knownLayers[diffID]
+			if !ok {
+				return nil, "", errors.Errorf("Internal inconsistency: Information about layer %s missing", diffID)
+			}
+			m.LayersDescriptors = append(m.LayersDescriptors, manifest.Schema2Descriptor{
+				Digest:    diffID, // diffID is a digest of the uncompressed tarball
+				MediaType: manifest.DockerV2Schema2LayerMediaType,
+				Size:      li.size,
+			})
+		}
+		manifestBytes, err := json.Marshal(&m)
+		if err != nil {
+			return nil, "", err
+		}
+		s.generatedManifest = manifestBytes
+	}
+	return s.generatedManifest, manifest.DockerV2Schema2MediaType, nil
+}
+
+// uncompressedReadCloser is an io.ReadCloser that closes both the uncompressed stream and the underlying input.
+type uncompressedReadCloser struct {
+	io.Reader
+	underlyingCloser   func() error
+	uncompressedCloser func() error
+}
+
+func (r uncompressedReadCloser) Close() error {
+	var res error
+	if err := r.uncompressedCloser(); err != nil {
+		res = err
+	}
+	if err := r.underlyingCloser(); err != nil && res == nil {
+		res = err
+	}
+	return res
+}
+
+// HasThreadSafeGetBlob indicates whether GetBlob can be executed concurrently.
+func (s *Source) HasThreadSafeGetBlob() bool {
+	return true
+}
+
+// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown).
+// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
+// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location.
+func (s *Source) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
+	if err := s.ensureCachedDataIsPresent(); err != nil {
+		return nil, 0, err
+	}
+
+	if info.Digest == s.configDigest { // FIXME? Implement a more general algorithm matching instead of assuming sha256.
+		return ioutil.NopCloser(bytes.NewReader(s.configBytes)), int64(len(s.configBytes)), nil
+	}
+
+	if li, ok := s.knownLayers[info.Digest]; ok { // diffID is a digest of the uncompressed tarball,
+		underlyingStream, err := s.archive.openTarComponent(li.path)
+		if err != nil {
+			return nil, 0, err
+		}
+		closeUnderlyingStream := true
+		defer func() {
+			if closeUnderlyingStream {
+				underlyingStream.Close()
+			}
+		}()
+
+		// In order to handle the fact that digests != diffIDs (and thus that a
+		// caller which is trying to verify the blob will run into problems),
+		// we need to decompress blobs. This is a bit ugly, but it's a
+		// consequence of making everything addressable by their DiffID rather
+		// than by their digest...
+		//
+		// In particular, because the v2s2 manifest being generated uses
+		// DiffIDs, any caller of GetBlob is going to be asking for DiffIDs of
+		// layers not their _actual_ digest. The result is that copy/... will
+		// be verifing a "digest" which is not the actual layer's digest (but
+		// is instead the DiffID).
+
+		uncompressedStream, _, err := compression.AutoDecompress(underlyingStream)
+		if err != nil {
+			return nil, 0, errors.Wrapf(err, "Error auto-decompressing blob %s", info.Digest)
+		}
+
+		newStream := uncompressedReadCloser{
+			Reader:             uncompressedStream,
+			underlyingCloser:   underlyingStream.Close,
+			uncompressedCloser: uncompressedStream.Close,
+		}
+		closeUnderlyingStream = false
+
+		return newStream, li.size, nil
+	}
+
+	return nil, 0, errors.Errorf("Unknown blob %s", info.Digest)
+}
+
+// GetSignatures returns the image's signatures.  It may use a remote (= slow) service.
+// This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil,
+// as there can be no secondary manifests.
+func (s *Source) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) {
+	if instanceDigest != nil {
+		// How did we even get here? GetManifest(ctx, nil) has returned a manifest.DockerV2Schema2MediaType.
+		return nil, errors.Errorf(`Manifest lists are not supported by "docker-daemon:"`)
+	}
+	return [][]byte{}, nil
+}
+
+// LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer
+// blobsums that are listed in the image's manifest.  If values are returned, they should be used when using GetBlob()
+// to read the image's layers.
+// This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil,
+// as the primary manifest can not be a list, so there can be no secondary manifests.
+// The Digest field is guaranteed to be provided; Size may be -1.
+// WARNING: The list may contain duplicates, and they are semantically relevant.
+func (s *Source) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) {
+	return nil, nil
+}
diff --git a/docker/tarfile/src.go b/docker/tarfile/src.go
index f04fc16c..ee341eb3 100644
--- a/docker/tarfile/src.go
+++ b/docker/tarfile/src.go
@@ -1,51 +1,20 @@
 package tarfile
 
 import (
-	"archive/tar"
-	"bytes"
 	"context"
-	"encoding/json"
 	"io"
-	"io/ioutil"
-	"os"
-	"path"
-	"sync"
 
-	"github.com/containers/image/v5/internal/iolimits"
-	"github.com/containers/image/v5/internal/tmpdir"
-	"github.com/containers/image/v5/manifest"
-	"github.com/containers/image/v5/pkg/compression"
+	internal "github.com/containers/image/v5/docker/internal/tarfile"
 	"github.com/containers/image/v5/types"
 	digest "github.com/opencontainers/go-digest"
-	"github.com/pkg/errors"
 )
 
 // Source is a partial implementation of types.ImageSource for reading from tarPath.
+// Most users should use this via implementations of ImageReference from docker/archive or docker/daemon.
 type Source struct {
-	tarPath              string
-	removeTarPathOnClose bool // Remove temp file on close if true
-	// The following data is only available after ensureCachedDataIsPresent() succeeds
-	tarManifest       *ManifestItem // nil if not available yet.
-	configBytes       []byte
-	configDigest      digest.Digest
-	orderedDiffIDList []digest.Digest
-	knownLayers       map[digest.Digest]*layerInfo
-	// Other state
-	generatedManifest []byte    // Private cache for GetManifest(), nil if not set yet.
-	cacheDataLock     sync.Once // Private state for ensureCachedDataIsPresent to make it concurrency-safe
-	cacheDataResult   error     // Private state for ensureCachedDataIsPresent
+	internal *internal.Source
 }
 
-type layerInfo struct {
-	path string
-	size int64
-}
-
-// TODO: We could add support for multiple images in a single archive, so
-//       that people could use docker-archive:opensuse.tar:opensuse:leap as
-//       the source of an image.
-// 	To do for both the NewSourceFromFile and NewSourceFromStream functions
-
 // NewSourceFromFile returns a tarfile.Source for the specified path.
 // Deprecated: Please use NewSourceFromFileWithContext which will allows you to configure temp directory
 // for big files through SystemContext.BigFilesTemporaryDir
@@ -55,25 +24,12 @@ func NewSourceFromFile(path string) (*Source, error) {
 
 // NewSourceFromFileWithContext returns a tarfile.Source for the specified path.
 func NewSourceFromFileWithContext(sys *types.SystemContext, path string) (*Source, error) {
-	file, err := os.Open(path)
-	if err != nil {
-		return nil, errors.Wrapf(err, "error opening file %q", path)
-	}
-	defer file.Close()
-
-	// If the file is already not compressed we can just return the file itself
-	// as a source. Otherwise we pass the stream to NewSourceFromStream.
-	stream, isCompressed, err := compression.AutoDecompress(file)
+	archive, err := internal.NewReaderFromFile(sys, path)
 	if err != nil {
-		return nil, errors.Wrapf(err, "Error detecting compression for file %q", path)
-	}
-	defer stream.Close()
-	if !isCompressed {
-		return &Source{
-			tarPath: path,
-		}, nil
+		return nil, err
 	}
-	return NewSourceFromStreamWithSystemContext(sys, stream)
+	src := internal.NewSource(archive, true, nil, -1)
+	return &Source{internal: src}, nil
 }
 
 // NewSourceFromStream returns a tarfile.Source for the specified inputStream,
@@ -89,282 +45,22 @@ func NewSourceFromStream(inputStream io.Reader) (*Source, error) {
 // which can be either compressed or uncompressed. The caller can close the
 // inputStream immediately after NewSourceFromFile returns.
 func NewSourceFromStreamWithSystemContext(sys *types.SystemContext, inputStream io.Reader) (*Source, error) {
-	// FIXME: use SystemContext here.
-	// Save inputStream to a temporary file
-	tarCopyFile, err := ioutil.TempFile(tmpdir.TemporaryDirectoryForBigFiles(sys), "docker-tar")
-	if err != nil {
-		return nil, errors.Wrap(err, "error creating temporary file")
-	}
-	defer tarCopyFile.Close()
-
-	succeeded := false
-	defer func() {
-		if !succeeded {
-			os.Remove(tarCopyFile.Name())
-		}
-	}()
-
-	// In order to be compatible with docker-load, we need to support
-	// auto-decompression (it's also a nice quality-of-life thing to avoid
-	// giving users really confusing "invalid tar header" errors).
-	uncompressedStream, _, err := compression.AutoDecompress(inputStream)
-	if err != nil {
-		return nil, errors.Wrap(err, "Error auto-decompressing input")
-	}
-	defer uncompressedStream.Close()
-
-	// Copy the plain archive to the temporary file.
-	//
-	// TODO: This can take quite some time, and should ideally be cancellable
-	//       using a context.Context.
-	if _, err := io.Copy(tarCopyFile, uncompressedStream); err != nil {
-		return nil, errors.Wrapf(err, "error copying contents to temporary file %q", tarCopyFile.Name())
-	}
-	succeeded = true
-
-	return &Source{
-		tarPath:              tarCopyFile.Name(),
-		removeTarPathOnClose: true,
-	}, nil
-}
-
-// tarReadCloser is a way to close the backing file of a tar.Reader when the user no longer needs the tar component.
-type tarReadCloser struct {
-	*tar.Reader
-	backingFile *os.File
-}
-
-func (t *tarReadCloser) Close() error {
-	return t.backingFile.Close()
-}
-
-// openTarComponent returns a ReadCloser for the specific file within the archive.
-// This is linear scan; we assume that the tar file will have a fairly small amount of files (~layers),
-// and that filesystem caching will make the repeated seeking over the (uncompressed) tarPath cheap enough.
-// The caller should call .Close() on the returned stream.
-func (s *Source) openTarComponent(componentPath string) (io.ReadCloser, error) {
-	f, err := os.Open(s.tarPath)
-	if err != nil {
-		return nil, err
-	}
-	succeeded := false
-	defer func() {
-		if !succeeded {
-			f.Close()
-		}
-	}()
-
-	tarReader, header, err := findTarComponent(f, componentPath)
-	if err != nil {
-		return nil, err
-	}
-	if header == nil {
-		return nil, os.ErrNotExist
-	}
-	if header.FileInfo().Mode()&os.ModeType == os.ModeSymlink { // FIXME: untested
-		// We follow only one symlink; so no loops are possible.
-		if _, err := f.Seek(0, io.SeekStart); err != nil {
-			return nil, err
-		}
-		// The new path could easily point "outside" the archive, but we only compare it to existing tar headers without extracting the archive,
-		// so we don't care.
-		tarReader, header, err = findTarComponent(f, path.Join(path.Dir(componentPath), header.Linkname))
-		if err != nil {
-			return nil, err
-		}
-		if header == nil {
-			return nil, os.ErrNotExist
-		}
-	}
-
-	if !header.FileInfo().Mode().IsRegular() {
-		return nil, errors.Errorf("Error reading tar archive component %s: not a regular file", header.Name)
-	}
-	succeeded = true
-	return &tarReadCloser{Reader: tarReader, backingFile: f}, nil
-}
-
-// findTarComponent returns a header and a reader matching componentPath within inputFile,
-// or (nil, nil, nil) if not found.
-func findTarComponent(inputFile io.Reader, componentPath string) (*tar.Reader, *tar.Header, error) {
-	t := tar.NewReader(inputFile)
-	componentPath = path.Clean(componentPath)
-	for {
-		h, err := t.Next()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return nil, nil, err
-		}
-		if path.Clean(h.Name) == componentPath {
-			return t, h, nil
-		}
-	}
-	return nil, nil, nil
-}
-
-// readTarComponent returns full contents of componentPath.
-func (s *Source) readTarComponent(path string, limit int) ([]byte, error) {
-	file, err := s.openTarComponent(path)
-	if err != nil {
-		return nil, errors.Wrapf(err, "Error loading tar component %s", path)
-	}
-	defer file.Close()
-	bytes, err := iolimits.ReadAtMost(file, limit)
-	if err != nil {
-		return nil, err
-	}
-	return bytes, nil
-}
-
-// ensureCachedDataIsPresent loads data necessary for any of the public accessors.
-// It is safe to call this from multi-threaded code.
-func (s *Source) ensureCachedDataIsPresent() error {
-	s.cacheDataLock.Do(func() {
-		s.cacheDataResult = s.ensureCachedDataIsPresentPrivate()
-	})
-	return s.cacheDataResult
-}
-
-// ensureCachedDataIsPresentPrivate is a private implementation detail of ensureCachedDataIsPresent.
-// Call ensureCachedDataIsPresent instead.
-func (s *Source) ensureCachedDataIsPresentPrivate() error {
-	// Read and parse manifest.json
-	tarManifest, err := s.loadTarManifest()
-	if err != nil {
-		return err
-	}
-
-	// Check to make sure length is 1
-	if len(tarManifest) != 1 {
-		return errors.Errorf("Unexpected tar manifest.json: expected 1 item, got %d", len(tarManifest))
-	}
-
-	// Read and parse config.
-	configBytes, err := s.readTarComponent(tarManifest[0].Config, iolimits.MaxConfigBodySize)
-	if err != nil {
-		return err
-	}
-	var parsedConfig manifest.Schema2Image // There's a lot of info there, but we only really care about layer DiffIDs.
-	if err := json.Unmarshal(configBytes, &parsedConfig); err != nil {
-		return errors.Wrapf(err, "Error decoding tar config %s", tarManifest[0].Config)
-	}
-	if parsedConfig.RootFS == nil {
-		return errors.Errorf("Invalid image config (rootFS is not set): %s", tarManifest[0].Config)
-	}
-
-	knownLayers, err := s.prepareLayerData(&tarManifest[0], &parsedConfig)
-	if err != nil {
-		return err
-	}
-
-	// Success; commit.
-	s.tarManifest = &tarManifest[0]
-	s.configBytes = configBytes
-	s.configDigest = digest.FromBytes(configBytes)
-	s.orderedDiffIDList = parsedConfig.RootFS.DiffIDs
-	s.knownLayers = knownLayers
-	return nil
-}
-
-// loadTarManifest loads and decodes the manifest.json.
-func (s *Source) loadTarManifest() ([]ManifestItem, error) {
-	// FIXME? Do we need to deal with the legacy format?
-	bytes, err := s.readTarComponent(manifestFileName, iolimits.MaxTarFileManifestSize)
+	archive, err := internal.NewReaderFromStream(sys, inputStream)
 	if err != nil {
 		return nil, err
 	}
-	var items []ManifestItem
-	if err := json.Unmarshal(bytes, &items); err != nil {
-		return nil, errors.Wrap(err, "Error decoding tar manifest.json")
-	}
-	return items, nil
+	src := internal.NewSource(archive, true, nil, -1)
+	return &Source{internal: src}, nil
 }
 
 // Close removes resources associated with an initialized Source, if any.
 func (s *Source) Close() error {
-	if s.removeTarPathOnClose {
-		return os.Remove(s.tarPath)
-	}
-	return nil
+	return s.internal.Close()
 }
 
 // LoadTarManifest loads and decodes the manifest.json
 func (s *Source) LoadTarManifest() ([]ManifestItem, error) {
-	return s.loadTarManifest()
-}
-
-func (s *Source) prepareLayerData(tarManifest *ManifestItem, parsedConfig *manifest.Schema2Image) (map[digest.Digest]*layerInfo, error) {
-	// Collect layer data available in manifest and config.
-	if len(tarManifest.Layers) != len(parsedConfig.RootFS.DiffIDs) {
-		return nil, errors.Errorf("Inconsistent layer count: %d in manifest, %d in config", len(tarManifest.Layers), len(parsedConfig.RootFS.DiffIDs))
-	}
-	knownLayers := map[digest.Digest]*layerInfo{}
-	unknownLayerSizes := map[string]*layerInfo{} // Points into knownLayers, a "to do list" of items with unknown sizes.
-	for i, diffID := range parsedConfig.RootFS.DiffIDs {
-		if _, ok := knownLayers[diffID]; ok {
-			// Apparently it really can happen that a single image contains the same layer diff more than once.
-			// In that case, the diffID validation ensures that both layers truly are the same, and it should not matter
-			// which of the tarManifest.Layers paths is used; (docker save) actually makes the duplicates symlinks to the original.
-			continue
-		}
-		layerPath := path.Clean(tarManifest.Layers[i])
-		if _, ok := unknownLayerSizes[layerPath]; ok {
-			return nil, errors.Errorf("Layer tarfile %s used for two different DiffID values", layerPath)
-		}
-		li := &layerInfo{ // A new element in each iteration
-			path: layerPath,
-			size: -1,
-		}
-		knownLayers[diffID] = li
-		unknownLayerSizes[layerPath] = li
-	}
-
-	// Scan the tar file to collect layer sizes.
-	file, err := os.Open(s.tarPath)
-	if err != nil {
-		return nil, err
-	}
-	defer file.Close()
-	t := tar.NewReader(file)
-	for {
-		h, err := t.Next()
-		if err == io.EOF {
-			break
-		}
-		if err != nil {
-			return nil, err
-		}
-		layerPath := path.Clean(h.Name)
-		if li, ok := unknownLayerSizes[layerPath]; ok {
-			// Since GetBlob will decompress layers that are compressed we need
-			// to do the decompression here as well, otherwise we will
-			// incorrectly report the size. Pretty critical, since tools like
-			// umoci always compress layer blobs. Obviously we only bother with
-			// the slower method of checking if it's compressed.
-			uncompressedStream, isCompressed, err := compression.AutoDecompress(t)
-			if err != nil {
-				return nil, errors.Wrapf(err, "Error auto-decompressing %s to determine its size", layerPath)
-			}
-			defer uncompressedStream.Close()
-
-			uncompressedSize := h.Size
-			if isCompressed {
-				uncompressedSize, err = io.Copy(ioutil.Discard, uncompressedStream)
-				if err != nil {
-					return nil, errors.Wrapf(err, "Error reading %s to find its size", layerPath)
-				}
-			}
-			li.size = uncompressedSize
-			delete(unknownLayerSizes, layerPath)
-		}
-	}
-	if len(unknownLayerSizes) != 0 {
-		return nil, errors.Errorf("Some layer tarfiles are missing in the tarball") // This could do with a better error reporting, if this ever happened in practice.
-	}
-
-	return knownLayers, nil
+	return s.internal.TarManifest(), nil
 }
 
 // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).
@@ -374,130 +70,26 @@ func (s *Source) prepareLayerData(tarManifest *ManifestItem, parsedConfig *manif
 // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil,
 // as the primary manifest can not be a list, so there can be no secondary instances.
 func (s *Source) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) {
-	if instanceDigest != nil {
-		// How did we even get here? GetManifest(ctx, nil) has returned a manifest.DockerV2Schema2MediaType.
-		return nil, "", errors.New(`Manifest lists are not supported by "docker-daemon:"`)
-	}
-	if s.generatedManifest == nil {
-		if err := s.ensureCachedDataIsPresent(); err != nil {
-			return nil, "", err
-		}
-		m := manifest.Schema2{
-			SchemaVersion: 2,
-			MediaType:     manifest.DockerV2Schema2MediaType,
-			ConfigDescriptor: manifest.Schema2Descriptor{
-				MediaType: manifest.DockerV2Schema2ConfigMediaType,
-				Size:      int64(len(s.configBytes)),
-				Digest:    s.configDigest,
-			},
-			LayersDescriptors: []manifest.Schema2Descriptor{},
-		}
-		for _, diffID := range s.orderedDiffIDList {
-			li, ok := s.knownLayers[diffID]
-			if !ok {
-				return nil, "", errors.Errorf("Internal inconsistency: Information about layer %s missing", diffID)
-			}
-			m.LayersDescriptors = append(m.LayersDescriptors, manifest.Schema2Descriptor{
-				Digest:    diffID, // diffID is a digest of the uncompressed tarball
-				MediaType: manifest.DockerV2Schema2LayerMediaType,
-				Size:      li.size,
-			})
-		}
-		manifestBytes, err := json.Marshal(&m)
-		if err != nil {
-			return nil, "", err
-		}
-		s.generatedManifest = manifestBytes
-	}
-	return s.generatedManifest, manifest.DockerV2Schema2MediaType, nil
-}
-
-// uncompressedReadCloser is an io.ReadCloser that closes both the uncompressed stream and the underlying input.
-type uncompressedReadCloser struct {
-	io.Reader
-	underlyingCloser   func() error
-	uncompressedCloser func() error
-}
-
-func (r uncompressedReadCloser) Close() error {
-	var res error
-	if err := r.uncompressedCloser(); err != nil {
-		res = err
-	}
-	if err := r.underlyingCloser(); err != nil && res == nil {
-		res = err
-	}
-	return res
+	return s.internal.GetManifest(ctx, instanceDigest)
 }
 
 // HasThreadSafeGetBlob indicates whether GetBlob can be executed concurrently.
 func (s *Source) HasThreadSafeGetBlob() bool {
-	return true
+	return s.internal.HasThreadSafeGetBlob()
 }
 
 // GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown).
 // The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
 // May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location.
 func (s *Source) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
-	if err := s.ensureCachedDataIsPresent(); err != nil {
-		return nil, 0, err
-	}
-
-	if info.Digest == s.configDigest { // FIXME? Implement a more general algorithm matching instead of assuming sha256.
-		return ioutil.NopCloser(bytes.NewReader(s.configBytes)), int64(len(s.configBytes)), nil
-	}
-
-	if li, ok := s.knownLayers[info.Digest]; ok { // diffID is a digest of the uncompressed tarball,
-		underlyingStream, err := s.openTarComponent(li.path)
-		if err != nil {
-			return nil, 0, err
-		}
-		closeUnderlyingStream := true
-		defer func() {
-			if closeUnderlyingStream {
-				underlyingStream.Close()
-			}
-		}()
-
-		// In order to handle the fact that digests != diffIDs (and thus that a
-		// caller which is trying to verify the blob will run into problems),
-		// we need to decompress blobs. This is a bit ugly, but it's a
-		// consequence of making everything addressable by their DiffID rather
-		// than by their digest...
-		//
-		// In particular, because the v2s2 manifest being generated uses
-		// DiffIDs, any caller of GetBlob is going to be asking for DiffIDs of
-		// layers not their _actual_ digest. The result is that copy/... will
-		// be verifing a "digest" which is not the actual layer's digest (but
-		// is instead the DiffID).
-
-		uncompressedStream, _, err := compression.AutoDecompress(underlyingStream)
-		if err != nil {
-			return nil, 0, errors.Wrapf(err, "Error auto-decompressing blob %s", info.Digest)
-		}
-
-		newStream := uncompressedReadCloser{
-			Reader:             uncompressedStream,
-			underlyingCloser:   underlyingStream.Close,
-			uncompressedCloser: uncompressedStream.Close,
-		}
-		closeUnderlyingStream = false
-
-		return newStream, li.size, nil
-	}
-
-	return nil, 0, errors.Errorf("Unknown blob %s", info.Digest)
+	return s.internal.GetBlob(ctx, info, cache)
 }
 
 // GetSignatures returns the image's signatures.  It may use a remote (= slow) service.
 // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil,
 // as there can be no secondary manifests.
 func (s *Source) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) {
-	if instanceDigest != nil {
-		// How did we even get here? GetManifest(ctx, nil) has returned a manifest.DockerV2Schema2MediaType.
-		return nil, errors.Errorf(`Manifest lists are not supported by "docker-daemon:"`)
-	}
-	return [][]byte{}, nil
+	return s.internal.GetSignatures(ctx, instanceDigest)
 }
 
 // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer
@@ -507,6 +99,6 @@ func (s *Source) GetSignatures(ctx context.Context, instanceDigest *digest.Diges
 // as the primary manifest can not be a list, so there can be no secondary manifests.
 // The Digest field is guaranteed to be provided; Size may be -1.
 // WARNING: The list may contain duplicates, and they are semantically relevant.
-func (s *Source) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) {
-	return nil, nil
+func (s *Source) LayerInfosForCopy(ctx context.Context, instanceDigest *digest.Digest) ([]types.BlobInfo, error) {
+	return s.internal.LayerInfosForCopy(ctx, instanceDigest)
 }
diff --git a/docker/tarfile/types.go b/docker/tarfile/types.go
index 358d304e..0f14389e 100644
--- a/docker/tarfile/types.go
+++ b/docker/tarfile/types.go
@@ -4,12 +4,5 @@ import (
 	internal "github.com/containers/image/v5/docker/internal/tarfile"
 )
 
-// Various data structures.
-
-// Based on github.com/docker/docker/image/tarexport/tarexport.go
-const (
-	manifestFileName = "manifest.json"
-)
-
 // ManifestItem is an element of the array stored in the top-level manifest.json file.
 type ManifestItem = internal.ManifestItem // All public members from the internal package remain accessible.
diff --git a/docs/containers-transports.5.md b/docs/containers-transports.5.md
index 61763241..c87283fb 100644
--- a/docs/containers-transports.5.md
+++ b/docs/containers-transports.5.md
@@ -41,10 +41,14 @@ If `name` does not contain a slash, it is treated as `docker.io/library/name`.
 Otherwise, the component before the first slash is checked if it is recognized as a `hostname[:port]` (i.e., it contains either a . or a :, or the component is exactly localhost).
 If the first component of name is not recognized as a `hostname[:port]`, `name` is treated as `docker.io/name`.
 
-### **docker-archive:**_path[:docker-reference]_
+### **docker-archive:**_path[:{docker-reference|@source-index}]_
 
 An image is stored in the docker-save(1) formatted file.
-_docker-reference_ is only used when creating such a file, and it must not contain a digest.
+_docker-reference_ must not contain a digest.
+Alternatively, for reading archives, @_source-index_ is a zero-based index in archive manifest
+(to access untagged images).
+If neither _docker-reference_ nor @_source_index is specified when reading an archive, the archive must contain exactly one image.
+
 It is further possible to copy data to stdin by specifying `docker-archive:/dev/stdin` but note that the used file must be seekable.
 
 ### **docker-daemon:**_docker-reference|algo:digest_


Test patch:
diff --git a/docker/archive/transport_test.go b/docker/archive/transport_test.go
index 632ce4f3..2b4b853b 100644
--- a/docker/archive/transport_test.go
+++ b/docker/archive/transport_test.go
@@ -47,16 +47,26 @@ func TestParseReference(t *testing.T) {
 
 // testParseReference is a test shared for Transport.ParseReference and ParseReference.
 func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) {
-	for _, c := range []struct{ input, expectedPath, expectedRef string }{
-		{"", "", ""}, // Empty input is explicitly rejected
-		{"/path", "/path", ""},
-		{"/path:busybox:notlatest", "/path", "docker.io/library/busybox:notlatest"}, // Explicit tag
-		{"/path:busybox" + sha256digest, "", ""},                                    // Digest references are forbidden
-		{"/path:busybox", "/path", "docker.io/library/busybox:latest"},              // Default tag
+	for _, c := range []struct {
+		input, expectedPath, expectedRef string
+		expectedSourceIndex              int
+	}{
+		{"", "", "", -1}, // Empty input is explicitly rejected
+		{"/path", "/path", "", -1},
+		{"/path:busybox:notlatest", "/path", "docker.io/library/busybox:notlatest", -1}, // Explicit tag
+		{"/path:busybox" + sha256digest, "", "", -1},                                    // Digest references are forbidden
+		{"/path:busybox", "/path", "docker.io/library/busybox:latest", -1},              // Default tag
 		// A github.com/distribution/reference value can have a tag and a digest at the same time!
-		{"/path:busybox:latest" + sha256digest, "", ""},                                         // Both tag and digest is rejected
-		{"/path:docker.io/library/busybox:latest", "/path", "docker.io/library/busybox:latest"}, // All implied values explicitly specified
-		{"/path:UPPERCASEISINVALID", "", ""},                                                    // Invalid input
+		{"/path:busybox:latest" + sha256digest, "", "", -1},                                         // Both tag and digest is rejected
+		{"/path:docker.io/library/busybox:latest", "/path", "docker.io/library/busybox:latest", -1}, // All implied reference parts explicitly specified
+		{"/path:UPPERCASEISINVALID", "", "", -1},                                                    // Invalid reference format
+		{"/path:@", "", "", -1},                                                                     // Missing source index
+		{"/path:@0", "/path", "", 0},                                                                // Valid source index
+		{"/path:@999999", "/path", "", 999999},                                                      // Valid source index
+		{"/path:@-2", "", "", -1},                                                                   // Negative source index
+		{"/path:@-1", "", "", -1},                                                                   // Negative source index, using the placeholder value
+		{"/path:busybox@0", "", "", -1},                                                             // References and source indices can’t be combined.
+		{"/path:@0:busybox", "", "", -1},                                                            // References and source indices can’t be combined.
 	} {
 		ref, err := fn(c.input)
 		if c.expectedPath == "" {
@@ -67,15 +77,25 @@ func testParseReference(t *testing.T, fn func(string) (types.ImageReference, err
 			require.True(t, ok, c.input)
 			assert.Equal(t, c.expectedPath, archiveRef.path, c.input)
 			if c.expectedRef == "" {
-				assert.Nil(t, archiveRef.destinationRef, c.input)
+				assert.Nil(t, archiveRef.ref, c.input)
 			} else {
-				require.NotNil(t, archiveRef.destinationRef, c.input)
-				assert.Equal(t, c.expectedRef, archiveRef.destinationRef.String(), c.input)
+				require.NotNil(t, archiveRef.ref, c.input)
+				assert.Equal(t, c.expectedRef, archiveRef.ref.String(), c.input)
 			}
+			assert.Equal(t, c.expectedSourceIndex, archiveRef.sourceIndex, c.input)
 		}
 	}
 }
 
+// namedTaggedRef returns a reference.NamedTagged for input
+func namedTaggedRef(t *testing.T, input string) reference.NamedTagged {
+	named, err := reference.ParseNormalizedNamed(input)
+	require.NoError(t, err, input)
+	nt, ok := named.(reference.NamedTagged)
+	require.True(t, ok, input)
+	return nt
+}
+
 func TestNewReference(t *testing.T) {
 	for _, path := range []string{"relative", "/absolute"} {
 		for _, c := range []struct {
@@ -88,11 +108,7 @@ func TestNewReference(t *testing.T) {
 		} {
 			var ntRef reference.NamedTagged = nil
 			if c.ref != "" {
-				namedRef, err := reference.ParseNormalizedNamed(c.ref)
-				require.NoError(t, err, c.ref)
-				nt, ok := namedRef.(reference.NamedTagged)
-				require.True(t, ok, c.ref)
-				ntRef = nt
+				ntRef = namedTaggedRef(t, c.ref)
 			}
 
 			res, err := NewReference(path, ntRef)
@@ -104,13 +120,46 @@ func TestNewReference(t *testing.T) {
 				require.True(t, ok, c.ref)
 				assert.Equal(t, path, archiveRef.path)
 				if c.ref == "" {
-					assert.Nil(t, archiveRef.destinationRef, c.ref)
+					assert.Nil(t, archiveRef.ref, c.ref)
 				} else {
-					require.NotNil(t, archiveRef.destinationRef, c.ref)
-					assert.Equal(t, ntRef.String(), archiveRef.destinationRef.String(), c.ref)
+					require.NotNil(t, archiveRef.ref, c.ref)
+					assert.Equal(t, ntRef.String(), archiveRef.ref.String(), c.ref)
 				}
+				assert.Equal(t, -1, archiveRef.sourceIndex, c.ref)
 			}
+		}
+	}
+	_, err := NewReference("with:colon", nil)
+	assert.Error(t, err)
+
+	// Complete coverage testing of the private newReference here as well
+	ntRef := namedTaggedRef(t, "busybox:latest")
+	_, err = newReference("path", ntRef, 0, nil, nil)
+	assert.Error(t, err)
+}
 
+func TestNewIndexReference(t *testing.T) {
+	for _, path := range []string{"relative", "/absolute"} {
+		for _, c := range []struct {
+			index int
+			ok    bool
+		}{
+			{0, true},
+			{9999990, true},
+			{-1, true},
+			{-2, false},
+		} {
+			res, err := NewIndexReference(path, c.index)
+			if !c.ok {
+				assert.Error(t, err, c.index)
+			} else {
+				require.NoError(t, err, c.index)
+				archiveRef, ok := res.(archiveReference)
+				require.True(t, ok, c.index)
+				assert.Equal(t, path, archiveRef.path)
+				assert.Nil(t, archiveRef.ref, c.index)
+				assert.Equal(t, c.index, archiveRef.sourceIndex)
+			}
 		}
 	}
 	_, err := NewReference("with:colon", nil)
@@ -118,11 +167,17 @@ func TestNewReference(t *testing.T) {
 }
 
 // A common list of reference formats to test for the various ImageReference methods.
-var validReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{
-	{"/pathonly", "", "/pathonly"},
-	{"/path:busybox:notlatest", "docker.io/library/busybox:notlatest", "/path:docker.io/library/busybox:notlatest"},          // Explicit tag
-	{"/path:docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "/path:docker.io/library/busybox:latest"}, // All implied values explicitly specified
-	{"/path:example.com/ns/foo:bar", "example.com/ns/foo:bar", "/path:example.com/ns/foo:bar"},                               // All values explicitly specified
+var validReferenceTestCases = []struct {
+	input, dockerRef      string
+	sourceIndex           int
+	stringWithinTransport string
+}{
+	{"/pathonly", "", -1, "/pathonly"},
+	{"/path:busybox:notlatest", "docker.io/library/busybox:notlatest", -1, "/path:docker.io/library/busybox:notlatest"},          // Explicit tag
+	{"/path:docker.io/library/busybox:latest", "docker.io/library/busybox:latest", -1, "/path:docker.io/library/busybox:latest"}, // All implied reference part explicitly specified
+	{"/path:example.com/ns/foo:bar", "example.com/ns/foo:bar", -1, "/path:example.com/ns/foo:bar"},                               // All values explicitly specified
+	{"/path:@0", "", 0, "/path:@0"},
+	{"/path:@999999", "", 999999, "/path:@999999"},
 }
 
 func TestReferenceTransport(t *testing.T) {
@@ -176,21 +231,21 @@ func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
 }
 
 func TestReferenceNewImage(t *testing.T) {
-	for _, suffix := range []string{"", ":thisisignoredbutaccepted"} {
+	for _, suffix := range []string{"", ":emptyimage:latest", ":@0"} {
 		ref, err := ParseReference(tarFixture + suffix)
 		require.NoError(t, err, suffix)
 		img, err := ref.NewImage(context.Background(), nil)
-		assert.NoError(t, err, suffix)
+		require.NoError(t, err, suffix)
 		defer img.Close()
 	}
 }
 
 func TestReferenceNewImageSource(t *testing.T) {
-	for _, suffix := range []string{"", ":thisisignoredbutaccepted"} {
+	for _, suffix := range []string{"", ":emptyimage:latest", ":@0"} {
 		ref, err := ParseReference(tarFixture + suffix)
 		require.NoError(t, err, suffix)
 		src, err := ref.NewImageSource(context.Background(), nil)
-		assert.NoError(t, err, suffix)
+		require.NoError(t, err, suffix)
 		defer src.Close()
 	}
 }
@@ -218,7 +273,7 @@ func TestReferenceDeleteImage(t *testing.T) {
 	require.NoError(t, err)
 	defer os.RemoveAll(tmpDir)
 
-	for i, suffix := range []string{"", ":thisisignoredbutaccepted"} {
+	for i, suffix := range []string{"", ":some-reference", ":@0"} {
 		testFile := filepath.Join(tmpDir, fmt.Sprintf("file%d.tar", i))
 		err := ioutil.WriteFile(testFile, []byte("nonempty"), 0644)
 		require.NoError(t, err, suffix)
diff --git a/docker/tarfile/src_test.go b/docker/internal/tarfile/src_test.go
similarity index 87%
rename from docker/tarfile/src_test.go
rename to docker/internal/tarfile/src_test.go
index 8b22aa5a..f578f50c 100644
--- a/docker/tarfile/src_test.go
+++ b/docker/internal/tarfile/src_test.go
@@ -26,7 +26,8 @@ func TestSourcePrepareLayerData(t *testing.T) {
 		var tarfileBuffer bytes.Buffer
 		ctx := context.Background()
 
-		dest := NewDestinationWithContext(nil, &tarfileBuffer, nil)
+		writer := NewWriter(&tarfileBuffer)
+		dest := NewDestination(nil, writer, nil)
 		// No layers
 		configInfo, err := dest.PutBlob(ctx, bytes.NewBufferString(c.config),
 			types.BlobInfo{Size: -1}, cache, true)
@@ -40,10 +41,12 @@ func TestSourcePrepareLayerData(t *testing.T) {
 		require.NoError(t, err, c.config)
 		err = dest.PutManifest(ctx, manifest, nil)
 		require.NoError(t, err, c.config)
-		err = dest.Commit(ctx)
+		err = writer.Close()
 		require.NoError(t, err, c.config)
 
-		src, err := NewSourceFromStreamWithSystemContext(nil, &tarfileBuffer)
+		reader, err := NewReaderFromStream(nil, &tarfileBuffer)
+		require.NoError(t, err, c.config)
+		src := NewSource(reader, true, nil, -1)
 		require.NoError(t, err, c.config)
 		defer src.Close()
 		configStream, _, err := src.GetBlob(ctx, types.BlobInfo{