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
1,285 changes: 1,050 additions & 235 deletions filesystem/ext4/ext4.go

Large diffs are not rendered by default.

22 changes: 18 additions & 4 deletions filesystem/ext4/ext4_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
iofs "io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
Expand Down Expand Up @@ -217,10 +218,10 @@ func testCreateImgCopyFrom(t *testing.T, src string) string {
return outfile
}

func testCreateEmptyFile(t *testing.T, size int64) *os.File {
func testCreateEmptyFile(t *testing.T, size int64) (outfile string, f *os.File) {
t.Helper()
dir := t.TempDir()
outfile := filepath.Join(dir, "ext4.img")
outfile = filepath.Join(dir, "ext4.img")
f, err := os.Create(outfile)
if err != nil {
t.Fatalf("Error creating empty image file: %v", err)
Expand All @@ -231,7 +232,7 @@ func testCreateEmptyFile(t *testing.T, size int64) *os.File {
if err != nil {
t.Fatalf("Error truncating image file: %v", err)
}
return f
return outfile, f
}

func TestWriteFile(t *testing.T) {
Expand Down Expand Up @@ -528,14 +529,27 @@ func TestMkdir(t *testing.T) {
}

func TestCreate(t *testing.T) {
f := testCreateEmptyFile(t, 100*MB)
outfile, f := testCreateEmptyFile(t, 100*MB)
fs, err := Create(file.New(f, false), 100*MB, 0, 512, &Params{})
if err != nil {
t.Fatalf("Error creating ext4 filesystem: %v", err)
}
if fs == nil {
t.Fatalf("Expected non-nil filesystem after creation")
}
// Sync the file to disk before running e2fsck
if err := f.Sync(); err != nil {
t.Fatalf("Error syncing file: %v", err)
}
// check that the filesystem is valid using external tools
cmd := exec.Command("e2fsck", "-f", "-n", "-vv", outfile)
stdout := bytes.NewBuffer(nil)
stderr := bytes.NewBuffer(nil)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
t.Fatalf("e2fsck failed: %v,\nstdout:\n%s,\n\nstderr:\n%s", err, stdout.String(), stderr.String())
}
}

func TestChtimes(t *testing.T) {
Expand Down
25 changes: 19 additions & 6 deletions filesystem/ext4/extent.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ func (e *extent) equal(a *extent) bool {
return *e == *a
}

// blockCount how many blocks are covered in the extents
// blockCount how many filesystem blocks are covered in the extents.
// Remember that these are filesystem blocks, which can vary, not the fixed 512-byte sectors on disk,
// often used in superblock or inode in various places.
//
//nolint:unused // useful function for future
func (e extents) blockCount() uint64 {
Expand Down Expand Up @@ -669,12 +671,11 @@ func splitInternalNode(node *extentInternalNode, newChild *extentChildPtr, fs *F
}

func writeNodeToDisk(node extentBlockFinder, fs *FileSystem, parent *extentInternalNode) error {
var blockNumber uint64
if parent != nil {
blockNumber = getBlockNumberFromNode(node, parent)
} else {
blockNumber = getNewBlockNumber(fs)
// Root nodes live in the inode; only write when there's a parent block.
if parent == nil {
return nil
}
blockNumber := getBlockNumberFromNode(node, parent)

if blockNumber == 0 {
return fmt.Errorf("block number not found for node")
Expand Down Expand Up @@ -736,3 +737,15 @@ func loadChildNode(childPtr *extentChildPtr, fs *FileSystem) (extentBlockFinder,
// Implement the logic to decode the node from the data
return node, nil
}

func extentsBlockFinderFromExtents(exts extents, blocksize uint32) extentBlockFinder {
return &extentLeafNode{
extentNodeHeader: extentNodeHeader{
depth: 0,
entries: uint16(len(exts)),
max: 4, // assuming max 4 for leaf nodes in inode
blockSize: blocksize,
},
extents: exts,
}
}
18 changes: 10 additions & 8 deletions filesystem/ext4/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,16 @@ func (f *featureFlags) toInts() (compatFlags, incompatFlags, roCompatFlags uint3
features = has_journal,extent,huge_file,flex_bg,uninit_bg,64bit,dir_nlink,extra_isize
*/
var defaultFeatureFlags = featureFlags{
largeFile: true,
hugeFile: true,
sparseSuperblock: true,
flexBlockGroups: true,
hasJournal: true,
extents: true,
fs64Bit: true,
extendedAttributes: true,
largeFile: true,
hugeFile: true,
sparseSuperblock: true,
flexBlockGroups: true,
hasJournal: true,
extents: true,
fs64Bit: true,
extendedAttributes: true,
directoryEntriesRecordFileType: true,
reservedGDTBlocksForExpansion: true,
}

type FeatureOpt func(*featureFlags)
Expand Down
14 changes: 10 additions & 4 deletions filesystem/ext4/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ var _ fs.File = (*File)(nil)

// File represents a single file in an ext4 filesystem
type File struct {
*directoryEntry
*inode
isReadWrite bool
isAppend bool
offset int64
filesystem *FileSystem
extents extents
fileType directoryFileType
filename string
}

// Read reads up to len(b) bytes from the File.
Expand Down Expand Up @@ -124,9 +125,9 @@ func (fl *File) Write(b []byte) (int, error) {
if fl.size%blocksize > 0 {
newBlockCount++
}
blocksNeeded := newBlockCount - blockCount
bytesNeeded := blocksNeeded * blocksize
if newBlockCount > blockCount {
blocksNeeded := newBlockCount - blockCount
bytesNeeded := blocksNeeded * blocksize
newExtents, err := fl.filesystem.allocateExtents(bytesNeeded, &fl.extents)
if err != nil {
return 0, fmt.Errorf("could not allocate disk space for file %w", err)
Expand All @@ -136,6 +137,11 @@ func (fl *File) Write(b []byte) (int, error) {
return 0, fmt.Errorf("could not convert extents into tree: %w", err)
}
fl.inode.extents = extentTreeParsed
updatedExtents, err := fl.inode.extents.blocks(fl.filesystem)
if err != nil {
return 0, fmt.Errorf("could not read updated extents: %w", err)
}
fl.extents = updatedExtents
fl.blocks = newBlockCount
}

Expand Down Expand Up @@ -225,7 +231,7 @@ func (fl *File) Stat() (fs.FileInfo, error) {
modTime: fl.modifyTime,
name: fl.filename,
size: int64(fl.size),
isDir: fl.directoryEntry.fileType == dirFileTypeDirectory,
isDir: fl.fileType == dirFileTypeDirectory,
mode: fl.permissionsToMode(),
sys: &StatT{
UID: fl.owner,
Expand Down
2 changes: 1 addition & 1 deletion filesystem/ext4/groupdescriptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func (gds *groupDescriptors) toBytes(checksumType gdtChecksumType, hashSeed uint
}

// byFreeBlocks provides a sorted list of groupDescriptors by free blocks, descending.
// If you want them ascending, sort if.
// If you want them ascending, sort it.
func (gds *groupDescriptors) byFreeBlocks() []groupDescriptor {
// make a copy of the slice
gdSlice := make([]groupDescriptor, len(gds.descriptors))
Expand Down
21 changes: 19 additions & 2 deletions filesystem/ext4/inode.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ type inode struct {
inodeSize uint16
project uint32
extents extentBlockFinder
blockPointers [15]uint32
linkTarget string
}

Expand Down Expand Up @@ -263,7 +264,7 @@ func inodeFromBytes(b []byte, sb *superblock, number uint32) (*inode, error) {
)
if fileType == fileTypeSymbolicLink && fileSizeNum < 60 {
linkTarget = string(extentInfo[:fileSizeNum])
} else {
} else if flags.usesExtents {
// parse the extent information in the inode to get the root of the extents tree
// we do not walk the entire tree, to get a slice of blocks for the file.
// If we want to do that, we call the extentBlockFinder.blocks() method
Expand All @@ -273,6 +274,14 @@ func inodeFromBytes(b []byte, sb *superblock, number uint32) (*inode, error) {
}
}

var blockPointers [15]uint32
if !flags.usesExtents && (fileType != fileTypeSymbolicLink || fileSizeNum >= 60) {
for i := 0; i < 15; i++ {
offset := i * 4
blockPointers[i] = binary.LittleEndian.Uint32(extentInfo[offset : offset+4])
}
}

i := inode{
number: number,
permissionsGroup: parseGroupPermissions(mode),
Expand All @@ -297,6 +306,7 @@ func inodeFromBytes(b []byte, sb *superblock, number uint32) (*inode, error) {
extendedAttributeBlock: binary.LittleEndian.Uint64(extendedAttributeBlock),
project: binary.LittleEndian.Uint32(b[0x9c:0x100]),
extents: allExtents,
blockPointers: blockPointers,
linkTarget: linkTarget,
}
checksum := binary.LittleEndian.Uint32(checksumBytes)
Expand Down Expand Up @@ -369,7 +379,14 @@ func (i *inode) toBytes(sb *superblock) []byte {
copy(b[0x1c:0x20], blocks[0:4])
binary.LittleEndian.PutUint32(b[0x20:0x24], i.flags.toInt())
copy(b[0x24:0x28], version[0:4])
copy(b[0x28:0x64], i.extents.toBytes())
if i.flags != nil && i.flags.usesExtents {
copy(b[0x28:0x64], i.extents.toBytes())
} else {
for idx, ptr := range i.blockPointers {
base := 0x28 + idx*4
binary.LittleEndian.PutUint32(b[base:base+4], ptr)
}
}
binary.LittleEndian.PutUint32(b[0x64:0x68], i.nfsFileVersion)
copy(b[0x68:0x6c], extendedAttributeBlock[0:4])
copy(b[0x6c:0x70], fileSize[4:8])
Expand Down
Loading