7
7
"fmt"
8
8
"io"
9
9
"maps"
10
+ "os"
10
11
"slices"
11
12
"strconv"
12
13
"time"
@@ -18,6 +19,7 @@ import (
18
19
"github.com/vbatts/tar-split/archive/tar"
19
20
"github.com/vbatts/tar-split/tar/asm"
20
21
"github.com/vbatts/tar-split/tar/storage"
22
+ "golang.org/x/sys/unix"
21
23
)
22
24
23
25
const (
@@ -158,9 +160,11 @@ func readEstargzChunkedManifest(blobStream ImageSourceSeekable, blobSize int64,
158
160
}
159
161
160
162
// readZstdChunkedManifest reads the zstd:chunked manifest from the seekable stream blobStream.
163
+ // tmpDir is a directory where the tar-split temporary file is written to. The file is opened with
164
+ // O_TMPFILE so that it is automatically removed when it is closed.
161
165
// Returns (manifest blob, parsed manifest, tar-split blob or nil, manifest offset).
162
166
// It may return an error matching ErrFallbackToOrdinaryLayerDownload / errFallbackCanConvert.
163
- func readZstdChunkedManifest (blobStream ImageSourceSeekable , tocDigest digest.Digest , annotations map [string ]string ) (_ []byte , _ * minimal.TOC , _ [] byte , _ int64 , retErr error ) {
167
+ func readZstdChunkedManifest (tmpDir string , blobStream ImageSourceSeekable , tocDigest digest.Digest , annotations map [string ]string ) (_ []byte , _ * minimal.TOC , _ * os. File , _ int64 , retErr error ) {
164
168
offsetMetadata := annotations [minimal .ManifestInfoKey ]
165
169
if offsetMetadata == "" {
166
170
return nil , nil , nil , 0 , fmt .Errorf ("%q annotation missing" , minimal .ManifestInfoKey )
@@ -245,7 +249,7 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
245
249
return nil , nil , nil , 0 , fmt .Errorf ("unmarshaling TOC: %w" , err )
246
250
}
247
251
248
- var decodedTarSplit [] byte = nil
252
+ var decodedTarSplit * os. File
249
253
if toc .TarSplitDigest != "" {
250
254
if tarSplitChunk .Offset <= 0 {
251
255
return nil , nil , nil , 0 , fmt .Errorf ("TOC requires a tar-split, but the %s annotation does not describe a position" , minimal .TarSplitInfoKey )
@@ -254,14 +258,20 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
254
258
if err != nil {
255
259
return nil , nil , nil , 0 , err
256
260
}
257
- decodedTarSplit , err = decodeAndValidateBlob ( tarSplit , tarSplitLengthUncompressed , toc . TarSplitDigest . String () )
261
+ fd , err := unix . Open ( tmpDir , unix . O_TMPFILE | unix . O_RDWR | unix . O_CLOEXEC , 0o600 )
258
262
if err != nil {
263
+ return nil , nil , nil , 0 , err
264
+ }
265
+ decodedTarSplit = os .NewFile (uintptr (fd ), "decoded-tar-split" )
266
+ if err := decodeAndValidateBlobToStream (tarSplit , decodedTarSplit , toc .TarSplitDigest .String ()); err != nil {
267
+ decodedTarSplit .Close ()
259
268
return nil , nil , nil , 0 , fmt .Errorf ("validating and decompressing tar-split: %w" , err )
260
269
}
261
270
// We use the TOC for creating on-disk files, but the tar-split for creating metadata
262
271
// when exporting the layer contents. Ensure the two match, otherwise local inspection of a container
263
272
// might be misleading about the exported contents.
264
273
if err := ensureTOCMatchesTarSplit (toc , decodedTarSplit ); err != nil {
274
+ decodedTarSplit .Close ()
265
275
return nil , nil , nil , 0 , fmt .Errorf ("tar-split and TOC data is inconsistent: %w" , err )
266
276
}
267
277
} else if tarSplitChunk .Offset > 0 {
@@ -278,7 +288,7 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
278
288
}
279
289
280
290
// ensureTOCMatchesTarSplit validates that toc and tarSplit contain _exactly_ the same entries.
281
- func ensureTOCMatchesTarSplit (toc * minimal.TOC , tarSplit [] byte ) error {
291
+ func ensureTOCMatchesTarSplit (toc * minimal.TOC , tarSplit * os. File ) error {
282
292
pendingFiles := map [string ]* minimal.FileMetadata {} // Name -> an entry in toc.Entries
283
293
for i := range toc .Entries {
284
294
e := & toc .Entries [i ]
@@ -290,7 +300,11 @@ func ensureTOCMatchesTarSplit(toc *minimal.TOC, tarSplit []byte) error {
290
300
}
291
301
}
292
302
293
- unpacker := storage .NewJSONUnpacker (bytes .NewReader (tarSplit ))
303
+ if _ , err := tarSplit .Seek (0 , 0 ); err != nil {
304
+ return err
305
+ }
306
+
307
+ unpacker := storage .NewJSONUnpacker (tarSplit )
294
308
if err := asm .IterateHeaders (unpacker , func (hdr * tar.Header ) error {
295
309
e , ok := pendingFiles [hdr .Name ]
296
310
if ! ok {
@@ -320,10 +334,10 @@ func ensureTOCMatchesTarSplit(toc *minimal.TOC, tarSplit []byte) error {
320
334
}
321
335
322
336
// tarSizeFromTarSplit computes the total tarball size, using only the tarSplit metadata
323
- func tarSizeFromTarSplit (tarSplit [] byte ) (int64 , error ) {
337
+ func tarSizeFromTarSplit (tarSplit io. Reader ) (int64 , error ) {
324
338
var res int64 = 0
325
339
326
- unpacker := storage .NewJSONUnpacker (bytes . NewReader ( tarSplit ) )
340
+ unpacker := storage .NewJSONUnpacker (tarSplit )
327
341
for {
328
342
entry , err := unpacker .Next ()
329
343
if err != nil {
@@ -464,3 +478,18 @@ func decodeAndValidateBlob(blob []byte, lengthUncompressed uint64, expectedCompr
464
478
b := make ([]byte , 0 , lengthUncompressed )
465
479
return decoder .DecodeAll (blob , b )
466
480
}
481
+
482
+ func decodeAndValidateBlobToStream (blob []byte , w * os.File , expectedCompressedChecksum string ) error {
483
+ if err := validateBlob (blob , expectedCompressedChecksum ); err != nil {
484
+ return err
485
+ }
486
+
487
+ decoder , err := zstd .NewReader (bytes .NewReader (blob )) //nolint:contextcheck
488
+ if err != nil {
489
+ return err
490
+ }
491
+ defer decoder .Close ()
492
+
493
+ _ , err = decoder .WriteTo (w )
494
+ return err
495
+ }
0 commit comments