mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Regular file reads copy data out of the image's shared mapping. When the image is backed by slow storage such as a FUSE mount, walking the mapping faults it in one page at a time, turning a large read into many small, largely serialized requests to the FUSE server. This makes erofs much slower than fsimpl/gofer for large reads on such backends. On those filesystems, read the data directly from the image file descriptor with preadv2(2) instead, so the host serves a large read with a single syscall and its own readahead, matching how fsimpl/gofer reads from host FDs. The mapping copy is kept everywhere else, since it avoids a syscall and is faster when the page cache is warm. The backing filesystem is detected at mount time via fstatfs(2); currently only FUSE opts into FD reads. Inode.Data, which returned the data as a mapping-backed BlockSeq, is replaced by Inode.DataRanges, which returns the image byte ranges that hold the inode's data.
930 lines
25 KiB
Go
930 lines
25 KiB
Go
// Copyright 2023 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package erofs provides the ability to access the contents in an EROFS [1] image.
|
|
//
|
|
// The design principle of this package is that, it will just provide the ability
|
|
// to access the contents in the image, and it will never cache any objects internally.
|
|
// The whole disk image is mapped via a read-only/shared mapping, and it relies on
|
|
// host kernel to cache the blocks/pages transparently.
|
|
//
|
|
// [1] https://docs.kernel.org/filesystems/erofs.html
|
|
package erofs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"os"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"gvisor.dev/gvisor/pkg/abi/linux"
|
|
"gvisor.dev/gvisor/pkg/cleanup"
|
|
"gvisor.dev/gvisor/pkg/errors/linuxerr"
|
|
"gvisor.dev/gvisor/pkg/gohacks"
|
|
"gvisor.dev/gvisor/pkg/hostarch"
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/marshal"
|
|
)
|
|
|
|
const (
|
|
// Definitions for superblock.
|
|
SuperBlockMagicV1 = 0xe0f5e1e2
|
|
SuperBlockOffset = 1024
|
|
|
|
// Inode slot size in bit shift.
|
|
InodeSlotBits = 5
|
|
|
|
// Max file name length.
|
|
MaxNameLen = 255
|
|
)
|
|
|
|
// Bit definitions for Inode*::Format.
|
|
const (
|
|
InodeLayoutBit = 0
|
|
InodeLayoutBits = 1
|
|
|
|
InodeDataLayoutBit = 1
|
|
InodeDataLayoutBits = 3
|
|
)
|
|
|
|
// Inode layouts.
|
|
const (
|
|
InodeLayoutCompact = 0
|
|
InodeLayoutExtended = 1
|
|
)
|
|
|
|
// Inode data layouts.
|
|
const (
|
|
InodeDataLayoutFlatPlain = iota
|
|
InodeDataLayoutFlatCompressionLegacy
|
|
InodeDataLayoutFlatInline
|
|
InodeDataLayoutFlatCompression
|
|
InodeDataLayoutChunkBased
|
|
InodeDataLayoutMax
|
|
)
|
|
|
|
// Features w/ backward compatibility.
|
|
// This is not exhaustive, unused features are not listed.
|
|
const (
|
|
FeatureCompatSuperBlockChecksum = 0x00000001
|
|
)
|
|
|
|
// Features w/o backward compatibility.
|
|
//
|
|
// Any features that aren't in FeatureIncompatSupported are incompatible
|
|
// with this implementation.
|
|
//
|
|
// This is not exhaustive, unused features are not listed.
|
|
const (
|
|
FeatureIncompatSupported = 0x0
|
|
)
|
|
|
|
// Sizes of on-disk structures in bytes.
|
|
const (
|
|
SuperBlockSize = 128
|
|
InodeCompactSize = 32
|
|
InodeExtendedSize = 64
|
|
DirentSize = 12
|
|
)
|
|
|
|
// SuperBlock represents on-disk superblock.
|
|
//
|
|
// +marshal
|
|
// +stateify savable
|
|
type SuperBlock struct {
|
|
Magic uint32
|
|
Checksum uint32
|
|
FeatureCompat uint32
|
|
BlockSizeBits uint8
|
|
ExtSlots uint8
|
|
RootNid uint16
|
|
Inodes uint64
|
|
BuildTime uint64
|
|
BuildTimeNsec uint32
|
|
Blocks uint32
|
|
MetaBlockAddr uint32
|
|
XattrBlockAddr uint32
|
|
UUID [16]uint8
|
|
VolumeName [16]uint8
|
|
FeatureIncompat uint32
|
|
Union1 uint16
|
|
ExtraDevices uint16
|
|
DevTableSlotOff uint16
|
|
Reserved [38]uint8
|
|
}
|
|
|
|
// BlockSize returns the block size.
|
|
func (sb *SuperBlock) BlockSize() uint32 {
|
|
return 1 << sb.BlockSizeBits
|
|
}
|
|
|
|
// BlockAddrToOffset converts block addr to the offset in image file.
|
|
func (sb *SuperBlock) BlockAddrToOffset(addr uint32) uint64 {
|
|
return uint64(addr) << sb.BlockSizeBits
|
|
}
|
|
|
|
// MetaOffset returns the offset of metadata area in image file.
|
|
func (sb *SuperBlock) MetaOffset() uint64 {
|
|
return sb.BlockAddrToOffset(sb.MetaBlockAddr)
|
|
}
|
|
|
|
// NidToOffset converts inode number to the offset in image file.
|
|
func (sb *SuperBlock) NidToOffset(nid uint64) uint64 {
|
|
return sb.MetaOffset() + (nid << InodeSlotBits)
|
|
}
|
|
|
|
// InodeCompact represents 32-byte reduced form of on-disk inode.
|
|
//
|
|
// +marshal
|
|
type InodeCompact struct {
|
|
Format uint16
|
|
XattrCount uint16
|
|
Mode uint16
|
|
Nlink uint16
|
|
Size uint32
|
|
Reserved uint32
|
|
RawBlockAddr uint32
|
|
Ino uint32
|
|
UID uint16
|
|
GID uint16
|
|
Reserved2 uint32
|
|
}
|
|
|
|
// InodeExtended represents 64-byte complete form of on-disk inode.
|
|
//
|
|
// +marshal
|
|
type InodeExtended struct {
|
|
Format uint16
|
|
XattrCount uint16
|
|
Mode uint16
|
|
Reserved uint16
|
|
Size uint64
|
|
RawBlockAddr uint32
|
|
Ino uint32
|
|
UID uint32
|
|
GID uint32
|
|
Mtime uint64
|
|
MtimeNsec uint32
|
|
Nlink uint32
|
|
Reserved2 [16]uint8
|
|
}
|
|
|
|
// Dirent represents on-disk directory entry.
|
|
//
|
|
// +marshal
|
|
type Dirent struct {
|
|
NidLow uint32
|
|
NidHigh uint32
|
|
NameOff uint16
|
|
FileType uint8
|
|
Reserved uint8
|
|
}
|
|
|
|
// Nid returns the inode number of the inode referenced by this dirent.
|
|
func (d *Dirent) Nid() uint64 {
|
|
// EROFS on-disk structures are always in little endian.
|
|
// TODO: This implementation does not support big endian yet.
|
|
return (uint64(d.NidHigh) << 32) | uint64(d.NidLow)
|
|
}
|
|
|
|
// Image represents an open EROFS image.
|
|
//
|
|
// +stateify savable
|
|
type Image struct {
|
|
src *os.File `state:"nosave"`
|
|
bytes []byte `state:"nosave"`
|
|
sb SuperBlock
|
|
}
|
|
|
|
// OpenImage returns an Image providing access to the contents in the image file src.
|
|
//
|
|
// On success, the ownership of src is transferred to Image.
|
|
func OpenImage(src *os.File) (*Image, error) {
|
|
i := &Image{src: src}
|
|
|
|
var cu cleanup.Cleanup
|
|
defer cu.Clean()
|
|
|
|
stat, err := i.src.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i.bytes, err = unix.Mmap(int(i.src.Fd()), 0, int(stat.Size()), unix.PROT_READ, unix.MAP_SHARED)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cu.Add(func() { unix.Munmap(i.bytes) })
|
|
|
|
if err := i.initSuperBlock(); err != nil {
|
|
return nil, err
|
|
}
|
|
cu.Release()
|
|
return i, nil
|
|
}
|
|
|
|
// Close closes the image.
|
|
func (i *Image) Close() {
|
|
unix.Munmap(i.bytes)
|
|
i.src.Close()
|
|
}
|
|
|
|
// SuperBlock returns a copy of the image's superblock.
|
|
func (i *Image) SuperBlock() SuperBlock {
|
|
return i.sb
|
|
}
|
|
|
|
// BlockSize returns the block size of this image.
|
|
func (i *Image) BlockSize() uint32 {
|
|
return i.sb.BlockSize()
|
|
}
|
|
|
|
// Blocks returns the total blocks of this image.
|
|
func (i *Image) Blocks() uint32 {
|
|
return i.sb.Blocks
|
|
}
|
|
|
|
// RootNid returns the root inode number of this image.
|
|
func (i *Image) RootNid() uint64 {
|
|
return uint64(i.sb.RootNid)
|
|
}
|
|
|
|
// initSuperBlock initializes the superblock of this image.
|
|
func (i *Image) initSuperBlock() error {
|
|
// i.sb is used in the hot path. Let's save a copy of the superblock.
|
|
if err := i.unmarshalAt(&i.sb, SuperBlockOffset); err != nil {
|
|
return fmt.Errorf("image size is too small")
|
|
}
|
|
|
|
if i.sb.Magic != SuperBlockMagicV1 {
|
|
return fmt.Errorf("unknown magic: 0x%x", i.sb.Magic)
|
|
}
|
|
|
|
if err := i.verifyChecksum(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if featureIncompat := i.sb.FeatureIncompat & ^uint32(FeatureIncompatSupported); featureIncompat != 0 {
|
|
return fmt.Errorf("unsupported incompatible features detected: 0x%x", featureIncompat)
|
|
}
|
|
|
|
if i.BlockSize()%hostarch.PageSize != 0 {
|
|
return fmt.Errorf("unsupported block size: 0x%x", i.BlockSize())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// verifyChecksum verifies the checksum of the superblock.
|
|
func (i *Image) verifyChecksum() error {
|
|
if i.sb.FeatureCompat&FeatureCompatSuperBlockChecksum == 0 {
|
|
return nil
|
|
}
|
|
|
|
sb := i.sb
|
|
sb.Checksum = 0
|
|
table := crc32.MakeTable(crc32.Castagnoli)
|
|
checksum := crc32.Checksum(marshal.Marshal(&sb), table)
|
|
|
|
off := SuperBlockOffset + uint64(i.sb.SizeBytes())
|
|
if bytes, err := i.BytesAt(off, uint64(i.BlockSize())-off); err != nil {
|
|
return fmt.Errorf("image size is too small")
|
|
} else {
|
|
checksum = ^crc32.Update(checksum, table, bytes)
|
|
}
|
|
if checksum != i.sb.Checksum {
|
|
return fmt.Errorf("invalid checksum: 0x%x, expected: 0x%x", checksum, i.sb.Checksum)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FD returns the host FD of underlying image file.
|
|
func (i *Image) FD() int {
|
|
return int(i.src.Fd())
|
|
}
|
|
|
|
// checkRange checks whether the range [off, off+n) is valid.
|
|
func (i *Image) checkRange(off, n uint64) bool {
|
|
size := uint64(len(i.bytes))
|
|
end := off + n
|
|
return off < size && off <= end && end <= size
|
|
}
|
|
|
|
// BytesAt returns the bytes at [off, off+n) of the image.
|
|
func (i *Image) BytesAt(off, n uint64) ([]byte, error) {
|
|
if ok := i.checkRange(off, n); !ok {
|
|
log.Warningf("Invalid byte range (off: 0x%x, n: 0x%x) for image (size: 0x%x)", off, n, len(i.bytes))
|
|
return nil, linuxerr.EFAULT
|
|
}
|
|
return i.bytes[off : off+n], nil
|
|
}
|
|
|
|
// checkInodeAlignment checks whether off matches inode's alignment requirement.
|
|
func checkInodeAlignment(off uint64) bool {
|
|
// Each valid inode should be aligned with an inode slot, which is
|
|
// a fixed value (32 bytes).
|
|
return off&((1<<InodeSlotBits)-1) == 0
|
|
}
|
|
|
|
// inodeFormatAt returns the format of the inode at offset off within the
|
|
// memory backed by image.
|
|
func (i *Image) inodeFormatAt(off uint64) (uint16, error) {
|
|
if ok := checkInodeAlignment(off); !ok {
|
|
return 0, linuxerr.EFAULT
|
|
}
|
|
if ok := i.checkRange(off, 2); !ok {
|
|
return 0, linuxerr.EFAULT
|
|
}
|
|
return *(*uint16)(i.pointerAt(off)), nil
|
|
}
|
|
|
|
// inodeCompactAt returns a pointer to the compact inode at offset off within
|
|
// the memory backed by image.
|
|
func (i *Image) inodeCompactAt(off uint64) (*InodeCompact, error) {
|
|
if ok := checkInodeAlignment(off); !ok {
|
|
return nil, linuxerr.EFAULT
|
|
}
|
|
if ok := i.checkRange(off, InodeCompactSize); !ok {
|
|
return nil, linuxerr.EFAULT
|
|
}
|
|
return (*InodeCompact)(i.pointerAt(off)), nil
|
|
}
|
|
|
|
// inodeExtendedAt returns a pointer to the extended inode at offset off within
|
|
// the memory backed by image.
|
|
func (i *Image) inodeExtendedAt(off uint64) (*InodeExtended, error) {
|
|
if ok := checkInodeAlignment(off); !ok {
|
|
return nil, linuxerr.EFAULT
|
|
}
|
|
if ok := i.checkRange(off, InodeExtendedSize); !ok {
|
|
return nil, linuxerr.EFAULT
|
|
}
|
|
return (*InodeExtended)(i.pointerAt(off)), nil
|
|
}
|
|
|
|
// direntAt returns a pointer to the dirent at offset off within the memory
|
|
// backed by image.
|
|
func (i *Image) direntAt(off uint64) (*Dirent, error) {
|
|
// Each valid dirent should be aligned to 4 bytes.
|
|
if off&3 != 0 {
|
|
return nil, linuxerr.EFAULT
|
|
}
|
|
if ok := i.checkRange(off, DirentSize); !ok {
|
|
return nil, linuxerr.EFAULT
|
|
}
|
|
return (*Dirent)(i.pointerAt(off)), nil
|
|
}
|
|
|
|
// unmarshalAt deserializes data from the bytes at [off, off+n) of the image.
|
|
func (i *Image) unmarshalAt(data marshal.Marshallable, off uint64) error {
|
|
bytes, err := i.BytesAt(off, uint64(data.SizeBytes()))
|
|
if err != nil {
|
|
log.Warningf("Failed to deserialize %T from 0x%x.", data, off)
|
|
return err
|
|
}
|
|
data.UnmarshalUnsafe(bytes)
|
|
return nil
|
|
}
|
|
|
|
// Inode returns the inode identified by nid.
|
|
func (i *Image) Inode(nid uint64) (Inode, error) {
|
|
inode := Inode{
|
|
image: i,
|
|
nid: nid,
|
|
}
|
|
|
|
off := i.sb.NidToOffset(nid)
|
|
if format, err := i.inodeFormatAt(off); err != nil {
|
|
return Inode{}, err
|
|
} else {
|
|
inode.format = format
|
|
}
|
|
|
|
var (
|
|
rawBlockAddr uint32
|
|
inodeSize int
|
|
)
|
|
|
|
switch layout := inode.Layout(); layout {
|
|
case InodeLayoutCompact:
|
|
ino, err := i.inodeCompactAt(off)
|
|
if err != nil {
|
|
return Inode{}, err
|
|
}
|
|
|
|
if ino.XattrCount != 0 {
|
|
log.Warningf("Unsupported xattr at inode (nid=%v)", nid)
|
|
return Inode{}, linuxerr.ENOTSUP
|
|
}
|
|
|
|
rawBlockAddr = ino.RawBlockAddr
|
|
inodeSize = ino.SizeBytes()
|
|
|
|
inode.size = uint64(ino.Size)
|
|
inode.nlink = uint32(ino.Nlink)
|
|
inode.mode = ino.Mode
|
|
inode.uid = uint32(ino.UID)
|
|
inode.gid = uint32(ino.GID)
|
|
inode.mtime = i.sb.BuildTime
|
|
inode.mtimeNsec = i.sb.BuildTimeNsec
|
|
|
|
case InodeLayoutExtended:
|
|
ino, err := i.inodeExtendedAt(off)
|
|
if err != nil {
|
|
return Inode{}, err
|
|
}
|
|
|
|
if ino.XattrCount != 0 {
|
|
log.Warningf("Unsupported xattr at inode (nid=%v)", nid)
|
|
return Inode{}, linuxerr.ENOTSUP
|
|
}
|
|
|
|
rawBlockAddr = ino.RawBlockAddr
|
|
inodeSize = ino.SizeBytes()
|
|
|
|
inode.size = ino.Size
|
|
inode.nlink = ino.Nlink
|
|
inode.mode = ino.Mode
|
|
inode.uid = ino.UID
|
|
inode.gid = ino.GID
|
|
inode.mtime = ino.Mtime
|
|
inode.mtimeNsec = ino.MtimeNsec
|
|
|
|
default:
|
|
log.Warningf("Unsupported layout 0x%x at inode (nid=%v)", layout, nid)
|
|
return Inode{}, linuxerr.ENOTSUP
|
|
}
|
|
|
|
blockSize := uint64(i.BlockSize())
|
|
inode.blocks = (inode.size + (blockSize - 1)) / blockSize
|
|
|
|
switch dataLayout := inode.DataLayout(); dataLayout {
|
|
case InodeDataLayoutFlatInline:
|
|
// Inline data follows the inode and must fit within the block that holds
|
|
// it. The inode itself may straddle a block boundary, so measure the room
|
|
// from idataOff's offset within its block, not from blockSize-inodeSize.
|
|
//
|
|
// Linux also adds xattr_isize to inodeSize when locating the inline data.
|
|
// Safe to skip here because XattrCount != 0 is rejected above; revisit if
|
|
// EROFS xattr support is added.
|
|
idataOff := off + uint64(inodeSize)
|
|
tailSize := inode.size & (blockSize - 1)
|
|
if tailSize == 0 || (idataOff&(blockSize-1))+tailSize > blockSize {
|
|
log.Warningf("Inline data not found or cross block boundary at inode (nid=%v)", nid)
|
|
return Inode{}, linuxerr.EUCLEAN
|
|
}
|
|
inode.idataOff = idataOff
|
|
fallthrough
|
|
|
|
case InodeDataLayoutFlatPlain:
|
|
inode.dataOff = i.sb.BlockAddrToOffset(rawBlockAddr)
|
|
|
|
default:
|
|
log.Warningf("Unsupported data layout 0x%x at inode (nid=%v)", dataLayout, nid)
|
|
return Inode{}, linuxerr.ENOTSUP
|
|
}
|
|
|
|
return inode, nil
|
|
}
|
|
|
|
// Inode represents in-memory inode object.
|
|
//
|
|
// +stateify savable
|
|
type Inode struct {
|
|
// image is the underlying image. Inode should not perform writable
|
|
// operations (e.g. Close()) on the image.
|
|
image *Image
|
|
|
|
// dataOff points to the data of this inode in the data blocks.
|
|
dataOff uint64
|
|
|
|
// idataOff points to the tail packing inline data of this inode
|
|
// if it's not zero in the metadata block.
|
|
idataOff uint64
|
|
|
|
// blocks indicates the count of blocks that store the data associated
|
|
// with this inode. It will count in the metadata block that includes
|
|
// the inline data as well.
|
|
blocks uint64
|
|
|
|
// format is the format of this inode.
|
|
format uint16
|
|
|
|
// Metadata.
|
|
mode uint16
|
|
nid uint64
|
|
size uint64
|
|
mtime uint64
|
|
mtimeNsec uint32
|
|
uid uint32
|
|
gid uint32
|
|
nlink uint32
|
|
}
|
|
|
|
// bitRange returns the bits within the range [bit, bit+bits) in value.
|
|
func bitRange(value, bit, bits uint16) uint16 {
|
|
return (value >> bit) & ((1 << bits) - 1)
|
|
}
|
|
|
|
// Layout returns the inode layout.
|
|
func (i *Inode) Layout() uint16 {
|
|
return bitRange(i.format, InodeLayoutBit, InodeLayoutBits)
|
|
}
|
|
|
|
// DataLayout returns the inode data layout.
|
|
func (i *Inode) DataLayout() uint16 {
|
|
return bitRange(i.format, InodeDataLayoutBit, InodeDataLayoutBits)
|
|
}
|
|
|
|
// IsRegular indicates whether i represents a regular file.
|
|
func (i *Inode) IsRegular() bool {
|
|
return i.mode&linux.S_IFMT == linux.S_IFREG
|
|
}
|
|
|
|
// IsDir indicates whether i represents a directory.
|
|
func (i *Inode) IsDir() bool {
|
|
return i.mode&linux.S_IFMT == linux.S_IFDIR
|
|
}
|
|
|
|
// IsCharDev indicates whether i represents a character device.
|
|
func (i *Inode) IsCharDev() bool {
|
|
return i.mode&linux.S_IFMT == linux.S_IFCHR
|
|
}
|
|
|
|
// IsBlockDev indicates whether i represents a block device.
|
|
func (i *Inode) IsBlockDev() bool {
|
|
return i.mode&linux.S_IFMT == linux.S_IFBLK
|
|
}
|
|
|
|
// IsFIFO indicates whether i represents a named pipe.
|
|
func (i *Inode) IsFIFO() bool {
|
|
return i.mode&linux.S_IFMT == linux.S_IFIFO
|
|
}
|
|
|
|
// IsSocket indicates whether i represents a socket.
|
|
func (i *Inode) IsSocket() bool {
|
|
return i.mode&linux.S_IFMT == linux.S_IFSOCK
|
|
}
|
|
|
|
// IsSymlink indicates whether i represents a symbolic link.
|
|
func (i *Inode) IsSymlink() bool {
|
|
return i.mode&linux.S_IFMT == linux.S_IFLNK
|
|
}
|
|
|
|
// Nid returns the inode number.
|
|
func (i *Inode) Nid() uint64 {
|
|
return i.nid
|
|
}
|
|
|
|
// Size returns the data size.
|
|
func (i *Inode) Size() uint64 {
|
|
return i.size
|
|
}
|
|
|
|
// Nlink returns the number of hard links.
|
|
func (i *Inode) Nlink() uint32 {
|
|
return i.nlink
|
|
}
|
|
|
|
// Mtime returns the time of last modification.
|
|
func (i *Inode) Mtime() uint64 {
|
|
return i.mtime
|
|
}
|
|
|
|
// MtimeNsec returns the nano second part of Mtime.
|
|
func (i *Inode) MtimeNsec() uint32 {
|
|
return i.mtimeNsec
|
|
}
|
|
|
|
// Mode returns the file type and permissions.
|
|
func (i *Inode) Mode() uint16 {
|
|
return i.mode
|
|
}
|
|
|
|
// UID returns the user ID of the owner.
|
|
func (i *Inode) UID() uint32 {
|
|
return i.uid
|
|
}
|
|
|
|
// GID returns the group ID of the owner.
|
|
func (i *Inode) GID() uint32 {
|
|
return i.gid
|
|
}
|
|
|
|
// DataOffset returns the data offset of this inode in image file.
|
|
func (i *Inode) DataOffset() (uint64, error) {
|
|
// TODO: We don't support regular files with inline data yet, which means the image
|
|
// should be created with the "-E noinline_data" option. The "-E noinline_data" option
|
|
// was introduced for the DAX feature support in Linux [1].
|
|
// [1] https://github.com/erofs/erofs-utils/commit/60549d52c3b636f0ddd1d51b0c1517c1dee22595
|
|
if dataLayout := i.DataLayout(); dataLayout != InodeDataLayoutFlatPlain {
|
|
log.Warningf("Unsupported data layout 0x%x at inode (nid=%v)", dataLayout, i.Nid())
|
|
return 0, linuxerr.ENOTSUP
|
|
}
|
|
return i.dataOff, nil
|
|
}
|
|
|
|
// FileRange represents a range of bytes within the image file.
|
|
type FileRange struct {
|
|
// Off is the offset of the range within the image file.
|
|
Off uint64
|
|
// Size is the length of the range in bytes.
|
|
Size uint64
|
|
// Bytes is the byte slice of the range within the image file.
|
|
Bytes []byte
|
|
}
|
|
|
|
// DataRanges returns the ranges within the image file that hold this inode's
|
|
// data, in file order. An inode's data occupies at most two ranges: a run of
|
|
// plain data blocks and, for files with tail-packed inline data, the inline
|
|
// tail. Entries that are not used (or hold no data) have a zero Size.
|
|
func (i *Inode) DataRanges() ([2]FileRange, error) {
|
|
var ranges [2]FileRange
|
|
switch dataLayout := i.DataLayout(); dataLayout {
|
|
case InodeDataLayoutFlatPlain:
|
|
ranges[0] = FileRange{Off: i.dataOff, Size: i.size}
|
|
|
|
case InodeDataLayoutFlatInline:
|
|
idataSize := i.size & (uint64(i.image.BlockSize()) - 1)
|
|
ranges[0] = FileRange{Off: i.dataOff, Size: i.size - idataSize}
|
|
ranges[1] = FileRange{Off: i.idataOff, Size: idataSize}
|
|
|
|
default:
|
|
log.Warningf("Unsupported data layout 0x%x at inode (nid=%v)", dataLayout, i.Nid())
|
|
return [2]FileRange{}, linuxerr.ENOTSUP
|
|
}
|
|
for j := range ranges {
|
|
if ranges[j].Size == 0 {
|
|
continue
|
|
}
|
|
bytes, err := i.image.BytesAt(ranges[j].Off, ranges[j].Size)
|
|
if err != nil {
|
|
return [2]FileRange{}, err
|
|
}
|
|
ranges[j].Bytes = bytes
|
|
}
|
|
return ranges, nil
|
|
}
|
|
|
|
// blockData represents the information of the data in a block.
|
|
type blockData struct {
|
|
// base indicates the data offset within the image.
|
|
base uint64
|
|
// size indicates the data size.
|
|
size uint32
|
|
}
|
|
|
|
// valid indicates whether this is valid information about the data in a block.
|
|
func (b *blockData) valid() bool {
|
|
// The data offset within the image will never be zero.
|
|
return b.base > 0
|
|
}
|
|
|
|
// getBlockDataInfo returns the information of the data in the block identified by
|
|
// blockIdx of this inode.
|
|
//
|
|
// Precondition: blockIdx < i.blocks.
|
|
func (i *Inode) getBlockDataInfo(blockIdx uint64) blockData {
|
|
blockSize := i.image.BlockSize()
|
|
lastBlock := blockIdx == i.blocks-1
|
|
base := i.idataOff
|
|
if !lastBlock || base == 0 {
|
|
base = i.dataOff + blockIdx*uint64(blockSize)
|
|
}
|
|
size := blockSize
|
|
if lastBlock {
|
|
if tailSize := uint32(i.size) & (blockSize - 1); tailSize != 0 {
|
|
size = tailSize
|
|
}
|
|
}
|
|
return blockData{base, size}
|
|
}
|
|
|
|
// getDirentName returns the name of dirent d in the given block of this inode.
|
|
//
|
|
// The on-disk format of one block looks like this:
|
|
//
|
|
// ___________________________
|
|
// / |
|
|
// / ______________|________________
|
|
// / / | nameoff1 | nameoffN-1
|
|
// ____________.______________._______________v________________v__________
|
|
// | dirent | dirent | ... | dirent | filename | filename | ... | filename |
|
|
// |___.0___|____1___|_____|___N-1__|____0_____|____1_____|_____|___N-1____|
|
|
// \ ^
|
|
// \ | * could have
|
|
// \ | trailing '\0'
|
|
// \________________________| nameoff0
|
|
// Directory block
|
|
//
|
|
// The on-disk format of one directory looks like this:
|
|
//
|
|
// [ (block 1) dirent 1 | dirent 2 | dirent 3 | name 1 | name 2 | name 3 | optional padding ]
|
|
// [ (block 2) dirent 4 | dirent 5 | name 4 | name 5 | optional padding ]
|
|
// ...
|
|
// [ (block N) dirent M | dirent M+1 | name M | name M+1 | optional padding ]
|
|
//
|
|
// [ (metadata block) inode | optional fields | dirent M+2 | dirent M+3 | name M+2 | name M+3 | optional padding ]
|
|
//
|
|
// Refer: https://docs.kernel.org/filesystems/erofs.html#directories
|
|
func (i *Inode) getDirentName(d *Dirent, block blockData, lastDirent bool) ([]byte, error) {
|
|
corruptionErr := func() ([]byte, error) {
|
|
log.Warningf("Corrupted dirent at inode (nid=%v)", i.Nid())
|
|
return nil, linuxerr.EUCLEAN
|
|
}
|
|
nameEnd := block.size
|
|
if !lastDirent {
|
|
nameEnd = uint32(direntAfter(d).NameOff)
|
|
}
|
|
if uint32(d.NameOff) >= nameEnd || nameEnd > block.size {
|
|
return corruptionErr()
|
|
}
|
|
nameLen := uint64(nameEnd - uint32(d.NameOff))
|
|
name, err := i.image.BytesAt(block.base+uint64(d.NameOff), nameLen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if lastDirent {
|
|
if n := bytes.IndexByte(name, 0); n != -1 {
|
|
name = name[:n]
|
|
}
|
|
}
|
|
if len(name) == 0 || len(name) > MaxNameLen {
|
|
return corruptionErr()
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
// getDirent0 returns a pointer to the first dirent in the given block of this inode.
|
|
func (i *Inode) getDirent0(block blockData) (*Dirent, error) {
|
|
d0, err := i.image.direntAt(block.base)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if d0.NameOff < DirentSize || uint32(d0.NameOff) >= block.size {
|
|
log.Warningf("Invalid nameOff0 %v at inode (nid=%v)", d0.NameOff, i.Nid())
|
|
return nil, linuxerr.EUCLEAN
|
|
}
|
|
return d0, nil
|
|
}
|
|
|
|
// Lookup looks up a child by the name. The child inode number will be returned on success.
|
|
func (i *Inode) Lookup(name string) (uint64, error) {
|
|
if !i.IsDir() {
|
|
return 0, linuxerr.ENOTDIR
|
|
}
|
|
|
|
// Currently (Go 1.21), there is no safe and efficient way to do three-way
|
|
// string comparisons, so let's convert the string to a byte slice first.
|
|
nameBytes := gohacks.ImmutableBytesFromString(name)
|
|
|
|
// In EROFS, all directory entries are _strictly_ recorded in alphabetical
|
|
// order. The lookup is done by directly performing binary search on the
|
|
// disk data similar to what Linux does in fs/erofs/namei.c:erofs_namei().
|
|
var (
|
|
targetBlock blockData
|
|
targetNumDirents uint16
|
|
)
|
|
|
|
// Find the block that may contain the target dirent first.
|
|
bLeft, bRight := int64(0), int64(i.blocks)-1
|
|
for bLeft <= bRight {
|
|
// Cast to uint64 to avoid overflow.
|
|
mid := uint64(bLeft+bRight) >> 1
|
|
block := i.getBlockDataInfo(mid)
|
|
d0, err := i.getDirent0(block)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
numDirents := d0.NameOff / DirentSize
|
|
d0Name, err := i.getDirentName(d0, block, numDirents == 1)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
switch bytes.Compare(nameBytes, d0Name) {
|
|
case 0:
|
|
// Found the target dirent.
|
|
return d0.Nid(), nil
|
|
case 1:
|
|
// name > d0Name, this block may contain the target dirent.
|
|
targetBlock = block
|
|
targetNumDirents = numDirents
|
|
bLeft = int64(mid) + 1
|
|
case -1:
|
|
// name < d0Name, this is not the block we're looking for.
|
|
bRight = int64(mid) - 1
|
|
}
|
|
}
|
|
|
|
if !targetBlock.valid() {
|
|
// The target block was not found.
|
|
return 0, linuxerr.ENOENT
|
|
}
|
|
|
|
// Find the target dirent in the target block. Note that, as the 0th dirent
|
|
// has already been checked during the block binary search, we don't need to
|
|
// check it again and can define dLeft/dRight as unsigned types.
|
|
dLeft, dRight := uint16(1), targetNumDirents-1
|
|
for dLeft <= dRight {
|
|
// The sum will never lead to a uint16 overflow, as the maximum value of
|
|
// the operands is MaxUint16/DirentSize.
|
|
mid := (dLeft + dRight) >> 1
|
|
direntOff := targetBlock.base + uint64(mid)*DirentSize
|
|
d, err := i.image.direntAt(direntOff)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
dName, err := i.getDirentName(d, targetBlock, mid == targetNumDirents-1)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
switch bytes.Compare(nameBytes, dName) {
|
|
case 0:
|
|
// Found the target dirent.
|
|
return d.Nid(), nil
|
|
case 1:
|
|
// name > dName.
|
|
dLeft = mid + 1
|
|
case -1:
|
|
// name < dName.
|
|
dRight = mid - 1
|
|
}
|
|
}
|
|
|
|
return 0, linuxerr.ENOENT
|
|
}
|
|
|
|
// IterDirents invokes cb on each entry in the directory represented by this inode.
|
|
// The directory entries will be iterated in alphabetical order.
|
|
func (i *Inode) IterDirents(cb func(name string, typ uint8, nid uint64) error) error {
|
|
if !i.IsDir() {
|
|
return linuxerr.ENOTDIR
|
|
}
|
|
|
|
// Iterate all the blocks which contain dirents.
|
|
for blockIdx := uint64(0); blockIdx < i.blocks; blockIdx++ {
|
|
block := i.getBlockDataInfo(blockIdx)
|
|
d, err := i.getDirent0(block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Iterate all the dirents in this block.
|
|
numDirents := d.NameOff / DirentSize
|
|
for {
|
|
name, err := i.getDirentName(d, block, numDirents == 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := cb(string(name), d.FileType, d.Nid()); err != nil {
|
|
return err
|
|
}
|
|
if numDirents--; numDirents == 0 {
|
|
break
|
|
}
|
|
d = direntAfter(d)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Readlink reads the link target.
|
|
func (i *Inode) Readlink() (string, error) {
|
|
if !i.IsSymlink() {
|
|
return "", linuxerr.EINVAL
|
|
}
|
|
off := i.dataOff
|
|
size := i.size
|
|
if i.idataOff != 0 {
|
|
// Inline symlink data shouldn't cross block boundary.
|
|
if i.blocks > 1 {
|
|
log.Warningf("Inline data cross block boundary at inode (nid=%v)", i.Nid())
|
|
return "", linuxerr.EUCLEAN
|
|
}
|
|
off = i.idataOff
|
|
} else {
|
|
// This matches Linux's behaviour in fs/namei.c:page_get_link() and
|
|
// include/linux/namei.h:nd_terminate_link().
|
|
if size > hostarch.PageSize-1 {
|
|
size = hostarch.PageSize - 1
|
|
}
|
|
}
|
|
target, err := i.image.BytesAt(off, size)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(target), nil
|
|
}
|