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