@@ -5,13 +5,16 @@ import (
5
5
"errors"
6
6
"fmt"
7
7
"io"
8
+ "maps"
8
9
"strconv"
10
+ "time"
9
11
10
12
"github.com/containers/storage/pkg/chunked/internal"
11
13
"github.com/klauspost/compress/zstd"
12
14
"github.com/klauspost/pgzip"
13
15
digest "github.com/opencontainers/go-digest"
14
16
"github.com/vbatts/tar-split/archive/tar"
17
+ expMaps "golang.org/x/exp/maps"
15
18
)
16
19
17
20
var typesToTar = map [string ]byte {
@@ -221,6 +224,12 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
221
224
if err != nil {
222
225
return nil , nil , nil , 0 , fmt .Errorf ("validating and decompressing tar-split: %w" , err )
223
226
}
227
+ // We use the TOC for creating on-disk files, but the tar-split for creating metadata
228
+ // when exporting the layer contents. Ensure the two match, otherwise local inspection of a container
229
+ // might be misleading about the exported contents.
230
+ if err := ensureTOCMatchesTarSplit (toc , decodedTarSplit ); err != nil {
231
+ return nil , nil , nil , 0 , fmt .Errorf ("tar-split and TOC data is inconsistent: %w" , err )
232
+ }
224
233
} else if tarSplitChunk .Offset > 0 {
225
234
// We must ignore the tar-split when the digest is not present in the TOC, because we can’t authenticate it.
226
235
//
@@ -234,6 +243,121 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
234
243
return decodedBlob , toc , decodedTarSplit , int64 (manifestChunk .Offset ), err
235
244
}
236
245
246
+ // ensureTOCMatchesTarSplit validates that toc and tarSplit contain _exactly_ the same entries.
247
+ func ensureTOCMatchesTarSplit (toc * internal.TOC , tarSplit []byte ) error {
248
+ pendingFiles := map [string ]* internal.FileMetadata {} // Name -> an entry in toc.Entries
249
+ for i := range toc .Entries {
250
+ e := & toc .Entries [i ]
251
+ if e .Type != internal .TypeChunk {
252
+ if _ , ok := pendingFiles [e .Name ]; ok {
253
+ return fmt .Errorf ("TOC contains duplicate entries for path %q" , e .Name )
254
+ }
255
+ pendingFiles [e .Name ] = e
256
+ }
257
+ }
258
+
259
+ if err := iterateTarSplit (tarSplit , func (hdr * tar.Header ) error {
260
+ e , ok := pendingFiles [hdr .Name ]
261
+ if ! ok {
262
+ return fmt .Errorf ("tar-split contains an entry for %q missing in TOC" , hdr .Name )
263
+ }
264
+ delete (pendingFiles , hdr .Name )
265
+ expected , err := internal .NewFileMetadata (hdr )
266
+ if err != nil {
267
+ return fmt .Errorf ("determining expected metadata for %q: %w" , hdr .Name , err )
268
+ }
269
+ if err := ensureFileMetadataAttributesMatch (e , & expected ); err != nil {
270
+ return fmt .Errorf ("TOC and tar-split metadata doesn’t match: %w" , err )
271
+ }
272
+
273
+ return nil
274
+ }); err != nil {
275
+ return err
276
+ }
277
+ if len (pendingFiles ) != 0 {
278
+ remaining := expMaps .Keys (pendingFiles )
279
+ if len (remaining ) > 5 {
280
+ remaining = remaining [:5 ] // Just to limit the size of the output.
281
+ }
282
+ return fmt .Errorf ("TOC contains entries not present in tar-split, incl. %q" , remaining )
283
+ }
284
+ return nil
285
+ }
286
+
287
+ // ensureTimePointersMatch ensures that a and b are equal
288
+ func ensureTimePointersMatch (a , b * time.Time ) error {
289
+ switch {
290
+ case a == nil && b == nil :
291
+ return nil
292
+ case a == nil :
293
+ return fmt .Errorf ("nil != %v" , * b )
294
+ case b == nil :
295
+ return fmt .Errorf ("%v != nil" , * a )
296
+ default :
297
+ if a .Equal (* b ) {
298
+ return nil
299
+ }
300
+ return fmt .Errorf ("%v != %v" , * a , * b )
301
+ }
302
+ }
303
+
304
+ // ensureFileMetadataAttributesMatch ensures that a and b match in file attributes (it ignores entries relevant to locating data
305
+ // in the tar stream or matching contents)
306
+ func ensureFileMetadataAttributesMatch (a , b * internal.FileMetadata ) error {
307
+ // Keep this in sync with internal.FileMetadata!
308
+
309
+ if a .Type != b .Type {
310
+ return fmt .Errorf ("mismatch of Type: %q != %q" , a .Type , b .Type )
311
+ }
312
+ if a .Name != b .Name {
313
+ return fmt .Errorf ("mismatch of Name: %q != %q" , a .Name , b .Name )
314
+ }
315
+ if a .Linkname != b .Linkname {
316
+ return fmt .Errorf ("mismatch of Linkname: %q != %q" , a .Linkname , b .Linkname )
317
+ }
318
+ if a .Mode != b .Mode {
319
+ return fmt .Errorf ("mismatch of Mode: %q != %q" , a .Mode , b .Mode )
320
+ }
321
+ if a .Size != b .Size {
322
+ return fmt .Errorf ("mismatch of Size: %q != %q" , a .Size , b .Size )
323
+ }
324
+ if a .UID != b .UID {
325
+ return fmt .Errorf ("mismatch of UID: %q != %q" , a .UID , b .UID )
326
+ }
327
+ if a .GID != b .GID {
328
+ return fmt .Errorf ("mismatch of GID: %q != %q" , a .GID , b .GID )
329
+ }
330
+
331
+ if err := ensureTimePointersMatch (a .ModTime , b .ModTime ); err != nil {
332
+ return fmt .Errorf ("mismatch of ModTime: %w" , err )
333
+ }
334
+ if err := ensureTimePointersMatch (a .AccessTime , b .AccessTime ); err != nil {
335
+ return fmt .Errorf ("mismatch of AccessTime: %w" , err )
336
+ }
337
+ if err := ensureTimePointersMatch (a .ChangeTime , b .ChangeTime ); err != nil {
338
+ return fmt .Errorf ("mismatch of ChangeTime: %w" , err )
339
+ }
340
+ if a .Devmajor != b .Devmajor {
341
+ return fmt .Errorf ("mismatch of Devmajor: %q != %q" , a .Devmajor , b .Devmajor )
342
+ }
343
+ if a .Devminor != b .Devminor {
344
+ return fmt .Errorf ("mismatch of Devminor: %q != %q" , a .Devminor , b .Devminor )
345
+ }
346
+ if ! maps .Equal (a .Xattrs , b .Xattrs ) {
347
+ return fmt .Errorf ("mismatch of Xattrs: %q != %q" , a .Xattrs , b .Xattrs )
348
+ }
349
+
350
+ // Digest is not compared
351
+ // Offset is not compared
352
+ // EndOffset is not compared
353
+
354
+ // ChunkSize is not compared
355
+ // ChunkOffset is not compared
356
+ // ChunkDigest is not compared
357
+ // ChunkType is not compared
358
+ return nil
359
+ }
360
+
237
361
func decodeAndValidateBlob (blob []byte , lengthUncompressed uint64 , expectedCompressedChecksum string ) ([]byte , error ) {
238
362
d , err := digest .Parse (expectedCompressedChecksum )
239
363
if err != nil {
0 commit comments