mirror of
https://github.com/containerd/containerd.git
synced 2026-06-24 08:48:48 +00:00
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:
2
go.mod
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
114
vendor/github.com/erofs/go-erofs/README.md
generated
vendored
114
vendor/github.com/erofs/go-erofs/README.md
generated
vendored
@@ -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()
|
||||
```
|
||||
|
||||
391
vendor/github.com/erofs/go-erofs/erofs.go
generated
vendored
391
vendor/github.com/erofs/go-erofs/erofs.go
generated
vendored
@@ -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
137
vendor/github.com/erofs/go-erofs/format.go
generated
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
27
vendor/github.com/erofs/go-erofs/internal/builder/entry.go
generated
vendored
Normal file
27
vendor/github.com/erofs/go-erofs/internal/builder/entry.go
generated
vendored
Normal 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
|
||||
}
|
||||
1
vendor/github.com/erofs/go-erofs/internal/disk/types.go
generated
vendored
1
vendor/github.com/erofs/go-erofs/internal/disk/types.go
generated
vendored
@@ -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
220
vendor/github.com/erofs/go-erofs/layout.go
generated
vendored
Normal 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
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
26
vendor/github.com/erofs/go-erofs/mkfs_darwin.go
generated
vendored
Normal 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
500
vendor/github.com/erofs/go-erofs/mkfs_image.go
generated
vendored
Normal 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
16
vendor/github.com/erofs/go-erofs/mkfs_other.go
generated
vendored
Normal 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
30
vendor/github.com/erofs/go-erofs/mkfs_unix.go
generated
vendored
Normal 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
673
vendor/github.com/erofs/go-erofs/writer.go
generated
vendored
Normal 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
|
||||
}
|
||||
78
vendor/github.com/erofs/go-erofs/xattr.go
generated
vendored
78
vendor/github.com/erofs/go-erofs/xattr.go
generated
vendored
@@ -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
3
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user