Skip to content

Commit ced78eb

Browse files
committed
tarconv: add WithTarIndexData option for EROFS chunk-index mode
WithTarIndexData enables tar-index mode in Apply. When set, file payload bytes are written to an external data file (via go-erofs WithDataFile) at 512-byte-block-aligned positions, and the EROFS writer records chunk-index entries pointing into that file rather than copying bytes into its internal spool. The resulting EROFS image is metadata-only: filesystem structure, inode table, and chunk-index table. Combining it with the data file produces a blob where a kernel consumer can locate file content directly via DeviceID=1 chunk references. This replaces the mkfs.erofs --tar=i --aufs subprocess invocation with a pure-Go implementation. Block size is set to 512 to match tar's natural granularity (each header block is 512 bytes). Adds four tests: - TestWithTarIndexDataBasic: round-trips a file through the combined blob - TestWithTarIndexDataFileListing: compares file listing with full extraction - TestWithTarIndexDataWhiteouts: verifies OCI whiteout translation - TestWithTarIndexDataXattrs: reads back multi-block payloads whose inodes also carry xattrs, which offsets the chunk-index map within the inode Signed-off-by: Derek McGowan <derek@mcg.dev>
1 parent ce359e5 commit ced78eb

2 files changed

Lines changed: 526 additions & 3 deletions

File tree

tarconv/apply.go

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
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
//
@@ -11,6 +27,14 @@
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.
1438
package tarconv
1539

1640
import (
@@ -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.
5681
type 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.
85131
type 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 {
532596
func 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

Comments
 (0)