diff --git a/README.md b/README.md index 6badc71..85169b4 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/erofs.go b/erofs.go index b8fde63..d7dc47f 100644 --- a/erofs.go +++ b/erofs.go @@ -1,6 +1,7 @@ package erofs import ( + "bufio" "encoding/binary" "errors" "fmt" @@ -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 { @@ -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 -} +} \ No newline at end of file diff --git a/erofs_test.go b/erofs_test.go index 577a0dd..c27499b 100644 --- a/erofs_test.go +++ b/erofs_test.go @@ -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) diff --git a/internal/disk/types.go b/internal/disk/types.go index a536874..55a2652 100644 --- a/internal/disk/types.go +++ b/internal/disk/types.go @@ -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 @@ -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 } diff --git a/testdata/basic-chunk-4096.erofs b/testdata/basic-chunk-4096.erofs index a39131f..0dec2b4 100644 Binary files a/testdata/basic-chunk-4096.erofs and b/testdata/basic-chunk-4096.erofs differ diff --git a/testdata/basic-chunk-8192.erofs b/testdata/basic-chunk-8192.erofs index a3003f7..6e9cde5 100644 Binary files a/testdata/basic-chunk-8192.erofs and b/testdata/basic-chunk-8192.erofs differ diff --git a/testdata/basic-default.erofs b/testdata/basic-default.erofs index 2a410e8..227327d 100644 Binary files a/testdata/basic-default.erofs and b/testdata/basic-default.erofs differ diff --git a/xattr.go b/xattr.go index 3b1fe5a..e8843c4 100644 --- a/xattr.go +++ b/xattr.go @@ -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() } @@ -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() }