@@ -6,7 +6,11 @@ import (
6
6
"errors"
7
7
"fmt"
8
8
"io"
9
+ "io/fs"
9
10
"maps"
11
+ "math/rand"
12
+ "os"
13
+ "path/filepath"
10
14
"slices"
11
15
"strconv"
12
16
"time"
@@ -18,6 +22,7 @@ import (
18
22
"github.com/vbatts/tar-split/archive/tar"
19
23
"github.com/vbatts/tar-split/tar/asm"
20
24
"github.com/vbatts/tar-split/tar/storage"
25
+ "golang.org/x/sys/unix"
21
26
)
22
27
23
28
const (
@@ -157,10 +162,42 @@ func readEstargzChunkedManifest(blobStream ImageSourceSeekable, blobSize int64,
157
162
return manifestUncompressed , tocOffset , nil
158
163
}
159
164
165
+ func openTmpFile (tmpDir string ) (fd int , err error ) {
166
+ fd , err = unix .Open (tmpDir , unix .O_TMPFILE | unix .O_RDWR | unix .O_CLOEXEC , 0o600 )
167
+ if err == nil {
168
+ return fd , nil
169
+ }
170
+ return openTmpFileNoTmpFile (tmpDir )
171
+
172
+ }
173
+
174
+ // openTmpFileNoTmpFile is a fallback used by openTmpFile when the underlying file system does not
175
+ // support O_TMPFILE.
176
+ func openTmpFileNoTmpFile (tmpDir string ) (fd int , err error ) {
177
+ for i := 0 ; i < 100 ; i ++ {
178
+ name := fmt .Sprintf (".tmpfile-%d" , rand .Int63 ())
179
+ path := filepath .Join (tmpDir , name )
180
+
181
+ fd , err := unix .Open (path , unix .O_RDWR | unix .O_CREAT | unix .O_EXCL | unix .O_CLOEXEC , 0o600 )
182
+ if err == nil {
183
+ // Unlink the file immediately so that only the open fd refers to it.
184
+ _ = os .Remove (path )
185
+ return fd , nil
186
+ }
187
+ if ! errors .Is (err , os .ErrExist ) {
188
+ return - 1 , & fs.PathError {Op : "open" , Path : tmpDir , Err : err }
189
+ }
190
+ }
191
+ // report the original error if the fallback failed
192
+ return - 1 , & fs.PathError {Op : "open O_TMPFILE" , Path : tmpDir , Err : err }
193
+ }
194
+
160
195
// readZstdChunkedManifest reads the zstd:chunked manifest from the seekable stream blobStream.
196
+ // tmpDir is a directory where the tar-split temporary file is written to. The file is opened with
197
+ // O_TMPFILE so that it is automatically removed when it is closed.
161
198
// Returns (manifest blob, parsed manifest, tar-split blob or nil, manifest offset).
162
199
// 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 ) {
200
+ func readZstdChunkedManifest (tmpDir string , blobStream ImageSourceSeekable , tocDigest digest.Digest , annotations map [string ]string ) (_ []byte , _ * minimal.TOC , _ * os. File , _ int64 , retErr error ) {
164
201
offsetMetadata := annotations [minimal .ManifestInfoKey ]
165
202
if offsetMetadata == "" {
166
203
return nil , nil , nil , 0 , fmt .Errorf ("%q annotation missing" , minimal .ManifestInfoKey )
@@ -245,7 +282,7 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
245
282
return nil , nil , nil , 0 , fmt .Errorf ("unmarshaling TOC: %w" , err )
246
283
}
247
284
248
- var decodedTarSplit [] byte = nil
285
+ var decodedTarSplit * os. File
249
286
if toc .TarSplitDigest != "" {
250
287
if tarSplitChunk .Offset <= 0 {
251
288
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 +291,20 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
254
291
if err != nil {
255
292
return nil , nil , nil , 0 , err
256
293
}
257
- decodedTarSplit , err = decodeAndValidateBlob ( tarSplit , tarSplitLengthUncompressed , toc . TarSplitDigest . String () )
294
+ fd , err := openTmpFile ( tmpDir )
258
295
if err != nil {
296
+ return nil , nil , nil , 0 , err
297
+ }
298
+ decodedTarSplit = os .NewFile (uintptr (fd ), "decoded-tar-split" )
299
+ if err := decodeAndValidateBlobToStream (tarSplit , decodedTarSplit , toc .TarSplitDigest .String ()); err != nil {
300
+ decodedTarSplit .Close ()
259
301
return nil , nil , nil , 0 , fmt .Errorf ("validating and decompressing tar-split: %w" , err )
260
302
}
261
303
// We use the TOC for creating on-disk files, but the tar-split for creating metadata
262
304
// when exporting the layer contents. Ensure the two match, otherwise local inspection of a container
263
305
// might be misleading about the exported contents.
264
306
if err := ensureTOCMatchesTarSplit (toc , decodedTarSplit ); err != nil {
307
+ decodedTarSplit .Close ()
265
308
return nil , nil , nil , 0 , fmt .Errorf ("tar-split and TOC data is inconsistent: %w" , err )
266
309
}
267
310
} else if tarSplitChunk .Offset > 0 {
@@ -278,7 +321,7 @@ func readZstdChunkedManifest(blobStream ImageSourceSeekable, tocDigest digest.Di
278
321
}
279
322
280
323
// ensureTOCMatchesTarSplit validates that toc and tarSplit contain _exactly_ the same entries.
281
- func ensureTOCMatchesTarSplit (toc * minimal.TOC , tarSplit [] byte ) error {
324
+ func ensureTOCMatchesTarSplit (toc * minimal.TOC , tarSplit * os. File ) error {
282
325
pendingFiles := map [string ]* minimal.FileMetadata {} // Name -> an entry in toc.Entries
283
326
for i := range toc .Entries {
284
327
e := & toc .Entries [i ]
@@ -290,7 +333,11 @@ func ensureTOCMatchesTarSplit(toc *minimal.TOC, tarSplit []byte) error {
290
333
}
291
334
}
292
335
293
- unpacker := storage .NewJSONUnpacker (bytes .NewReader (tarSplit ))
336
+ if _ , err := tarSplit .Seek (0 , 0 ); err != nil {
337
+ return err
338
+ }
339
+
340
+ unpacker := storage .NewJSONUnpacker (tarSplit )
294
341
if err := asm .IterateHeaders (unpacker , func (hdr * tar.Header ) error {
295
342
e , ok := pendingFiles [hdr .Name ]
296
343
if ! ok {
@@ -320,10 +367,10 @@ func ensureTOCMatchesTarSplit(toc *minimal.TOC, tarSplit []byte) error {
320
367
}
321
368
322
369
// tarSizeFromTarSplit computes the total tarball size, using only the tarSplit metadata
323
- func tarSizeFromTarSplit (tarSplit [] byte ) (int64 , error ) {
370
+ func tarSizeFromTarSplit (tarSplit io. Reader ) (int64 , error ) {
324
371
var res int64 = 0
325
372
326
- unpacker := storage .NewJSONUnpacker (bytes . NewReader ( tarSplit ) )
373
+ unpacker := storage .NewJSONUnpacker (tarSplit )
327
374
for {
328
375
entry , err := unpacker .Next ()
329
376
if err != nil {
@@ -464,3 +511,18 @@ func decodeAndValidateBlob(blob []byte, lengthUncompressed uint64, expectedCompr
464
511
b := make ([]byte , 0 , lengthUncompressed )
465
512
return decoder .DecodeAll (blob , b )
466
513
}
514
+
515
+ func decodeAndValidateBlobToStream (blob []byte , w * os.File , expectedCompressedChecksum string ) error {
516
+ if err := validateBlob (blob , expectedCompressedChecksum ); err != nil {
517
+ return err
518
+ }
519
+
520
+ decoder , err := zstd .NewReader (bytes .NewReader (blob )) //nolint:contextcheck
521
+ if err != nil {
522
+ return err
523
+ }
524
+ defer decoder .Close ()
525
+
526
+ _ , err = decoder .WriteTo (w )
527
+ return err
528
+ }
0 commit comments