1+ /*
2+ Copyright The containerd Authors.
3+
4+ Licensed under the Apache License, Version 2.0 (the "License");
5+ you may not use this file except in compliance with the License.
6+ You may obtain a copy of the License at
7+
8+ http://www.apache.org/licenses/LICENSE-2.0
9+
10+ Unless required by applicable law or agreed to in writing, software
11+ distributed under the License is distributed on an "AS IS" BASIS,
12+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ See the License for the specific language governing permissions and
14+ limitations under the License.
15+ */
16+
117// Package tarconv ingests OCI/Docker tar layer streams into an [erofs.Writer]
218// via direct writer calls, without staging an intermediate fs.FS.
319//
1127// Suitable for flat merged images where all layers are applied in sequence.
1228// - [WithPreserveWhiteouts]: keep .wh.* entries as plain files.
1329// Suitable for tooling that needs the raw tar content.
30+ //
31+ // Tar-index mode is enabled by creating the [erofs.Writer] with
32+ // [erofs.WithDataFile] pointing at a file that receives the raw tar bytes,
33+ // and passing that same file to Apply via [WithTarIndexData]. In this mode
34+ // Apply records file-data ranges in the EROFS image as chunk-index entries
35+ // that reference the external data file rather than copying bytes into the
36+ // EROFS spool. The result is a compact metadata-only EROFS image whose
37+ // chunk table points into the appended original tar content.
1438package tarconv
1539
1640import (
@@ -19,6 +43,7 @@ import (
1943 "fmt"
2044 "io"
2145 "io/fs"
46+ "os"
2247 "path"
2348 "strings"
2449
@@ -54,7 +79,8 @@ const (
5479
5580// config holds the parsed options for an Apply call.
5681type config struct {
57- whiteouts whiteoutMode
82+ whiteouts whiteoutMode
83+ tarIndexData * os.File // non-nil: tar-index mode — data written here, not into EROFS spool
5884}
5985
6086// Option configures an [Apply] call.
@@ -80,6 +106,26 @@ func WithPreserveWhiteouts() Option {
80106 return func (c * config ) { c .whiteouts = whiteoutPreserve }
81107}
82108
109+ // WithTarIndexData enables tar-index mode.
110+ //
111+ // In tar-index mode Apply does not copy file payload bytes into the EROFS
112+ // writer's internal spool. Instead, each regular file's data is written to
113+ // dataFile at a 512-byte-aligned offset that matches the file's position in
114+ // the tar stream, and the corresponding EROFS inode is stored as a
115+ // chunk-index entry referencing dataFile.
116+ //
117+ // The caller must create the [erofs.Writer] with [erofs.WithDataFile](dataFile)
118+ // using the same *os.File so that go-erofs can record the correct chunk
119+ // DeviceID. The resulting EROFS image contains only filesystem metadata and
120+ // chunk indexes; raw tar content is appended to dataFile by the caller after
121+ // Apply returns to produce the final combined blob.
122+ //
123+ // Block size: the Writer's block size must be 512 (tar's natural granularity).
124+ // Use [erofs.WithBlockSize](512) when calling [erofs.Create].
125+ func WithTarIndexData (dataFile * os.File ) Option {
126+ return func (c * config ) { c .tarIndexData = dataFile }
127+ }
128+
83129// pendingLink records a hard link whose target had not yet appeared when the
84130// link entry was processed.
85131type pendingLink struct {
@@ -121,6 +167,14 @@ func Apply(w *erofs.Writer, r io.Reader, opts ...Option) error {
121167 o (& cfg )
122168 }
123169
170+ // In tar-index mode we wrap r with a counter so we can determine
171+ // each file's data start offset within the tar stream.
172+ var cr * countingReader
173+ if cfg .tarIndexData != nil {
174+ cr = & countingReader {r : r }
175+ r = cr
176+ }
177+
124178 tr := archivetar .NewReader (r )
125179
126180 // pending records hard links whose targets haven't appeared yet.
@@ -208,8 +262,18 @@ func Apply(w *erofs.Writer, r io.Reader, opts ...Option) error {
208262 case archivetar .TypeReg , archivetar .TypeRegA : //nolint:staticcheck
209263 // Remove any existing entry to handle tar overwrite semantics.
210264 removeExisting (w , p )
211- if err := addFile (w , p , hdr , tr ); err != nil {
212- return fmt .Errorf ("tarconv: %s: %w" , p , err )
265+ if cfg .tarIndexData != nil {
266+ // Tar-index mode: record chunk indexes into the data file.
267+ // cr.n is the byte position *after* the tar header; that is
268+ // exactly where this file's data starts in the stream.
269+ dataOffset := cr .n
270+ if err := addFileTarIndex (w , p , hdr , tr , dataOffset ); err != nil {
271+ return fmt .Errorf ("tarconv: %s: %w" , p , err )
272+ }
273+ } else {
274+ if err := addFile (w , p , hdr , tr ); err != nil {
275+ return fmt .Errorf ("tarconv: %s: %w" , p , err )
276+ }
213277 }
214278 pending = replayPending (w , pending )
215279
@@ -532,3 +596,54 @@ func cleanTarPath(name string) string {
532596func mkdev (major , minor int64 ) uint32 {
533597 return uint32 ((major << 8 ) | (minor & 0xff ) | ((minor & ^ int64 (0xff )) << 12 ))
534598}
599+
600+ // addFileTarIndex adds a regular file in tar-index mode.
601+ //
602+ // The file's payload bytes are consumed from tr and discarded (the EROFS
603+ // Writer, created with WithDataFile, will record chunk indexes based on
604+ // dataOffset and hdr.Size). We create the EROFS File, write the bytes
605+ // through it so that the Writer tracks the data-file position correctly,
606+ // and then apply metadata.
607+ //
608+ // dataOffset is the byte position of this file's data within the underlying
609+ // tar stream (i.e. the value of countingReader.n immediately after the
610+ // archive/tar package has read the header for this entry).
611+ func addFileTarIndex (w * erofs.Writer , p string , hdr * archivetar.Header , tr * archivetar.Reader , _ int64 ) error {
612+ // Create the EROFS file entry. With the Writer in WithDataFile mode,
613+ // f.Write() forwards data to the external data file and closeDataFile()
614+ // records the corresponding chunk indexes.
615+ f , err := w .Create (p )
616+ if err != nil {
617+ return err
618+ }
619+ // Copy file data: in WithDataFile mode this writes to the external data
620+ // file and advances the Writer's dataOff counter.
621+ if _ , err := io .Copy (f , tr ); err != nil {
622+ _ = f .Close ()
623+ return fmt .Errorf ("copy data (tar-index): %w" , err )
624+ }
625+ if err := f .Chmod (tarModeToGoMode (hdr .Mode )); err != nil {
626+ _ = f .Close ()
627+ return err
628+ }
629+ if err := f .Chown (hdr .Uid , hdr .Gid ); err != nil {
630+ _ = f .Close ()
631+ return err
632+ }
633+ if err := f .Close (); err != nil {
634+ return err
635+ }
636+ return applyMetadata (w , p , hdr )
637+ }
638+
639+ // countingReader wraps an io.Reader and counts bytes read.
640+ type countingReader struct {
641+ r io.Reader
642+ n int64 // total bytes read so far
643+ }
644+
645+ func (c * countingReader ) Read (p []byte ) (int , error ) {
646+ n , err := c .r .Read (p )
647+ c .n += int64 (n )
648+ return n , err
649+ }
0 commit comments