@@ -70,22 +70,11 @@ func LoadImage(ctx context.Context, inputPath string, tmpDir string) (*Standalon
7070 if err != nil {
7171 return nil , fmt .Errorf ("failed to read index.json from %s: %w" , inputPath , err )
7272 }
73- rootDesc , err := parseRootDescriptor ( indexData )
73+ rootDesc , err := resolveLayoutRoot ( tmpDir , indexData )
7474 if err != nil {
7575 return nil , err
7676 }
7777
78- // If the root descriptor is a manifest list (e.g. from nerdctl save),
79- // resolve it to available platform manifests. This handles partial exports
80- // where the manifest list references all platforms but only a subset of
81- // platform blobs were exported.
82- if images .IsIndexType (rootDesc .MediaType ) {
83- rootDesc , err = resolveManifestList (tmpDir , rootDesc )
84- if err != nil {
85- return nil , err
86- }
87- }
88-
8978 orasStore , err := oci .New (tmpDir )
9079 if err != nil {
9180 return nil , fmt .Errorf ("failed to create writable OCI store: %w" , err )
@@ -139,73 +128,81 @@ func SaveImageToDir(srcDir string, desc ocispec.Descriptor, outputPath string) e
139128 return os .WriteFile (filepath .Join (outputPath , "index.json" ), indexData , 0644 )
140129}
141130
142- // parseRootDescriptor unmarshals OCI index JSON and returns the manifest descriptor.
143- func parseRootDescriptor (indexData []byte ) (ocispec.Descriptor , error ) {
144- var index ocispec.Index
145- if err := json .Unmarshal (indexData , & index ); err != nil {
146- return ocispec.Descriptor {}, fmt .Errorf ("failed to unmarshal index.json: %w" , err )
147- }
148- if len (index .Manifests ) == 0 {
131+ // resolveLayoutRoot returns a root descriptor for the OCI image layout. The result
132+ // is either a single image manifest descriptor or a manifest list descriptor.
133+ //
134+ // It accepts index.json shapes produced by common tools: a single image manifest,
135+ // a single descriptor pointing at a nested manifest list (e.g. nerdctl save), or
136+ // a flat list of per-platform manifests (e.g. go-containerregistry layout.Write).
137+ // If some children are missing their blobs, they are filtered out and a new
138+ // manifest list blob is written into layoutDir.
139+ func resolveLayoutRoot (layoutDir string , indexData []byte ) (ocispec.Descriptor , error ) {
140+ var top ocispec.Index
141+ if err := json .Unmarshal (indexData , & top ); err != nil {
142+ return ocispec.Descriptor {}, fmt .Errorf ("unmarshal index.json: %w" , err )
143+ }
144+ if len (top .Manifests ) == 0 {
149145 return ocispec.Descriptor {}, errors .New ("index.json contains no manifests" )
150146 }
151- return index .Manifests [0 ], nil
152- }
153-
154- // resolveManifestList reads a manifest list blob and resolves it based on which
155- // platform blobs are actually present in the layout. If all platforms are available,
156- // it returns the original manifest list. If only one is available, it returns that
157- // platform manifest directly. If multiple (but not all) are available, it writes a
158- // filtered manifest list containing only the available platforms. This handles tools
159- // like `nerdctl save` that export a manifest list referencing all platforms even when
160- // only a subset was pulled.
161- func resolveManifestList (layoutDir string , listDesc ocispec.Descriptor ) (ocispec.Descriptor , error ) {
162- blobPath := filepath .Join (layoutDir , "blobs" , listDesc .Digest .Algorithm ().String (), listDesc .Digest .Encoded ())
163- listData , err := os .ReadFile (blobPath )
164- if err != nil {
165- return ocispec.Descriptor {}, fmt .Errorf ("failed to read manifest list blob: %w" , err )
166- }
167-
168- var manifestList ocispec.Index
169- if err := json .Unmarshal (listData , & manifestList ); err != nil {
170- return ocispec.Descriptor {}, fmt .Errorf ("failed to unmarshal manifest list: %w" , err )
147+ // Single non-index entry: a plain single-platform image.
148+ if len (top .Manifests ) == 1 && ! images .IsIndexType (top .Manifests [0 ].MediaType ) {
149+ return top .Manifests [0 ], nil
171150 }
172151
173- // Find which platform manifests have their blobs present
174- var available []ocispec.Descriptor
175- for _ , desc := range manifestList .Manifests {
176- p := filepath .Join (layoutDir , "blobs" , desc .Digest .Algorithm ().String (), desc .Digest .Encoded ())
177- if _ , err := os .Stat (p ); err == nil {
178- available = append (available , desc )
152+ // Locate the manifest list to walk. Either index.json points at a nested list
153+ // blob, or index.json is itself the list.
154+ var (
155+ listBytes = indexData
156+ mediaType = top .MediaType
157+ )
158+ if len (top .Manifests ) == 1 {
159+ mediaType = top .Manifests [0 ].MediaType
160+ b , err := os .ReadFile (blobPath (layoutDir , top .Manifests [0 ].Digest ))
161+ if err != nil {
162+ return ocispec.Descriptor {}, fmt .Errorf ("read manifest list: %w" , err )
179163 }
164+ listBytes = b
180165 }
181- if len ( available ) == 0 {
182- return ocispec.Descriptor {}, errors . New ( "manifest list contains no manifests with available blobs" )
166+ if mediaType == "" {
167+ mediaType = ocispec .MediaTypeImageIndex
183168 }
184169
185- // If all platforms are available, keep the original manifest list
186- if len ( available ) == len ( manifestList . Manifests ) {
187- return listDesc , nil
170+ var list ocispec. Index
171+ if err := json . Unmarshal ( listBytes , & list ); err != nil {
172+ return ocispec. Descriptor {}, fmt . Errorf ( "unmarshal manifest list: %w" , err )
188173 }
189174
190- // If only one platform is available, return it directly as a single manifest
191- if len (available ) == 1 {
175+ available := make ([]ocispec.Descriptor , 0 , len (list .Manifests ))
176+ for _ , d := range list .Manifests {
177+ if _ , err := os .Stat (blobPath (layoutDir , d .Digest )); err == nil {
178+ available = append (available , d )
179+ }
180+ }
181+ switch {
182+ case len (available ) == 0 :
183+ return ocispec.Descriptor {}, errors .New ("manifest list contains no entries with available blobs" )
184+ case len (available ) == 1 && images .IsManifestType (available [0 ].MediaType ):
192185 return available [0 ], nil
186+ case len (top .Manifests ) == 1 && len (available ) == len (list .Manifests ):
187+ return top .Manifests [0 ], nil
193188 }
194189
195- // Multiple (but not all) platforms available: write a filtered manifest list
196- manifestList .Manifests = available
197- filteredData , err := json .Marshal (manifestList )
190+ list . MediaType = mediaType
191+ list .Manifests = available
192+ data , err := json .Marshal (list )
198193 if err != nil {
199- return ocispec.Descriptor {}, fmt .Errorf ("failed to marshal filtered manifest list: %w" , err )
194+ return ocispec.Descriptor {}, fmt .Errorf ("marshal manifest list: %w" , err )
200195 }
201- filteredDigest := digest .FromBytes (filteredData )
202- filteredPath := filepath .Join (layoutDir , "blobs" , filteredDigest .Algorithm ().String (), filteredDigest .Encoded ())
203- if err := os .WriteFile (filteredPath , filteredData , 0644 ); err != nil {
204- return ocispec.Descriptor {}, fmt .Errorf ("failed to write filtered manifest list: %w" , err )
196+ dgst := digest .FromBytes (data )
197+ if err := os .MkdirAll (filepath .Dir (blobPath (layoutDir , dgst )), 0755 ); err != nil {
198+ return ocispec.Descriptor {}, err
205199 }
206- return ocispec.Descriptor {
207- MediaType : listDesc .MediaType ,
208- Digest : filteredDigest ,
209- Size : int64 (len (filteredData )),
210- }, nil
200+ if err := os .WriteFile (blobPath (layoutDir , dgst ), data , 0644 ); err != nil {
201+ return ocispec.Descriptor {}, err
202+ }
203+ return ocispec.Descriptor {MediaType : mediaType , Digest : dgst , Size : int64 (len (data ))}, nil
204+ }
205+
206+ func blobPath (layoutDir string , dgst digest.Digest ) string {
207+ return filepath .Join (layoutDir , "blobs" , dgst .Algorithm ().String (), dgst .Encoded ())
211208}
0 commit comments