@@ -18,6 +18,7 @@ import (
1818 "github.com/sirupsen/logrus"
1919 "github.com/vbauerster/mpb/v8"
2020 "go.podman.io/image/v5/docker/reference"
21+ "go.podman.io/image/v5/internal/digests"
2122 "go.podman.io/image/v5/internal/image"
2223 "go.podman.io/image/v5/internal/pkg/platform"
2324 "go.podman.io/image/v5/internal/private"
@@ -173,6 +174,21 @@ func (c *copier) copySingleImage(ctx context.Context, unparsedImage *image.Unpar
173174 }
174175 }
175176
177+ // FIXME: zstd:chunked requires sha256 for TOC/chunk digests; fall back to plain zstd with other algorithms.
178+ if forcedAlgo := ic .c .options .digestOptions .MustUseSet (); forcedAlgo != "" && forcedAlgo != digest .SHA256 {
179+ format := ic .compressionFormat
180+ if format == nil {
181+ format = defaultCompressionFormat
182+ }
183+ if format .Name () == compressiontypes .ZstdChunkedAlgorithmName {
184+ if ic .requireCompressionFormatMatch {
185+ return copySingleImageResult {}, fmt .Errorf ("cannot combine zstd:chunked with forced digest algorithm %s (zstd:chunked currently only supports sha256); use plain zstd instead" , forcedAlgo )
186+ }
187+ logrus .Warnf ("Compression using zstd:chunked with forced digest algorithm %s is not supported (zstd:chunked uses sha256 for internal digests), using plain zstd instead" , forcedAlgo )
188+ ic .compressionFormat = & compression .Zstd
189+ }
190+ }
191+
176192 // Decide whether we can substitute blobs with semantic equivalents:
177193 // - Don’t do that if we can’t modify the manifest at all
178194 // - Ensure _this_ copy sees exactly the intended data when either processing a signed image or signing it.
@@ -200,6 +216,15 @@ func (c *copier) copySingleImage(ctx context.Context, unparsedImage *image.Unpar
200216 if err != nil {
201217 return copySingleImageResult {}, err
202218 }
219+
220+ // Schema1 lacks separate config and only supports sha256; fail early with other algorithms.
221+ if forcedAlgo := ic .c .options .digestOptions .MustUseSet (); forcedAlgo != "" && forcedAlgo != digest .SHA256 {
222+ isSchema1 := ic .manifestConversionPlan .preferredMIMEType == manifest .DockerV2Schema1MediaType ||
223+ ic .manifestConversionPlan .preferredMIMEType == manifest .DockerV2Schema1SignedMediaType
224+ if isSchema1 {
225+ return copySingleImageResult {}, fmt .Errorf ("cannot force digest algorithm %s with Docker schema1 manifests (only sha256 is supported)" , forcedAlgo )
226+ }
227+ }
203228 // We set up this part of ic.manifestUpdates quite early, not just around the
204229 // code that calls copyUpdatedConfigAndManifest, so that other parts of the copy code
205230 // (e.g. the UpdatedImageNeedsLayerDiffIDs check just below) can make decisions based
@@ -592,12 +617,27 @@ func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanc
592617 return nil , "" , fmt .Errorf ("reading manifest: %w" , err )
593618 }
594619
595- if err := ic .copyConfig (ctx , pendingImage ); err != nil {
620+ newConfigDigest , err := ic .copyConfig (ctx , pendingImage )
621+ if err != nil {
596622 return nil , "" , err
597623 }
598624
625+ // FIXME: Single image only; multi-arch needs per-instance config digest updates.
626+ // See https://github.com/containers/container-libs/pull/552#discussion_r2611627578
627+ if newConfigDigest != nil {
628+ man , err = ic .updateManifestConfigDigest (man , pendingImage , * newConfigDigest )
629+ if err != nil {
630+ return nil , "" , fmt .Errorf ("updating manifest config digest: %w" , err )
631+ }
632+ }
633+
599634 ic .c .Printf ("Writing manifest to image destination\n " )
600- manifestDigest , err := manifest .Digest (man )
635+ // Choose the digest algorithm based on digest options
636+ manifestDigestAlgo , err := ic .c .options .digestOptions .Choose (digests.Situation {})
637+ if err != nil {
638+ return nil , "" , fmt .Errorf ("choosing manifest digest algorithm: %w" , err )
639+ }
640+ manifestDigest , err := manifest .DigestWithAlgorithm (man , manifestDigestAlgo )
601641 if err != nil {
602642 return nil , "" , err
603643 }
@@ -611,13 +651,33 @@ func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanc
611651 return man , manifestDigest , nil
612652}
613653
654+ // updateManifestConfigDigest updates the config digest in the manifest using the manifest abstraction layer.
655+ func (ic * imageCopier ) updateManifestConfigDigest (manifestBlob []byte , src types.Image , newConfigDigest digest.Digest ) ([]byte , error ) {
656+ _ , mt , err := src .Manifest (context .Background ())
657+ if err != nil {
658+ return nil , fmt .Errorf ("getting manifest type: %w" , err )
659+ }
660+
661+ m , err := manifest .FromBlob (manifestBlob , mt )
662+ if err != nil {
663+ return nil , fmt .Errorf ("parsing manifest: %w" , err )
664+ }
665+
666+ if err := m .UpdateConfigDigest (newConfigDigest ); err != nil {
667+ return nil , err
668+ }
669+
670+ return m .Serialize ()
671+ }
672+
614673// copyConfig copies config.json, if any, from src to dest.
615- func (ic * imageCopier ) copyConfig (ctx context.Context , src types.Image ) error {
674+ // It returns the new config digest if it changed (due to digest algorithm forcing), or nil otherwise.
675+ func (ic * imageCopier ) copyConfig (ctx context.Context , src types.Image ) (* digest.Digest , error ) {
616676 srcInfo := src .ConfigInfo ()
617677 if srcInfo .Digest != "" {
618678 if err := ic .c .concurrentBlobCopiesSemaphore .Acquire (ctx , 1 ); err != nil {
619679 // This can only fail with ctx.Err(), so no need to blame acquiring the semaphore.
620- return fmt .Errorf ("copying config: %w" , err )
680+ return nil , fmt .Errorf ("copying config: %w" , err )
621681 }
622682 defer ic .c .concurrentBlobCopiesSemaphore .Release (1 )
623683
@@ -645,13 +705,17 @@ func (ic *imageCopier) copyConfig(ctx context.Context, src types.Image) error {
645705 return destInfo , nil
646706 }()
647707 if err != nil {
648- return err
708+ return nil , err
649709 }
650710 if destInfo .Digest != srcInfo .Digest {
651- return fmt .Errorf ("Internal error: copying uncompressed config blob %s changed digest to %s" , srcInfo .Digest , destInfo .Digest )
711+ // If algorithms match, the whole digest values must match
712+ if destInfo .Digest .Algorithm () == srcInfo .Digest .Algorithm () {
713+ return nil , fmt .Errorf ("Internal error: copying uncompressed config blob %s changed digest to %s" , srcInfo .Digest , destInfo .Digest )
714+ }
715+ return & destInfo .Digest , nil
652716 }
653717 }
654- return nil
718+ return nil , nil
655719}
656720
657721// diffIDResult contains both a digest value and an error from diffIDComputationGoroutine.
@@ -718,7 +782,14 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to
718782 // (e.g. if we know the DiffID of an encrypted compressed layer, it might not be necessary to pull, decrypt and decompress again),
719783 // but it’s not trivially safe to do such things, so until someone takes the effort to make a comprehensive argument, let’s not.
720784 encryptingOrDecrypting := toEncrypt || (isOciEncrypted (srcInfo .MediaType ) && ic .c .options .OciDecryptConfig != nil )
721- canAvoidProcessingCompleteLayer := ! diffIDIsNeeded && ! encryptingOrDecrypting
785+ // Forcing different digest requires processing full layer; PutBlobPartial incompatible.
786+ forcingDifferentDigestAlgo := false
787+ if forcedAlgo := ic .c .options .digestOptions .MustUseSet (); forcedAlgo != "" {
788+ if srcInfo .Digest .Algorithm () != forcedAlgo {
789+ forcingDifferentDigestAlgo = true
790+ }
791+ }
792+ canAvoidProcessingCompleteLayer := ! diffIDIsNeeded && ! encryptingOrDecrypting && ! forcingDifferentDigestAlgo
722793
723794 // Don’t read the layer from the source if we already have the blob, and optimizations are acceptable.
724795 if canAvoidProcessingCompleteLayer {
@@ -743,19 +814,35 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to
743814 tocDigest = * d
744815 }
745816
746- reused , reusedBlob , err := ic .c .dest .TryReusingBlobWithOptions (ctx , srcInfo , private.TryReusingBlobOptions {
747- Cache : ic .c .blobInfoCache ,
748- CanSubstitute : canSubstitute ,
749- EmptyLayer : emptyLayer ,
750- LayerIndex : & layerIndex ,
751- SrcRef : srcRef ,
752- PossibleManifestFormats : append ([]string {ic .manifestConversionPlan .preferredMIMEType }, ic .manifestConversionPlan .otherMIMETypeCandidates ... ),
753- RequiredCompression : requiredCompression ,
754- OriginalCompression : srcInfo .CompressionAlgorithm ,
755- TOCDigest : tocDigest ,
756- })
757- if err != nil {
758- return types.BlobInfo {}, "" , fmt .Errorf ("trying to reuse blob %s at destination: %w" , srcInfo .Digest , err )
817+ // FIXME: Blob reuse disabled when forcing different digest algorithm.
818+ // Future: support cross-digest reuse via BlobInfoCache (like c/storage does).
819+ canTryReuse := true
820+ if forcedAlgo := ic .c .options .digestOptions .MustUseSet (); forcedAlgo != "" {
821+ if srcInfo .Digest .Algorithm () != forcedAlgo {
822+ logrus .Debugf ("Skipping blob reuse for %s: digest algorithm %s doesn't match forced algorithm %s" ,
823+ srcInfo .Digest , srcInfo .Digest .Algorithm (), forcedAlgo )
824+ canTryReuse = false
825+ }
826+ }
827+
828+ reused := false
829+ var reusedBlob private.ReusedBlob
830+ if canTryReuse {
831+ var err error
832+ reused , reusedBlob , err = ic .c .dest .TryReusingBlobWithOptions (ctx , srcInfo , private.TryReusingBlobOptions {
833+ Cache : ic .c .blobInfoCache ,
834+ CanSubstitute : canSubstitute ,
835+ EmptyLayer : emptyLayer ,
836+ LayerIndex : & layerIndex ,
837+ SrcRef : srcRef ,
838+ PossibleManifestFormats : append ([]string {ic .manifestConversionPlan .preferredMIMEType }, ic .manifestConversionPlan .otherMIMETypeCandidates ... ),
839+ RequiredCompression : requiredCompression ,
840+ OriginalCompression : srcInfo .CompressionAlgorithm ,
841+ TOCDigest : tocDigest ,
842+ })
843+ if err != nil {
844+ return types.BlobInfo {}, "" , fmt .Errorf ("trying to reuse blob %s at destination: %w" , srcInfo .Digest , err )
845+ }
759846 }
760847 if reused {
761848 logrus .Debugf ("Skipping blob %s (already present):" , srcInfo .Digest )
0 commit comments