Merge pull request #47597 from vvoland/c8d-list-fix-shared-size

c8d/list: Fix shared size calculation
diff --git a/daemon/containerd/image_list.go b/daemon/containerd/image_list.go
index 600260a..80b2409 100644
--- a/daemon/containerd/image_list.go
+++ b/daemon/containerd/image_list.go
@@ -255,11 +255,13 @@
 
 		target := img.Target()
 
-		chainIDs, err := img.RootFS(ctx)
+		diffIDs, err := img.RootFS(ctx)
 		if err != nil {
 			return err
 		}
 
+		chainIDs := identity.ChainIDs(diffIDs)
+
 		ts, _, err := i.singlePlatformSize(ctx, img)
 		if err != nil {
 			return err
@@ -650,6 +652,11 @@
 		}
 		size, err := sizeFn(chainID)
 		if err != nil {
+			// Several images might share the same layer and neither of them
+			// might be unpacked (for example if it's a non-host platform).
+			if cerrdefs.IsNotFound(err) {
+				continue
+			}
 			return 0, err
 		}
 		sharedSize += size
diff --git a/integration/image/list_test.go b/integration/image/list_test.go
index b653802..54d7253 100644
--- a/integration/image/list_test.go
+++ b/integration/image/list_test.go
@@ -10,10 +10,14 @@
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/image"
 	"github.com/docker/docker/integration/internal/container"
+	"github.com/docker/docker/internal/testutils/specialimage"
 	"github.com/docker/docker/testutil"
+	"github.com/docker/docker/testutil/daemon"
 	"github.com/google/go-cmp/cmp/cmpopts"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
+	"gotest.tools/v3/skip"
 )
 
 // Regression : #38171
@@ -192,3 +196,34 @@
 		})
 	}
 }
+
+// Verify that the size calculation operates on ChainIDs and not DiffIDs.
+// This test calls an image list with two images that share one, top layer.
+func TestAPIImagesListSizeShared(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
+
+	ctx := setupTest(t)
+
+	daemon := daemon.New(t)
+	daemon.Start(t)
+	defer daemon.Stop(t)
+
+	client := daemon.NewClientT(t)
+
+	specialimage.Load(ctx, t, client, func(dir string) (*ocispec.Index, error) {
+		return specialimage.MultiLayerCustom(dir, "multilayer:latest", []specialimage.SingleFileLayer{
+			{Name: "bar", Content: []byte("2")},
+			{Name: "foo", Content: []byte("1")},
+		})
+	})
+
+	specialimage.Load(ctx, t, client, func(dir string) (*ocispec.Index, error) {
+		return specialimage.MultiLayerCustom(dir, "multilayer2:latest", []specialimage.SingleFileLayer{
+			{Name: "asdf", Content: []byte("3")},
+			{Name: "foo", Content: []byte("1")},
+		})
+	})
+
+	_, err := client.ImageList(ctx, image.ListOptions{SharedSize: true})
+	assert.NilError(t, err)
+}
diff --git a/internal/testutils/specialimage/multilayer.go b/internal/testutils/specialimage/multilayer.go
index ee20380..b7352d0 100644
--- a/internal/testutils/specialimage/multilayer.go
+++ b/internal/testutils/specialimage/multilayer.go
@@ -16,20 +16,32 @@
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 )
 
-func MultiLayer(dir string) (*ocispec.Index, error) {
-	const imageRef = "multilayer:latest"
+type SingleFileLayer struct {
+	Name    string
+	Content []byte
+}
 
-	layer1Desc, err := writeLayerWithOneFile(dir, "foo", []byte("1"))
-	if err != nil {
-		return nil, err
-	}
-	layer2Desc, err := writeLayerWithOneFile(dir, "bar", []byte("2"))
-	if err != nil {
-		return nil, err
-	}
-	layer3Desc, err := writeLayerWithOneFile(dir, "hello", []byte("world"))
-	if err != nil {
-		return nil, err
+func MultiLayer(dir string) (*ocispec.Index, error) {
+	return MultiLayerCustom(dir, "multilayer:latest", []SingleFileLayer{
+		{Name: "foo", Content: []byte("1")},
+		{Name: "bar", Content: []byte("2")},
+		{Name: "hello", Content: []byte("world")},
+	})
+}
+
+func MultiLayerCustom(dir string, imageRef string, layers []SingleFileLayer) (*ocispec.Index, error) {
+	var layerDescs []ocispec.Descriptor
+	var layerDgsts []digest.Digest
+	var layerBlobs []string
+	for _, layer := range layers {
+		layerDesc, err := writeLayerWithOneFile(dir, layer.Name, layer.Content)
+		if err != nil {
+			return nil, err
+		}
+
+		layerDescs = append(layerDescs, layerDesc)
+		layerDgsts = append(layerDgsts, layerDesc.Digest)
+		layerBlobs = append(layerBlobs, blobPath(layerDesc))
 	}
 
 	configDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{
@@ -39,7 +51,7 @@
 		},
 		RootFS: ocispec.RootFS{
 			Type:    "layers",
-			DiffIDs: []digest.Digest{layer1Desc.Digest, layer2Desc.Digest, layer3Desc.Digest},
+			DiffIDs: layerDgsts,
 		},
 	})
 	if err != nil {
@@ -49,14 +61,14 @@
 	manifest := ocispec.Manifest{
 		MediaType: ocispec.MediaTypeImageManifest,
 		Config:    configDesc,
-		Layers:    []ocispec.Descriptor{layer1Desc, layer2Desc, layer3Desc},
+		Layers:    layerDescs,
 	}
 
 	legacyManifests := []manifestItem{
 		{
 			Config:   blobPath(configDesc),
 			RepoTags: []string{imageRef},
-			Layers:   []string{blobPath(layer1Desc), blobPath(layer2Desc), blobPath(layer3Desc)},
+			Layers:   layerBlobs,
 		},
 	}
 
@@ -128,6 +140,7 @@
 	if err != nil {
 		return ocispec.Descriptor{}, err
 	}
+	defer rd.Close()
 
 	return writeBlob(dir, ocispec.MediaTypeImageLayer, rd)
 }