Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
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
130 changes: 128 additions & 2 deletions erofs.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,140 @@ 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 superblock
func (img *image) loadLongPrefixes() error {
img.prefixesOnce.Do(func() {
if img.sb.XattrPrefixCount == 0 {
return
}

// Long prefixes are stored in the packed inode, after the inode header
Copy link
Member

@hsiangkao hsiangkao Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Derek @dmcgowan, long xattr prefixes (Linux 6.4~6.16)were defined as:

if INCOMPAT_FRAGMENTS is set, the long prefix table is located in the packed inode (XattrPrefixStart * 4);
if INCOMPAT_FRAGMENTS is not set, the long prefix table is directly in the ondisk offset (XattrPrefixStart * 4);

However this way is flew because:

  1. The packed inode is used to compress unaligned tail data together for example, but some users may not need to compress the long xattr prefixes (although some part of the packed inode can be specified as uncompressed);
  2. Since metadata compression is introduced (INCOMPAT_METABOX, Linux 6.17+, including this year LTS), a dedicated metadata (metabox) inode is introduced to contain compressed metadata, so long xattr prefixes would be better to be located in the metabox too.
    So long xattr prefixes placement is fixed as: https://git.kernel.org/xiang/erofs-utils/c/0cf1039782b2
    I admit it was messy initially due to lack of more deeply thoughts when this feature was firstly landed, but it's now fixed.
    the implementation can be refered: https://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git/tree/lib/xattr.c?id=0cf1039782b2#n1680
    and
    https://git.kernel.org/torvalds/c/1fcf686def19

// The packed inode is typically a compact inode (32 bytes)
// Address = MetaBlkAddr + (PackedNid * 32) + inode_size + (XattrPrefixStart * 4)
// Note: XattrPrefixStart is in units of 4 bytes from the end of the packed inode
baseAddr := img.blkOffset() + int64(img.sb.PackedNid)*32 + 32 + int64(img.sb.XattrPrefixStart)*4
Copy link

Copilot AI Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 32 appears twice in this address calculation. Consider defining a constant like packedInodeSize = 32 to make the calculation more self-documenting and maintainable.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If long xattr prefixes are located in packed inode or metabox inode, I suggest just using regular inode read logic to read XattrPrefixStart*4 directly, instead of just read data after the inode metadata (because the packed inode can be compressed, or non-inline, etc.)


img.longPrefixes = make([]string, img.sb.XattrPrefixCount)

// We'll read the prefixes incrementally since they can be large
// and may span multiple blocks
currentAddr := baseAddr
var blk *block
var buf []byte
var err error
bufOffset := 0 // offset within the current buffer

for i := 0; i < int(img.sb.XattrPrefixCount); i++ {
// Ensure we have at least 2 bytes to read the length
if blk == nil || bufOffset+2 > len(buf) {
if blk != nil {
img.putBlock(blk)
}
blk, err = img.loadAt(currentAddr, int64(1<<img.sb.BlkSizeBits))
if err != nil {
img.prefixesErr = fmt.Errorf("failed to load long xattr prefix data at index %d: %w", i, err)
return
}
buf = blk.bytes()
bufOffset = 0
}

// Read length (little endian uint16) - includes base_index byte + infix bytes
prefixLen := int(binary.LittleEndian.Uint16(buf[bufOffset : bufOffset+2]))
bufOffset += 2
currentAddr += 2

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

// Check if we have enough data for the prefix in current buffer
if bufOffset+prefixLen > len(buf) {
// Need to read more data - reload from current position
if blk != nil {
img.putBlock(blk)
}
// Load enough to cover this prefix
loadSize := int64(prefixLen)
if loadSize < int64(1<<img.sb.BlkSizeBits) {
loadSize = int64(1 << img.sb.BlkSizeBits)
}
blk, err = img.loadAt(currentAddr, loadSize)
if err != nil {
img.prefixesErr = fmt.Errorf("failed to load long xattr prefix data for index %d: %w", i, err)
return
}
buf = blk.bytes()
bufOffset = 0

// Verify we have enough now
if prefixLen > len(buf) {
img.putBlock(blk)
img.prefixesErr = fmt.Errorf("long xattr prefix at index %d too large (%d bytes)", i, prefixLen)
return
}
}

// First byte is the base_index
baseIndex := xattrIndex(buf[bufOffset])
bufOffset++
currentAddr++
prefixLen--

// Remaining bytes are the infix
infix := string(buf[bufOffset : bufOffset+prefixLen])
bufOffset += prefixLen
currentAddr += int64(prefixLen)

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

// Align to 4-byte boundary
// Total length field (2 bytes) + data (base_index + infix)
totalLen := 2 + 1 + prefixLen
if rem := totalLen % 4; rem != 0 {
padding := 4 - rem
Comment on lines +192 to +194
Copy link

Copilot AI Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic numbers 2, 1, and 4 in the alignment calculation should be defined as named constants (e.g., lengthFieldSize = 2, baseIndexSize = 1, alignmentBoundary = 4) to improve code clarity.

Copilot uses AI. Check for mistakes.
bufOffset += padding
currentAddr += int64(padding)
}
}

if blk != nil {
img.putBlock(blk)
}
})

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
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
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