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 (
@@ -157,10 +159,32 @@ func readEstargzChunkedManifest(blobStream ImageSourceSeekable, blobSize int64,
157
159
return manifestUncompressed , tocOffset , nil
158
160
}
159
161
162
+ func openTmpFile (tmpDir string ) (* os.File , error ) {
163
+ file , err := os .OpenFile (tmpDir , unix .O_TMPFILE | unix .O_RDWR | unix .O_CLOEXEC | unix .O_EXCL , 0o600 )
164
+ if err == nil {
165
+ return file , nil
166
+ }
167
+ return openTmpFileNoTmpFile (tmpDir )
168
+ }
169
+
170
+ // openTmpFileNoTmpFile is a fallback used by openTmpFile when the underlying file system does not
171
+ // support O_TMPFILE.
172
+ func openTmpFileNoTmpFile (tmpDir string ) (* os.File , error ) {
173
+ file , err := os .CreateTemp (tmpDir , ".tmpfile" )
174
+ if err != nil {
175
+ return nil , err
176
+ }
177
+ // Unlink the file immediately so that only the open fd refers to it.
178
+ _ = os .Remove (file .Name ())
179
+ return file , nil
180
+ }
181
+
160
182
// readZstdChunkedManifest reads the zstd:chunked manifest from the seekable stream blobStream.
183
+ // tmpDir is a directory where the tar-split temporary file is written to. The file is opened with
184
+ // O_TMPFILE so that it is automatically removed when it is closed.
161
185
// Returns (manifest blob, parsed manifest, tar-split blob or nil, manifest offset).
162
186
// 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 ) {
187
+ func readZstdChunkedManifest (tmpDir string , blobStream ImageSourceSeekable , tocDigest digest.Digest , annotations map [string ]string ) (_ []byte , _ * minimal.TOC , _ * os. File , _ int64 , retErr error ) {
164
188
offsetMetadata := annotations [minimal .ManifestInfoKey ]
165
189
if offsetMetadata == "" {
166
190
return nil , nil , nil , 0 , fmt .Errorf ("%q annotation missing" , minimal .ManifestInfoKey )
@@ -245,7 +269,7 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
245
269
return nil , nil , nil , 0 , fmt .Errorf ("unmarshaling TOC: %w" , err )
246
270
}
247
271
248
- var decodedTarSplit [] byte = nil
272
+ var decodedTarSplit * os. File
249
273
if toc .TarSplitDigest != "" {
250
274
if tarSplitChunk .Offset <= 0 {
251
275
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 +278,19 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
254
278
if err != nil {
255
279
return nil , nil , nil , 0 , err
256
280
}
257
- decodedTarSplit , err = decodeAndValidateBlob ( tarSplit , tarSplitLengthUncompressed , toc . TarSplitDigest . String () )
281
+ decodedTarSplit , err = openTmpFile ( tmpDir )
258
282
if err != nil {
283
+ return nil , nil , nil , 0 , err
284
+ }
285
+ if err := decodeAndValidateBlobToStream (tarSplit , decodedTarSplit , toc .TarSplitDigest .String ()); err != nil {
286
+ decodedTarSplit .Close ()
259
287
return nil , nil , nil , 0 , fmt .Errorf ("validating and decompressing tar-split: %w" , err )
260
288
}
261
289
// We use the TOC for creating on-disk files, but the tar-split for creating metadata
262
290
// when exporting the layer contents. Ensure the two match, otherwise local inspection of a container
263
291
// might be misleading about the exported contents.
264
292
if err := ensureTOCMatchesTarSplit (toc , decodedTarSplit ); err != nil {
293
+ decodedTarSplit .Close ()
265
294
return nil , nil , nil , 0 , fmt .Errorf ("tar-split and TOC data is inconsistent: %w" , err )
266
295
}
267
296
} else if tarSplitChunk .Offset > 0 {
@@ -278,7 +307,7 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
278
307
}
279
308
280
309
// ensureTOCMatchesTarSplit validates that toc and tarSplit contain _exactly_ the same entries.
281
- func ensureTOCMatchesTarSplit (toc * minimal.TOC , tarSplit [] byte ) error {
310
+ func ensureTOCMatchesTarSplit (toc * minimal.TOC , tarSplit * os. File ) error {
282
311
pendingFiles := map [string ]* minimal.FileMetadata {} // Name -> an entry in toc.Entries
283
312
for i := range toc .Entries {
284
313
e := & toc .Entries [i ]
@@ -290,7 +319,11 @@ func ensureTOCMatchesTarSplit(toc *minimal.TOC, tarSplit []byte) error {
290
319
}
291
320
}
292
321
293
- unpacker := storage .NewJSONUnpacker (bytes .NewReader (tarSplit ))
322
+ if _ , err := tarSplit .Seek (0 , 0 ); err != nil {
323
+ return err
324
+ }
325
+
326
+ unpacker := storage .NewJSONUnpacker (tarSplit )
294
327
if err := asm .IterateHeaders (unpacker , func (hdr * tar.Header ) error {
295
328
e , ok := pendingFiles [hdr .Name ]
296
329
if ! ok {
@@ -320,10 +353,10 @@ func ensureTOCMatchesTarSplit(toc *minimal.TOC, tarSplit []byte) error {
320
353
}
321
354
322
355
// tarSizeFromTarSplit computes the total tarball size, using only the tarSplit metadata
323
- func tarSizeFromTarSplit (tarSplit [] byte ) (int64 , error ) {
356
+ func tarSizeFromTarSplit (tarSplit io. Reader ) (int64 , error ) {
324
357
var res int64 = 0
325
358
326
- unpacker := storage .NewJSONUnpacker (bytes . NewReader ( tarSplit ) )
359
+ unpacker := storage .NewJSONUnpacker (tarSplit )
327
360
for {
328
361
entry , err := unpacker .Next ()
329
362
if err != nil {
@@ -464,3 +497,18 @@ func decodeAndValidateBlob(blob []byte, lengthUncompressed uint64, expectedCompr
464
497
b := make ([]byte , 0 , lengthUncompressed )
465
498
return decoder .DecodeAll (blob , b )
466
499
}
500
+
501
+ func decodeAndValidateBlobToStream (blob []byte , w * os.File , expectedCompressedChecksum string ) error {
502
+ if err := validateBlob (blob , expectedCompressedChecksum ); err != nil {
503
+ return err
504
+ }
505
+
506
+ decoder , err := zstd .NewReader (bytes .NewReader (blob )) //nolint:contextcheck
507
+ if err != nil {
508
+ return err
509
+ }
510
+ defer decoder .Close ()
511
+
512
+ _ , err = decoder .WriteTo (w )
513
+ return err
514
+ }
0 commit comments