Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ could provide an interface to create erofs files as well.
- [x] Read erofs files created with default `mkfs.erofs` options
- [x] Read chunk-based erofs files (without indexes)
- [x] Xattr support
- [ ] Long xattr prefix support
- [x] Long xattr prefix support
- [ ] Read erofs files with compression
- [ ] Extra devices for chunked data and chunk indexes
- [ ] Creating erofs files
Expand Down
123 changes: 120 additions & 3 deletions erofs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package erofs

import (
"bufio"
"encoding/binary"
"errors"
"fmt"
Expand Down Expand Up @@ -84,14 +85,130 @@ func EroFS(r io.ReaderAt) (fs.FS, error) {
type image struct {
sb disk.SuperBlock

meta io.ReaderAt
blkPool sync.Pool
meta io.ReaderAt
blkPool sync.Pool
longPrefixes []string // cached long xattr prefixes
prefixesOnce sync.Once
prefixesErr error
}

func (img *image) blkOffset() int64 {
return int64(img.sb.MetaBlkAddr) << int64(img.sb.BlkSizeBits)
}

// loadLongPrefixes loads and caches the long xattr prefixes from the packed inode
// using the regular inode read logic to handle compressed/non-inline data.
//
// Long xattr name prefixes are used to optimize storage of xattrs with common
// prefixes. They are stored sequentially in a special "packed inode" or
// "meta inode".
// See: https://docs.kernel.org/filesystems/erofs.html#extended-attributes
func (img *image) loadLongPrefixes() error {
img.prefixesOnce.Do(func() {
if img.sb.XattrPrefixCount == 0 {
return
}

// Long prefixes are stored in the packed inode at offset XattrPrefixStart * 4.
// The packed inode (identified by PackedNid in the superblock) is a special
// inode used for shared data and metadata.
// We use ".packed" as a descriptive name for this internal inode.
f := &file{
img: img,
name: ".packed",
inode: img.sb.PackedNid,
ftype: 0, // regular file
}

// Read inode info to determine size and layout
fi, err := f.readInfo(false)
if err != nil {
img.prefixesErr = fmt.Errorf("failed to read packed inode: %w", err)
return
}

// Calculate the starting offset. XattrPrefixStart is defined in the
// superblock as being in units of 4 bytes from the start of the
// packed inode's data.
startOffset := int64(img.sb.XattrPrefixStart) * 4
if startOffset > fi.size {
img.prefixesErr = fmt.Errorf("xattr prefix start offset %d exceeds packed inode size %d", startOffset, fi.size)
return
}

// Set the read offset
f.offset = startOffset

r := bufio.NewReader(f)
img.longPrefixes = make([]string, img.sb.XattrPrefixCount)

for i := 0; i < int(img.sb.XattrPrefixCount); i++ {
// Each long prefix entry consists of:
// - A 2-byte little-endian length field (prefixLen)
// - A 1-byte base_index (short xattr prefix)
// - The infix string of length prefixLen - 1
// - Padding to align the entire entry (2 + prefixLen) to a 4-byte boundary.
var lenBuf [2]byte
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
img.prefixesErr = fmt.Errorf("failed to read long xattr prefix length at index %d: %w", i, err)
return
}
prefixLen := int(binary.LittleEndian.Uint16(lenBuf[:]))

if prefixLen < 1 {
img.prefixesErr = fmt.Errorf("invalid long xattr prefix length %d at index %d", prefixLen, i)
return
}

// Read data (base_index + infix)
data := make([]byte, prefixLen)
if _, err := io.ReadFull(r, data); err != nil {
img.prefixesErr = fmt.Errorf("failed to read long xattr prefix data at index %d: %w", i, err)
return
}

// First byte is the base_index referencing a standard xattr prefix
baseIndex := xattrIndex(data[0])

// Remaining bytes are the infix to be appended to the base prefix
infix := string(data[1:])

// Construct full prefix: base prefix + infix
img.longPrefixes[i] = baseIndex.String() + infix

// Align to 4-byte boundary. The entry starts with a 2-byte length field
// followed by prefixLen bytes of data.
totalLen := 2 + prefixLen
if rem := totalLen % 4; rem != 0 {
padding := 4 - rem
if _, err := r.Discard(padding); err != nil {
// If we are at the last prefix and hit EOF, it's acceptable if the file ends without padding
if i == int(img.sb.XattrPrefixCount)-1 && errors.Is(err, io.EOF) {
return
}
img.prefixesErr = fmt.Errorf("failed to discard padding at index %d: %w", i, err)
return
}
}
}
})

return img.prefixesErr
}

// getLongPrefix returns the long xattr prefix at the given index
func (img *image) getLongPrefix(index uint8) (string, error) {
if err := img.loadLongPrefixes(); err != nil {
return "", err
}

if int(index) >= len(img.longPrefixes) {
return "", fmt.Errorf("long xattr prefix index %d out of range (max %d)", index, len(img.longPrefixes)-1)
}

return img.longPrefixes[index], nil
}

func (img *image) loadAt(addr, size int64) (*block, error) {
blkSize := int64(1 << img.sb.BlkSizeBits)
if size > blkSize {
Expand Down Expand Up @@ -647,4 +764,4 @@ func decodeSuperBlock(b [disk.SizeSuperBlock]byte, sb *disk.SuperBlock) error {
return fmt.Errorf("invalid super block: invalid magic number %x", sb.MagicNumber)
}
return nil
}
}
11 changes: 11 additions & 0 deletions erofs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ func TestBasic(t *testing.T) {
"user.xdg.comment": "comment for f4",
"user.common": "same-value",
})
// Value is defined in /usr/lib/generated/generate.sh of testdata
longPrefix := "user.long.prefix.vfvzyrvujoemkjztekxczhyyqpzncyav.xiksvigqpjttnvcvxgaxpnrghppufylkopprkdsfncibznsvmbicfknlkbnuntpuqmwffxkrnuhtpucxwllkxrfzmbvmdcluahylidncngjrxnlipwikplkxgfpiiiqtzsnigpcojpkxtzbzqcosttdxhtspbxltuezcakskakmskmaznvpwcqjakbyapaglwd."
longValue := "value1-ppufylkopprkdsfncibznsvmbicfknlkbnuntpuqmwffxkrnuhtpucxwllkxrfzmbvmdcluahylidncngjrxnlipwikplkxgfpiiiqtzsnigpcojpkxtzbzqcosttdxhtspbxltuezcakskakmskmaznvpwcqjakbyapaglwdqfgvgkrgdwcegjpfmelrejllrjkpbwindlfynuzjgvcgygyayjvmtxgsbjkzrydoswbsknrrwjkwzxhasowuzdoxlhbxso"
checkXattrs(t, efs, "/usr/lib/generated/xattrs/long-prefix-xattrs", map[string]string{
longPrefix + "long-value": longValue,
longPrefix + "shortvalue": "y",
})
checkXattrs(t, efs, "/usr/lib/generated/xattrs/short-prefix-xattrs", map[string]string{
"user.short.long-value": longValue,
"user.short.shortvalue": "y",
})
checkDevice(t, efs, "/dev/block0", fs.ModeDevice, 0x00000101)
checkDevice(t, efs, "/dev/block1", fs.ModeDevice, 0)
checkDevice(t, efs, "/dev/char0", fs.ModeCharDevice, 0x00000202)
Expand Down
52 changes: 28 additions & 24 deletions internal/disk/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const (
LayoutChunkFormatIndexes = 0x0020
)

