Repository: containers/image
Pull request #1576 (2022-06-11 12:56:04)

Make c/image/image available as c/image/internal/image
This is a part of #1574, per https://github.com/containers/image/pull/1408#issuecomment-1029487477 :
- Move the implementation of c/image/image to c/image/internal/image, so that we can adjust the API without affecting external callers (notably without changing `types.Image`). Keep c/image/image with trivial forwarders, for compatibility.
- Add `image.FromReference`, use it to simplify transports’ implementation of `NewImage` (and to fix a bug in most of them).


Patch:
diff --git a/copy/copy.go b/copy/copy.go
index 123c23e0..aa5ffb9f 100644
--- a/copy/copy.go
+++ b/copy/copy.go
@@ -12,8 +12,8 @@ import (
 	"time"
 
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
 	internalblobinfocache "github.com/containers/image/v5/internal/blobinfocache"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/internal/imagedestination"
 	"github.com/containers/image/v5/internal/imagesource"
 	"github.com/containers/image/v5/internal/pkg/platform"
diff --git a/directory/directory_transport.go b/directory/directory_transport.go
index e542d888..56240447 100644
--- a/directory/directory_transport.go
+++ b/directory/directory_transport.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/containers/image/v5/directory/explicitfilepath"
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/transports"
 	"github.com/containers/image/v5/types"
 	"github.com/opencontainers/go-digest"
@@ -140,8 +140,7 @@ func (ref dirReference) PolicyConfigurationNamespaces() []string {
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 // WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
 func (ref dirReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src := newImageSource(ref)
-	return image.FromSource(ctx, sys, src)
+	return image.FromReference(ctx, sys, ref)
 }
 
 // NewImageSource returns a types.ImageSource for this reference.
diff --git a/docker/archive/transport.go b/docker/archive/transport.go
index 9a48cb46..f00b7793 100644
--- a/docker/archive/transport.go
+++ b/docker/archive/transport.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/containers/image/v5/docker/internal/tarfile"
 	"github.com/containers/image/v5/docker/reference"
-	ctrImage "github.com/containers/image/v5/image"
+	ctrImage "github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/transports"
 	"github.com/containers/image/v5/types"
 	"github.com/pkg/errors"
@@ -185,11 +185,7 @@ func (ref archiveReference) PolicyConfigurationNamespaces() []string {
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 // WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
 func (ref archiveReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src, err := newImageSource(ctx, sys, ref)
-	if err != nil {
-		return nil, err
-	}
-	return ctrImage.FromSource(ctx, sys, src)
+	return ctrImage.FromReference(ctx, sys, ref)
 }
 
 // NewImageSource returns a types.ImageSource for this reference.
diff --git a/docker/daemon/daemon_transport.go b/docker/daemon/daemon_transport.go
index 4e4ed688..d7557978 100644
--- a/docker/daemon/daemon_transport.go
+++ b/docker/daemon/daemon_transport.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/containers/image/v5/docker/policyconfiguration"
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/transports"
 	"github.com/containers/image/v5/types"
 	"github.com/opencontainers/go-digest"
@@ -195,11 +195,7 @@ func (ref daemonReference) PolicyConfigurationNamespaces() []string {
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 // WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
 func (ref daemonReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src, err := newImageSource(ctx, sys, ref)
-	if err != nil {
-		return nil, err
-	}
-	return image.FromSource(ctx, sys, src)
+	return image.FromReference(ctx, sys, ref)
 }
 
 // NewImageSource returns a types.ImageSource for this reference.
diff --git a/docker/docker_image.go b/docker/docker_image.go
index c84bb37d..73687e86 100644
--- a/docker/docker_image.go
+++ b/docker/docker_image.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/manifest"
 	"github.com/containers/image/v5/types"
 	"github.com/opencontainers/go-digest"
diff --git a/image/docker_schema2.go b/image/docker_schema2.go
index b250a6b1..e5a3b899 100644
--- a/image/docker_schema2.go
+++ b/image/docker_schema2.go
@@ -1,400 +1,14 @@
 package image
 
 import (
-	"bytes"
-	"context"
-	"crypto/sha256"
-	"encoding/hex"
-	"encoding/json"
-	"fmt"
-	"strings"
-
-	"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/blobinfocache/none"
-	"github.com/containers/image/v5/types"
-	"github.com/opencontainers/go-digest"
-	imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
-	"github.com/pkg/errors"
-	"github.com/sirupsen/logrus"
+	"github.com/containers/image/v5/internal/image"
 )
 
 // GzippedEmptyLayer is a gzip-compressed version of an empty tar file (1024 NULL bytes)
 // This comes from github.com/docker/distribution/manifest/schema1/config_builder.go; there is
 // a non-zero embedded timestamp; we could zero that, but that would just waste storage space
 // in registries, so let’s use the same values.
-var GzippedEmptyLayer = []byte{
-	31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88,
-	0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0,
-}
+var GzippedEmptyLayer = image.GzippedEmptyLayer
 
 // GzippedEmptyLayerDigest is a digest of GzippedEmptyLayer
-const GzippedEmptyLayerDigest = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
-
-type manifestSchema2 struct {
-	src        types.ImageSource // May be nil if configBlob is not nil
-	configBlob []byte            // If set, corresponds to contents of ConfigDescriptor.
-	m          *manifest.Schema2
-}
-
-func manifestSchema2FromManifest(src types.ImageSource, manifestBlob []byte) (genericManifest, error) {
-	m, err := manifest.Schema2FromManifest(manifestBlob)
-	if err != nil {
-		return nil, err
-	}
-	return &manifestSchema2{
-		src: src,
-		m:   m,
-	}, nil
-}
-
-// manifestSchema2FromComponents builds a new manifestSchema2 from the supplied data:
-func manifestSchema2FromComponents(config manifest.Schema2Descriptor, src types.ImageSource, configBlob []byte, layers []manifest.Schema2Descriptor) *manifestSchema2 {
-	return &manifestSchema2{
-		src:        src,
-		configBlob: configBlob,
-		m:          manifest.Schema2FromComponents(config, layers),
-	}
-}
-
-func (m *manifestSchema2) serialize() ([]byte, error) {
-	return m.m.Serialize()
-}
-
-func (m *manifestSchema2) manifestMIMEType() string {
-	return m.m.MediaType
-}
-
-// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
-// Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below.
-func (m *manifestSchema2) ConfigInfo() types.BlobInfo {
-	return m.m.ConfigInfo()
-}
-
-// OCIConfig returns the image configuration as per OCI v1 image-spec. Information about
-// layers in the resulting configuration isn't guaranteed to be returned to due how
-// old image manifests work (docker v2s1 especially).
-func (m *manifestSchema2) OCIConfig(ctx context.Context) (*imgspecv1.Image, error) {
-	configBlob, err := m.ConfigBlob(ctx)
-	if err != nil {
-		return nil, err
-	}
-	// docker v2s2 and OCI v1 are mostly compatible but v2s2 contains more fields
-	// than OCI v1. This unmarshal makes sure we drop docker v2s2
-	// fields that aren't needed in OCI v1.
-	configOCI := &imgspecv1.Image{}
-	if err := json.Unmarshal(configBlob, configOCI); err != nil {
-		return nil, err
-	}
-	return configOCI, nil
-}
-
-// ConfigBlob returns the blob described by ConfigInfo, iff ConfigInfo().Digest != ""; nil otherwise.
-// The result is cached; it is OK to call this however often you need.
-func (m *manifestSchema2) ConfigBlob(ctx context.Context) ([]byte, error) {
-	if m.configBlob == nil {
-		if m.src == nil {
-			return nil, errors.Errorf("Internal error: neither src nor configBlob set in manifestSchema2")
-		}
-		stream, _, err := m.src.GetBlob(ctx, manifest.BlobInfoFromSchema2Descriptor(m.m.ConfigDescriptor), none.NoCache)
-		if err != nil {
-			return nil, err
-		}
-		defer stream.Close()
-		blob, err := iolimits.ReadAtMost(stream, iolimits.MaxConfigBodySize)
-		if err != nil {
-			return nil, err
-		}
-		computedDigest := digest.FromBytes(blob)
-		if computedDigest != m.m.ConfigDescriptor.Digest {
-			return nil, errors.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.m.ConfigDescriptor.Digest)
-		}
-		m.configBlob = blob
-	}
-	return m.configBlob, nil
-}
-
-// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
-// The Digest field is guaranteed to be provided; Size may be -1.
-// WARNING: The list may contain duplicates, and they are semantically relevant.
-func (m *manifestSchema2) LayerInfos() []types.BlobInfo {
-	return manifestLayerInfosToBlobInfos(m.m.LayerInfos())
-}
-
-// EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref.
-// It returns false if the manifest does not embed a Docker reference.
-// (This embedding unfortunately happens for Docker schema1, please do not add support for this in any new formats.)
-func (m *manifestSchema2) EmbeddedDockerReferenceConflicts(ref reference.Named) bool {
-	return false
-}
-
-// Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration.
-func (m *manifestSchema2) Inspect(ctx context.Context) (*types.ImageInspectInfo, error) {
-	getter := func(info types.BlobInfo) ([]byte, error) {
-		if info.Digest != m.ConfigInfo().Digest {
-			// Shouldn't ever happen
-			return nil, errors.New("asked for a different config blob")
-		}
-		config, err := m.ConfigBlob(ctx)
-		if err != nil {
-			return nil, err
-		}
-		return config, nil
-	}
-	return m.m.Inspect(getter)
-}
-
-// UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs.
-// This is a horribly specific interface, but computing InformationOnly.LayerDiffIDs can be very expensive to compute
-// (most importantly it forces us to download the full layers even if they are already present at the destination).
-func (m *manifestSchema2) UpdatedImageNeedsLayerDiffIDs(options types.ManifestUpdateOptions) bool {
-	return false
-}
-
-// UpdatedImage returns a types.Image modified according to options.
-// This does not change the state of the original Image object.
-// The returned error will be a manifest.ManifestLayerCompressionIncompatibilityError
-// if the CompressionOperation and CompressionAlgorithm specified in one or more
-// options.LayerInfos items is anything other than gzip.
-func (m *manifestSchema2) UpdatedImage(ctx context.Context, options types.ManifestUpdateOptions) (types.Image, error) {
-	copy := manifestSchema2{ // NOTE: This is not a deep copy, it still shares slices etc.
-		src:        m.src,
-		configBlob: m.configBlob,
-		m:          manifest.Schema2Clone(m.m),
-	}
-
-	converted, err := convertManifestIfRequiredWithUpdate(ctx, options, map[string]manifestConvertFn{
-		manifest.DockerV2Schema1MediaType:       copy.convertToManifestSchema1,
-		manifest.DockerV2Schema1SignedMediaType: copy.convertToManifestSchema1,
-		imgspecv1.MediaTypeImageManifest:        copy.convertToManifestOCI1,
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	if converted != nil {
-		return converted, nil
-	}
-
-	// No conversion required, update manifest
-	if options.LayerInfos != nil {
-		if err := copy.m.UpdateLayerInfos(options.LayerInfos); err != nil {
-			return nil, err
-		}
-	}
-	// Ignore options.EmbeddedDockerReference: it may be set when converting from schema1 to schema2, but we really don't care.
-
-	return memoryImageFromManifest(&copy), nil
-}
-
-func oci1DescriptorFromSchema2Descriptor(d manifest.Schema2Descriptor) imgspecv1.Descriptor {
-	return imgspecv1.Descriptor{
-		MediaType: d.MediaType,
-		Size:      d.Size,
-		Digest:    d.Digest,
-		URLs:      d.URLs,
-	}
-}
-
-// convertToManifestOCI1 returns a genericManifest implementation converted to imgspecv1.MediaTypeImageManifest.
-// It may use options.InformationOnly and also adjust *options to be appropriate for editing the returned
-// value.
-// This does not change the state of the original manifestSchema2 object.
-func (m *manifestSchema2) convertToManifestOCI1(ctx context.Context, _ *types.ManifestUpdateOptions) (genericManifest, error) {
-	configOCI, err := m.OCIConfig(ctx)
-	if err != nil {
-		return nil, err
-	}
-	configOCIBytes, err := json.Marshal(configOCI)
-	if err != nil {
-		return nil, err
-	}
-
-	config := imgspecv1.Descriptor{
-		MediaType: imgspecv1.MediaTypeImageConfig,
-		Size:      int64(len(configOCIBytes)),
-		Digest:    digest.FromBytes(configOCIBytes),
-	}
-
-	layers := make([]imgspecv1.Descriptor, len(m.m.LayersDescriptors))
-	for idx := range layers {
-		layers[idx] = oci1DescriptorFromSchema2Descriptor(m.m.LayersDescriptors[idx])
-		switch m.m.LayersDescriptors[idx].MediaType {
-		case manifest.DockerV2Schema2ForeignLayerMediaType:
-			layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributable
-		case manifest.DockerV2Schema2ForeignLayerMediaTypeGzip:
-			layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributableGzip
-		case manifest.DockerV2SchemaLayerMediaTypeUncompressed:
-			layers[idx].MediaType = imgspecv1.MediaTypeImageLayer
-		case manifest.DockerV2Schema2LayerMediaType:
-			layers[idx].MediaType = imgspecv1.MediaTypeImageLayerGzip
-		default:
-			return nil, fmt.Errorf("Unknown media type during manifest conversion: %q", m.m.LayersDescriptors[idx].MediaType)
-		}
-	}
-
-	return manifestOCI1FromComponents(config, m.src, configOCIBytes, layers), nil
-}
-
-// convertToManifestSchema1 returns a genericManifest implementation converted to manifest.DockerV2Schema1{Signed,}MediaType.
-// It may use options.InformationOnly and also adjust *options to be appropriate for editing the returned
-// value.
-// This does not change the state of the original manifestSchema2 object.
-//
-// Based on docker/distribution/manifest/schema1/config_builder.go
-func (m *manifestSchema2) convertToManifestSchema1(ctx context.Context, options *types.ManifestUpdateOptions) (genericManifest, error) {
-	dest := options.InformationOnly.Destination
-
-	var convertedLayerUpdates []types.BlobInfo // Only used if options.LayerInfos != nil
-	if options.LayerInfos != nil {
-		if len(options.LayerInfos) != len(m.m.LayersDescriptors) {
-			return nil, fmt.Errorf("Error converting image: layer edits for %d layers vs %d existing layers",
-				len(options.LayerInfos), len(m.m.LayersDescriptors))
-		}
-		convertedLayerUpdates = []types.BlobInfo{}
-	}
-
-	configBytes, err := m.ConfigBlob(ctx)
-	if err != nil {
-		return nil, err
-	}
-	imageConfig := &manifest.Schema2Image{}
-	if err := json.Unmarshal(configBytes, imageConfig); err != nil {
-		return nil, err
-	}
-
-	// Build fsLayers and History, discarding all configs. We will patch the top-level config in later.
-	fsLayers := make([]manifest.Schema1FSLayers, len(imageConfig.History))
-	history := make([]manifest.Schema1History, len(imageConfig.History))
-	nonemptyLayerIndex := 0
-	var parentV1ID string // Set in the loop
-	v1ID := ""
-	haveGzippedEmptyLayer := false
-	if len(imageConfig.History) == 0 {
-		// What would this even mean?! Anyhow, the rest of the code depends on fsLayers[0] and history[0] existing.
-		return nil, errors.Errorf("Cannot convert an image with 0 history entries to %s", manifest.DockerV2Schema1SignedMediaType)
-	}
-	for v2Index, historyEntry := range imageConfig.History {
-		parentV1ID = v1ID
-		v1Index := len(imageConfig.History) - 1 - v2Index
-
-		var blobDigest digest.Digest
-		if historyEntry.EmptyLayer {
-			emptyLayerBlobInfo := types.BlobInfo{Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}
-
-			if !haveGzippedEmptyLayer {
-				logrus.Debugf("Uploading empty layer during conversion to schema 1")
-				// Ideally we should update the relevant BlobInfoCache about this layer, but that would require passing it down here,
-				// and anyway this blob is so small that it’s easier to just copy it than to worry about figuring out another location where to get it.
-				info, err := dest.PutBlob(ctx, bytes.NewReader(GzippedEmptyLayer), emptyLayerBlobInfo, none.NoCache, false)
-				if err != nil {
-					return nil, errors.Wrap(err, "uploading empty layer")
-				}
-				if info.Digest != emptyLayerBlobInfo.Digest {
-					return nil, errors.Errorf("Internal error: Uploaded empty layer has digest %#v instead of %s", info.Digest, emptyLayerBlobInfo.Digest)
-				}
-				haveGzippedEmptyLayer = true
-			}
-			if options.LayerInfos != nil {
-				convertedLayerUpdates = append(convertedLayerUpdates, emptyLayerBlobInfo)
-			}
-			blobDigest = emptyLayerBlobInfo.Digest
-		} else {
-			if nonemptyLayerIndex >= len(m.m.LayersDescriptors) {
-				return nil, errors.Errorf("Invalid image configuration, needs more than the %d distributed layers", len(m.m.LayersDescriptors))
-			}
-			if options.LayerInfos != nil {
-				convertedLayerUpdates = append(convertedLayerUpdates, options.LayerInfos[nonemptyLayerIndex])
-			}
-			blobDigest = m.m.LayersDescriptors[nonemptyLayerIndex].Digest
-			nonemptyLayerIndex++
-		}
-
-		// AFAICT pull ignores these ID values, at least nowadays, so we could use anything unique, including a simple counter. Use what Docker uses for cargo-cult consistency.
-		v, err := v1IDFromBlobDigestAndComponents(blobDigest, parentV1ID)
-		if err != nil {
-			return nil, err
-		}
-		v1ID = v
-
-		fakeImage := manifest.Schema1V1Compatibility{
-			ID:        v1ID,
-			Parent:    parentV1ID,
-			Comment:   historyEntry.Comment,
-			Created:   historyEntry.Created,
-			Author:    historyEntry.Author,
-			ThrowAway: historyEntry.EmptyLayer,
-		}
-		fakeImage.ContainerConfig.Cmd = []string{historyEntry.CreatedBy}
-		v1CompatibilityBytes, err := json.Marshal(&fakeImage)
-		if err != nil {
-			return nil, errors.Errorf("Internal error: Error creating v1compatibility for %#v", fakeImage)
-		}
-
-		fsLayers[v1Index] = manifest.Schema1FSLayers{BlobSum: blobDigest}
-		history[v1Index] = manifest.Schema1History{V1Compatibility: string(v1CompatibilityBytes)}
-		// Note that parentV1ID of the top layer is preserved when exiting this loop
-	}
-
-	// Now patch in real configuration for the top layer (v1Index == 0)
-	v1ID, err = v1IDFromBlobDigestAndComponents(fsLayers[0].BlobSum, parentV1ID, string(configBytes)) // See above WRT v1ID value generation and cargo-cult consistency.
-	if err != nil {
-		return nil, err
-	}
-	v1Config, err := v1ConfigFromConfigJSON(configBytes, v1ID, parentV1ID, imageConfig.History[len(imageConfig.History)-1].EmptyLayer)
-	if err != nil {
-		return nil, err
-	}
-	history[0].V1Compatibility = string(v1Config)
-
-	if options.LayerInfos != nil {
-		options.LayerInfos = convertedLayerUpdates
-	}
-	m1, err := manifestSchema1FromComponents(dest.Reference().DockerReference(), fsLayers, history, imageConfig.Architecture)
-	if err != nil {
-		return nil, err // This should never happen, we should have created all the components correctly.
-	}
-	return m1, nil
-}
-
-func v1IDFromBlobDigestAndComponents(blobDigest digest.Digest, others ...string) (string, error) {
-	if err := blobDigest.Validate(); err != nil {
-		return "", err
-	}
-	parts := append([]string{blobDigest.Hex()}, others...)
-	v1IDHash := sha256.Sum256([]byte(strings.Join(parts, " ")))
-	return hex.EncodeToString(v1IDHash[:]), nil
-}
-
-func v1ConfigFromConfigJSON(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
-	// Preserve everything we don't specifically know about.
-	// (This must be a *json.RawMessage, even though *[]byte is fairly redundant, because only *RawMessage implements json.Marshaler.)
-	rawContents := map[string]*json.RawMessage{}
-	if err := json.Unmarshal(configJSON, &rawContents); err != nil { // We have already unmarshaled it before, using a more detailed schema?!
-		return nil, err
-	}
-	delete(rawContents, "rootfs")
-	delete(rawContents, "history")
-
-	updates := map[string]interface{}{"id": v1ID}
-	if parentV1ID != "" {
-		updates["parent"] = parentV1ID
-	}
-	if throwaway {
-		updates["throwaway"] = throwaway
-	}
-	for field, value := range updates {
-		encoded, err := json.Marshal(value)
-		if err != nil {
-			return nil, err
-		}
-		rawContents[field] = (*json.RawMessage)(&encoded)
-	}
-	return json.Marshal(rawContents)
-}
-
-// SupportsEncryption returns if encryption is supported for the manifest type
-func (m *manifestSchema2) SupportsEncryption(context.Context) bool {
-	return false
-}
+const GzippedEmptyLayerDigest = image.GzippedEmptyLayerDigest
diff --git a/image/sourced.go b/image/sourced.go
index 3a016e1d..2b7f6b14 100644
--- a/image/sourced.go
+++ b/image/sourced.go
@@ -6,17 +6,10 @@ package image
 import (
 	"context"
 
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/types"
 )
 
-// imageCloser implements types.ImageCloser, perhaps allowing simple users
-// to use a single object without having keep a reference to a types.ImageSource
-// only to call types.ImageSource.Close().
-type imageCloser struct {
-	types.Image
-	src types.ImageSource
-}
-
 // FromSource returns a types.ImageCloser implementation for the default instance of source.
 // If source is a manifest list, .Manifest() still returns the manifest list,
 // but other methods transparently return data from an appropriate image instance.
@@ -31,33 +24,7 @@ type imageCloser struct {
 // NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource,
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage instead of calling this function.
 func FromSource(ctx context.Context, sys *types.SystemContext, src types.ImageSource) (types.ImageCloser, error) {
-	img, err := FromUnparsedImage(ctx, sys, UnparsedInstance(src, nil))
-	if err != nil {
-		return nil, err
-	}
-	return &imageCloser{
-		Image: img,
-		src:   src,
-	}, nil
-}
-
-func (ic *imageCloser) Close() error {
-	return ic.src.Close()
-}
-
-// sourcedImage is a general set of utilities for working with container images,
-// whatever is their underlying location (i.e. dockerImageSource-independent).
-// Note the existence of skopeo/docker.Image: some instances of a `types.Image`
-// may not be a `sourcedImage` directly. However, most users of `types.Image`
-// do not care, and those who care about `skopeo/docker.Image` know they do.
-type sourcedImage struct {
-	*UnparsedImage
-	manifestBlob     []byte
-	manifestMIMEType string
-	// genericManifest contains data corresponding to manifestBlob.
-	// NOTE: The manifest may have been modified in the process; DO NOT reserialize and store genericManifest
-	// if you want to preserve the original manifest; use manifestBlob directly.
-	genericManifest
+	return image.FromSource(ctx, sys, src)
 }
 
 // FromUnparsedImage returns a types.Image implementation for unparsed.
@@ -66,39 +33,5 @@ type sourcedImage struct {
 //
 // The Image must not be used after the underlying ImageSource is Close()d.
 func FromUnparsedImage(ctx context.Context, sys *types.SystemContext, unparsed *UnparsedImage) (types.Image, error) {
-	// Note that the input parameter above is specifically *image.UnparsedImage, not types.UnparsedImage:
-	// we want to be able to use unparsed.src.  We could make that an explicit interface, but, well,
-	// this is the only UnparsedImage implementation around, anyway.
-
-	// NOTE: It is essential for signature verification that all parsing done in this object happens on the same manifest which is returned by unparsed.Manifest().
-	manifestBlob, manifestMIMEType, err := unparsed.Manifest(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	parsedManifest, err := manifestInstanceFromBlob(ctx, sys, unparsed.src, manifestBlob, manifestMIMEType)
-	if err != nil {
-		return nil, err
-	}
-
-	return &sourcedImage{
-		UnparsedImage:    unparsed,
-		manifestBlob:     manifestBlob,
-		manifestMIMEType: manifestMIMEType,
-		genericManifest:  parsedManifest,
-	}, nil
-}
-
-// Size returns the size of the image as stored, if it's known, or -1 if it isn't.
-func (i *sourcedImage) Size() (int64, error) {
-	return -1, nil
-}
-
-// Manifest overrides the UnparsedImage.Manifest to always use the fields which we have already fetched.
-func (i *sourcedImage) Manifest(ctx context.Context) ([]byte, string, error) {
-	return i.manifestBlob, i.manifestMIMEType, nil
-}
-
-func (i *sourcedImage) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) {
-	return i.UnparsedImage.src.LayerInfosForCopy(ctx, i.UnparsedImage.instanceDigest)
+	return image.FromUnparsedImage(ctx, sys, unparsed)
 }
diff --git a/image/unparsed.go b/image/unparsed.go
index c64852f7..123f6ce6 100644
--- a/image/unparsed.go
+++ b/image/unparsed.go
@@ -1,95 +1,19 @@
 package image
 
 import (
-	"context"
-
-	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/manifest"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/types"
 	"github.com/opencontainers/go-digest"
-	"github.com/pkg/errors"
 )
 
 // UnparsedImage implements types.UnparsedImage .
 // An UnparsedImage is a pair of (ImageSource, instance digest); it can represent either a manifest list or a single image instance.
-type UnparsedImage struct {
-	src            types.ImageSource
-	instanceDigest *digest.Digest
-	cachedManifest []byte // A private cache for Manifest(); nil if not yet known.
-	// A private cache for Manifest(), may be the empty string if guessing failed.
-	// Valid iff cachedManifest is not nil.
-	cachedManifestMIMEType string
-	cachedSignatures       [][]byte // A private cache for Signatures(); nil if not yet known.
-}
+type UnparsedImage = image.UnparsedImage
 
 // UnparsedInstance returns a types.UnparsedImage implementation for (source, instanceDigest).
 // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list).
 //
 // The UnparsedImage must not be used after the underlying ImageSource is Close()d.
 func UnparsedInstance(src types.ImageSource, instanceDigest *digest.Digest) *UnparsedImage {
-	return &UnparsedImage{
-		src:            src,
-		instanceDigest: instanceDigest,
-	}
-}
-
-// Reference returns the reference used to set up this source, _as specified by the user_
-// (not as the image itself, or its underlying storage, claims).  This can be used e.g. to determine which public keys are trusted for this image.
-func (i *UnparsedImage) Reference() types.ImageReference {
-	// Note that this does not depend on instanceDigest; e.g. all instances within a manifest list need to be signed with the manifest list identity.
-	return i.src.Reference()
-}
-
-// Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need.
-func (i *UnparsedImage) Manifest(ctx context.Context) ([]byte, string, error) {
-	if i.cachedManifest == nil {
-		m, mt, err := i.src.GetManifest(ctx, i.instanceDigest)
-		if err != nil {
-			return nil, "", err
-		}
-
-		// ImageSource.GetManifest does not do digest verification, but we do;
-		// this immediately protects also any user of types.Image.
-		if digest, haveDigest := i.expectedManifestDigest(); haveDigest {
-			matches, err := manifest.MatchesDigest(m, digest)
-			if err != nil {
-				return nil, "", errors.Wrap(err, "computing manifest digest")
-			}
-			if !matches {
-				return nil, "", errors.Errorf("Manifest does not match provided manifest digest %s", digest)
-			}
-		}
-
-		i.cachedManifest = m
-		i.cachedManifestMIMEType = mt
-	}
-	return i.cachedManifest, i.cachedManifestMIMEType, nil
-}
-
-// expectedManifestDigest returns a the expected value of the manifest digest, and an indicator whether it is known.
-// The bool return value seems redundant with digest != ""; it is used explicitly
-// to refuse (unexpected) situations when the digest exists but is "".
-func (i *UnparsedImage) expectedManifestDigest() (digest.Digest, bool) {
-	if i.instanceDigest != nil {
-		return *i.instanceDigest, true
-	}
-	ref := i.Reference().DockerReference()
-	if ref != nil {
-		if canonical, ok := ref.(reference.Canonical); ok {
-			return canonical.Digest(), true
-		}
-	}
-	return "", false
-}
-
-// Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
-func (i *UnparsedImage) Signatures(ctx context.Context) ([][]byte, error) {
-	if i.cachedSignatures == nil {
-		sigs, err := i.src.GetSignatures(ctx, i.instanceDigest)
-		if err != nil {
-			return nil, err
-		}
-		i.cachedSignatures = sigs
-	}
-	return i.cachedSignatures, nil
+	return image.UnparsedInstance(src, instanceDigest)
 }
diff --git a/image/docker_list.go b/internal/image/docker_list.go
similarity index 100%
rename from image/docker_list.go
rename to internal/image/docker_list.go
diff --git a/image/docker_schema1.go b/internal/image/docker_schema1.go
similarity index 100%
rename from image/docker_schema1.go
rename to internal/image/docker_schema1.go
diff --git a/image/docker_schema1_test.go b/internal/image/docker_schema1_test.go
similarity index 100%
rename from image/docker_schema1_test.go
rename to internal/image/docker_schema1_test.go
diff --git a/internal/image/docker_schema2.go b/internal/image/docker_schema2.go
new file mode 100644
index 00000000..ca55d96c
--- /dev/null
+++ b/internal/image/docker_schema2.go
@@ -0,0 +1,404 @@
+package image
+
+import (
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"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/blobinfocache/none"
+	"github.com/containers/image/v5/types"
+	"github.com/opencontainers/go-digest"
+	imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+)
+
+// GzippedEmptyLayer is a gzip-compressed version of an empty tar file (1024 NULL bytes)
+// This comes from github.com/docker/distribution/manifest/schema1/config_builder.go; there is
+// a non-zero embedded timestamp; we could zero that, but that would just waste storage space
+// in registries, so let’s use the same values.
+//
+// This is publicly visible as c/image/image.GzippedEmptyLayer.
+var GzippedEmptyLayer = []byte{
+	31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88,
+	0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0,
+}
+
+// GzippedEmptyLayerDigest is a digest of GzippedEmptyLayer
+//
+// This is publicly visible as c/image/image.GzippedEmptyLayerDigest.
+const GzippedEmptyLayerDigest = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
+
+type manifestSchema2 struct {
+	src        types.ImageSource // May be nil if configBlob is not nil
+	configBlob []byte            // If set, corresponds to contents of ConfigDescriptor.
+	m          *manifest.Schema2
+}
+
+func manifestSchema2FromManifest(src types.ImageSource, manifestBlob []byte) (genericManifest, error) {
+	m, err := manifest.Schema2FromManifest(manifestBlob)
+	if err != nil {
+		return nil, err
+	}
+	return &manifestSchema2{
+		src: src,
+		m:   m,
+	}, nil
+}
+
+// manifestSchema2FromComponents builds a new manifestSchema2 from the supplied data:
+func manifestSchema2FromComponents(config manifest.Schema2Descriptor, src types.ImageSource, configBlob []byte, layers []manifest.Schema2Descriptor) *manifestSchema2 {
+	return &manifestSchema2{
+		src:        src,
+		configBlob: configBlob,
+		m:          manifest.Schema2FromComponents(config, layers),
+	}
+}
+
+func (m *manifestSchema2) serialize() ([]byte, error) {
+	return m.m.Serialize()
+}
+
+func (m *manifestSchema2) manifestMIMEType() string {
+	return m.m.MediaType
+}
+
+// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
+// Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below.
+func (m *manifestSchema2) ConfigInfo() types.BlobInfo {
+	return m.m.ConfigInfo()
+}
+
+// OCIConfig returns the image configuration as per OCI v1 image-spec. Information about
+// layers in the resulting configuration isn't guaranteed to be returned to due how
+// old image manifests work (docker v2s1 especially).
+func (m *manifestSchema2) OCIConfig(ctx context.Context) (*imgspecv1.Image, error) {
+	configBlob, err := m.ConfigBlob(ctx)
+	if err != nil {
+		return nil, err
+	}
+	// docker v2s2 and OCI v1 are mostly compatible but v2s2 contains more fields
+	// than OCI v1. This unmarshal makes sure we drop docker v2s2
+	// fields that aren't needed in OCI v1.
+	configOCI := &imgspecv1.Image{}
+	if err := json.Unmarshal(configBlob, configOCI); err != nil {
+		return nil, err
+	}
+	return configOCI, nil
+}
+
+// ConfigBlob returns the blob described by ConfigInfo, iff ConfigInfo().Digest != ""; nil otherwise.
+// The result is cached; it is OK to call this however often you need.
+func (m *manifestSchema2) ConfigBlob(ctx context.Context) ([]byte, error) {
+	if m.configBlob == nil {
+		if m.src == nil {
+			return nil, errors.Errorf("Internal error: neither src nor configBlob set in manifestSchema2")
+		}
+		stream, _, err := m.src.GetBlob(ctx, manifest.BlobInfoFromSchema2Descriptor(m.m.ConfigDescriptor), none.NoCache)
+		if err != nil {
+			return nil, err
+		}
+		defer stream.Close()
+		blob, err := iolimits.ReadAtMost(stream, iolimits.MaxConfigBodySize)
+		if err != nil {
+			return nil, err
+		}
+		computedDigest := digest.FromBytes(blob)
+		if computedDigest != m.m.ConfigDescriptor.Digest {
+			return nil, errors.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.m.ConfigDescriptor.Digest)
+		}
+		m.configBlob = blob
+	}
+	return m.configBlob, nil
+}
+
+// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
+// The Digest field is guaranteed to be provided; Size may be -1.
+// WARNING: The list may contain duplicates, and they are semantically relevant.
+func (m *manifestSchema2) LayerInfos() []types.BlobInfo {
+	return manifestLayerInfosToBlobInfos(m.m.LayerInfos())
+}
+
+// EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref.
+// It returns false if the manifest does not embed a Docker reference.
+// (This embedding unfortunately happens for Docker schema1, please do not add support for this in any new formats.)
+func (m *manifestSchema2) EmbeddedDockerReferenceConflicts(ref reference.Named) bool {
+	return false
+}
+
+// Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration.
+func (m *manifestSchema2) Inspect(ctx context.Context) (*types.ImageInspectInfo, error) {
+	getter := func(info types.BlobInfo) ([]byte, error) {
+		if info.Digest != m.ConfigInfo().Digest {
+			// Shouldn't ever happen
+			return nil, errors.New("asked for a different config blob")
+		}
+		config, err := m.ConfigBlob(ctx)
+		if err != nil {
+			return nil, err
+		}
+		return config, nil
+	}
+	return m.m.Inspect(getter)
+}
+
+// UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs.
+// This is a horribly specific interface, but computing InformationOnly.LayerDiffIDs can be very expensive to compute
+// (most importantly it forces us to download the full layers even if they are already present at the destination).
+func (m *manifestSchema2) UpdatedImageNeedsLayerDiffIDs(options types.ManifestUpdateOptions) bool {
+	return false
+}
+
+// UpdatedImage returns a types.Image modified according to options.
+// This does not change the state of the original Image object.
+// The returned error will be a manifest.ManifestLayerCompressionIncompatibilityError
+// if the CompressionOperation and CompressionAlgorithm specified in one or more
+// options.LayerInfos items is anything other than gzip.
+func (m *manifestSchema2) UpdatedImage(ctx context.Context, options types.ManifestUpdateOptions) (types.Image, error) {
+	copy := manifestSchema2{ // NOTE: This is not a deep copy, it still shares slices etc.
+		src:        m.src,
+		configBlob: m.configBlob,
+		m:          manifest.Schema2Clone(m.m),
+	}
+
+	converted, err := convertManifestIfRequiredWithUpdate(ctx, options, map[string]manifestConvertFn{
+		manifest.DockerV2Schema1MediaType:       copy.convertToManifestSchema1,
+		manifest.DockerV2Schema1SignedMediaType: copy.convertToManifestSchema1,
+		imgspecv1.MediaTypeImageManifest:        copy.convertToManifestOCI1,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	if converted != nil {
+		return converted, nil
+	}
+
+	// No conversion required, update manifest
+	if options.LayerInfos != nil {
+		if err := copy.m.UpdateLayerInfos(options.LayerInfos); err != nil {
+			return nil, err
+		}
+	}
+	// Ignore options.EmbeddedDockerReference: it may be set when converting from schema1 to schema2, but we really don't care.
+
+	return memoryImageFromManifest(&copy), nil
+}
+
+func oci1DescriptorFromSchema2Descriptor(d manifest.Schema2Descriptor) imgspecv1.Descriptor {
+	return imgspecv1.Descriptor{
+		MediaType: d.MediaType,
+		Size:      d.Size,
+		Digest:    d.Digest,
+		URLs:      d.URLs,
+	}
+}
+
+// convertToManifestOCI1 returns a genericManifest implementation converted to imgspecv1.MediaTypeImageManifest.
+// It may use options.InformationOnly and also adjust *options to be appropriate for editing the returned
+// value.
+// This does not change the state of the original manifestSchema2 object.
+func (m *manifestSchema2) convertToManifestOCI1(ctx context.Context, _ *types.ManifestUpdateOptions) (genericManifest, error) {
+	configOCI, err := m.OCIConfig(ctx)
+	if err != nil {
+		return nil, err
+	}
+	configOCIBytes, err := json.Marshal(configOCI)
+	if err != nil {
+		return nil, err
+	}
+
+	config := imgspecv1.Descriptor{
+		MediaType: imgspecv1.MediaTypeImageConfig,
+		Size:      int64(len(configOCIBytes)),
+		Digest:    digest.FromBytes(configOCIBytes),
+	}
+
+	layers := make([]imgspecv1.Descriptor, len(m.m.LayersDescriptors))
+	for idx := range layers {
+		layers[idx] = oci1DescriptorFromSchema2Descriptor(m.m.LayersDescriptors[idx])
+		switch m.m.LayersDescriptors[idx].MediaType {
+		case manifest.DockerV2Schema2ForeignLayerMediaType:
+			layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributable
+		case manifest.DockerV2Schema2ForeignLayerMediaTypeGzip:
+			layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributableGzip
+		case manifest.DockerV2SchemaLayerMediaTypeUncompressed:
+			layers[idx].MediaType = imgspecv1.MediaTypeImageLayer
+		case manifest.DockerV2Schema2LayerMediaType:
+			layers[idx].MediaType = imgspecv1.MediaTypeImageLayerGzip
+		default:
+			return nil, fmt.Errorf("Unknown media type during manifest conversion: %q", m.m.LayersDescriptors[idx].MediaType)
+		}
+	}
+
+	return manifestOCI1FromComponents(config, m.src, configOCIBytes, layers), nil
+}
+
+// convertToManifestSchema1 returns a genericManifest implementation converted to manifest.DockerV2Schema1{Signed,}MediaType.
+// It may use options.InformationOnly and also adjust *options to be appropriate for editing the returned
+// value.
+// This does not change the state of the original manifestSchema2 object.
+//
+// Based on docker/distribution/manifest/schema1/config_builder.go
+func (m *manifestSchema2) convertToManifestSchema1(ctx context.Context, options *types.ManifestUpdateOptions) (genericManifest, error) {
+	dest := options.InformationOnly.Destination
+
+	var convertedLayerUpdates []types.BlobInfo // Only used if options.LayerInfos != nil
+	if options.LayerInfos != nil {
+		if len(options.LayerInfos) != len(m.m.LayersDescriptors) {
+			return nil, fmt.Errorf("Error converting image: layer edits for %d layers vs %d existing layers",
+				len(options.LayerInfos), len(m.m.LayersDescriptors))
+		}
+		convertedLayerUpdates = []types.BlobInfo{}
+	}
+
+	configBytes, err := m.ConfigBlob(ctx)
+	if err != nil {
+		return nil, err
+	}
+	imageConfig := &manifest.Schema2Image{}
+	if err := json.Unmarshal(configBytes, imageConfig); err != nil {
+		return nil, err
+	}
+
+	// Build fsLayers and History, discarding all configs. We will patch the top-level config in later.
+	fsLayers := make([]manifest.Schema1FSLayers, len(imageConfig.History))
+	history := make([]manifest.Schema1History, len(imageConfig.History))
+	nonemptyLayerIndex := 0
+	var parentV1ID string // Set in the loop
+	v1ID := ""
+	haveGzippedEmptyLayer := false
+	if len(imageConfig.History) == 0 {
+		// What would this even mean?! Anyhow, the rest of the code depends on fsLayers[0] and history[0] existing.
+		return nil, errors.Errorf("Cannot convert an image with 0 history entries to %s", manifest.DockerV2Schema1SignedMediaType)
+	}
+	for v2Index, historyEntry := range imageConfig.History {
+		parentV1ID = v1ID
+		v1Index := len(imageConfig.History) - 1 - v2Index
+
+		var blobDigest digest.Digest
+		if historyEntry.EmptyLayer {
+			emptyLayerBlobInfo := types.BlobInfo{Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}
+
+			if !haveGzippedEmptyLayer {
+				logrus.Debugf("Uploading empty layer during conversion to schema 1")
+				// Ideally we should update the relevant BlobInfoCache about this layer, but that would require passing it down here,
+				// and anyway this blob is so small that it’s easier to just copy it than to worry about figuring out another location where to get it.
+				info, err := dest.PutBlob(ctx, bytes.NewReader(GzippedEmptyLayer), emptyLayerBlobInfo, none.NoCache, false)
+				if err != nil {
+					return nil, errors.Wrap(err, "uploading empty layer")
+				}
+				if info.Digest != emptyLayerBlobInfo.Digest {
+					return nil, errors.Errorf("Internal error: Uploaded empty layer has digest %#v instead of %s", info.Digest, emptyLayerBlobInfo.Digest)
+				}
+				haveGzippedEmptyLayer = true
+			}
+			if options.LayerInfos != nil {
+				convertedLayerUpdates = append(convertedLayerUpdates, emptyLayerBlobInfo)
+			}
+			blobDigest = emptyLayerBlobInfo.Digest
+		} else {
+			if nonemptyLayerIndex >= len(m.m.LayersDescriptors) {
+				return nil, errors.Errorf("Invalid image configuration, needs more than the %d distributed layers", len(m.m.LayersDescriptors))
+			}
+			if options.LayerInfos != nil {
+				convertedLayerUpdates = append(convertedLayerUpdates, options.LayerInfos[nonemptyLayerIndex])
+			}
+			blobDigest = m.m.LayersDescriptors[nonemptyLayerIndex].Digest
+			nonemptyLayerIndex++
+		}
+
+		// AFAICT pull ignores these ID values, at least nowadays, so we could use anything unique, including a simple counter. Use what Docker uses for cargo-cult consistency.
+		v, err := v1IDFromBlobDigestAndComponents(blobDigest, parentV1ID)
+		if err != nil {
+			return nil, err
+		}
+		v1ID = v
+
+		fakeImage := manifest.Schema1V1Compatibility{
+			ID:        v1ID,
+			Parent:    parentV1ID,
+			Comment:   historyEntry.Comment,
+			Created:   historyEntry.Created,
+			Author:    historyEntry.Author,
+			ThrowAway: historyEntry.EmptyLayer,
+		}
+		fakeImage.ContainerConfig.Cmd = []string{historyEntry.CreatedBy}
+		v1CompatibilityBytes, err := json.Marshal(&fakeImage)
+		if err != nil {
+			return nil, errors.Errorf("Internal error: Error creating v1compatibility for %#v", fakeImage)
+		}
+
+		fsLayers[v1Index] = manifest.Schema1FSLayers{BlobSum: blobDigest}
+		history[v1Index] = manifest.Schema1History{V1Compatibility: string(v1CompatibilityBytes)}
+		// Note that parentV1ID of the top layer is preserved when exiting this loop
+	}
+
+	// Now patch in real configuration for the top layer (v1Index == 0)
+	v1ID, err = v1IDFromBlobDigestAndComponents(fsLayers[0].BlobSum, parentV1ID, string(configBytes)) // See above WRT v1ID value generation and cargo-cult consistency.
+	if err != nil {
+		return nil, err
+	}
+	v1Config, err := v1ConfigFromConfigJSON(configBytes, v1ID, parentV1ID, imageConfig.History[len(imageConfig.History)-1].EmptyLayer)
+	if err != nil {
+		return nil, err
+	}
+	history[0].V1Compatibility = string(v1Config)
+
+	if options.LayerInfos != nil {
+		options.LayerInfos = convertedLayerUpdates
+	}
+	m1, err := manifestSchema1FromComponents(dest.Reference().DockerReference(), fsLayers, history, imageConfig.Architecture)
+	if err != nil {
+		return nil, err // This should never happen, we should have created all the components correctly.
+	}
+	return m1, nil
+}
+
+func v1IDFromBlobDigestAndComponents(blobDigest digest.Digest, others ...string) (string, error) {
+	if err := blobDigest.Validate(); err != nil {
+		return "", err
+	}
+	parts := append([]string{blobDigest.Hex()}, others...)
+	v1IDHash := sha256.Sum256([]byte(strings.Join(parts, " ")))
+	return hex.EncodeToString(v1IDHash[:]), nil
+}
+
+func v1ConfigFromConfigJSON(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
+	// Preserve everything we don't specifically know about.
+	// (This must be a *json.RawMessage, even though *[]byte is fairly redundant, because only *RawMessage implements json.Marshaler.)
+	rawContents := map[string]*json.RawMessage{}
+	if err := json.Unmarshal(configJSON, &rawContents); err != nil { // We have already unmarshaled it before, using a more detailed schema?!
+		return nil, err
+	}
+	delete(rawContents, "rootfs")
+	delete(rawContents, "history")
+
+	updates := map[string]interface{}{"id": v1ID}
+	if parentV1ID != "" {
+		updates["parent"] = parentV1ID
+	}
+	if throwaway {
+		updates["throwaway"] = throwaway
+	}
+	for field, value := range updates {
+		encoded, err := json.Marshal(value)
+		if err != nil {
+			return nil, err
+		}
+		rawContents[field] = (*json.RawMessage)(&encoded)
+	}
+	return json.Marshal(rawContents)
+}
+
+// SupportsEncryption returns if encryption is supported for the manifest type
+func (m *manifestSchema2) SupportsEncryption(context.Context) bool {
+	return false
+}
diff --git a/image/docker_schema2_test.go b/internal/image/docker_schema2_test.go
similarity index 100%
rename from image/docker_schema2_test.go
rename to internal/image/docker_schema2_test.go
diff --git a/image/fixtures/oci1-all-media-types-to-schema2.json b/internal/image/fixtures/oci1-all-media-types-to-schema2.json
similarity index 100%
rename from image/fixtures/oci1-all-media-types-to-schema2.json
rename to internal/image/fixtures/oci1-all-media-types-to-schema2.json
diff --git a/image/fixtures/oci1-all-media-types.json b/internal/image/fixtures/oci1-all-media-types.json
similarity index 100%
rename from image/fixtures/oci1-all-media-types.json
rename to internal/image/fixtures/oci1-all-media-types.json
diff --git a/image/fixtures/oci1-config.json b/internal/image/fixtures/oci1-config.json
similarity index 100%
rename from image/fixtures/oci1-config.json
rename to internal/image/fixtures/oci1-config.json
diff --git a/image/fixtures/oci1-invalid-media-type.json b/internal/image/fixtures/oci1-invalid-media-type.json
similarity index 100%
rename from image/fixtures/oci1-invalid-media-type.json
rename to internal/image/fixtures/oci1-invalid-media-type.json
diff --git a/image/fixtures/oci1-to-schema1.json b/internal/image/fixtures/oci1-to-schema1.json
similarity index 100%
rename from image/fixtures/oci1-to-schema1.json
rename to internal/image/fixtures/oci1-to-schema1.json
diff --git a/image/fixtures/oci1-to-schema2.json b/internal/image/fixtures/oci1-to-schema2.json
similarity index 100%
rename from image/fixtures/oci1-to-schema2.json
rename to internal/image/fixtures/oci1-to-schema2.json
diff --git a/image/fixtures/oci1.json b/internal/image/fixtures/oci1.json
similarity index 100%
rename from image/fixtures/oci1.json
rename to internal/image/fixtures/oci1.json
diff --git a/image/fixtures/oci1index.json b/internal/image/fixtures/oci1index.json
similarity index 100%
rename from image/fixtures/oci1index.json
rename to internal/image/fixtures/oci1index.json
diff --git a/image/fixtures/schema1-for-oci-config.json b/internal/image/fixtures/schema1-for-oci-config.json
similarity index 100%
rename from image/fixtures/schema1-for-oci-config.json
rename to internal/image/fixtures/schema1-for-oci-config.json
diff --git a/image/fixtures/schema1-to-oci1-config.json b/internal/image/fixtures/schema1-to-oci1-config.json
similarity index 100%
rename from image/fixtures/schema1-to-oci1-config.json
rename to internal/image/fixtures/schema1-to-oci1-config.json
diff --git a/image/fixtures/schema1-to-oci1.json b/internal/image/fixtures/schema1-to-oci1.json
similarity index 100%
rename from image/fixtures/schema1-to-oci1.json
rename to internal/image/fixtures/schema1-to-oci1.json
diff --git a/image/fixtures/schema1-to-schema2-config.json b/internal/image/fixtures/schema1-to-schema2-config.json
similarity index 100%
rename from image/fixtures/schema1-to-schema2-config.json
rename to internal/image/fixtures/schema1-to-schema2-config.json
diff --git a/image/fixtures/schema1-to-schema2.json b/internal/image/fixtures/schema1-to-schema2.json
similarity index 100%
rename from image/fixtures/schema1-to-schema2.json
rename to internal/image/fixtures/schema1-to-schema2.json
diff --git a/image/fixtures/schema1.json b/internal/image/fixtures/schema1.json
similarity index 100%
rename from image/fixtures/schema1.json
rename to internal/image/fixtures/schema1.json
diff --git a/image/fixtures/schema2-all-media-types-to-oci1.json b/internal/image/fixtures/schema2-all-media-types-to-oci1.json
similarity index 100%
rename from image/fixtures/schema2-all-media-types-to-oci1.json
rename to internal/image/fixtures/schema2-all-media-types-to-oci1.json
diff --git a/image/fixtures/schema2-all-media-types.json b/internal/image/fixtures/schema2-all-media-types.json
similarity index 100%
rename from image/fixtures/schema2-all-media-types.json
rename to internal/image/fixtures/schema2-all-media-types.json
diff --git a/image/fixtures/schema2-config.json b/internal/image/fixtures/schema2-config.json
similarity index 100%
rename from image/fixtures/schema2-config.json
rename to internal/image/fixtures/schema2-config.json
diff --git a/image/fixtures/schema2-invalid-media-type.json b/internal/image/fixtures/schema2-invalid-media-type.json
similarity index 100%
rename from image/fixtures/schema2-invalid-media-type.json
rename to internal/image/fixtures/schema2-invalid-media-type.json
diff --git a/image/fixtures/schema2-to-oci1.json b/internal/image/fixtures/schema2-to-oci1.json
similarity index 100%
rename from image/fixtures/schema2-to-oci1.json
rename to internal/image/fixtures/schema2-to-oci1.json
diff --git a/image/fixtures/schema2-to-schema1-by-docker.json b/internal/image/fixtures/schema2-to-schema1-by-docker.json
similarity index 100%
rename from image/fixtures/schema2-to-schema1-by-docker.json
rename to internal/image/fixtures/schema2-to-schema1-by-docker.json
diff --git a/image/fixtures/schema2.json b/internal/image/fixtures/schema2.json
similarity index 100%
rename from image/fixtures/schema2.json
rename to internal/image/fixtures/schema2.json
diff --git a/image/fixtures/schema2list-variants.json b/internal/image/fixtures/schema2list-variants.json
similarity index 100%
rename from image/fixtures/schema2list-variants.json
rename to internal/image/fixtures/schema2list-variants.json
diff --git a/image/fixtures/schema2list.json b/internal/image/fixtures/schema2list.json
similarity index 100%
rename from image/fixtures/schema2list.json
rename to internal/image/fixtures/schema2list.json
diff --git a/image/manifest.go b/internal/image/manifest.go
similarity index 100%
rename from image/manifest.go
rename to internal/image/manifest.go
diff --git a/image/manifest_test.go b/internal/image/manifest_test.go
similarity index 100%
rename from image/manifest_test.go
rename to internal/image/manifest_test.go
diff --git a/image/memory.go b/internal/image/memory.go
similarity index 100%
rename from image/memory.go
rename to internal/image/memory.go
diff --git a/image/oci.go b/internal/image/oci.go
similarity index 100%
rename from image/oci.go
rename to internal/image/oci.go
diff --git a/image/oci_index.go b/internal/image/oci_index.go
similarity index 100%
rename from image/oci_index.go
rename to internal/image/oci_index.go
diff --git a/image/oci_test.go b/internal/image/oci_test.go
similarity index 100%
rename from image/oci_test.go
rename to internal/image/oci_test.go
diff --git a/internal/image/sourced.go b/internal/image/sourced.go
new file mode 100644
index 00000000..7dfabfa3
--- /dev/null
+++ b/internal/image/sourced.go
@@ -0,0 +1,131 @@
+// Package image consolidates knowledge about various container image formats
+// (as opposed to image storage mechanisms, which are handled by types.ImageSource)
+// and exposes all of them using an unified interface.
+package image
+
+import (
+	"context"
+
+	"github.com/containers/image/v5/types"
+)
+
+// FromReference returns a types.ImageCloser implementation for the default instance reading from reference.
+// If reference poitns to a manifest list, .Manifest() still returns the manifest list,
+// but other methods transparently return data from an appropriate image instance.
+//
+// The caller must call .Close() on the returned ImageCloser.
+//
+// NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource,
+// verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage instead of calling this function.
+func FromReference(ctx context.Context, sys *types.SystemContext, ref types.ImageReference) (types.ImageCloser, error) {
+	src, err := ref.NewImageSource(ctx, sys)
+	if err != nil {
+		return nil, err
+	}
+	img, err := FromSource(ctx, sys, src)
+	if err != nil {
+		src.Close()
+		return nil, err
+	}
+	return img, nil
+}
+
+// imageCloser implements types.ImageCloser, perhaps allowing simple users
+// to use a single object without having keep a reference to a types.ImageSource
+// only to call types.ImageSource.Close().
+type imageCloser struct {
+	types.Image
+	src types.ImageSource
+}
+
+// FromSource returns a types.ImageCloser implementation for the default instance of source.
+// If source is a manifest list, .Manifest() still returns the manifest list,
+// but other methods transparently return data from an appropriate image instance.
+//
+// The caller must call .Close() on the returned ImageCloser.
+//
+// FromSource “takes ownership” of the input ImageSource and will call src.Close()
+// when the image is closed.  (This does not prevent callers from using both the
+// Image and ImageSource objects simultaneously, but it means that they only need to
+// the Image.)
+//
+// NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource,
+// verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage instead of calling this function.
+//
+// Most callers can use either FromUnparsedImage or FromReference instead.
+//
+// This is publicly visible as c/image/image.FromSource.
+func FromSource(ctx context.Context, sys *types.SystemContext, src types.ImageSource) (types.ImageCloser, error) {
+	img, err := FromUnparsedImage(ctx, sys, UnparsedInstance(src, nil))
+	if err != nil {
+		return nil, err
+	}
+	return &imageCloser{
+		Image: img,
+		src:   src,
+	}, nil
+}
+
+func (ic *imageCloser) Close() error {
+	return ic.src.Close()
+}
+
+// sourcedImage is a general set of utilities for working with container images,
+// whatever is their underlying location (i.e. dockerImageSource-independent).
+// Note the existence of skopeo/docker.Image: some instances of a `types.Image`
+// may not be a `sourcedImage` directly. However, most users of `types.Image`
+// do not care, and those who care about `skopeo/docker.Image` know they do.
+type sourcedImage struct {
+	*UnparsedImage
+	manifestBlob     []byte
+	manifestMIMEType string
+	// genericManifest contains data corresponding to manifestBlob.
+	// NOTE: The manifest may have been modified in the process; DO NOT reserialize and store genericManifest
+	// if you want to preserve the original manifest; use manifestBlob directly.
+	genericManifest
+}
+
+// FromUnparsedImage returns a types.Image implementation for unparsed.
+// If unparsed represents a manifest list, .Manifest() still returns the manifest list,
+// but other methods transparently return data from an appropriate single image.
+//
+// The Image must not be used after the underlying ImageSource is Close()d.
+//
+// This is publicly visible as c/image/image.FromUnparsedImage.
+func FromUnparsedImage(ctx context.Context, sys *types.SystemContext, unparsed *UnparsedImage) (types.Image, error) {
+	// Note that the input parameter above is specifically *image.UnparsedImage, not types.UnparsedImage:
+	// we want to be able to use unparsed.src.  We could make that an explicit interface, but, well,
+	// this is the only UnparsedImage implementation around, anyway.
+
+	// NOTE: It is essential for signature verification that all parsing done in this object happens on the same manifest which is returned by unparsed.Manifest().
+	manifestBlob, manifestMIMEType, err := unparsed.Manifest(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	parsedManifest, err := manifestInstanceFromBlob(ctx, sys, unparsed.src, manifestBlob, manifestMIMEType)
+	if err != nil {
+		return nil, err
+	}
+
+	return &sourcedImage{
+		UnparsedImage:    unparsed,
+		manifestBlob:     manifestBlob,
+		manifestMIMEType: manifestMIMEType,
+		genericManifest:  parsedManifest,
+	}, nil
+}
+
+// Size returns the size of the image as stored, if it's known, or -1 if it isn't.
+func (i *sourcedImage) Size() (int64, error) {
+	return -1, nil
+}
+
+// Manifest overrides the UnparsedImage.Manifest to always use the fields which we have already fetched.
+func (i *sourcedImage) Manifest(ctx context.Context) ([]byte, string, error) {
+	return i.manifestBlob, i.manifestMIMEType, nil
+}
+
+func (i *sourcedImage) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) {
+	return i.UnparsedImage.src.LayerInfosForCopy(ctx, i.UnparsedImage.instanceDigest)
+}
diff --git a/internal/image/unparsed.go b/internal/image/unparsed.go
new file mode 100644
index 00000000..8ea0f61b
--- /dev/null
+++ b/internal/image/unparsed.go
@@ -0,0 +1,99 @@
+package image
+
+import (
+	"context"
+
+	"github.com/containers/image/v5/docker/reference"
+	"github.com/containers/image/v5/manifest"
+	"github.com/containers/image/v5/types"
+	"github.com/opencontainers/go-digest"
+	"github.com/pkg/errors"
+)
+
+// UnparsedImage implements types.UnparsedImage .
+// An UnparsedImage is a pair of (ImageSource, instance digest); it can represent either a manifest list or a single image instance.
+//
+// This is publicly visible as c/image/image.UnparsedImage.
+type UnparsedImage struct {
+	src            types.ImageSource
+	instanceDigest *digest.Digest
+	cachedManifest []byte // A private cache for Manifest(); nil if not yet known.
+	// A private cache for Manifest(), may be the empty string if guessing failed.
+	// Valid iff cachedManifest is not nil.
+	cachedManifestMIMEType string
+	cachedSignatures       [][]byte // A private cache for Signatures(); nil if not yet known.
+}
+
+// UnparsedInstance returns a types.UnparsedImage implementation for (source, instanceDigest).
+// If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list).
+//
+// The UnparsedImage must not be used after the underlying ImageSource is Close()d.
+//
+// This is publicly visible as c/image/image.UnparsedInstance.
+func UnparsedInstance(src types.ImageSource, instanceDigest *digest.Digest) *UnparsedImage {
+	return &UnparsedImage{
+		src:            src,
+		instanceDigest: instanceDigest,
+	}
+}
+
+// Reference returns the reference used to set up this source, _as specified by the user_
+// (not as the image itself, or its underlying storage, claims).  This can be used e.g. to determine which public keys are trusted for this image.
+func (i *UnparsedImage) Reference() types.ImageReference {
+	// Note that this does not depend on instanceDigest; e.g. all instances within a manifest list need to be signed with the manifest list identity.
+	return i.src.Reference()
+}
+
+// Manifest is like ImageSource.GetManifest, but the result is cached; it is OK to call this however often you need.
+func (i *UnparsedImage) Manifest(ctx context.Context) ([]byte, string, error) {
+	if i.cachedManifest == nil {
+		m, mt, err := i.src.GetManifest(ctx, i.instanceDigest)
+		if err != nil {
+			return nil, "", err
+		}
+
+		// ImageSource.GetManifest does not do digest verification, but we do;
+		// this immediately protects also any user of types.Image.
+		if digest, haveDigest := i.expectedManifestDigest(); haveDigest {
+			matches, err := manifest.MatchesDigest(m, digest)
+			if err != nil {
+				return nil, "", errors.Wrap(err, "computing manifest digest")
+			}
+			if !matches {
+				return nil, "", errors.Errorf("Manifest does not match provided manifest digest %s", digest)
+			}
+		}
+
+		i.cachedManifest = m
+		i.cachedManifestMIMEType = mt
+	}
+	return i.cachedManifest, i.cachedManifestMIMEType, nil
+}
+
+// expectedManifestDigest returns a the expected value of the manifest digest, and an indicator whether it is known.
+// The bool return value seems redundant with digest != ""; it is used explicitly
+// to refuse (unexpected) situations when the digest exists but is "".
+func (i *UnparsedImage) expectedManifestDigest() (digest.Digest, bool) {
+	if i.instanceDigest != nil {
+		return *i.instanceDigest, true
+	}
+	ref := i.Reference().DockerReference()
+	if ref != nil {
+		if canonical, ok := ref.(reference.Canonical); ok {
+			return canonical.Digest(), true
+		}
+	}
+	return "", false
+}
+
+// Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
+func (i *UnparsedImage) Signatures(ctx context.Context) ([][]byte, error) {
+	if i.cachedSignatures == nil {
+		sigs, err := i.src.GetSignatures(ctx, i.instanceDigest)
+		if err != nil {
+			return nil, err
+		}
+		i.cachedSignatures = sigs
+	}
+	return i.cachedSignatures, nil
+}
diff --git a/manifest/fixtures/schema2-to-schema1-by-docker.json b/manifest/fixtures/schema2-to-schema1-by-docker.json
index 79e638a3..2073e288 120000
--- a/manifest/fixtures/schema2-to-schema1-by-docker.json
+++ b/manifest/fixtures/schema2-to-schema1-by-docker.json
@@ -1,1 +1,1 @@
-../../image/fixtures/schema2-to-schema1-by-docker.json
\ No newline at end of file
+../../internal/image/fixtures/schema2-to-schema1-by-docker.json
\ No newline at end of file
diff --git a/oci/archive/oci_transport.go b/oci/archive/oci_transport.go
index 4fa91276..74fefbd4 100644
--- a/oci/archive/oci_transport.go
+++ b/oci/archive/oci_transport.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/containers/image/v5/directory/explicitfilepath"
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/internal/tmpdir"
 	"github.com/containers/image/v5/oci/internal"
 	ocilayout "github.com/containers/image/v5/oci/layout"
@@ -122,11 +122,7 @@ func (ref ociArchiveReference) PolicyConfigurationNamespaces() []string {
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 // WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
 func (ref ociArchiveReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src, err := newImageSource(ctx, sys, ref)
-	if err != nil {
-		return nil, err
-	}
-	return image.FromSource(ctx, sys, src)
+	return image.FromReference(ctx, sys, ref)
 }
 
 // NewImageSource returns a types.ImageSource for this reference.
diff --git a/oci/layout/oci_transport.go b/oci/layout/oci_transport.go
index a99b6315..a9029a60 100644
--- a/oci/layout/oci_transport.go
+++ b/oci/layout/oci_transport.go
@@ -10,7 +10,7 @@ import (
 
 	"github.com/containers/image/v5/directory/explicitfilepath"
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/oci/internal"
 	"github.com/containers/image/v5/transports"
 	"github.com/containers/image/v5/types"
@@ -154,11 +154,7 @@ func (ref ociReference) PolicyConfigurationNamespaces() []string {
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 // WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
 func (ref ociReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src, err := newImageSource(sys, ref)
-	if err != nil {
-		return nil, err
-	}
-	return image.FromSource(ctx, sys, src)
+	return image.FromReference(ctx, sys, ref)
 }
 
 // getIndex returns a pointer to the index references by this ociReference. If an error occurs opening an index nil is returned together
diff --git a/openshift/openshift_transport.go b/openshift/openshift_transport.go
index 6bbb43be..c8d65c78 100644
--- a/openshift/openshift_transport.go
+++ b/openshift/openshift_transport.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/containers/image/v5/docker/policyconfiguration"
 	"github.com/containers/image/v5/docker/reference"
-	genericImage "github.com/containers/image/v5/image"
+	genericImage "github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/transports"
 	"github.com/containers/image/v5/types"
 	"github.com/pkg/errors"
@@ -132,11 +132,7 @@ func (ref openshiftReference) PolicyConfigurationNamespaces() []string {
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 // WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
 func (ref openshiftReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src, err := newImageSource(sys, ref)
-	if err != nil {
-		return nil, err
-	}
-	return genericImage.FromSource(ctx, sys, src)
+	return genericImage.FromReference(ctx, sys, ref)
 }
 
 // NewImageSource returns a types.ImageSource for this reference.
diff --git a/ostree/ostree_transport.go b/ostree/ostree_transport.go
index 1e35ab60..6c426236 100644
--- a/ostree/ostree_transport.go
+++ b/ostree/ostree_transport.go
@@ -14,7 +14,7 @@ import (
 
 	"github.com/containers/image/v5/directory/explicitfilepath"
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/transports"
 	"github.com/containers/image/v5/types"
 	"github.com/pkg/errors"
@@ -184,17 +184,7 @@ func (s *ostreeImageCloser) Size() (int64, error) {
 // NOTE: If any kind of signature verification should happen, build an UnparsedImage from the value returned by NewImageSource,
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 func (ref ostreeReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	var tmpDir string
-	if sys == nil || sys.OSTreeTmpDirPath == "" {
-		tmpDir = os.TempDir()
-	} else {
-		tmpDir = sys.OSTreeTmpDirPath
-	}
-	src, err := newImageSource(tmpDir, ref)
-	if err != nil {
-		return nil, err
-	}
-	return image.FromSource(ctx, sys, src)
+	return image.FromReference(ctx, sys, ref)
 }
 
 // NewImageSource returns a types.ImageSource for this reference.
diff --git a/pkg/blobcache/blobcache.go b/pkg/blobcache/blobcache.go
index 8b22733a..c9971cbd 100644
--- a/pkg/blobcache/blobcache.go
+++ b/pkg/blobcache/blobcache.go
@@ -9,7 +9,7 @@ import (
 	"sync"
 
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/manifest"
 	"github.com/containers/image/v5/pkg/compression"
 	"github.com/containers/image/v5/transports"
@@ -158,11 +158,7 @@ func (b *BlobCache) ClearCache() error {
 }
 
 func (b *BlobCache) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src, err := b.NewImageSource(ctx, sys)
-	if err != nil {
-		return nil, errors.Wrapf(err, "error creating new image %q", transports.ImageName(b.reference))
-	}
-	return image.FromSource(ctx, sys, src)
+	return image.FromReference(ctx, sys, b)
 }
 
 func (b *BlobCache) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) {
diff --git a/sif/transport.go b/sif/transport.go
index 18d894bc..2037f250 100644
--- a/sif/transport.go
+++ b/sif/transport.go
@@ -9,7 +9,7 @@ import (
 
 	"github.com/containers/image/v5/directory/explicitfilepath"
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/transports"
 	"github.com/containers/image/v5/types"
 )
@@ -139,11 +139,7 @@ func (ref sifReference) PolicyConfigurationNamespaces() []string {
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 // WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
 func (ref sifReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src, err := newImageSource(ctx, sys, ref)
-	if err != nil {
-		return nil, err
-	}
-	return image.FromSource(ctx, sys, src)
+	return image.FromReference(ctx, sys, ref)
 }
 
 // NewImageSource returns a types.ImageSource for this reference.
diff --git a/storage/storage_image.go b/storage/storage_image.go
index 1eb60f48..971b398d 100644
--- a/storage/storage_image.go
+++ b/storage/storage_image.go
@@ -16,7 +16,7 @@ import (
 	"sync/atomic"
 
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/internal/private"
 	"github.com/containers/image/v5/internal/putblobdigest"
 	"github.com/containers/image/v5/internal/tmpdir"
diff --git a/tarball/tarball_reference.go b/tarball/tarball_reference.go
index 23f67c49..690067ec 100644
--- a/tarball/tarball_reference.go
+++ b/tarball/tarball_reference.go
@@ -7,7 +7,7 @@ import (
 	"strings"
 
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/types"
 
 	imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -67,16 +67,7 @@ func (r *tarballReference) PolicyConfigurationNamespaces() []string {
 // verify that UnparsedImage, and convert it into a real Image via image.FromUnparsedImage.
 // WARNING: This may not do the right thing for a manifest list, see image.FromSource for details.
 func (r *tarballReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) {
-	src, err := r.NewImageSource(ctx, sys)
-	if err != nil {
-		return nil, err
-	}
-	img, err := image.FromSource(ctx, sys, src)
-	if err != nil {
-		src.Close()
-		return nil, err
-	}
-	return img, nil
+	return image.FromReference(ctx, sys, r)
 }
 
 func (r *tarballReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error {


Test patch:
diff --git a/manifest/list_test.go b/manifest/list_test.go
index 47d5e091..540836d2 100644
--- a/manifest/list_test.go
+++ b/manifest/list_test.go
@@ -113,7 +113,7 @@ func TestChooseInstance(t *testing.T) {
 			},
 		},
 	} {
-		rawManifest, err := os.ReadFile(filepath.Join("..", "image", "fixtures", manifestList.listFile))
+		rawManifest, err := os.ReadFile(filepath.Join("..", "internal", "image", "fixtures", manifestList.listFile))
 		require.NoError(t, err)
 		list, err := ListFromBlob(rawManifest, GuessMIMEType(rawManifest))
 		require.NoError(t, err)
diff --git a/oci/layout/oci_dest_test.go b/oci/layout/oci_dest_test.go
index 18dd2750..1af48508 100644
--- a/oci/layout/oci_dest_test.go
+++ b/oci/layout/oci_dest_test.go
@@ -129,7 +129,7 @@ func TestPutTwoDifferentTags(t *testing.T) {
 }
 
 func putTestConfig(t *testing.T, ociRef ociReference, tmpDir string) {
-	data, err := os.ReadFile("../../image/fixtures/oci1-config.json")
+	data, err := os.ReadFile("../../internal/image/fixtures/oci1-config.json")
 	assert.NoError(t, err)
 	imageDest, err := newImageDestination(nil, ociRef)
 	assert.NoError(t, err)
@@ -154,7 +154,7 @@ func putTestConfig(t *testing.T, ociRef ociReference, tmpDir string) {
 }
 
 func putTestManifest(t *testing.T, ociRef ociReference, tmpDir string) {
-	data, err := os.ReadFile("../../image/fixtures/oci1.json")
+	data, err := os.ReadFile("../../internal/image/fixtures/oci1.json")
 	assert.NoError(t, err)
 	imageDest, err := newImageDestination(nil, ociRef)
 	assert.NoError(t, err)
diff --git a/pkg/blobcache/blobcache_test.go b/pkg/blobcache/blobcache_test.go
index da83d361..6f9fedc2 100644
--- a/pkg/blobcache/blobcache_test.go
+++ b/pkg/blobcache/blobcache_test.go
@@ -14,7 +14,7 @@ import (
 
 	cp "github.com/containers/image/v5/copy"
 	"github.com/containers/image/v5/directory"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/pkg/blobinfocache/none"
 	"github.com/containers/image/v5/signature"
 	"github.com/containers/image/v5/types"
diff --git a/signature/policy_eval_signedby_test.go b/signature/policy_eval_signedby_test.go
index 3f241939..bc72f1bc 100644
--- a/signature/policy_eval_signedby_test.go
+++ b/signature/policy_eval_signedby_test.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/containers/image/v5/directory"
 	"github.com/containers/image/v5/docker/reference"
-	"github.com/containers/image/v5/image"
+	"github.com/containers/image/v5/internal/image"
 	"github.com/containers/image/v5/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"