Skip to content

Commit 237e4b3

Browse files
dmcgowanhsiangkao
authored andcommitted
Add support for long xattr prefixes
Signed-off-by: Derek McGowan <derek@mcg.dev>
1 parent 8a54422 commit 237e4b3

File tree

3 files changed

+135
-10
lines changed

3 files changed

+135
-10
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ could provide an interface to create erofs files as well.
1414
- [x] Read erofs files created with default `mkfs.erofs` options
1515
- [x] Read chunk-based erofs files (without indexes)
1616
- [x] Xattr support
17-
- [ ] Long xattr prefix support
17+
- [x] Long xattr prefix support
1818
- [ ] Read erofs files with compression
1919
- [ ] Extra devices for chunked data and chunk indexes
2020
- [ ] Creating erofs files

erofs.go

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package erofs
22

33
import (
4+
"bufio"
45
"encoding/binary"
56
"errors"
67
"fmt"
@@ -84,14 +85,130 @@ func EroFS(r io.ReaderAt) (fs.FS, error) {
8485
type image struct {
8586
sb disk.SuperBlock
8687

87-
meta io.ReaderAt
88-
blkPool sync.Pool
88+
meta io.ReaderAt
89+
blkPool sync.Pool
90+
longPrefixes []string // cached long xattr prefixes
91+
prefixesOnce sync.Once
92+
prefixesErr error
8993
}
9094

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

99+
// loadLongPrefixes loads and caches the long xattr prefixes from the packed inode
100+
// using the regular inode read logic to handle compressed/non-inline data.
101+
//
102+
// Long xattr name prefixes are used to optimize storage of xattrs with common
103+
// prefixes. They are stored sequentially in a special "packed inode" or
104+
// "meta inode".
105+
// See: https://docs.kernel.org/filesystems/erofs.html#extended-attributes
106+
func (img *image) loadLongPrefixes() error {
107+
img.prefixesOnce.Do(func() {
108+
if img.sb.XattrPrefixCount == 0 {
109+
return
110+
}
111+
112+
// Long prefixes are stored in the packed inode at offset XattrPrefixStart * 4.
113+
// The packed inode (identified by PackedNid in the superblock) is a special
114+
// inode used for shared data and metadata.
115+
// We use ".packed" as a descriptive name for this internal inode.
116+
f := &file{
117+
img: img,
118+
name: ".packed",
119+
inode: img.sb.PackedNid,
120+
ftype: 0, // regular file
121+
}
122+
123+
// Read inode info to determine size and layout
124+
fi, err := f.readInfo(false)
125+
if err != nil {
126+
img.prefixesErr = fmt.Errorf("failed to read packed inode: %w", err)
127+
return
128+
}
129+
130+
// Calculate the starting offset. XattrPrefixStart is defined in the
131+
// superblock as being in units of 4 bytes from the start of the
132+
// packed inode's data.
133+
startOffset := int64(img.sb.XattrPrefixStart) * 4
134+
if startOffset > fi.size {
135+
img.prefixesErr = fmt.Errorf("xattr prefix start offset %d exceeds packed inode size %d", startOffset, fi.size)
136+
return
137+
}
138+
139+
// Set the read offset
140+
f.offset = startOffset
141+
142+
r := bufio.NewReader(f)
143+
img.longPrefixes = make([]string, img.sb.XattrPrefixCount)
144+
145+
for i := 0; i < int(img.sb.XattrPrefixCount); i++ {
146+
// Each long prefix entry consists of:
147+
// - A 2-byte little-endian length field (prefixLen)
148+
// - A 1-byte base_index (short xattr prefix)
149+
// - The infix string of length prefixLen - 1
150+
// - Padding to align the entire entry (2 + prefixLen) to a 4-byte boundary.
151+
var lenBuf [2]byte
152+
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
153+
img.prefixesErr = fmt.Errorf("failed to read long xattr prefix length at index %d: %w", i, err)
154+
return
155+
}
156+
prefixLen := int(binary.LittleEndian.Uint16(lenBuf[:]))
157+
158+
if prefixLen < 1 {
159+
img.prefixesErr = fmt.Errorf("invalid long xattr prefix length %d at index %d", prefixLen, i)
160+
return
161+
}
162+
163+
// Read data (base_index + infix)
164+
data := make([]byte, prefixLen)
165+
if _, err := io.ReadFull(r, data); err != nil {
166+
img.prefixesErr = fmt.Errorf("failed to read long xattr prefix data at index %d: %w", i, err)
167+
return
168+
}
169+
170+
// First byte is the base_index referencing a standard xattr prefix
171+
baseIndex := xattrIndex(data[0])
172+
173+
// Remaining bytes are the infix to be appended to the base prefix
174+
infix := string(data[1:])
175+
176+
// Construct full prefix: base prefix + infix
177+
img.longPrefixes[i] = baseIndex.String() + infix
178+
179+
// Align to 4-byte boundary. The entry starts with a 2-byte length field
180+
// followed by prefixLen bytes of data.
181+
totalLen := 2 + prefixLen
182+
if rem := totalLen % 4; rem != 0 {
183+
padding := 4 - rem
184+
if _, err := r.Discard(padding); err != nil {
185+
// If we are at the last prefix and hit EOF, it's acceptable if the file ends without padding
186+
if i == int(img.sb.XattrPrefixCount)-1 && errors.Is(err, io.EOF) {
187+
return
188+
}
189+
img.prefixesErr = fmt.Errorf("failed to discard padding at index %d: %w", i, err)
190+
return
191+
}
192+
}
193+
}
194+
})
195+
196+
return img.prefixesErr
197+
}
198+
199+
// getLongPrefix returns the long xattr prefix at the given index
200+
func (img *image) getLongPrefix(index uint8) (string, error) {
201+
if err := img.loadLongPrefixes(); err != nil {
202+
return "", err
203+
}
204+
205+
if int(index) >= len(img.longPrefixes) {
206+
return "", fmt.Errorf("long xattr prefix index %d out of range (max %d)", index, len(img.longPrefixes)-1)
207+
}
208+
209+
return img.longPrefixes[index], nil
210+
}
211+
95212
func (img *image) loadAt(addr, size int64) (*block, error) {
96213
blkSize := int64(1 << img.sb.BlkSizeBits)
97214
if size > blkSize {
@@ -647,4 +764,4 @@ func decodeSuperBlock(b [disk.SizeSuperBlock]byte, sb *disk.SuperBlock) error {
647764
return fmt.Errorf("invalid super block: invalid magic number %x", sb.MagicNumber)
648765
}
649766
return nil
650-
}
767+
}

xattr.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,13 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
8888
sb = sb[disk.SizeXattrEntry:]
8989
var prefix string
9090
if xattrEntry.NameIndex&0x80 == 0x80 {
91-
//nameIndex := xattrEntry.NameIndex & 0x7F
92-
// TODO: Get long prefix
93-
return fmt.Errorf("shared xattr with long prefix not implemented for nid %d", b.inode)
91+
// Long prefix: highest bit set
92+
longPrefixIndex := xattrEntry.NameIndex & 0x7F
93+
var err error
94+
prefix, err = b.img.getLongPrefix(longPrefixIndex)
95+
if err != nil {
96+
return fmt.Errorf("failed to get long prefix for shared xattr nid %d: %w", b.inode, err)
97+
}
9498
} else if xattrEntry.NameIndex != 0 {
9599
prefix = xattrIndex(xattrEntry.NameIndex).String()
96100
}
@@ -130,9 +134,13 @@ func setXattrs(b *file, addr int64, blk *block) (err error) {
130134
xb = xb[disk.SizeXattrEntry:]
131135
var prefix string
132136
if xattrEntry.NameIndex&0x80 == 0x80 {
133-
//nameIndex := xattrEntry.NameIndex & 0x7F
134-
// TODO: Get long prefix
135-
return fmt.Errorf("shared xattr with long prefix not implemented for nid %d", b.inode)
137+
// Long prefix: highest bit set
138+
longPrefixIndex := xattrEntry.NameIndex & 0x7F
139+
var err error
140+
prefix, err = b.img.getLongPrefix(longPrefixIndex)
141+
if err != nil {
142+
return fmt.Errorf("failed to get long prefix for inline xattr nid %d: %w", b.inode, err)
143+
}
136144
} else if xattrEntry.NameIndex != 0 {
137145
prefix = xattrIndex(xattrEntry.NameIndex).String()
138146
}

0 commit comments

Comments
 (0)