// SuperBlock represents the EROFS on-disk superblock.
// See: https://docs.kernel.org/filesystems/erofs.html#on-disk-layout
type SuperBlock struct {
MagicNumber uint32
Checksum uint32
Expand All @@ -43,38 +45,40 @@ type SuperBlock struct {
DirBlkBits uint8
XattrPrefixCount uint8
XattrPrefixStart uint32
PackedNid uint64
PackedNid uint64 // Nid of the special "packed" inode for shared data/prefixes
XattrFilterRes uint8
Reserved [23]uint8
}

// InodeCompact represents the 32-byte on-disk compact inode.
type InodeCompact struct {
Format uint16
XattrCount uint16
Mode uint16
Nlink uint16
Size uint32
Reserved uint32
InodeData uint32
Inode uint32
UID uint16
GID uint16
Reserved2 uint32
Format uint16 // i_format
XattrCount uint16 // i_xattr_icount
Mode uint16 // i_mode
Nlink uint16 // i_nlink
Size uint32 // i_size
Reserved uint32 // i_reserved
InodeData uint32 // i_u (i_raw_blkaddr, i_rdev, etc.)
Inode uint32 // i_ino
UID uint16 // i_uid
GID uint16 // i_gid
Reserved2 uint32 // i_reserved2
}

// InodeExtended represents the 64-byte on-disk extended inode.
type InodeExtended struct {
Format uint16
XattrCount uint16
Mode uint16
Reserved uint16
Size uint64
InodeData uint32 // RawBlockAddr | Rdev | Compressed Count | Chunk Format
Inode uint32
UID uint32
GID uint32
Mtime uint64
MtimeNs uint32
Nlink uint32
Format uint16 // i_format
XattrCount uint16 // i_xattr_icount
Mode uint16 // i_mode
Reserved uint16 // i_reserved
Size uint64 // i_size
InodeData uint32 // i_u (i_raw_blkaddr, i_rdev, etc.)
Inode uint32 // i_ino
UID uint32 // i_uid
GID uint32 // i_gid
Mtime uint64 // i_mtime
MtimeNs uint32 // i_mtime_nsec
Nlink uint32 // i_nlink
Reserved2 [16]uint8
}

Expand Down
Binary file modified testdata/basic-chunk-4096.erofs
Binary file not shown.
Binary file modified testdata/basic-chunk-8192.erofs
Binary file not shown.
Binary file modified testdata/basic-default.erofs
Binary file not shown.
20 changes: 14 additions & 6 deletions xattr.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,13 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
sb = sb[disk.SizeXattrEntry:]
var prefix string
if xattrEntry.NameIndex&0x80 == 0x80 {
//nameIndex := xattrEntry.NameIndex & 0x7F
// TODO: Get long prefix
return fmt.Errorf("shared xattr with long prefix not implemented for nid %d", b.inode)
// Long prefix: highest bit set
longPrefixIndex := xattrEntry.NameIndex & 0x7F
var err error
prefix, err = b.img.getLongPrefix(longPrefixIndex)
if err != nil {
return fmt.Errorf("failed to get long prefix for shared xattr nid %d: %w", b.inode, err)
}
} else if xattrEntry.NameIndex != 0 {
prefix = xattrIndex(xattrEntry.NameIndex).String()
}
Expand Down Expand Up @@ -130,9 +134,13 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
xb = xb[disk.SizeXattrEntry:]
var prefix string
if xattrEntry.NameIndex&0x80 == 0x80 {
//nameIndex := xattrEntry.NameIndex & 0x7F
// TODO: Get long prefix
return fmt.Errorf("shared xattr with long prefix not implemented for nid %d", b.inode)
// Long prefix: highest bit set
longPrefixIndex := xattrEntry.NameIndex & 0x7F
var err error
prefix, err = b.img.getLongPrefix(longPrefixIndex)
if err != nil {
return fmt.Errorf("failed to get long prefix for inline xattr nid %d: %w", b.inode, err)
}
} else if xattrEntry.NameIndex != 0 {
prefix = xattrIndex(xattrEntry.NameIndex).String()
}
Expand Down
Loading