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