build(deps): bump github.com/erofs/go-erofs from 0.2.1 to 0.3.0

Bumps [github.com/erofs/go-erofs](https://github.com/erofs/go-erofs) from 0.2.1 to 0.3.0.
- [Release notes](https://github.com/erofs/go-erofs/releases)
- [Commits](https://github.com/erofs/go-erofs/compare/v0.2.1...v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/erofs/go-erofs
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2026-04-21 00:17:32 +00:00
committed by GitHub
parent 5fa03e6bbb
commit 62e835ecb9
16 changed files with 3384 additions and 187 deletions

2
go.mod
View File

@@ -37,7 +37,7 @@ require (
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
github.com/docker/go-metrics v0.0.1
github.com/docker/go-units v0.5.0
github.com/erofs/go-erofs v0.2.1
github.com/erofs/go-erofs v0.3.0
github.com/fsnotify/fsnotify v1.9.0
github.com/google/certtostore v1.0.6
github.com/google/go-cmp v0.7.0

4
go.sum
View File

@@ -107,8 +107,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erofs/go-erofs v0.2.1 h1:6tFEewfzPTAVrLmNR16hdzQJGH62an9m75gJTbnGEPw=
github.com/erofs/go-erofs v0.2.1/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs=
github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I=
github.com/erofs/go-erofs v0.3.0/go.mod h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=

View File

@@ -1,56 +1,82 @@
# go-erofs
A Go library for opening erofs files as a Go stdlib [fs.FS](https://pkg.go.dev/io/fs#FS).
A Go library for reading and creating [EROFS](https://erofs.docs.kernel.org/) filesystem images using the standard [fs.FS](https://pkg.go.dev/io/fs#FS) interface.
## Scope
## Features
This library is designed to allow erofs files to be usable in any Go operation that uses
the standard filesystem interface. This could be useful for accessing an erofs file just
as you would a plain directory without needing to unpack. In the future this library
could provide an interface to create erofs files as well.
- **Read** EROFS images through Go's `fs.FS` interface
- **Create** EROFS images from directories or any `fs.FS`
- **Merge** multiple filesystem sources with overlay whiteout support
- **Metadata-only** mode for container layer indexing (chunk-based references to original data)
- Pure Go, no CGO — uses only the standard library
## Current state
### Status
- [x] Read erofs files created with default `mkfs.erofs` options
- [x] Read chunk-based erofs files (without indexes)
- [x] Xattr support
- [x] Long xattr prefix support
- [x] Extra devices for chunked data and chunk indexes
- [x] Read chunk-based erofs files with indexes
- [x] Xattr support including long xattr prefixes
- [x] Extra devices for chunked data
- [x] Create erofs files from any `fs.FS`
- [x] Directory to erofs packing
- [x] AUFS whiteout to overlayfs conversion
- [x] Merge multiple filesystem layers with whiteout processing
- [ ] Read erofs files with compression
- [ ] Creating erofs files
- [ ] Tar to erofs conversion
## Example use
## Reading an EROFS image
Print out all the files in an erofs file
```
package main
import (
"fmt"
"io/fs"
"log"
"os"
"github.com/erofs/go-erofs"
)
func main() {
f, err := os.Open("testdata/basic-default.erofs")
if err != nil {
log.Fatal(err)
}
defer f.Close()
img, err := erofs.EroFS(f)
if err != nil {
log.Fatal(err)
}
fs.WalkDir(img, "/", func(path string, entry fs.DirEntry, err error) error {
fmt.Println(path)
return nil
})
```go
f, err := os.Open("image.erofs")
if err != nil {
log.Fatal(err)
}
defer f.Close()
img, err := erofs.Open(f)
if err != nil {
log.Fatal(err)
}
fs.WalkDir(img, ".", func(path string, d fs.DirEntry, err error) error {
fmt.Println(path)
return nil
})
```
## Merging multiple layers
Combine multiple filesystem sources into one image. The `Merge` option enables overlay semantics — AUFS-style whiteout files (`.wh.<name>`) delete entries from prior layers:
```go
outFile, _ := os.Create("merged.erofs")
w := erofs.Create(outFile)
w.CopyFrom(baseLayer)
w.CopyFrom(overlayLayer, erofs.Merge())
w.Close()
```
Merge can also be combined with `MetadataOnly` to build a merged index without copying data:
```go
w := erofs.Create(outFile)
w.CopyFrom(layer1, erofs.MetadataOnly())
w.CopyFrom(layer2, erofs.MetadataOnly(), erofs.Merge())
w.Close()
```
## Building an image programmatically
```go
outFile, _ := os.Create("image.erofs")
w := erofs.Create(outFile)
f, _ := w.Create("/hello.txt")
f.Write([]byte("hello world\n"))
f.Close()
w.Mkdir("/dir", 0o755)
w.Symlink("hello.txt", "/link")
w.Close()
outFile.Close()
```

View File

@@ -1,3 +1,28 @@
// Package erofs reads and creates EROFS filesystem images.
//
// # Reading
//
// Use [Open] to read an existing EROFS image through Go's standard [fs.FS]
// interface:
//
// img, err := erofs.Open(f)
// data, err := fs.ReadFile(img, "etc/hostname")
//
// # Writing
//
// Use [Create] to build a new EROFS image. Entries can be added one at a
// time, or bulk-copied from any [fs.FS] via [Writer.CopyFrom]:
//
// w := erofs.Create(outFile)
// w.CopyFrom(srcFS)
// w.Close()
//
// For metadata-only images that reference data in an external source
// (e.g. for container layer indexing), pass [MetadataOnly] to CopyFrom:
//
// w := erofs.Create(outFile)
// w.CopyFrom(srcFS, erofs.MetadataOnly())
// w.Close()
package erofs
import (
@@ -46,13 +71,24 @@ var (
ErrLoop = fmt.Errorf("too many symlinks: %w", ErrInvalid)
)
// Stat is the erofs specific stat data returned by Stat and FileInfo requests
// Stat is the raw erofs stat data returned by Sys() on [fs.FileInfo] values.
// It is a plain data struct analogous to [syscall.Stat_t].
//
// For cross-platform fs.FS compatibility, callers should prefer
// type-asserting the [fs.FileInfo] to accessor interfaces rather
// than inspecting Stat fields directly. The returned [fs.FileInfo]
// implements the following single-method interfaces:
//
// Ownership: UID() uint32, GID() uint32
// InodeInfo: Ino() uint64, Nlink() uint64
// DeviceInfo: Rdev() uint64
// Xattrs: GetAllXattr() map[string]string, GetXattr(string) (string, bool)
type Stat struct {
Mode fs.FileMode
Size int64
InodeLayout uint8
Rdev uint32
Inode int64
Ino int64
UID uint32
GID uint32
Mtime uint64
@@ -240,6 +276,108 @@ func (img *image) mapDev(deviceID uint16, pa int64) (io.ReaderAt, int64, error)
return img.meta, pa, nil
}
// blockSize returns the filesystem block size.
func (img *image) blockSize() uint32 { return 1 << img.sb.BlkSizeBits }
// buildTime returns the build timestamp from the superblock.
func (img *image) buildTime() uint64 { return img.sb.BuildTime }
// deviceBlocks returns the total block count across all extra devices.
// Each device's block count is reported at the device's native block size
// (matching the superblock block size).
func (img *image) deviceBlocks() []uint64 {
if len(img.devices) == 0 {
return nil
}
blocks := make([]uint64, len(img.devices))
for i, d := range img.devices {
blocks[i] = uint64(d.blocks)
}
return blocks
}
// openDirect returns an io.Reader for a file's data that reads directly
// from the underlying metadata reader, bypassing the block-at-a-time
// Read path. Returns nil if direct reading is not possible (e.g.
// chunk-based or compressed files).
func (img *image) openDirect(ino *inode) io.Reader {
if ino.size <= 0 {
return nil
}
blockSize := int64(1 << img.sb.BlkSizeBits)
switch ino.inodeLayout {
case disk.LayoutFlatPlain:
// Data is contiguous starting at dataBlkAddr.
dataOffset := int64(ino.inodeData) << img.sb.BlkSizeBits
return io.NewSectionReader(img.meta, dataOffset, ino.size)
case disk.LayoutFlatInline:
// Last block is inline after the inode; earlier blocks at dataBlkAddr.
// Only use direct read for single-block files (all data inline).
if ino.size > blockSize {
return nil
}
inodeAddr := img.metaStartPos() + int64(ino.nid)*disk.SizeInodeCompact
trailingAddr := inodeAddr + ino.flatDataOffset()
return io.NewSectionReader(img.meta, trailingAddr, ino.size)
case disk.LayoutChunkBased:
// Chunk-based files store data at the physical block addresses
// listed in the chunk index. For contiguous single-device files,
// the data is laid out consecutively and can be read directly.
chunkFmt := uint16(ino.inodeData)
if chunkFmt&disk.LayoutChunkFormatIndexes == 0 {
return nil
}
chunkBits := img.sb.BlkSizeBits + uint8(chunkFmt&disk.LayoutChunkFormatBits)
nchunks := int((ino.size-1)>>chunkBits) + 1
// Read chunk index entries to check contiguity.
inodeStart := img.metaStartPos() + int64(ino.nid)*disk.SizeInodeCompact
baseOffset := inodeStart + ino.flatDataOffset()
if baseOffset%8 != 0 {
baseOffset = (baseOffset + 7) & ^int64(7)
}
needed := int64(nchunks * disk.SizeChunkIndex)
idxBuf := make([]byte, needed)
if _, err := img.meta.ReadAt(idxBuf, baseOffset); err != nil {
return nil
}
// Check that all chunks are contiguous on the same device.
var startBlock uint64
var deviceID uint16
for i := range nchunks {
off := i * disk.SizeChunkIndex
blkLo := binary.LittleEndian.Uint32(idxBuf[off+4 : off+8])
if ^blkLo == 0 {
return nil // hole
}
blkHi := binary.LittleEndian.Uint16(idxBuf[off : off+2])
did := binary.LittleEndian.Uint16(idxBuf[off+2:off+4]) & img.deviceIDMask
phys := (uint64(blkHi) << 32) | uint64(blkLo)
blocksPerChunk := uint64(1 << (chunkBits - img.sb.BlkSizeBits))
if i == 0 {
startBlock = phys
deviceID = did
} else {
expected := startBlock + uint64(i)*blocksPerChunk
if phys != expected || did != deviceID {
return nil // not contiguous or different device
}
}
}
// All chunks contiguous — resolve through the device.
dataOffset := int64(startBlock) << img.sb.BlkSizeBits
if deviceID > 0 && int(deviceID) <= len(img.devices) {
return io.NewSectionReader(img.devices[deviceID-1].device, dataOffset, ino.size)
}
return io.NewSectionReader(img.meta, dataOffset, ino.size)
default:
return nil
}
}
func (img *image) readMetadata(r io.Reader) ([]byte, error) {
// - A 2-byte little-endian length field, which is aligned to a 4-byte boundary
// - The length bytes of payload data
@@ -302,7 +440,7 @@ func (img *image) loadLongPrefixes() error {
}
// Read inode info to determine size and layout
fi, err := f.readInfo(false)
fi, err := f.readInfo()
if err != nil {
img.prefixesErr = fmt.Errorf("failed to read packed inode: %w", err)
return
@@ -375,7 +513,7 @@ func (img *image) loadAt(addr, size int64) (*block, error) {
}
// loadBlock loads the block with the given data
func (img *image) loadBlock(fi *fileInfo, pos int64) (*block, error) {
func (img *image) loadBlock(fi *inode, pos int64) (*block, error) {
nblocks := calculateBlocks(img.sb.BlkSizeBits, fi.size)
bn := int(pos >> int(img.sb.BlkSizeBits))
if bn >= nblocks {
@@ -400,15 +538,18 @@ func (img *image) loadBlock(fi *fileInfo, pos int64) (*block, error) {
// Move to the data offset from the start of the inode
addr += fi.flatDataOffset()
// Get the ooffset from the start of the block
// Get the offset from the start of the block
blockOffset = int(addr & int64(blockSize-1))
// Move addr to start of block
addr = (addr & ^int64(blockSize-1))
// Compute end of inline data within the block (before adjusting
// blockOffset for the read position).
blockEnd = int(fi.size-int64(bn*blockSize)) + blockOffset
// Move the offset within the block based on position within file
blockOffset += int(pos - int64(bn<<int(img.sb.BlkSizeBits)))
blockEnd = int(fi.size-int64(bn*blockSize)) + blockOffset
// Ensure the last block is not exceeded
if blockEnd > blockSize {
@@ -502,6 +643,9 @@ func (img *image) loadBlock(fi *fileInfo, pos int64) (*block, error) {
}
addr = mappedAddr
if blockOffset < 0 || blockEnd > blockSize || blockOffset >= blockEnd {
return nil, fmt.Errorf("invalid chunk block bounds [%d:%d] for nid %d: %w", blockOffset, blockEnd, fi.nid, ErrInvalid)
}
b := img.getBlock()
if n, err := reader.ReadAt(b.buf[blockOffset:blockEnd], addr+int64(blockOffset)); err != nil {
img.putBlock(b)
@@ -518,8 +662,8 @@ func (img *image) loadBlock(fi *fileInfo, pos int64) (*block, error) {
default:
return nil, fmt.Errorf("inode layout (%d) for %d: %w", fi.inodeLayout, fi.nid, ErrInvalid)
}
if blockOffset >= blockEnd {
return nil, fmt.Errorf("no remaining items in block: %w", io.EOF)
if blockOffset < 0 || blockEnd > blockSize || blockOffset >= blockEnd {
return nil, fmt.Errorf("invalid block bounds [%d:%d] for nid %d: %w", blockOffset, blockEnd, fi.nid, ErrInvalid)
}
b := img.getBlock()
@@ -554,7 +698,7 @@ const maxSymlinkSize = 4096
// readLink reads the symlink target for the given nid.
func (i *image) readLink(nid uint64, name string) (string, error) {
f := &file{img: i, name: name, nid: nid, ftype: fs.ModeSymlink}
fi, err := f.readInfo(false)
fi, err := f.readInfo()
if err != nil {
return "", err
}
@@ -692,7 +836,7 @@ func (i *image) Stat(name string) (fs.FileInfo, error) {
return nil, err
}
f := &file{img: i, name: basename, nid: nid, ftype: ftype}
return f.readInfo(true)
return f.statInfo()
}
// ReadFile reads the named file and returns its contents.
@@ -707,7 +851,7 @@ func (i *image) ReadFile(name string) ([]byte, error) {
return nil, &fs.PathError{Op: "read", Path: name, Err: ErrIsDirectory}
}
f := &file{img: i, name: basename, nid: nid, ftype: ftype}
fi, err := f.readInfo(false)
fi, err := f.readInfo()
if err != nil {
return nil, err
}
@@ -759,7 +903,7 @@ func (i *image) Lstat(name string) (fs.FileInfo, error) {
return nil, err
}
f := &file{img: i, name: basename, nid: nid, ftype: ftype}
return f.readInfo(true)
return f.statInfo()
}
type file struct {
@@ -769,11 +913,11 @@ type file struct {
ftype fs.FileMode
// Mutable fields, open file should not be accessed concurrently
offset int64 // current offset for read operations
info *fileInfo // cached fileInfo
offset int64 // current offset for read operations
info *inode // cached inode
}
func (b *file) readInfo(infoOnly bool) (fi *fileInfo, err error) {
func (b *file) readInfo() (ino *inode, err error) {
if b.info != nil {
return b.info, nil
}
@@ -801,80 +945,60 @@ func (b *file) readInfo(infoOnly bool) (fi *fileInfo, err error) {
}()
ino := blk.bytes()
_, err = b.img.meta.ReadAt(ino, addr)
buf := blk.bytes()
_, err = b.img.meta.ReadAt(buf, addr)
if err != nil {
return nil, err
}
var format, xcnt uint16
if _, err = binary.Decode(ino[:2], binary.LittleEndian, &format); err != nil {
if _, err = binary.Decode(buf[:2], binary.LittleEndian, &format); err != nil {
return nil, err
}
layout := uint8((format & 0x0E) >> 1)
if format&0x01 == 0 {
var inode disk.InodeCompact
if _, err := binary.Decode(ino[:disk.SizeInodeCompact], binary.LittleEndian, &inode); err != nil {
var di disk.InodeCompact
if _, err := binary.Decode(buf[:disk.SizeInodeCompact], binary.LittleEndian, &di); err != nil {
return nil, err
}
b.info = &fileInfo{
b.info = &inode{
name: b.name,
nid: b.nid,
icsize: disk.SizeInodeCompact,
inodeLayout: layout,
inodeData: inode.InodeData,
size: int64(inode.Size),
mode: (fs.FileMode(inode.Mode) & ^fs.ModeType) | b.ftype,
modTime: time.Unix(int64(b.img.sb.BuildTime), int64(b.img.sb.BuildTimeNs)),
inodeData: di.InodeData,
size: int64(di.Size),
mode: (fs.FileMode(di.Mode) & ^fs.ModeType) | b.ftype,
rawMode: di.Mode,
uid: uint32(di.UID),
gid: uint32(di.GID),
nlink: int(di.Nlink),
mtime: b.img.sb.BuildTime,
mtimeNs: b.img.sb.BuildTimeNs,
}
xcnt = inode.XattrCount
if infoOnly {
b.info.stat = &Stat{
Mode: disk.EroFSModeToGoFileMode(inode.Mode),
Size: int64(inode.Size),
InodeLayout: layout,
Inode: int64(b.nid),
Rdev: disk.RdevFromMode(inode.Mode, inode.InodeData),
UID: uint32(inode.UID),
GID: uint32(inode.GID),
Nlink: int(inode.Nlink),
Mtime: b.img.sb.BuildTime,
MtimeNs: b.img.sb.BuildTimeNs,
}
}
addr += disk.SizeInodeCompact
xcnt = di.XattrCount
} else {
var inode disk.InodeExtended
if _, err = binary.Decode(ino[:disk.SizeInodeExtended], binary.LittleEndian, &inode); err != nil {
var di disk.InodeExtended
if _, err = binary.Decode(buf[:disk.SizeInodeExtended], binary.LittleEndian, &di); err != nil {
return nil, err
}
b.info = &fileInfo{
b.info = &inode{
name: b.name,
nid: b.nid,
icsize: disk.SizeInodeExtended,
inodeLayout: layout,
inodeData: inode.InodeData,
size: int64(inode.Size),
mode: (fs.FileMode(inode.Mode) & ^fs.ModeType) | b.ftype,
modTime: time.Unix(int64(inode.Mtime), int64(inode.MtimeNs)),
inodeData: di.InodeData,
size: int64(di.Size),
mode: (fs.FileMode(di.Mode) & ^fs.ModeType) | b.ftype,
rawMode: di.Mode,
uid: di.UID,
gid: di.GID,
nlink: int(di.Nlink),
mtime: di.Mtime,
mtimeNs: di.MtimeNs,
}
xcnt = inode.XattrCount
if infoOnly {
b.info.stat = &Stat{
Mode: disk.EroFSModeToGoFileMode(inode.Mode),
Size: int64(inode.Size),
InodeLayout: layout,
Inode: int64(b.nid),
Rdev: disk.RdevFromMode(inode.Mode, inode.InodeData),
UID: inode.UID,
GID: inode.GID,
Nlink: int(inode.Nlink),
Mtime: inode.Mtime,
MtimeNs: inode.MtimeNs,
}
}
addr += disk.SizeInodeExtended
xcnt = di.XattrCount
}
if xcnt > 0 {
@@ -882,11 +1006,7 @@ func (b *file) readInfo(infoOnly bool) (fi *fileInfo, err error) {
}
switch {
case infoOnly && b.info.xsize > 0:
if err = setXattrs(b, addr, blk); err != nil {
return nil, err
}
case infoOnly || b.info.inodeLayout == disk.LayoutFlatPlain || b.info.size == 0 || blk.end != blkSize:
case b.info.inodeLayout == disk.LayoutFlatPlain || b.info.size == 0 || blk.end != blkSize:
b.img.putBlock(blk)
default:
// If the inode has trailing data used later, cache it
@@ -895,12 +1015,52 @@ func (b *file) readInfo(infoOnly bool) (fi *fileInfo, err error) {
return b.info, nil
}
// statInfo reads the inode and builds a fileInfo with full stat data
// including extended attributes. The cached block is released since
// stat callers do not need inline data.
func (b *file) statInfo() (*fileInfo, error) {
ino, err := b.readInfo()
if err != nil {
return nil, err
}
fi := &fileInfo{
name: ino.name,
size: ino.size,
mode: ino.mode,
mtime: ino.mtime,
mtimeNs: ino.mtimeNs,
stat: &Stat{
Mode: disk.EroFSModeToGoFileMode(ino.rawMode),
Size: ino.size,
InodeLayout: ino.inodeLayout,
Ino: int64(ino.nid),
Rdev: disk.RdevFromMode(ino.rawMode, ino.inodeData),
UID: ino.uid,
GID: ino.gid,
Nlink: ino.nlink,
Mtime: ino.mtime,
MtimeNs: ino.mtimeNs,
},
}
if ino.xsize > 0 {
if err := loadXattrs(b, fi.stat); err != nil {
return nil, err
}
}
// Release cached block - stat callers don't need inline data
if ino.cached != nil {
b.img.putBlock(ino.cached)
ino.cached = nil
}
return fi, nil
}
func (b *file) Stat() (fs.FileInfo, error) {
return b.readInfo(true)
return b.statInfo()
}
func (b *file) Read(p []byte) (int, error) {
fi, err := b.readInfo(false)
fi, err := b.readInfo()
if err != nil {
return 0, err
}
@@ -954,7 +1114,7 @@ func (d *direntry) Type() fs.FileMode {
}
func (d *direntry) Info() (fs.FileInfo, error) {
return d.readInfo(true)
return d.statInfo()
}
type dir struct {
@@ -968,7 +1128,7 @@ type dir struct {
}
func (d *dir) ReadDir(n int) ([]fs.DirEntry, error) {
fi, err := d.readInfo(false)
fi, err := d.readInfo()
if err != nil {
return nil, fmt.Errorf("readInfo failed: %w", err)
}
@@ -1038,7 +1198,13 @@ func (d *dir) ReadDir(n int) ([]fs.DirEntry, error) {
d.img.putBlock(b)
return ents, fmt.Errorf("invalid dirent name offset %d (buf size %d): %w", dirents[0].NameOff, bufLen, ErrInvalid)
}
name = string(buf[dirents[0].NameOff:])
// The last entry name extends to end of block;
// trim any NUL padding.
raw := buf[dirents[0].NameOff:]
if j := bytes.IndexByte(raw, 0); j >= 0 {
raw = raw[:j]
}
name = string(raw)
}
if i >= d.consumed && name != "." && name != ".." {
@@ -1086,7 +1252,7 @@ func (d *dir) ReadDir(n int) ([]fs.DirEntry, error) {
// intra-block binary search finds the entry.
// Returns the nid and file type if found, or fs.ErrNotExist if not.
func (d *dir) lookup(target string) (uint64, fs.FileMode, error) {
fi, err := d.readInfo(false)
fi, err := d.readInfo()
if err != nil {
return 0, 0, fmt.Errorf("readInfo failed: %w", err)
}
@@ -1257,7 +1423,9 @@ func lookupBlock(buf, target []byte) (uint64, fs.FileMode, error) {
return 0, 0, fs.ErrNotExist
}
type fileInfo struct {
// inode holds the parsed on-disk inode data needed for I/O operations.
// It is an internal type and is not returned to callers directly.
type inode struct {
name string
nid uint64
icsize int8
@@ -1266,38 +1434,53 @@ type fileInfo struct {
inodeData uint32
size int64
mode fs.FileMode
modTime time.Time
stat *Stat
rawMode uint16
uid uint32
gid uint32
nlink int
mtime uint64
mtimeNs uint32
cached *block
}
func (fi *fileInfo) Name() string {
return fi.name
}
func (fi *fileInfo) Size() int64 {
return fi.size
}
func (fi *fileInfo) Mode() fs.FileMode {
return fi.mode
}
func (fi *fileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi *fileInfo) IsDir() bool {
return fi.mode.IsDir()
}
func (fi *fileInfo) Sys() any {
// Return erofs stat object with extra fields and call for xattrs
return fi.stat
}
func (fi *fileInfo) flatDataOffset() int64 {
func (ino *inode) flatDataOffset() int64 {
// inode core size + xattr size
return int64(fi.icsize) + int64(fi.xsize)
return int64(ino.icsize) + int64(ino.xsize)
}
// fileInfo implements [fs.FileInfo] and provides extended metadata
// via type-assertable accessor methods. Callers can extract
// Unix-style metadata without importing this package:
//
// if u, ok := fi.(interface{ UID() uint32 }); ok { uid = u.UID() }
type fileInfo struct {
name string
size int64
mode fs.FileMode
mtime uint64
mtimeNs uint32
stat *Stat
}
func (fi *fileInfo) Name() string { return fi.name }
func (fi *fileInfo) Size() int64 { return fi.size }
func (fi *fileInfo) Mode() fs.FileMode { return fi.mode }
func (fi *fileInfo) IsDir() bool { return fi.mode.IsDir() }
func (fi *fileInfo) Sys() any { return fi.stat }
func (fi *fileInfo) ModTime() time.Time { return time.Unix(int64(fi.mtime), int64(fi.mtimeNs)) }
func (fi *fileInfo) UID() uint32 { return fi.stat.UID }
func (fi *fileInfo) GID() uint32 { return fi.stat.GID }
func (fi *fileInfo) Ino() uint64 { return uint64(fi.stat.Ino) }
func (fi *fileInfo) Nlink() uint64 { return uint64(fi.stat.Nlink) }
func (fi *fileInfo) Rdev() uint64 { return uint64(fi.stat.Rdev) }
// GetAllXattr returns all extended attributes.
func (fi *fileInfo) GetAllXattr() map[string]string { return fi.stat.Xattrs }
// GetXattr returns the value of a single extended attribute.
func (fi *fileInfo) GetXattr(name string) (string, bool) {
v, ok := fi.stat.Xattrs[name]
return v, ok
}
func decodeSuperBlock(b [disk.SizeSuperBlock]byte, sb *disk.SuperBlock) error {
n, err := binary.Decode(b[:], binary.LittleEndian, sb)

137
vendor/github.com/erofs/go-erofs/format.go generated vendored Normal file
View File

@@ -0,0 +1,137 @@
package erofs
import (
"io/fs"
"sort"
"strings"
"github.com/erofs/go-erofs/internal/disk"
)
// Standard xattr name prefix table (index → on-disk NameIndex).
var xattrPrefixes = [...]struct {
index uint8
prefix string
}{
{1, "user."},
{2, "system.posix_acl_access."},
{3, "system.posix_acl_default."},
{4, "trusted."},
{5, "lustre."},
{6, "security."},
}
// xattrSplit splits a full xattr name into (NameIndex, suffix).
func xattrSplit(name string) (uint8, string) {
for _, p := range xattrPrefixes {
if strings.HasPrefix(name, p.prefix) {
return p.index, name[len(p.prefix):]
}
}
return 0, name
}
// xattrEntrySize returns the on-disk size of a single xattr entry, padded to 4 bytes.
func xattrEntrySize(name, value string) int {
_, suffix := xattrSplit(name)
sz := disk.SizeXattrEntry + len(suffix) + len(value)
if sz%4 != 0 {
sz = (sz + 3) & ^3
}
return sz
}
// calcXattrSize returns the total xattr area size (header + entries), or 0.
func calcXattrSize(e *erofsEntry) int {
if len(e.xattrs) == 0 {
return 0
}
entriesSize := 0
for name, value := range e.xattrs {
entriesSize += xattrEntrySize(name, value)
}
return disk.SizeXattrBodyHeader + entriesSize
}
// xattrCount encodes the xattr area size into the inode XattrCount field.
func xattrCount(xattrSize int) uint16 {
if xattrSize == 0 {
return 0
}
return uint16((xattrSize-disk.SizeXattrBodyHeader)/disk.SizeXattrEntry) + 1
}
// sortedXattrKeys returns xattr keys in deterministic order.
func sortedXattrKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// inodeFormat builds the Format field: bit 0 = extended, bits 1-3 = layout.
func inodeFormat(layout uint8, compact bool) uint16 {
f := uint16(layout) << 1
if !compact {
f |= 1 // bit 0 = extended
}
return f
}
// goModeToUnixMode converts Go fs.FileMode to Unix mode bits.
func goModeToUnixMode(m fs.FileMode) uint16 {
mode := uint16(m.Perm())
if m&fs.ModeSetuid != 0 {
mode |= disk.StatTypeIsUID
}
if m&fs.ModeSetgid != 0 {
mode |= disk.StatTypeIsGID
}
if m&fs.ModeSticky != 0 {
mode |= disk.StatTypeIsVTX
}
switch m.Type() {
case 0: // regular file
mode |= disk.StatTypeReg
case fs.ModeDir:
mode |= disk.StatTypeDir
case fs.ModeSymlink:
mode |= disk.StatTypeSymlink
case fs.ModeDevice | fs.ModeCharDevice:
mode |= disk.StatTypeChrdev
case fs.ModeDevice:
mode |= disk.StatTypeBlkdev
case fs.ModeNamedPipe:
mode |= disk.StatTypeFifo
case fs.ModeSocket:
mode |= disk.StatTypeSock
}
return mode
}
// modeToFileType converts Unix mode bits to an EROFS file type.
func modeToFileType(mode uint16) uint8 {
switch mode & disk.StatTypeMask {
case disk.StatTypeReg:
return disk.FileTypeReg
case disk.StatTypeDir:
return disk.FileTypeDir
case disk.StatTypeChrdev:
return disk.FileTypeChrdev
case disk.StatTypeBlkdev:
return disk.FileTypeBlkdev
case disk.StatTypeFifo:
return disk.FileTypeFifo
case disk.StatTypeSock:
return disk.FileTypeSock
case disk.StatTypeSymlink:
return disk.FileTypeSymlink
default:
return 0
}
}

View File

@@ -0,0 +1,27 @@
// Package builder provides shared types for the mkfs sub-packages.
package builder
import "io"
// Entry carries extended metadata for a filesystem entry.
// Mode and Size come from fs.FileInfo; everything else lives here.
type Entry struct {
UID, GID uint32
Mtime uint64
MtimeNs uint32
Nlink uint32
Rdev uint32
Xattrs map[string]string
LinkTarget string
Data io.Reader // file content (full-image mode)
Chunks []Chunk // physical block refs (metadata-only mode)
Contiguous bool // data blocks are contiguous; flat-plain is sufficient
MetadataOnly bool // chunk-based layout even without chunks
}
// Chunk maps a range of logical blocks to physical blocks on a device.
type Chunk struct {
PhysicalBlock uint64 // physical block address
Count uint16 // number of contiguous blocks
DeviceID uint16 // 0 = primary, 1+ = extra device
}

View File

@@ -20,6 +20,7 @@ const (
SizeXattrBodyHeader = 12
SizeXattrEntry = 4
SizeDeviceSlot = 128
SizeChunkIndex = 8
LayoutFlatPlain = 0
LayoutCompressedFull = 1

220
vendor/github.com/erofs/go-erofs/layout.go generated vendored Normal file
View File

@@ -0,0 +1,220 @@
package erofs
import (
"sort"
"github.com/erofs/go-erofs/internal/disk"
)
// planLayout assigns NIDs and determines trailing data sizes for all entries.
func (w *erofsWriter) planLayout(root *erofsEntry) {
// Collect all entries in a deterministic order (DFS, pre-order).
// DFS keeps directory contents close to their parent inode,
// improving locality for operations like find and ls -lR.
w.entries = nil
var walk func(e *erofsEntry)
walk = func(e *erofsEntry) {
w.entries = append(w.entries, e)
if e.mode&disk.StatTypeMask == disk.StatTypeDir {
sort.Slice(e.children, func(i, j int) bool {
return e.children[i].name < e.children[j].name
})
for _, c := range e.children {
walk(c)
}
}
}
walk(root)
w.totalInodes = uint64(len(w.entries))
// Block 0 holds: 1024-byte pad + 128-byte superblock + device slot(s) + padding
// MetaBlkAddr is set later by write() depending on the on-disk layout.
// Assign NIDs sequentially.
// NID = byte offset from metaStartPos / 32.
// Each extended inode is 64 bytes = 2 NID slots.
// Trailing data follows and is padded to 32-byte boundary.
currentOff := 0 // byte offset from metaStartPos
for _, e := range w.entries {
e.nid = uint64(currentOff / 32)
e.xattrSize = calcXattrSize(e)
// Decide compact (32B) vs extended (64B) inode.
e.compact = e.uid <= 0xFFFF && e.gid <= 0xFFFF &&
e.nlink <= 0xFFFF && e.size <= 0xFFFFFFFF &&
e.mtime == w.buildTime && e.mtimeNs == 0
inodeSize := disk.SizeInodeExtended
if e.compact {
inodeSize = disk.SizeInodeCompact
}
// The inode header region is inode core + xattr area.
// Trailing data (dirents, chunk indexes, inline data) follows.
headerSize := inodeSize + e.xattrSize
// Determine layout
switch e.mode & disk.StatTypeMask {
case disk.StatTypeReg:
switch {
case e.size == 0 && len(e.chunks) == 0 && e.data == nil && !e.metadataOnly:
e.layout = disk.LayoutFlatPlain
case len(e.chunks) > 0 || e.metadataOnly:
e.layout = disk.LayoutChunkBased
if e.contiguous {
e.chunkBits = w.minChunkBits(e.size)
}
default:
// Full-image mode: decide inline vs plain
if int(e.size) <= w.blockSize-headerSize {
inBlockOff := (currentOff + headerSize) % w.blockSize
if inBlockOff+int(e.size) <= w.blockSize {
e.layout = disk.LayoutFlatInline
} else {
e.layout = disk.LayoutFlatPlain
}
} else {
e.layout = disk.LayoutFlatPlain
}
}
case disk.StatTypeDir:
direntDataSize := w.direntDataSize(e)
inBlockOff := (currentOff + headerSize) % w.blockSize
if direntDataSize > 0 && inBlockOff+direntDataSize <= w.blockSize {
e.layout = disk.LayoutFlatInline
} else {
e.layout = disk.LayoutFlatPlain
}
case disk.StatTypeSymlink:
inBlockOff := (currentOff + headerSize) % w.blockSize
if len(e.symTarget) > 0 && inBlockOff+len(e.symTarget) <= w.blockSize {
e.layout = disk.LayoutFlatInline
} else {
e.layout = disk.LayoutFlatPlain
}
default:
// Device files, fifos, sockets
e.layout = disk.LayoutFlatPlain
}
// Recalculate trailing size now that layout is decided
e.trailingSize = w.calcTrailingSize(e)
totalInodeSize := headerSize + e.trailingSize
// Pad to 32-byte boundary
if totalInodeSize%32 != 0 {
totalInodeSize = (totalInodeSize + 31) & ^31
}
// Check block boundary: inode core must not cross a block boundary
blockOff := currentOff % w.blockSize
if blockOff+inodeSize > w.blockSize {
// Align to next block
currentOff = (currentOff + w.blockSize - 1) & ^(w.blockSize - 1)
e.nid = uint64(currentOff / 32)
}
// Also check that trailing data doesn't cross block boundary for inline layouts
if e.layout == disk.LayoutFlatInline {
blockOff = currentOff % w.blockSize
if blockOff+headerSize+e.trailingSize > w.blockSize {
// Fall back to flat-plain (data would cross block boundary)
e.layout = disk.LayoutFlatPlain
e.trailingSize = w.calcTrailingSize(e)
totalInodeSize = headerSize + e.trailingSize
if totalInodeSize%32 != 0 {
totalInodeSize = (totalInodeSize + 31) & ^31
}
}
}
currentOff += totalInodeSize
}
w.rootNid = root.nid
}
// calcTrailingSize returns the number of bytes following the 64-byte inode.
func (w *erofsWriter) calcTrailingSize(e *erofsEntry) int {
switch e.mode & disk.StatTypeMask {
case disk.StatTypeReg:
if e.layout == disk.LayoutChunkBased {
if e.size == 0 && len(e.chunks) == 0 {
return 0
}
cs := w.entryChunkSize(e)
nchunks := (int(e.size) + cs - 1) / cs
return nchunks * disk.SizeChunkIndex
}
if e.layout == disk.LayoutFlatInline {
return int(e.size)
}
return 0
case disk.StatTypeDir:
if e.layout == disk.LayoutFlatInline {
return w.direntDataSize(e)
}
return 0
case disk.StatTypeSymlink:
if e.layout == disk.LayoutFlatInline {
return len(e.symTarget)
}
return 0
default:
return 0
}
}
// direntNames returns the sorted list of dirent names for a directory,
// including "." and "..". EROFS requires dirents within each block to
// be sorted alphabetically.
func direntNames(e *erofsEntry) []string {
names := make([]string, 0, len(e.children)+2)
names = append(names, ".", "..")
for _, c := range e.children {
names = append(names, c.name)
}
sort.Strings(names)
return names
}
// direntDataSize calculates the serialized EROFS dirent data size for a directory.
// For multi-block directories, this includes inter-block padding.
func (w *erofsWriter) direntDataSize(e *erofsEntry) int {
names := direntNames(e)
nEntries := len(names)
if len(e.children) == 0 {
// Empty dir still needs "." and ".." entries
return 2*disk.SizeDirent + 1 + 2
}
totalSize := 0
i := 0
for i < nEntries {
blockUsed := 0
start := i
nameSize := 0
for j := i; j < nEntries; j++ {
headerSize := (j - start + 1) * disk.SizeDirent
nameSize += len(names[j])
needed := headerSize + nameSize
if needed > w.blockSize {
break
}
blockUsed = needed
i = j + 1
}
if i == start {
blockUsed = disk.SizeDirent + len(names[i])
i++
}
// Pad non-final blocks to block boundary
if i < nEntries && blockUsed%w.blockSize != 0 {
blockUsed = (blockUsed + w.blockSize - 1) & ^(w.blockSize - 1)
}
totalSize += blockUsed
}
return totalSize
}

1349
vendor/github.com/erofs/go-erofs/mkfs.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

26
vendor/github.com/erofs/go-erofs/mkfs_darwin.go generated vendored Normal file
View File

@@ -0,0 +1,26 @@
package erofs
import (
"io/fs"
"syscall"
"github.com/erofs/go-erofs/internal/builder"
)
func entryFromSys(info fs.FileInfo) *builder.Entry {
switch sys := info.Sys().(type) {
case *builder.Entry:
return sys
case *syscall.Stat_t:
return &builder.Entry{
UID: sys.Uid,
GID: sys.Gid,
Mtime: uint64(sys.Mtimespec.Sec),
MtimeNs: uint32(sys.Mtimespec.Nsec),
Nlink: uint32(sys.Nlink),
Rdev: uint32(sys.Rdev),
}
default:
return nil
}
}

500
vendor/github.com/erofs/go-erofs/mkfs_image.go generated vendored Normal file
View File

@@ -0,0 +1,500 @@
package erofs
import (
"encoding/binary"
"fmt"
"io"
"path"
"github.com/erofs/go-erofs/internal/builder"
"github.com/erofs/go-erofs/internal/disk"
)
// newMetaReader returns an at() function backed by an eagerly-read
// metadata buffer plus an on-demand block cache for data blocks
// outside the metadata region.
func newMetaReader(ra io.ReaderAt, metaStart, totalBytes int64, blockSize int) func(int64) []byte {
metaSize := totalBytes - metaStart
if metaSize <= 0 {
return func(int64) []byte { return nil }
}
metaBuf := make([]byte, metaSize)
if n, err := ra.ReadAt(metaBuf, metaStart); err != nil || int64(n) != metaSize {
return func(int64) []byte { return nil }
}
cache := make(map[int64][]byte)
return func(off int64) []byte {
// Fast path: offset in metadata region.
if off >= metaStart {
o := off - metaStart
if o >= int64(len(metaBuf)) {
return nil
}
return metaBuf[o:]
}
// Outside metadata — flat-plain data block. Load on demand.
if off < 0 || off >= totalBytes {
return nil
}
blkAddr := off - off%int64(blockSize)
if cached, ok := cache[blkAddr]; ok {
return cached[off-blkAddr:]
}
sz := int64(blockSize)
if blkAddr+sz > totalBytes {
sz = totalBytes - blkAddr
}
buf := make([]byte, sz)
if n, err := ra.ReadAt(buf, blkAddr); err != nil || int64(n) != sz {
return nil
}
cache[blkAddr] = buf
return buf[off-blkAddr:]
}
}
// imgQEntry is a BFS queue entry for the image metadata walk.
type imgQEntry struct {
nid uint64
path string
}
// copyFromImage is a fast path for CopyFrom when the source is an *image.
// Instead of walking via the fs.FS interface (which does per-inode ReadAt
// syscalls), it reads the entire metadata area into memory and parses
// inodes, directory entries, xattrs, and chunk indexes directly from the
// buffer. This reduces thousands of syscalls to a single ReadAt.
func (fsys *Writer) copyFromImage(img *image) error {
metaStart := img.metaStartPos()
totalBytes := int64(img.sb.Blocks) << img.sb.BlkSizeBits
if totalBytes <= 0 {
return nil
}
blkBits := img.sb.BlkSizeBits
buildTime := img.sb.BuildTime
buildTimeNs := img.sb.BuildTimeNs
blockSize := int(1 << blkBits)
// Get an accessor for image data. Reads the metadata region eagerly
// and loads flat-plain data blocks on demand.
at := newMetaReader(img.meta, metaStart, totalBytes, blockSize)
// Shared xattr block address (if present). The at() function
// will load the block on demand when xattrs are parsed.
var sharedXattrOff int64
if img.sb.XattrBlkAddr > 0 {
sharedXattrOff = int64(img.sb.XattrBlkAddr) << blkBits
}
// Pre-allocate based on inode count from superblock.
inodeCount := int(img.sb.Inos)
if inodeCount == 0 {
inodeCount = 64
}
queue := make([]imgQEntry, 0, inodeCount)
queue = append(queue, imgQEntry{nid: uint64(img.sb.RootNid), path: "/"})
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
// Merge mode: process whiteout markers.
if fsys.copyMerge && cur.path != "/" {
base := path.Base(cur.path)
if len(base) > len(whiteoutPrefix) && base[:len(whiteoutPrefix)] == whiteoutPrefix {
if base == opaqueWhiteout {
fsys.removeChildren(path.Dir(cur.path))
} else {
target := path.Dir(cur.path) + "/" + base[len(whiteoutPrefix):]
if path.Dir(cur.path) == "/" {
target = "/" + base[len(whiteoutPrefix):]
}
fsys.remove(target)
}
continue
}
}
inodeAddr := metaStart + int64(cur.nid*disk.SizeInodeCompact)
buf := at(inodeAddr)
if len(buf) < disk.SizeInodeCompact {
return fmt.Errorf("inode %d out of range", cur.nid)
}
format := binary.LittleEndian.Uint16(buf[:2])
layout := uint8((format & 0x0E) >> 1)
compact := format&0x01 == 0
if compact && len(buf) < disk.SizeInodeCompact {
return fmt.Errorf("compact inode %d out of range", cur.nid)
}
if !compact && len(buf) < disk.SizeInodeExtended {
return fmt.Errorf("extended inode %d out of range", cur.nid)
}
var (
mode uint16
uid uint32
gid uint32
nlink uint32
size uint64
idata uint32
mtime uint64
mtimeNs uint32
xcnt uint16
icSize int
)
if compact {
var ino disk.InodeCompact
if _, err := binary.Decode(buf[:disk.SizeInodeCompact], binary.LittleEndian, &ino); err != nil {
return fmt.Errorf("decode compact inode %d: %w", cur.nid, err)
}
mode = ino.Mode
uid = uint32(ino.UID)
gid = uint32(ino.GID)
nlink = uint32(ino.Nlink)
size = uint64(ino.Size)
idata = ino.InodeData
mtime = buildTime
mtimeNs = buildTimeNs
xcnt = ino.XattrCount
icSize = disk.SizeInodeCompact
} else {
var ino disk.InodeExtended
if _, err := binary.Decode(buf[:disk.SizeInodeExtended], binary.LittleEndian, &ino); err != nil {
return fmt.Errorf("decode extended inode %d: %w", cur.nid, err)
}
mode = ino.Mode
uid = ino.UID
gid = ino.GID
nlink = ino.Nlink
size = ino.Size
idata = ino.InodeData
mtime = ino.Mtime
mtimeNs = ino.MtimeNs
xcnt = ino.XattrCount
icSize = disk.SizeInodeExtended
}
// Parse xattr area.
xattrSize := 0
if xcnt > 0 {
xattrSize = int(xcnt-1)*disk.SizeXattrEntry + disk.SizeXattrBodyHeader
}
var xattrs map[string]string
if xattrSize > 0 {
xattrAddr := inodeAddr + int64(icSize)
xb := at(xattrAddr)
if len(xb) >= xattrSize {
xattrs = parseXattrsFromBuf(xb[:xattrSize], at, sharedXattrOff, img.getLongPrefix)
}
}
trailingAddr := inodeAddr + int64(icSize) + int64(xattrSize)
typ := mode & disk.StatTypeMask
// Build fsEntry directly, bypassing builder.Entry + add() overhead.
fe := &fsEntry{
path: cur.path,
mode: mode,
uid: uid,
gid: gid,
mtime: mtime,
mtimeNs: mtimeNs,
size: size,
xattrs: xattrs,
}
if nlink > 0 {
fe.nlink = nlink
fe.nlinkSet = true
}
fe.fileClosed = true
if fsys.copyMetadataOnly {
fe.metadataOnly = true
}
switch typ {
case disk.StatTypeDir:
dirSize := int(size)
if dirSize > 0 {
var dirData []byte
switch layout {
case disk.LayoutFlatPlain:
dataAddr := int64(idata) << blkBits
d := at(dataAddr)
if d != nil && len(d) >= dirSize {
dirData = d[:dirSize]
} else {
dirData = make([]byte, dirSize)
if _, err := img.meta.ReadAt(dirData, dataAddr); err != nil {
return fmt.Errorf("read dir data for nid %d: %w", cur.nid, err)
}
}
case disk.LayoutFlatInline:
d := at(trailingAddr)
if d != nil && len(d) >= dirSize {
dirData = d[:dirSize]
}
}
if dirData != nil {
fsys.parseDirBlock(dirData, dirSize, blockSize, cur.path, &queue)
}
}
case disk.StatTypeSymlink:
if size > 0 {
var linkData []byte
if layout == disk.LayoutFlatPlain {
linkData = make([]byte, size)
if _, err := img.meta.ReadAt(linkData, int64(idata)<<blkBits); err != nil {
return fmt.Errorf("read symlink data for nid %d: %w", cur.nid, err)
}
} else {
linkData = at(trailingAddr)
}
if linkData != nil && int(size) <= len(linkData) {
fe.linkTarget = string(linkData[:size])
}
}
case disk.StatTypeReg:
if layout == disk.LayoutChunkBased && size > 0 {
chunkFmt := uint16(idata)
if chunkFmt&disk.LayoutChunkFormatIndexes != 0 {
chunkAddr := trailingAddr
if chunkAddr%8 != 0 {
chunkAddr = (chunkAddr + 7) & ^int64(7)
}
fe.chunks = fsys.parseChunks(at(chunkAddr), chunkFmt, size, blkBits, img.deviceIDMask)
fe.contiguous = true
}
}
case disk.StatTypeChrdev, disk.StatTypeBlkdev:
fe.rdev = disk.RdevFromMode(mode, idata)
}
// Remap chunk DeviceIDs for metadata-only sources.
if fsys.copyMetadataOnly && fsys.copyDeviceID > 0 {
offset := fsys.copyDeviceID - 1
for i := range fe.chunks {
fe.chunks[i].DeviceID += offset
}
}
// Register in the tree.
if cur.path == "/" {
// Update root metadata.
fsys.root.mode = fe.mode
fsys.root.uid = fe.uid
fsys.root.gid = fe.gid
fsys.root.mtime = fe.mtime
fsys.root.mtimeNs = fe.mtimeNs
fsys.root.nlink = fe.nlink
fsys.root.nlinkSet = fe.nlinkSet
fsys.root.xattrs = fe.xattrs
} else if existing, ok := fsys.byPath[cur.path]; ok {
// Merge overwrites: preserve tree linkage.
savedParent := existing.parent
savedChildren := existing.children
*existing = *fe
existing.parent = savedParent
existing.children = savedChildren
} else {
fsys.addChild(fe)
}
}
return nil
}
// parseDirBlock extracts directory entries from dirent data and enqueues
// child inodes for BFS traversal.
func (fsys *Writer) parseDirBlock(data []byte, dirSize, blockSize int, parentPath string, queue *[]imgQEntry) {
pos := 0
for pos < dirSize {
blockEnd := pos + blockSize
if blockEnd > dirSize {
blockEnd = dirSize
}
blk := data[pos:blockEnd]
if len(blk) < disk.SizeDirent {
break
}
firstNameOff := binary.LittleEndian.Uint16(blk[8:10])
nEntries := int(firstNameOff / disk.SizeDirent)
if nEntries == 0 || nEntries*disk.SizeDirent > len(blk) {
break
}
for i := 0; i < nEntries; i++ {
off := i * disk.SizeDirent
nid := binary.LittleEndian.Uint64(blk[off : off+8])
nameOff := int(binary.LittleEndian.Uint16(blk[off+8 : off+10]))
var nameEnd int
if i < nEntries-1 {
nameEnd = int(binary.LittleEndian.Uint16(blk[(i+1)*disk.SizeDirent+8 : (i+1)*disk.SizeDirent+10]))
} else {
nameEnd = len(blk)
}
if nameOff >= len(blk) || nameEnd > len(blk) || nameOff >= nameEnd {
continue
}
// Extract name, trimming trailing NUL padding.
nameBytes := blk[nameOff:nameEnd]
for len(nameBytes) > 0 && nameBytes[len(nameBytes)-1] == 0 {
nameBytes = nameBytes[:len(nameBytes)-1]
}
name := string(nameBytes)
if name == "." || name == ".." || name == "" {
continue
}
childPath := parentPath + "/" + name
if parentPath == "/" {
childPath = "/" + name
}
*queue = append(*queue, imgQEntry{nid: nid, path: childPath})
}
pos = blockEnd
}
}
// parseChunks extracts chunk index entries from an in-memory buffer.
func (fsys *Writer) parseChunks(data []byte, chunkFmt uint16, fileSize uint64, blkBits uint8, deviceIDMask uint16) []builder.Chunk {
chunkBits := blkBits + uint8(chunkFmt&disk.LayoutChunkFormatBits)
nchunks := int((fileSize-1)>>chunkBits) + 1
blocksPerChunk := 1 << (chunkBits - blkBits)
// Align to 8 bytes for index entries.
needed := nchunks * disk.SizeChunkIndex
if len(data) < needed {
return nil
}
chunks := make([]builder.Chunk, 0, nchunks)
for i := range nchunks {
off := i * disk.SizeChunkIndex
startBlkLo := binary.LittleEndian.Uint32(data[off+4 : off+8])
if ^startBlkLo == 0 {
continue // null/hole
}
startBlkHi := binary.LittleEndian.Uint16(data[off : off+2])
deviceID := binary.LittleEndian.Uint16(data[off+2:off+4]) & deviceIDMask
physBlock := (uint64(startBlkHi) << 32) | uint64(startBlkLo)
if len(chunks) > 0 {
prev := &chunks[len(chunks)-1]
if prev.DeviceID == deviceID &&
prev.PhysicalBlock+uint64(prev.Count) == physBlock &&
int(prev.Count)+blocksPerChunk <= 65535 {
prev.Count += uint16(blocksPerChunk)
continue
}
}
chunks = append(chunks, builder.Chunk{
PhysicalBlock: physBlock,
Count: uint16(blocksPerChunk),
DeviceID: deviceID,
})
}
return chunks
}
// parseXattrsFromBuf parses xattr entries from an in-memory buffer.
// at provides on-demand access to the shared xattr block at sharedOff.
// longPrefix resolves long xattr prefix indexes (NameIndex with high bit set).
func parseXattrsFromBuf(buf []byte, at func(int64) []byte, sharedOff int64, longPrefix func(uint8) (string, error)) map[string]string {
if len(buf) < disk.SizeXattrBodyHeader {
return nil
}
var xh disk.XattrHeader
if _, err := binary.Decode(buf[:disk.SizeXattrBodyHeader], binary.LittleEndian, &xh); err != nil {
return nil
}
pos := disk.SizeXattrBodyHeader
xattrs := make(map[string]string)
// Resolve shared xattr references.
for i := 0; i < int(xh.SharedCount) && pos+4 <= len(buf); i++ {
idx := binary.LittleEndian.Uint32(buf[pos : pos+4])
pos += 4
if sharedOff == 0 {
continue
}
sharedBlock := at(sharedOff + int64(idx)*4)
if sharedBlock == nil || len(sharedBlock) < disk.SizeXattrEntry {
continue
}
var xe disk.XattrEntry
if _, err := binary.Decode(sharedBlock[:disk.SizeXattrEntry], binary.LittleEndian, &xe); err != nil {
continue
}
entryLen := int(xe.NameLen) + int(xe.ValueLen)
if disk.SizeXattrEntry+entryLen > len(sharedBlock) {
continue
}
sb := sharedBlock[disk.SizeXattrEntry:]
name := xattrName(xe, sb[:xe.NameLen], longPrefix)
value := string(sb[xe.NameLen : int(xe.NameLen)+int(xe.ValueLen)])
xattrs[name] = value
}
// Parse inline xattr entries.
for pos+disk.SizeXattrEntry <= len(buf) {
var xe disk.XattrEntry
if _, err := binary.Decode(buf[pos:pos+disk.SizeXattrEntry], binary.LittleEndian, &xe); err != nil {
break
}
pos += disk.SizeXattrEntry
entryLen := int(xe.NameLen) + int(xe.ValueLen)
if pos+entryLen > len(buf) {
break
}
name := xattrName(xe, buf[pos:pos+int(xe.NameLen)], longPrefix)
pos += int(xe.NameLen)
value := string(buf[pos : pos+int(xe.ValueLen)])
pos += int(xe.ValueLen)
xattrs[name] = value
// Round up to 4-byte boundary.
if rem := pos % 4; rem != 0 {
pos += 4 - rem
}
}
if len(xattrs) == 0 {
return nil
}
return xattrs
}
// xattrName builds the full xattr name from an entry and its raw name bytes.
// longPrefix resolves long prefix indexes when the high bit of NameIndex is set.
func xattrName(xe disk.XattrEntry, rawName []byte, longPrefix func(uint8) (string, error)) string {
var prefix string
if xe.NameIndex&0x80 != 0 {
// Long prefix: high bit set, low 7 bits index the prefix table.
if longPrefix != nil {
if p, err := longPrefix(xe.NameIndex & 0x7F); err == nil {
prefix = p
}
}
} else if xe.NameIndex != 0 {
prefix = xattrIndex(xe.NameIndex).String()
}
return prefix + string(rawName)
}

16
vendor/github.com/erofs/go-erofs/mkfs_other.go generated vendored Normal file
View File

@@ -0,0 +1,16 @@
//go:build !linux && !darwin
package erofs
import (
"io/fs"
"github.com/erofs/go-erofs/internal/builder"
)
func entryFromSys(info fs.FileInfo) *builder.Entry {
if be, ok := info.Sys().(*builder.Entry); ok {
return be
}
return nil
}

30
vendor/github.com/erofs/go-erofs/mkfs_unix.go generated vendored Normal file
View File

@@ -0,0 +1,30 @@
//go:build linux
package erofs
import (
"io/fs"
"syscall"
"github.com/erofs/go-erofs/internal/builder"
)
// entryFromSys extracts metadata from info.Sys(). Returns nil if the
// type is not recognized, allowing the caller to use a default.
func entryFromSys(info fs.FileInfo) *builder.Entry {
switch sys := info.Sys().(type) {
case *builder.Entry:
return sys
case *syscall.Stat_t:
return &builder.Entry{
UID: sys.Uid,
GID: sys.Gid,
Mtime: uint64(sys.Mtim.Sec),
MtimeNs: uint32(sys.Mtim.Nsec),
Nlink: uint32(sys.Nlink),
Rdev: uint32(sys.Rdev),
}
default:
return nil
}
}

673
vendor/github.com/erofs/go-erofs/writer.go generated vendored Normal file
View File

@@ -0,0 +1,673 @@
package erofs
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math"
"os"
"sort"
"github.com/erofs/go-erofs/internal/disk"
)
// maxBlockSize is the largest block size we support.
const maxBlockSize = 1 << 20
// onlyWriter wraps an io.Writer to hide io.ReaderFrom so that
// io.CopyBuffer uses the caller-provided buffer instead of
// the destination's ReadFrom (which allocates its own).
type onlyWriter struct{ io.Writer }
// erofsWriter serializes EROFS metadata to an io.Writer.
type erofsWriter struct {
entries []*erofsEntry // all entries in NID order
rootNid uint64
metaBlkAddr uint32
totalInodes uint64
buildTime uint64
buildTimeNs uint32
devices []uint64 // per-device block counts (one slot per entry)
blockSize int
chunkBits uint8 // log2(chunkSize / blockSize); chunkSize = blockSize << chunkBits
copyBuf []byte // reusable buffer for io.CopyBuffer
zeroBuf []byte // blockSize-length zero buffer for padding
inodeBuf [disk.SizeInodeExtended]byte // scratch buffer for writeInode
}
// inodeSize returns the on-disk inode header size for e.
func inodeCoreSize(e *erofsEntry) int {
if e.compact {
return disk.SizeInodeCompact
}
return disk.SizeInodeExtended
}
// entryChunkBits returns the chunk bits for a specific entry.
// Contiguous entries use a larger chunk size to minimize chunk indexes.
func (w *erofsWriter) entryChunkBits(e *erofsEntry) uint8 {
if e.chunkBits > 0 {
return e.chunkBits
}
return w.chunkBits
}
// entryChunkSize returns the chunk size in bytes for a specific entry.
func (w *erofsWriter) entryChunkSize(e *erofsEntry) int {
return w.blockSize << w.entryChunkBits(e)
}
// minChunkBits returns the minimum chunkBits such that file size fits in
// one chunk (chunkSize >= size). Capped at 31 (LayoutChunkFormatBits max).
func (w *erofsWriter) minChunkBits(size uint64) uint8 {
bits := w.chunkBits
for uint64(w.blockSize)<<bits < size && bits < 31 {
bits++
}
return bits
}
func (w *erofsWriter) write(out io.WriteSeeker) error {
w.copyBuf = make([]byte, 256*1024) // shared io.CopyBuffer buffer
return w.writeSeekable(out)
}
// writeSeekable uses a data-first on-disk layout: block0 (placeholder),
// data blocks, metadata. After everything is written, it seeks back to
// write the real superblock. This matches how mkfs.erofs lays out
// streaming sources — data is written as it arrives, metadata last.
func (w *erofsWriter) writeSeekable(out io.WriteSeeker) error {
// Data-first layout: sbArea, data blocks, metadata.
// Set metaBlkAddr to a sentinel so assignDataBlocks uses data-first.
w.metaBlkAddr = 0xFFFFFFFF
w.assignDataBlocks()
// Write placeholder superblock area.
if _, err := out.Write(make([]byte, w.sbAreaSize())); err != nil {
return err
}
// Stream data blocks directly to output.
if err := w.writeDataBlocks(out); err != nil {
return err
}
// Buffer and write metadata.
meta := w.newMetaBuffer()
if err := w.writeMetadataInodes(meta); err != nil {
return err
}
if _, err := meta.WriteTo(out); err != nil {
return err
}
// Seek back and write the real block 0 (superblock).
if _, err := out.Seek(0, io.SeekStart); err != nil {
return err
}
return w.writeBlock0(out)
}
// newMetaBuffer returns a pre-sized bytes.Buffer for metadata serialization.
func (w *erofsWriter) newMetaBuffer() *bytes.Buffer {
totalMetaBytes := 0
for _, e := range w.entries {
isz := disk.SizeInodeExtended
if e.compact {
isz = disk.SizeInodeCompact
}
sz := isz + e.xattrSize + e.trailingSize
if sz%32 != 0 {
sz = (sz + 31) & ^31
}
totalMetaBytes += sz
}
// SB area + metadata padded to block boundary.
capacity := w.blockSize + ((totalMetaBytes + w.blockSize - 1) & ^(w.blockSize - 1))
buf := bytes.NewBuffer(make([]byte, 0, capacity))
return buf
}
// assignDataBlocks assigns data block addresses to flat-plain entries.
// For metadata-first layout, data follows metadata.
// For data-first layout, data starts after the superblock area.
func (w *erofsWriter) assignDataBlocks() {
sbBlks := w.sbAreaBlocks()
if w.metaBlkAddr == uint32(sbBlks) {
// Metadata-first: data blocks come after metadata.
totalMetaBytes := 0
for _, e := range w.entries {
expectedOff := int(e.nid) * 32
sz := inodeCoreSize(e) + e.xattrSize + e.trailingSize
if sz%32 != 0 {
sz = (sz + 31) & ^31
}
end := expectedOff + sz
if end > totalMetaBytes {
totalMetaBytes = end
}
}
metaBlocks := (totalMetaBytes + w.blockSize - 1) / w.blockSize
addr := uint32(w.sbAreaBlocks() + metaBlocks)
for _, e := range w.entries {
if ds := w.flatPlainDataSize(e); ds > 0 {
e.dataBlkAddr = addr
addr += uint32((ds + w.blockSize - 1) / w.blockSize)
}
}
} else {
// Data-first: data starts after superblock area.
addr := uint32(w.sbAreaBlocks())
for _, e := range w.entries {
if ds := w.flatPlainDataSize(e); ds > 0 {
e.dataBlkAddr = addr
addr += uint32((ds + w.blockSize - 1) / w.blockSize)
}
}
w.metaBlkAddr = addr // metadata follows data
}
}
// sbAreaSize returns the number of bytes needed for the superblock area
// (blocks before metadata): 1024-byte pad + superblock + device slots,
// rounded up to block boundary.
func (w *erofsWriter) sbAreaSize() int {
n := disk.SuperBlockOffset + disk.SizeSuperBlock
if len(w.devices) > 0 {
n += len(w.devices) * disk.SizeDeviceSlot
}
return ((n + w.blockSize - 1) / w.blockSize) * w.blockSize
}
// sbAreaBlocks returns the number of blocks occupied by the superblock area.
func (w *erofsWriter) sbAreaBlocks() int {
return w.sbAreaSize() / w.blockSize
}
// metadataBytes computes the total size of the metadata area, including
// any zero-padding inserted to reach each inode's expected offset (NID * 32)
// and rounding each entry up to a 32-byte boundary.
func (w *erofsWriter) metadataBytes() int {
curOff := 0
for _, e := range w.entries {
expectedOff := int(e.nid) * 32
if curOff < expectedOff {
curOff = expectedOff
}
sz := inodeCoreSize(e) + e.xattrSize + e.trailingSize
if rem := sz % 32; rem != 0 {
sz += 32 - rem
}
curOff += sz
}
return curOff
}
func (w *erofsWriter) writeBlock0(buf io.Writer) error {
sbArea := make([]byte, w.sbAreaSize())
totalMetaBytes := w.metadataBytes()
metaBlocks := (totalMetaBytes + w.blockSize - 1) / w.blockSize
// Count data blocks.
dataBlocks := 0
for _, e := range w.entries {
if ds := w.flatPlainDataSize(e); ds > 0 {
dataBlocks += (ds + w.blockSize - 1) / w.blockSize
}
}
totalBlocks := w.sbAreaBlocks() + metaBlocks + dataBlocks
var featureIncompat uint32
var extraDevices uint16
var devtSlotOff uint16
if len(w.devices) > 0 {
featureIncompat |= disk.FeatureIncompatDeviceTable
extraDevices = uint16(len(w.devices))
devtSlotOff = uint16(disk.SizeSuperBlock / 16)
}
for _, e := range w.entries {
if len(e.chunks) > 0 {
featureIncompat |= disk.FeatureIncompatChunkedFile
break
}
}
sb := disk.SuperBlock{
MagicNumber: disk.MagicNumber,
BlkSizeBits: blkBits(w.blockSize),
RootNid: uint16(w.rootNid),
Inos: w.totalInodes,
BuildTime: w.buildTime,
BuildTimeNs: w.buildTimeNs,
Blocks: uint32(totalBlocks),
MetaBlkAddr: w.metaBlkAddr,
FeatureIncompat: featureIncompat,
ExtraDevices: extraDevices,
DevtSlotOff: devtSlotOff,
}
sbBuf := &bytes.Buffer{}
if err := binary.Write(sbBuf, binary.LittleEndian, &sb); err != nil {
return fmt.Errorf("write superblock: %w", err)
}
copy(sbArea[disk.SuperBlockOffset:], sbBuf.Bytes())
// Write device slots right after superblock.
for i, blocks := range w.devices {
if blocks > math.MaxUint32 {
return fmt.Errorf("device %d block count %d exceeds 32-bit limit", i+1, blocks)
}
devSlot := disk.DeviceSlot{
Blocks: uint32(blocks),
}
devBuf := &bytes.Buffer{}
if err := binary.Write(devBuf, binary.LittleEndian, &devSlot); err != nil {
return fmt.Errorf("write device slot: %w", err)
}
off := disk.SuperBlockOffset + disk.SizeSuperBlock + i*disk.SizeDeviceSlot
copy(sbArea[off:], devBuf.Bytes())
}
_, err := buf.Write(sbArea)
return err
}
// writeMetadataInodes writes inode metadata. Data block addresses must
// already be assigned on each entry before calling this method.
func (w *erofsWriter) writeMetadataInodes(buf io.Writer) error {
metaStart := 0
for _, e := range w.entries {
expectedOff := int(e.nid) * 32
if expectedOff > metaStart {
if _, err := buf.Write(w.zeroBuf[:expectedOff-metaStart]); err != nil {
return err
}
metaStart = expectedOff
}
if err := w.writeInode(buf, e); err != nil {
return fmt.Errorf("write inode for %s: %w", e.path, err)
}
if e.compact {
metaStart += disk.SizeInodeCompact
} else {
metaStart += disk.SizeInodeExtended
}
// Write xattr area
if e.xattrSize > 0 {
if err := w.writeXattrs(buf, e); err != nil {
return fmt.Errorf("write xattrs for %s: %w", e.path, err)
}
metaStart += e.xattrSize
}
// Write trailing data
switch e.mode & disk.StatTypeMask {
case disk.StatTypeReg:
if e.layout == disk.LayoutChunkBased && (e.size > 0 || len(e.chunks) > 0) {
if err := w.writeChunkIndexes(buf, e); err != nil {
return fmt.Errorf("write chunks for %s: %w", e.path, err)
}
metaStart += e.trailingSize
} else if e.layout == disk.LayoutFlatInline && e.size > 0 && e.data != nil {
n, err := io.CopyBuffer(onlyWriter{buf}, io.LimitReader(e.data, int64(e.size)), w.copyBuf)
if c, ok := e.data.(io.Closer); ok {
_ = c.Close()
}
if err != nil {
return fmt.Errorf("write inline data for %s: %w", e.path, err)
}
metaStart += int(n)
}
case disk.StatTypeDir:
if e.layout == disk.LayoutFlatInline {
n, err := w.writeDirents(buf, e)
if err != nil {
return fmt.Errorf("write dirents for %s: %w", e.path, err)
}
metaStart += n
}
case disk.StatTypeSymlink:
if e.layout == disk.LayoutFlatInline {
if _, err := io.WriteString(buf, e.symTarget); err != nil {
return fmt.Errorf("write symlink for %s: %w", e.path, err)
}
metaStart += len(e.symTarget)
}
}
// Pad to 32-byte boundary
inodeSize := disk.SizeInodeExtended
if e.compact {
inodeSize = disk.SizeInodeCompact
}
totalWritten := inodeSize + e.xattrSize + e.trailingSize
if totalWritten%32 != 0 {
padSize := 32 - (totalWritten % 32)
if _, err := buf.Write(w.zeroBuf[:padSize]); err != nil {
return err
}
metaStart += padSize
}
}
// Pad metadata to full block boundary
if metaStart%w.blockSize != 0 {
padSize := w.blockSize - (metaStart % w.blockSize)
if _, err := buf.Write(w.zeroBuf[:padSize]); err != nil {
return err
}
}
return nil
}
func (w *erofsWriter) writeInode(buf io.Writer, e *erofsEntry) error {
var inodeData uint32
switch e.mode & disk.StatTypeMask {
case disk.StatTypeReg:
if e.layout == disk.LayoutChunkBased {
inodeData = disk.LayoutChunkFormatIndexes | uint32(w.entryChunkBits(e))
} else if e.layout == disk.LayoutFlatPlain && e.size > 0 {
inodeData = e.dataBlkAddr
}
case disk.StatTypeDir, disk.StatTypeSymlink:
if e.layout == disk.LayoutFlatPlain {
inodeData = e.dataBlkAddr
}
case disk.StatTypeChrdev, disk.StatTypeBlkdev, disk.StatTypeFifo, disk.StatTypeSock:
inodeData = e.rdev
}
fileSize := e.size
switch e.mode & disk.StatTypeMask {
case disk.StatTypeDir:
fileSize = uint64(w.direntDataSize(e))
case disk.StatTypeSymlink:
fileSize = uint64(len(e.symTarget))
}
b := &w.inodeBuf
clear(b[:])
if e.compact {
binary.LittleEndian.PutUint16(b[0:2], inodeFormat(e.layout, true))
binary.LittleEndian.PutUint16(b[2:4], xattrCount(e.xattrSize))
binary.LittleEndian.PutUint16(b[4:6], e.mode)
binary.LittleEndian.PutUint16(b[6:8], uint16(e.nlink))
binary.LittleEndian.PutUint32(b[8:12], uint32(fileSize))
binary.LittleEndian.PutUint32(b[16:20], inodeData)
binary.LittleEndian.PutUint16(b[24:26], uint16(e.uid))
binary.LittleEndian.PutUint16(b[26:28], uint16(e.gid))
_, err := buf.Write(b[:disk.SizeInodeCompact])
return err
}
binary.LittleEndian.PutUint16(b[0:2], inodeFormat(e.layout, false))
binary.LittleEndian.PutUint16(b[2:4], xattrCount(e.xattrSize))
binary.LittleEndian.PutUint16(b[4:6], e.mode)
binary.LittleEndian.PutUint64(b[8:16], fileSize)
binary.LittleEndian.PutUint32(b[16:20], inodeData)
binary.LittleEndian.PutUint32(b[24:28], e.uid)
binary.LittleEndian.PutUint32(b[28:32], e.gid)
binary.LittleEndian.PutUint64(b[32:40], e.mtime)
binary.LittleEndian.PutUint32(b[40:44], e.mtimeNs)
binary.LittleEndian.PutUint32(b[44:48], e.nlink)
_, err := buf.Write(b[:disk.SizeInodeExtended])
return err
}
func (w *erofsWriter) writeXattrs(buf io.Writer, e *erofsEntry) error {
// XattrHeader: 4-byte name filter + 1-byte shared count + 7 reserved = 12 bytes
var xhdr [12]byte
binary.LittleEndian.PutUint32(xhdr[0:4], 0xFFFFFFFF) // name filter unused
if _, err := buf.Write(xhdr[:]); err != nil {
return err
}
for _, name := range sortedXattrKeys(e.xattrs) {
value := e.xattrs[name]
nameIndex, suffix := xattrSplit(name)
var xent [disk.SizeXattrEntry]byte
xent[0] = uint8(len(suffix))
xent[1] = nameIndex
binary.LittleEndian.PutUint16(xent[2:4], uint16(len(value)))
if _, err := buf.Write(xent[:]); err != nil {
return err
}
if _, err := io.WriteString(buf, suffix); err != nil {
return err
}
if _, err := io.WriteString(buf, value); err != nil {
return err
}
// Pad to 4-byte boundary
entryLen := disk.SizeXattrEntry + len(suffix) + len(value)
if entryLen%4 != 0 {
if _, err := buf.Write(w.zeroBuf[:4-entryLen%4]); err != nil {
return err
}
}
}
return nil
}
// writeChunkIndexes writes chunk index entries for a regular file.
// Each index entry covers one logical chunk (chunkSize bytes).
func (w *erofsWriter) writeChunkIndexes(buf io.Writer, e *erofsEntry) error {
cs := w.entryChunkSize(e)
blocksPerChunk := cs / w.blockSize
nchunks := (int(e.size) + cs - 1) / cs
// Null chunk index (no mapping): StartBlkHi=0xFFFF, DeviceID=0, StartBlkLo=NullAddr.
var nullIdx [disk.SizeChunkIndex]byte
binary.LittleEndian.PutUint16(nullIdx[0:2], 0xFFFF)
binary.LittleEndian.PutUint32(nullIdx[4:8], nullAddr)
if len(e.chunks) > 0 {
// Walk source chunks and emit one index per logical chunk.
// Source chunks use block-granularity counts; we step by blocksPerChunk.
var scratch [disk.SizeChunkIndex]byte
ci := 0 // index into source chunks
coff := 0 // block offset within current source chunk
for n := 0; n < nchunks; n++ {
if ci >= len(e.chunks) {
if _, err := buf.Write(nullIdx[:]); err != nil {
return err
}
continue
}
c := e.chunks[ci]
phys := c.PhysicalBlock + uint64(coff)
binary.LittleEndian.PutUint16(scratch[0:2], uint16(phys>>32))
binary.LittleEndian.PutUint16(scratch[2:4], c.DeviceID)
binary.LittleEndian.PutUint32(scratch[4:8], uint32(phys))
if _, err := buf.Write(scratch[:]); err != nil {
return err
}
coff += blocksPerChunk
for ci < len(e.chunks) && coff >= int(e.chunks[ci].Count) {
coff -= int(e.chunks[ci].Count)
ci++
}
}
} else {
for n := 0; n < nchunks; n++ {
if _, err := buf.Write(nullIdx[:]); err != nil {
return err
}
}
}
return nil
}
// writeDirents writes EROFS directory entries packed into block-sized chunks.
func (w *erofsWriter) writeDirents(buf io.Writer, e *erofsEntry) (int, error) {
type direntInfo struct {
name string
nid uint64
fileType uint8
}
// Build the full entry list including "." and ".." then sort
// alphabetically. EROFS requires dirents to be sorted within
// each block; "." and ".." are not guaranteed to be first.
allEnts := make([]direntInfo, 0, len(e.children)+2)
allEnts = append(allEnts, direntInfo{".", e.nid, disk.FileTypeDir})
allEnts = append(allEnts, direntInfo{"..", e.parentNid, disk.FileTypeDir})
for _, c := range e.children {
allEnts = append(allEnts, direntInfo{
name: c.name,
nid: c.nid,
fileType: c.erofsFileType,
})
}
sort.Slice(allEnts, func(i, j int) bool {
return allEnts[i].name < allEnts[j].name
})
totalWritten := 0
i := 0
for i < len(allEnts) {
// Determine how many entries fit in this block
start := i
blockUsed := 0
nameSize := 0
for j := i; j < len(allEnts); j++ {
headerSize := (j - start + 1) * disk.SizeDirent
nameSize += len(allEnts[j].name)
needed := headerSize + nameSize
if needed > w.blockSize {
break
}
blockUsed = needed
i = j + 1
}
if i == start {
// Single entry too large for a block (shouldn't happen)
blockUsed = disk.SizeDirent + len(allEnts[i].name)
i++
}
blockEnts := allEnts[start:i]
blockHeaderSize := len(blockEnts) * disk.SizeDirent
// Write dirent headers
var scratch [disk.SizeDirent]byte
nameOff := uint16(blockHeaderSize)
for j, de := range blockEnts {
if j > 0 {
nameOff += uint16(len(blockEnts[j-1].name))
}
binary.LittleEndian.PutUint64(scratch[0:8], de.nid)
binary.LittleEndian.PutUint16(scratch[8:10], nameOff)
scratch[10] = de.fileType
scratch[11] = 0
if _, err := buf.Write(scratch[:]); err != nil {
return totalWritten, err
}
totalWritten += disk.SizeDirent
}
// Write names
for _, de := range blockEnts {
n, err := io.WriteString(buf, de.name)
if err != nil {
return totalWritten, err
}
totalWritten += n
}
// Pad to block boundary if there are more entries
if i < len(allEnts) && blockUsed%w.blockSize != 0 {
padSize := w.blockSize - (blockUsed % w.blockSize)
if _, err := buf.Write(w.zeroBuf[:padSize]); err != nil {
return totalWritten, err
}
totalWritten += padSize
}
}
return totalWritten, nil
}
// writeDataBlocks writes data blocks for flat-plain entries directly to out.
func (w *erofsWriter) writeDataBlocks(out io.Writer) error {
for _, e := range w.entries {
ds := w.flatPlainDataSize(e)
if ds == 0 {
continue
}
var n int
switch e.mode & disk.StatTypeMask {
case disk.StatTypeReg:
expected := int64(ds)
var written int64
var err error
limited := io.LimitReader(e.data, expected)
// Use io.Copy for *os.File sources to enable copy_file_range.
if _, ok := e.data.(*os.File); ok {
written, err = io.Copy(out, limited)
} else {
written, err = io.CopyBuffer(onlyWriter{out}, limited, w.copyBuf)
}
if c, ok := e.data.(io.Closer); ok {
_ = c.Close()
}
if err != nil {
return fmt.Errorf("write data for %s: %w", e.path, err)
}
if written != expected {
return fmt.Errorf("write data for %s: short read: got %d bytes, expected %d", e.path, written, expected)
}
n = int(written)
case disk.StatTypeDir:
written, err := w.writeDirents(out, e)
if err != nil {
return fmt.Errorf("write dirents for %s: %w", e.path, err)
}
n = written
case disk.StatTypeSymlink:
written, err := io.WriteString(out, e.symTarget)
if err != nil {
return fmt.Errorf("write symlink data for %s: %w", e.path, err)
}
n = written
}
if n%w.blockSize != 0 {
padSize := w.blockSize - (n % w.blockSize)
if _, err := out.Write(w.zeroBuf[:padSize]); err != nil {
return fmt.Errorf("write padding for %s: %w", e.path, err)
}
}
}
return nil
}
// flatPlainDataSize returns the data size for a flat-plain entry, or 0.
func (w *erofsWriter) flatPlainDataSize(e *erofsEntry) int {
if e.layout != disk.LayoutFlatPlain {
return 0
}
switch e.mode & disk.StatTypeMask {
case disk.StatTypeReg:
if e.size > 0 && e.data != nil {
return int(e.size)
}
case disk.StatTypeDir:
return w.direntDataSize(e)
case disk.StatTypeSymlink:
return len(e.symTarget)
}
return 0
}

View File

@@ -3,8 +3,6 @@ package erofs
import (
"encoding/binary"
"fmt"
"io"
"strings"
"github.com/erofs/go-erofs/internal/disk"
)
@@ -39,22 +37,30 @@ func (idx xattrIndex) String() string {
}
}
func setXattrs(b *file, addr int64, blk *block) (err error) {
b.info.stat.Xattrs = map[string]string{}
blkSize := int32(1 << b.img.sb.BlkSizeBits)
// loadXattrs reads the extended attributes for the file's inode and
// populates the given Stat's Xattrs map.
func loadXattrs(b *file, stat *Stat) (err error) {
ino := b.info
addr := b.img.metaStartPos() + int64(ino.nid*disk.SizeInodeCompact) + int64(ino.icsize)
xsize := ino.xsize
blk.offset = int32(addr & int64(blkSize-1))
if blk.end != blkSize || blk.end-blk.offset < disk.SizeXattrBodyHeader {
b.img.putBlock(blk)
blk, err = b.img.loadAt(addr, int64(b.info.xsize))
if err != nil {
return fmt.Errorf("failed to read xattr body for nid %d: %w", b.nid, err)
}
stat.Xattrs = map[string]string{}
blk, err := b.img.loadAt(addr, int64(xsize))
if err != nil {
return fmt.Errorf("failed to read xattr body for nid %d: %w", b.nid, err)
}
var (
xb = blk.bytes()
xh disk.XattrHeader
)
defer func() {
if blk != nil {
b.img.putBlock(blk)
}
}()
xb := blk.bytes()
if len(xb) < disk.SizeXattrBodyHeader {
return fmt.Errorf("xattr body too small for nid %d: %w", b.nid, ErrInvalid)
}
var xh disk.XattrHeader
if _, err := binary.Decode(xb[:disk.SizeXattrBodyHeader], binary.LittleEndian, &xh); err != nil {
return err
}
@@ -64,7 +70,7 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
if len(xb) < 4 {
pos := disk.SizeXattrBodyHeader + int64(i)*4
b.img.putBlock(blk)
blk, err = b.img.loadAt(addr+pos, int64(b.info.xsize)-pos)
blk, err = b.img.loadAt(addr+pos, int64(xsize)-pos)
if err != nil {
return fmt.Errorf("failed to read xattr body for nid %d: %w", b.nid, err)
}
@@ -79,13 +85,18 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
}
// TODO: Cache shared xattr blocks
blk, err := b.img.loadAt(int64(b.img.sb.XattrBlkAddr)<<b.img.sb.BlkSizeBits+int64(xattrAddr*4), int64(blkSize))
sblk, err := b.img.loadAt(int64(b.img.sb.XattrBlkAddr)<<b.img.sb.BlkSizeBits+int64(xattrAddr*4), int64(1<<b.img.sb.BlkSizeBits))
if err != nil {
return fmt.Errorf("failed to read shared xattr body for nid %d: %w", b.nid, err)
}
sb := blk.bytes()
sb := sblk.bytes()
if len(sb) < disk.SizeXattrEntry {
b.img.putBlock(sblk)
return fmt.Errorf("shared xattr block too small for nid %d: %w", b.nid, ErrInvalid)
}
var xattrEntry disk.XattrEntry
if _, err := binary.Decode(sb[:disk.SizeXattrEntry], binary.LittleEndian, &xattrEntry); err != nil {
b.img.putBlock(sblk)
return err
}
sb = sb[disk.SizeXattrEntry:]
@@ -93,9 +104,9 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
if xattrEntry.NameIndex&0x80 == 0x80 {
// Long prefix: highest bit set
longPrefixIndex := xattrEntry.NameIndex & 0x7F
var err error
prefix, err = b.img.getLongPrefix(longPrefixIndex)
if err != nil {
b.img.putBlock(sblk)
return fmt.Errorf("failed to get long prefix for shared xattr nid %d: %w", b.nid, err)
}
} else if xattrEntry.NameIndex != 0 {
@@ -103,11 +114,13 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
}
if len(sb) < int(xattrEntry.NameLen)+int(xattrEntry.ValueLen) {
return fmt.Errorf("shared xattr too long for nid %d", b.nid)
b.img.putBlock(sblk)
return fmt.Errorf("shared xattr too long for nid %d: %w", b.nid, ErrInvalid)
}
name := prefix + string(sb[:xattrEntry.NameLen])
sb = sb[xattrEntry.NameLen:]
b.info.stat.Xattrs[name] = string(sb[:xattrEntry.ValueLen])
stat.Xattrs[name] = string(sb[:xattrEntry.ValueLen])
b.img.putBlock(sblk)
xb = xb[4:]
}
@@ -115,14 +128,14 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
pos := disk.SizeXattrBodyHeader + int(xh.SharedCount)*4
reload := func() error {
b.img.putBlock(blk)
blk, err = b.img.loadAt(addr+int64(pos), int64(b.info.xsize-pos))
blk, err = b.img.loadAt(addr+int64(pos), int64(xsize-pos))
if err != nil {
return fmt.Errorf("failed to read xattr body for nid %d: %w", b.nid, err)
}
xb = blk.bytes()
return nil
}
for pos < b.info.xsize {
for pos < xsize {
if len(xb) < disk.SizeXattrEntry {
if err := reload(); err != nil {
return err
@@ -166,7 +179,7 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
var value string
if len(xb) < int(xattrEntry.ValueLen) {
remaining := int(xattrEntry.ValueLen)
var b strings.Builder
buf := make([]byte, 0, remaining)
for remaining > 0 {
copySize := len(xb)
if copySize == 0 {
@@ -181,23 +194,18 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
if remaining < copySize {
copySize = remaining
}
n, err := b.Write(xb[:copySize])
if err != nil {
return err
} else if n != copySize {
return io.ErrShortWrite
}
remaining -= n
pos += n
buf = append(buf, xb[:copySize]...)
remaining -= copySize
pos += copySize
xb = xb[copySize:]
}
value = b.String()
value = string(buf)
} else {
value = string(xb[:xattrEntry.ValueLen])
pos += int(xattrEntry.ValueLen)
xb = xb[xattrEntry.ValueLen:]
}
b.info.stat.Xattrs[name] = value
stat.Xattrs[name] = value
// Round up to next 4 byte boundary
if rem := pos % 4; rem != 0 {

3
vendor/modules.txt vendored
View File

@@ -296,9 +296,10 @@ github.com/docker/go-units
## explicit; go 1.13
github.com/emicklei/go-restful/v3
github.com/emicklei/go-restful/v3/log
# github.com/erofs/go-erofs v0.2.1
# github.com/erofs/go-erofs v0.3.0
## explicit; go 1.23
github.com/erofs/go-erofs
github.com/erofs/go-erofs/internal/builder
github.com/erofs/go-erofs/internal/disk
# github.com/felixge/httpsnoop v1.0.4
## explicit; go 1.13