Files
MontaukOS/kernel/src/Fs/Ext2.cpp
T

1826 lines
69 KiB
C++

/*
* Ext2.cpp
* ext2 filesystem driver
* Copyright (c) 2026 Daniel Hammer
*/
#include "Ext2.hpp"
#include "FsProbe.hpp"
#include <Drivers/Storage/BlockDevice.hpp>
#include <Terminal/Terminal.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/PageFrameAllocator.hpp>
using namespace Kt;
namespace Fs::Ext2 {
// =========================================================================
// Constants
// =========================================================================
static constexpr int MaxInstances = 8;
static constexpr int MaxFilesPerInstance = 16;
static constexpr int MaxDirEntries = 128;
static constexpr int MaxNameLen = 256;
static constexpr uint16_t EXT2_MAGIC = 0xEF53;
// Inode types (from i_mode, upper 4 bits)
static constexpr uint16_t IMODE_FIFO = 0x1000;
static constexpr uint16_t IMODE_CHARDEV = 0x2000;
static constexpr uint16_t IMODE_DIR = 0x4000;
static constexpr uint16_t IMODE_BLKDEV = 0x6000;
static constexpr uint16_t IMODE_REG = 0x8000;
static constexpr uint16_t IMODE_SYMLINK = 0xA000;
static constexpr uint16_t IMODE_SOCKET = 0xC000;
static constexpr uint16_t IMODE_TYPE_MASK = 0xF000;
// Directory entry file types
static constexpr uint8_t EXT2_FT_UNKNOWN = 0;
static constexpr uint8_t EXT2_FT_REG_FILE = 1;
static constexpr uint8_t EXT2_FT_DIR = 2;
static constexpr uint8_t EXT2_FT_CHRDEV = 3;
static constexpr uint8_t EXT2_FT_BLKDEV = 4;
static constexpr uint8_t EXT2_FT_FIFO = 5;
static constexpr uint8_t EXT2_FT_SOCK = 6;
static constexpr uint8_t EXT2_FT_SYMLINK = 7;
// Special inode numbers
static constexpr uint32_t EXT2_ROOT_INODE = 2;
// =========================================================================
// On-disk structures
// =========================================================================
struct Superblock {
uint32_t s_inodes_count;
uint32_t s_blocks_count;
uint32_t s_r_blocks_count;
uint32_t s_free_blocks_count;
uint32_t s_free_inodes_count;
uint32_t s_first_data_block;
uint32_t s_log_block_size;
uint32_t s_log_frag_size;
uint32_t s_blocks_per_group;
uint32_t s_frags_per_group;
uint32_t s_inodes_per_group;
uint32_t s_mtime;
uint32_t s_wtime;
uint16_t s_mnt_count;
uint16_t s_max_mnt_count;
uint16_t s_magic;
uint16_t s_state;
uint16_t s_errors;
uint16_t s_minor_rev_level;
uint32_t s_lastcheck;
uint32_t s_checkinterval;
uint32_t s_creator_os;
uint32_t s_rev_level;
uint16_t s_def_resuid;
uint16_t s_def_resgid;
// Rev 1 fields
uint32_t s_first_ino;
uint16_t s_inode_size;
uint16_t s_block_group_nr;
uint32_t s_feature_compat;
uint32_t s_feature_incompat;
uint32_t s_feature_ro_compat;
uint8_t s_uuid[16];
char s_volume_name[16];
// ... more fields follow but are not needed
} __attribute__((packed));
struct BlockGroupDescriptor {
uint32_t bg_block_bitmap;
uint32_t bg_inode_bitmap;
uint32_t bg_inode_table;
uint16_t bg_free_blocks_count;
uint16_t bg_free_inodes_count;
uint16_t bg_used_dirs_count;
uint16_t bg_pad;
uint8_t bg_reserved[12];
} __attribute__((packed));
struct Inode {
uint16_t i_mode;
uint16_t i_uid;
uint32_t i_size;
uint32_t i_atime;
uint32_t i_ctime;
uint32_t i_mtime;
uint32_t i_dtime;
uint16_t i_gid;
uint16_t i_links_count;
uint32_t i_blocks;
uint32_t i_flags;
uint32_t i_osd1;
uint32_t i_block[15]; // 0-11: direct, 12: indirect, 13: double-indirect, 14: triple-indirect
uint32_t i_generation;
uint32_t i_file_acl;
uint32_t i_dir_acl; // upper 32 bits of size for regular files in rev 1
uint32_t i_faddr;
uint8_t i_osd2[12];
} __attribute__((packed));
struct DirEntry {
uint32_t inode;
uint16_t rec_len;
uint8_t name_len;
uint8_t file_type;
// name follows (up to 255 bytes, NOT null-terminated on disk)
} __attribute__((packed));
// =========================================================================
// Types
// =========================================================================
struct Ext2File {
bool inUse;
uint32_t inodeNum;
Inode inode;
bool isDirectory;
};
struct Ext2Instance {
bool active;
int blockDevIndex;
uint64_t partStartLba;
// Superblock fields
uint32_t blockSize; // 1024 << s_log_block_size
uint32_t inodeSize;
uint32_t inodesPerGroup;
uint32_t blocksPerGroup;
uint32_t totalInodes;
uint32_t totalBlocks;
uint32_t firstDataBlock;
uint32_t groupCount;
char volumeLabel[17];
// Block group descriptor table (cached in memory)
BlockGroupDescriptor* bgdt;
int bgdtPages;
// Temporary block buffer (one block, page-aligned)
uint8_t* blockBuf;
int blockBufPages;
// Open file handles
Ext2File files[MaxFilesPerInstance];
// ReadDir name cache
char dirNames[MaxDirEntries][MaxNameLen];
int dirNameCount;
};
// =========================================================================
// Instance table
// =========================================================================
static Ext2Instance g_instances[MaxInstances] = {};
static int g_instanceCount = 0;
// =========================================================================
// Low-level helpers
// =========================================================================
static bool ReadPartSectors(const Ext2Instance& inst, uint64_t partSector,
uint32_t count, void* buf) {
auto* dev = Drivers::Storage::GetBlockDevice(inst.blockDevIndex);
if (!dev) return false;
return dev->ReadSectors(dev->Ctx, inst.partStartLba + partSector, count, buf);
}
static bool WritePartSectors(const Ext2Instance& inst, uint64_t partSector,
uint32_t count, const void* buf) {
auto* dev = Drivers::Storage::GetBlockDevice(inst.blockDevIndex);
if (!dev) return false;
return dev->WriteSectors(dev->Ctx, inst.partStartLba + partSector, count, buf);
}
// Convert a block number to a partition-relative sector number
static uint64_t BlockToPartSector(const Ext2Instance& inst, uint32_t block) {
return (uint64_t)block * (inst.blockSize / 512);
}
static uint32_t SectorsPerBlock(const Ext2Instance& inst) {
return inst.blockSize / 512;
}
static bool ReadBlock(const Ext2Instance& inst, uint32_t blockNum, void* buf) {
if (blockNum == 0) return false;
return ReadPartSectors(inst, BlockToPartSector(inst, blockNum),
SectorsPerBlock(inst), buf);
}
static bool WriteBlock(const Ext2Instance& inst, uint32_t blockNum, const void* buf) {
if (blockNum == 0) return false;
return WritePartSectors(inst, BlockToPartSector(inst, blockNum),
SectorsPerBlock(inst), buf);
}
// =========================================================================
// Inode operations
// =========================================================================
static bool ReadInode(Ext2Instance& inst, uint32_t inodeNum, Inode* out) {
if (inodeNum == 0 || inodeNum > inst.totalInodes) return false;
uint32_t group = (inodeNum - 1) / inst.inodesPerGroup;
uint32_t indexInGroup = (inodeNum - 1) % inst.inodesPerGroup;
if (group >= inst.groupCount) return false;
uint32_t inodeTableBlock = inst.bgdt[group].bg_inode_table;
uint32_t inodeByteOffset = indexInGroup * inst.inodeSize;
uint32_t blockOffset = inodeByteOffset / inst.blockSize;
uint32_t offsetInBlock = inodeByteOffset % inst.blockSize;
if (!ReadBlock(inst, inodeTableBlock + blockOffset, inst.blockBuf)) return false;
memcpy(out, inst.blockBuf + offsetInBlock, sizeof(Inode));
return true;
}
static bool WriteInode(Ext2Instance& inst, uint32_t inodeNum, const Inode* inode) {
if (inodeNum == 0 || inodeNum > inst.totalInodes) return false;
uint32_t group = (inodeNum - 1) / inst.inodesPerGroup;
uint32_t indexInGroup = (inodeNum - 1) % inst.inodesPerGroup;
if (group >= inst.groupCount) return false;
uint32_t inodeTableBlock = inst.bgdt[group].bg_inode_table;
uint32_t inodeByteOffset = indexInGroup * inst.inodeSize;
uint32_t blockOffset = inodeByteOffset / inst.blockSize;
uint32_t offsetInBlock = inodeByteOffset % inst.blockSize;
if (!ReadBlock(inst, inodeTableBlock + blockOffset, inst.blockBuf)) return false;
memcpy(inst.blockBuf + offsetInBlock, inode, sizeof(Inode));
return WriteBlock(inst, inodeTableBlock + blockOffset, inst.blockBuf);
}
// =========================================================================
// Block addressing — resolve logical block index to physical block number
// =========================================================================
// Returns the physical block number for logical block index `logicalIdx`
// within the given inode. Handles direct, indirect, double-indirect, and
// triple-indirect blocks.
static uint32_t GetPhysicalBlock(Ext2Instance& inst, const Inode& inode,
uint32_t logicalIdx) {
uint32_t ptrsPerBlock = inst.blockSize / 4;
// Direct blocks (0-11)
if (logicalIdx < 12) {
return inode.i_block[logicalIdx];
}
logicalIdx -= 12;
// Single indirect (block 12)
if (logicalIdx < ptrsPerBlock) {
if (inode.i_block[12] == 0) return 0;
if (!ReadBlock(inst, inode.i_block[12], inst.blockBuf)) return 0;
uint32_t block;
memcpy(&block, inst.blockBuf + logicalIdx * 4, 4);
return block;
}
logicalIdx -= ptrsPerBlock;
// Double indirect (block 13)
uint32_t dblRange = ptrsPerBlock * ptrsPerBlock;
if (logicalIdx < dblRange) {
if (inode.i_block[13] == 0) return 0;
if (!ReadBlock(inst, inode.i_block[13], inst.blockBuf)) return 0;
uint32_t idx1 = logicalIdx / ptrsPerBlock;
uint32_t idx2 = logicalIdx % ptrsPerBlock;
uint32_t indirectBlock;
memcpy(&indirectBlock, inst.blockBuf + idx1 * 4, 4);
if (indirectBlock == 0) return 0;
if (!ReadBlock(inst, indirectBlock, inst.blockBuf)) return 0;
uint32_t block;
memcpy(&block, inst.blockBuf + idx2 * 4, 4);
return block;
}
logicalIdx -= dblRange;
// Triple indirect (block 14)
uint32_t triRange = ptrsPerBlock * ptrsPerBlock * ptrsPerBlock;
if (logicalIdx < triRange) {
if (inode.i_block[14] == 0) return 0;
if (!ReadBlock(inst, inode.i_block[14], inst.blockBuf)) return 0;
uint32_t idx1 = logicalIdx / (ptrsPerBlock * ptrsPerBlock);
uint32_t rem = logicalIdx % (ptrsPerBlock * ptrsPerBlock);
uint32_t idx2 = rem / ptrsPerBlock;
uint32_t idx3 = rem % ptrsPerBlock;
uint32_t dblBlock;
memcpy(&dblBlock, inst.blockBuf + idx1 * 4, 4);
if (dblBlock == 0) return 0;
if (!ReadBlock(inst, dblBlock, inst.blockBuf)) return 0;
uint32_t indBlock;
memcpy(&indBlock, inst.blockBuf + idx2 * 4, 4);
if (indBlock == 0) return 0;
if (!ReadBlock(inst, indBlock, inst.blockBuf)) return 0;
uint32_t block;
memcpy(&block, inst.blockBuf + idx3 * 4, 4);
return block;
}
return 0; // beyond addressable range
}
// =========================================================================
// Block allocation
// =========================================================================
static uint32_t AllocateBlock(Ext2Instance& inst, uint32_t preferGroup) {
// Try the preferred group first, then scan all groups
for (uint32_t attempt = 0; attempt < inst.groupCount; attempt++) {
uint32_t g = (preferGroup + attempt) % inst.groupCount;
if (inst.bgdt[g].bg_free_blocks_count == 0) continue;
uint32_t bitmapBlock = inst.bgdt[g].bg_block_bitmap;
if (!ReadBlock(inst, bitmapBlock, inst.blockBuf)) continue;
uint32_t blocksInGroup = inst.blocksPerGroup;
// Last group may have fewer blocks
if (g == inst.groupCount - 1) {
uint32_t remaining = inst.totalBlocks - g * inst.blocksPerGroup;
if (remaining < blocksInGroup) blocksInGroup = remaining;
}
for (uint32_t bit = 0; bit < blocksInGroup; bit++) {
uint32_t byteIdx = bit / 8;
uint8_t bitMask = 1 << (bit % 8);
if (!(inst.blockBuf[byteIdx] & bitMask)) {
// Found a free block — mark it used
inst.blockBuf[byteIdx] |= bitMask;
if (!WriteBlock(inst, bitmapBlock, inst.blockBuf)) continue;
inst.bgdt[g].bg_free_blocks_count--;
// Write updated BGDT entry back to disk
uint32_t bgdtBlock = inst.firstDataBlock + 1;
uint32_t bgdtOffset = g * sizeof(BlockGroupDescriptor);
uint32_t bgdtBlockIdx = bgdtBlock + bgdtOffset / inst.blockSize;
uint32_t bgdtOffInBlock = bgdtOffset % inst.blockSize;
uint8_t* tmpBuf = inst.blockBuf;
if (ReadBlock(inst, bgdtBlockIdx, tmpBuf)) {
memcpy(tmpBuf + bgdtOffInBlock, &inst.bgdt[g],
sizeof(BlockGroupDescriptor));
WriteBlock(inst, bgdtBlockIdx, tmpBuf);
}
return inst.firstDataBlock + g * inst.blocksPerGroup + bit;
}
}
}
return 0; // no free blocks
}
static void FreeBlock(Ext2Instance& inst, uint32_t blockNum) {
if (blockNum < inst.firstDataBlock || blockNum >= inst.totalBlocks) return;
uint32_t adjusted = blockNum - inst.firstDataBlock;
uint32_t g = adjusted / inst.blocksPerGroup;
uint32_t bit = adjusted % inst.blocksPerGroup;
if (g >= inst.groupCount) return;
uint32_t bitmapBlock = inst.bgdt[g].bg_block_bitmap;
if (!ReadBlock(inst, bitmapBlock, inst.blockBuf)) return;
uint32_t byteIdx = bit / 8;
uint8_t bitMask = 1 << (bit % 8);
inst.blockBuf[byteIdx] &= ~bitMask;
WriteBlock(inst, bitmapBlock, inst.blockBuf);
inst.bgdt[g].bg_free_blocks_count++;
// Write updated BGDT
uint32_t bgdtBlock = inst.firstDataBlock + 1;
uint32_t bgdtOffset = g * sizeof(BlockGroupDescriptor);
uint32_t bgdtBlockIdx = bgdtBlock + bgdtOffset / inst.blockSize;
uint32_t bgdtOffInBlock = bgdtOffset % inst.blockSize;
if (ReadBlock(inst, bgdtBlockIdx, inst.blockBuf)) {
memcpy(inst.blockBuf + bgdtOffInBlock, &inst.bgdt[g],
sizeof(BlockGroupDescriptor));
WriteBlock(inst, bgdtBlockIdx, inst.blockBuf);
}
}
// =========================================================================
// Inode allocation
// =========================================================================
static uint32_t AllocateInode(Ext2Instance& inst, uint32_t preferGroup) {
for (uint32_t attempt = 0; attempt < inst.groupCount; attempt++) {
uint32_t g = (preferGroup + attempt) % inst.groupCount;
if (inst.bgdt[g].bg_free_inodes_count == 0) continue;
uint32_t bitmapBlock = inst.bgdt[g].bg_inode_bitmap;
if (!ReadBlock(inst, bitmapBlock, inst.blockBuf)) continue;
for (uint32_t bit = 0; bit < inst.inodesPerGroup; bit++) {
uint32_t byteIdx = bit / 8;
uint8_t bitMask = 1 << (bit % 8);
if (!(inst.blockBuf[byteIdx] & bitMask)) {
inst.blockBuf[byteIdx] |= bitMask;
if (!WriteBlock(inst, bitmapBlock, inst.blockBuf)) continue;
inst.bgdt[g].bg_free_inodes_count--;
// Write updated BGDT entry
uint32_t bgdtBlock = inst.firstDataBlock + 1;
uint32_t bgdtOffset = g * sizeof(BlockGroupDescriptor);
uint32_t bgdtBlockIdx = bgdtBlock + bgdtOffset / inst.blockSize;
uint32_t bgdtOffInBlock = bgdtOffset % inst.blockSize;
if (ReadBlock(inst, bgdtBlockIdx, inst.blockBuf)) {
memcpy(inst.blockBuf + bgdtOffInBlock, &inst.bgdt[g],
sizeof(BlockGroupDescriptor));
WriteBlock(inst, bgdtBlockIdx, inst.blockBuf);
}
return g * inst.inodesPerGroup + bit + 1; // inodes are 1-based
}
}
}
return 0;
}
static void FreeInode(Ext2Instance& inst, uint32_t inodeNum) {
if (inodeNum == 0 || inodeNum > inst.totalInodes) return;
uint32_t g = (inodeNum - 1) / inst.inodesPerGroup;
uint32_t bit = (inodeNum - 1) % inst.inodesPerGroup;
if (g >= inst.groupCount) return;
uint32_t bitmapBlock = inst.bgdt[g].bg_inode_bitmap;
if (!ReadBlock(inst, bitmapBlock, inst.blockBuf)) return;
uint32_t byteIdx = bit / 8;
uint8_t bitMask = 1 << (bit % 8);
inst.blockBuf[byteIdx] &= ~bitMask;
WriteBlock(inst, bitmapBlock, inst.blockBuf);
inst.bgdt[g].bg_free_inodes_count++;
uint32_t bgdtBlock = inst.firstDataBlock + 1;
uint32_t bgdtOffset = g * sizeof(BlockGroupDescriptor);
uint32_t bgdtBlockIdx = bgdtBlock + bgdtOffset / inst.blockSize;
uint32_t bgdtOffInBlock = bgdtOffset % inst.blockSize;
if (ReadBlock(inst, bgdtBlockIdx, inst.blockBuf)) {
memcpy(inst.blockBuf + bgdtOffInBlock, &inst.bgdt[g],
sizeof(BlockGroupDescriptor));
WriteBlock(inst, bgdtBlockIdx, inst.blockBuf);
}
}
// =========================================================================
// Block assignment to inode — set a logical block index to a physical block
// =========================================================================
// Allocate an indirect block if needed, zero it, and return its number.
static uint32_t EnsureIndirectBlock(Ext2Instance& inst, uint32_t* blockPtr,
uint32_t preferGroup) {
if (*blockPtr != 0) return *blockPtr;
uint32_t newBlock = AllocateBlock(inst, preferGroup);
if (newBlock == 0) return 0;
// Zero the new indirect block
memset(inst.blockBuf, 0, inst.blockSize);
WriteBlock(inst, newBlock, inst.blockBuf);
*blockPtr = newBlock;
return newBlock;
}
// Set logical block `logicalIdx` in `inode` to point to `physBlock`.
// Allocates indirect blocks as needed. Returns true on success.
static bool SetPhysicalBlock(Ext2Instance& inst, Inode& inode,
uint32_t logicalIdx, uint32_t physBlock,
uint32_t preferGroup) {
uint32_t ptrsPerBlock = inst.blockSize / 4;
// Direct blocks (0-11)
if (logicalIdx < 12) {
inode.i_block[logicalIdx] = physBlock;
return true;
}
logicalIdx -= 12;
// Single indirect
if (logicalIdx < ptrsPerBlock) {
uint32_t indBlock = EnsureIndirectBlock(inst, &inode.i_block[12], preferGroup);
if (indBlock == 0) return false;
if (!ReadBlock(inst, indBlock, inst.blockBuf)) return false;
memcpy(inst.blockBuf + logicalIdx * 4, &physBlock, 4);
return WriteBlock(inst, indBlock, inst.blockBuf);
}
logicalIdx -= ptrsPerBlock;
// Double indirect
uint32_t dblRange = ptrsPerBlock * ptrsPerBlock;
if (logicalIdx < dblRange) {
uint32_t dblBlock = EnsureIndirectBlock(inst, &inode.i_block[13], preferGroup);
if (dblBlock == 0) return false;
if (!ReadBlock(inst, dblBlock, inst.blockBuf)) return false;
uint32_t idx1 = logicalIdx / ptrsPerBlock;
uint32_t idx2 = logicalIdx % ptrsPerBlock;
uint32_t indBlock;
memcpy(&indBlock, inst.blockBuf + idx1 * 4, 4);
// Need a separate buffer for the double-indirect table since
// EnsureIndirectBlock uses blockBuf. Save and restore.
uint8_t savedPtr[4];
if (indBlock == 0) {
indBlock = AllocateBlock(inst, preferGroup);
if (indBlock == 0) return false;
memset(inst.blockBuf, 0, inst.blockSize);
WriteBlock(inst, indBlock, inst.blockBuf);
// Re-read the double-indirect block and update
if (!ReadBlock(inst, dblBlock, inst.blockBuf)) return false;
memcpy(inst.blockBuf + idx1 * 4, &indBlock, 4);
if (!WriteBlock(inst, dblBlock, inst.blockBuf)) return false;
}
if (!ReadBlock(inst, indBlock, inst.blockBuf)) return false;
memcpy(inst.blockBuf + idx2 * 4, &physBlock, 4);
return WriteBlock(inst, indBlock, inst.blockBuf);
}
logicalIdx -= dblRange;
// Triple indirect — not implemented (would require very large files)
return false;
}
// Free all data blocks belonging to an inode (direct + indirect trees)
static void FreeInodeBlocks(Ext2Instance& inst, Inode& inode) {
uint32_t ptrsPerBlock = inst.blockSize / 4;
// Free direct blocks
for (int i = 0; i < 12; i++) {
if (inode.i_block[i]) {
FreeBlock(inst, inode.i_block[i]);
inode.i_block[i] = 0;
}
}
// Free single indirect
if (inode.i_block[12]) {
// Need a temporary buffer since blockBuf is used by FreeBlock
uint32_t indBlock = inode.i_block[12];
// Allocate a temp page for reading the indirect block
uint8_t* tmpBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (tmpBuf && ReadBlock(inst, indBlock, tmpBuf)) {
for (uint32_t i = 0; i < ptrsPerBlock; i++) {
uint32_t b;
memcpy(&b, tmpBuf + i * 4, 4);
if (b) FreeBlock(inst, b);
}
}
if (tmpBuf) Memory::g_pfa->Free(tmpBuf);
FreeBlock(inst, indBlock);
inode.i_block[12] = 0;
}
// Free double indirect
if (inode.i_block[13]) {
uint32_t dblBlock = inode.i_block[13];
uint8_t* tmpBuf1 = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (tmpBuf1 && ReadBlock(inst, dblBlock, tmpBuf1)) {
uint8_t* tmpBuf2 = (uint8_t*)Memory::g_pfa->AllocateZeroed();
for (uint32_t i = 0; i < ptrsPerBlock; i++) {
uint32_t indBlock;
memcpy(&indBlock, tmpBuf1 + i * 4, 4);
if (indBlock == 0) continue;
if (tmpBuf2 && ReadBlock(inst, indBlock, tmpBuf2)) {
for (uint32_t j = 0; j < ptrsPerBlock; j++) {
uint32_t b;
memcpy(&b, tmpBuf2 + j * 4, 4);
if (b) FreeBlock(inst, b);
}
}
FreeBlock(inst, indBlock);
}
if (tmpBuf2) Memory::g_pfa->Free(tmpBuf2);
}
if (tmpBuf1) Memory::g_pfa->Free(tmpBuf1);
FreeBlock(inst, dblBlock);
inode.i_block[13] = 0;
}
// Triple indirect — free recursively if present
if (inode.i_block[14]) {
// For simplicity, just free the top-level triple indirect block.
// Full triple-indirect traversal is rare and complex.
FreeBlock(inst, inode.i_block[14]);
inode.i_block[14] = 0;
}
inode.i_blocks = 0;
inode.i_size = 0;
}
// =========================================================================
// String helpers
// =========================================================================
static char ToLower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}
static bool StrEqual(const char* a, const char* b) {
while (*a && *b) {
if (*a != *b) return false;
a++; b++;
}
return *a == *b;
}
// Split a path into parent directory path and filename
static void SplitPath(const char* path, char* parentPath, int parentMax,
char* fileName, int nameMax) {
while (*path == '/') path++;
int len = 0;
while (path[len]) len++;
int lastSlash = -1;
for (int i = len - 1; i >= 0; i--) {
if (path[i] == '/') { lastSlash = i; break; }
}
if (lastSlash < 0) {
parentPath[0] = '\0';
int j = 0;
while (j < nameMax - 1 && path[j]) { fileName[j] = path[j]; j++; }
fileName[j] = '\0';
} else {
int j = 0;
for (int i = 0; i < lastSlash && j < parentMax - 1; i++) {
parentPath[j++] = path[i];
}
parentPath[j] = '\0';
j = 0;
for (int i = lastSlash + 1; i < len && j < nameMax - 1; i++) {
fileName[j++] = path[i];
}
fileName[j] = '\0';
}
}
// =========================================================================
// Directory operations
// =========================================================================
struct ParsedEntry {
char name[MaxNameLen];
uint32_t inodeNum;
uint8_t fileType;
};
// Find a single entry by name in a directory inode.
static bool FindInDirectory(Ext2Instance& inst, const Inode& dirInode,
const char* name, ParsedEntry* out) {
uint32_t dirSize = dirInode.i_size;
uint32_t blockSize = inst.blockSize;
uint32_t numBlocks = (dirSize + blockSize - 1) / blockSize;
// We need a separate buffer for directory data since GetPhysicalBlock
// uses inst.blockBuf for indirect block reads
uint8_t* dirBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!dirBuf) return false;
for (uint32_t bi = 0; bi < numBlocks; bi++) {
uint32_t physBlock = GetPhysicalBlock(inst, dirInode, bi);
if (physBlock == 0) continue;
if (!ReadBlock(inst, physBlock, dirBuf)) continue;
uint32_t pos = 0;
uint32_t remaining = dirSize - bi * blockSize;
if (remaining > blockSize) remaining = blockSize;
while (pos + 8 <= remaining) {
DirEntry* de = (DirEntry*)(dirBuf + pos);
if (de->rec_len == 0) break;
if (de->rec_len < 8 || pos + de->rec_len > blockSize) break;
if (de->inode != 0 && de->name_len > 0) {
char entryName[MaxNameLen];
int nameLen = de->name_len;
if (nameLen >= MaxNameLen) nameLen = MaxNameLen - 1;
memcpy(entryName, (uint8_t*)de + sizeof(DirEntry), nameLen);
entryName[nameLen] = '\0';
if (StrEqual(entryName, name)) {
out->inodeNum = de->inode;
out->fileType = de->file_type;
memcpy(out->name, entryName, nameLen + 1);
Memory::g_pfa->Free(dirBuf);
return true;
}
}
pos += de->rec_len;
}
}
Memory::g_pfa->Free(dirBuf);
return false;
}
// Read all entries from a directory
static int ReadDirectoryEntries(Ext2Instance& inst, const Inode& dirInode,
ParsedEntry* entries, int maxEntries) {
uint32_t dirSize = dirInode.i_size;
uint32_t blockSize = inst.blockSize;
uint32_t numBlocks = (dirSize + blockSize - 1) / blockSize;
int count = 0;
uint8_t* dirBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!dirBuf) return 0;
for (uint32_t bi = 0; bi < numBlocks && count < maxEntries; bi++) {
uint32_t physBlock = GetPhysicalBlock(inst, dirInode, bi);
if (physBlock == 0) continue;
if (!ReadBlock(inst, physBlock, dirBuf)) continue;
uint32_t pos = 0;
uint32_t remaining = dirSize - bi * blockSize;
if (remaining > blockSize) remaining = blockSize;
while (pos + 8 <= remaining && count < maxEntries) {
DirEntry* de = (DirEntry*)(dirBuf + pos);
if (de->rec_len == 0) break;
if (de->rec_len < 8 || pos + de->rec_len > blockSize) break;
if (de->inode != 0 && de->name_len > 0) {
int nameLen = de->name_len;
if (nameLen >= MaxNameLen) nameLen = MaxNameLen - 1;
memcpy(entries[count].name,
(uint8_t*)de + sizeof(DirEntry), nameLen);
entries[count].name[nameLen] = '\0';
// Skip . and ..
if (entries[count].name[0] == '.' &&
(entries[count].name[1] == '\0' ||
(entries[count].name[1] == '.' && entries[count].name[2] == '\0'))) {
pos += de->rec_len;
continue;
}
entries[count].inodeNum = de->inode;
entries[count].fileType = de->file_type;
count++;
}
pos += de->rec_len;
}
}
Memory::g_pfa->Free(dirBuf);
return count;
}
// Add a directory entry to a directory inode
static bool AddDirEntry(Ext2Instance& inst, uint32_t dirInodeNum, Inode& dirInode,
uint32_t childInodeNum, const char* name, uint8_t fileType) {
uint32_t nameLen = 0;
while (name[nameLen]) nameLen++;
uint32_t neededLen = ((sizeof(DirEntry) + nameLen + 3) / 4) * 4; // 4-byte aligned
uint32_t blockSize = inst.blockSize;
uint32_t numBlocks = (dirInode.i_size + blockSize - 1) / blockSize;
uint8_t* dirBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!dirBuf) return false;
// Try to find space in existing directory blocks
for (uint32_t bi = 0; bi < numBlocks; bi++) {
uint32_t physBlock = GetPhysicalBlock(inst, dirInode, bi);
if (physBlock == 0) continue;
if (!ReadBlock(inst, physBlock, dirBuf)) continue;
uint32_t pos = 0;
while (pos + 8 <= blockSize) {
DirEntry* de = (DirEntry*)(dirBuf + pos);
if (de->rec_len == 0) break;
if (de->rec_len < 8 || pos + de->rec_len > blockSize) break;
// Check if this entry has slack space we can use
uint32_t actualLen;
if (de->inode == 0) {
actualLen = 0; // unused entry — entire rec_len is available
} else {
actualLen = ((sizeof(DirEntry) + de->name_len + 3) / 4) * 4;
}
uint32_t slack = de->rec_len - actualLen;
if (slack >= neededLen) {
// Split this entry
if (de->inode != 0) {
uint16_t oldRecLen = de->rec_len;
de->rec_len = (uint16_t)actualLen;
DirEntry* newDe = (DirEntry*)(dirBuf + pos + actualLen);
newDe->inode = childInodeNum;
newDe->rec_len = (uint16_t)(oldRecLen - actualLen);
newDe->name_len = (uint8_t)nameLen;
newDe->file_type = fileType;
memcpy((uint8_t*)newDe + sizeof(DirEntry), name, nameLen);
} else {
// Reuse this empty entry
de->inode = childInodeNum;
// Keep rec_len as-is
de->name_len = (uint8_t)nameLen;
de->file_type = fileType;
memcpy((uint8_t*)de + sizeof(DirEntry), name, nameLen);
}
WriteBlock(inst, physBlock, dirBuf);
Memory::g_pfa->Free(dirBuf);
return true;
}
pos += de->rec_len;
}
}
// No space — allocate a new block for the directory
uint32_t group = (dirInodeNum - 1) / inst.inodesPerGroup;
uint32_t newBlock = AllocateBlock(inst, group);
if (newBlock == 0) {
Memory::g_pfa->Free(dirBuf);
return false;
}
// Initialize new block with the entry
memset(dirBuf, 0, blockSize);
DirEntry* de = (DirEntry*)dirBuf;
de->inode = childInodeNum;
de->rec_len = (uint16_t)blockSize; // fills entire block
de->name_len = (uint8_t)nameLen;
de->file_type = fileType;
memcpy((uint8_t*)de + sizeof(DirEntry), name, nameLen);
if (!WriteBlock(inst, newBlock, dirBuf)) {
FreeBlock(inst, newBlock);
Memory::g_pfa->Free(dirBuf);
return false;
}
// Assign new block to directory inode
uint32_t newLogicalIdx = numBlocks;
if (!SetPhysicalBlock(inst, dirInode, newLogicalIdx, newBlock, group)) {
FreeBlock(inst, newBlock);
Memory::g_pfa->Free(dirBuf);
return false;
}
dirInode.i_size += blockSize;
dirInode.i_blocks += blockSize / 512;
WriteInode(inst, dirInodeNum, &dirInode);
Memory::g_pfa->Free(dirBuf);
return true;
}
// Remove a directory entry by name
static bool RemoveDirEntry(Ext2Instance& inst, const Inode& dirInode,
const char* name) {
uint32_t dirSize = dirInode.i_size;
uint32_t blockSize = inst.blockSize;
uint32_t numBlocks = (dirSize + blockSize - 1) / blockSize;
uint8_t* dirBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!dirBuf) return false;
for (uint32_t bi = 0; bi < numBlocks; bi++) {
uint32_t physBlock = GetPhysicalBlock(inst, dirInode, bi);
if (physBlock == 0) continue;
if (!ReadBlock(inst, physBlock, dirBuf)) continue;
uint32_t pos = 0;
DirEntry* prevDe = nullptr;
while (pos + 8 <= blockSize) {
DirEntry* de = (DirEntry*)(dirBuf + pos);
if (de->rec_len == 0) break;
if (de->rec_len < 8 || pos + de->rec_len > blockSize) break;
if (de->inode != 0 && de->name_len > 0) {
char entryName[MaxNameLen];
int nameLen = de->name_len;
if (nameLen >= MaxNameLen) nameLen = MaxNameLen - 1;
memcpy(entryName, (uint8_t*)de + sizeof(DirEntry), nameLen);
entryName[nameLen] = '\0';
if (StrEqual(entryName, name)) {
if (prevDe) {
// Merge with previous entry
prevDe->rec_len += de->rec_len;
} else {
// First entry in block — just zero the inode
de->inode = 0;
}
WriteBlock(inst, physBlock, dirBuf);
Memory::g_pfa->Free(dirBuf);
return true;
}
}
prevDe = de;
pos += de->rec_len;
}
}
Memory::g_pfa->Free(dirBuf);
return false;
}
// =========================================================================
// Path traversal
// =========================================================================
// Traverse a full path from root. Returns true and fills out inode number
// and inode data.
static bool TraversePath(Ext2Instance& inst, const char* path,
uint32_t* outInodeNum, Inode* outInode) {
while (*path == '/') path++;
// Empty path = root directory
uint32_t currentInode = EXT2_ROOT_INODE;
Inode inode;
if (!ReadInode(inst, currentInode, &inode)) return false;
if (*path == '\0') {
*outInodeNum = currentInode;
*outInode = inode;
return true;
}
while (*path) {
// Current inode must be a directory
if ((inode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return false;
// Extract next path component
char component[MaxNameLen];
int len = 0;
while (*path && *path != '/' && len < MaxNameLen - 1) {
component[len++] = *path++;
}
component[len] = '\0';
while (*path == '/') path++;
ParsedEntry found;
if (!FindInDirectory(inst, inode, component, &found)) return false;
currentInode = found.inodeNum;
if (!ReadInode(inst, currentInode, &inode)) return false;
if (*path == '\0') {
*outInodeNum = currentInode;
*outInode = inode;
return true;
}
}
return false;
}
// =========================================================================
// FsDriver implementation functions
// =========================================================================
static int OpenImpl(int inst, const char* path) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
auto& self = g_instances[inst];
uint32_t inodeNum;
Inode inode;
if (!TraversePath(self, path, &inodeNum, &inode)) return -1;
for (int i = 0; i < MaxFilesPerInstance; i++) {
if (!self.files[i].inUse) {
self.files[i].inUse = true;
self.files[i].inodeNum = inodeNum;
self.files[i].inode = inode;
self.files[i].isDirectory =
(inode.i_mode & IMODE_TYPE_MASK) == IMODE_DIR;
return i;
}
}
return -1;
}
static int ReadImpl(int inst, int handle, uint8_t* buffer,
uint64_t offset, uint64_t size) {
if (inst < 0 || inst >= g_instanceCount) return -1;
auto& self = g_instances[inst];
if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1;
auto& file = self.files[handle];
if (file.isDirectory) return -1;
uint32_t fileSize = file.inode.i_size;
if (offset >= fileSize) return 0;
if (offset + size > fileSize) size = fileSize - offset;
if (size == 0) return 0;
uint32_t blockSize = self.blockSize;
uint64_t bytesRead = 0;
// We need a separate buffer for data reads since GetPhysicalBlock uses blockBuf
uint8_t* dataBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!dataBuf) return -1;
while (bytesRead < size) {
uint32_t logicalBlock = (uint32_t)((offset + bytesRead) / blockSize);
uint32_t blockOff = (uint32_t)((offset + bytesRead) % blockSize);
uint32_t physBlock = GetPhysicalBlock(self, file.inode, logicalBlock);
if (physBlock == 0) break;
if (!ReadBlock(self, physBlock, dataBuf)) break;
uint32_t available = blockSize - blockOff;
uint64_t toRead = size - bytesRead;
if (toRead > available) toRead = available;
memcpy(buffer + bytesRead, dataBuf + blockOff, toRead);
bytesRead += toRead;
}
Memory::g_pfa->Free(dataBuf);
return (int)bytesRead;
}
static uint64_t GetSizeImpl(int inst, int handle) {
if (inst < 0 || inst >= g_instanceCount) return 0;
auto& self = g_instances[inst];
if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return 0;
return self.files[handle].inode.i_size;
}
static void CloseImpl(int inst, int handle) {
if (inst < 0 || inst >= g_instanceCount) return;
auto& self = g_instances[inst];
if (handle < 0 || handle >= MaxFilesPerInstance) return;
self.files[handle].inUse = false;
}
static int ReadDirImpl(int inst, const char* path,
const char** outNames, int maxEntries) {
if (inst < 0 || inst >= g_instanceCount) return -1;
auto& self = g_instances[inst];
uint32_t inodeNum;
Inode inode;
if (!TraversePath(self, path, &inodeNum, &inode)) return -1;
if ((inode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return -1;
ParsedEntry entries[MaxDirEntries];
int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries;
int count = ReadDirectoryEntries(self, inode, entries, limit);
self.dirNameCount = count;
for (int i = 0; i < count; i++) {
int j = 0;
while (entries[i].name[j] && j < MaxNameLen - 1) {
self.dirNames[i][j] = entries[i].name[j];
j++;
}
self.dirNames[i][j] = '\0';
outNames[i] = self.dirNames[i];
}
return count;
}
static int WriteImpl(int inst, int handle, const uint8_t* buffer,
uint64_t offset, uint64_t size) {
if (inst < 0 || inst >= g_instanceCount) return -1;
auto& self = g_instances[inst];
if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1;
auto& file = self.files[handle];
if (file.isDirectory) return -1;
if (size == 0) return 0;
uint32_t blockSize = self.blockSize;
uint32_t group = (file.inodeNum - 1) / self.inodesPerGroup;
// Allocate a separate buffer for data I/O
uint8_t* dataBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!dataBuf) return -1;
uint64_t bytesWritten = 0;
while (bytesWritten < size) {
uint32_t logicalBlock = (uint32_t)((offset + bytesWritten) / blockSize);
uint32_t blockOff = (uint32_t)((offset + bytesWritten) % blockSize);
uint32_t physBlock = GetPhysicalBlock(self, file.inode, logicalBlock);
if (physBlock == 0) {
// Need to allocate a new block
physBlock = AllocateBlock(self, group);
if (physBlock == 0) break;
// Zero the new block
memset(dataBuf, 0, blockSize);
WriteBlock(self, physBlock, dataBuf);
if (!SetPhysicalBlock(self, file.inode, logicalBlock, physBlock, group)) {
FreeBlock(self, physBlock);
break;
}
file.inode.i_blocks += blockSize / 512;
}
// Read existing block for partial writes
if (!ReadBlock(self, physBlock, dataBuf)) break;
uint32_t available = blockSize - blockOff;
uint64_t toWrite = size - bytesWritten;
if (toWrite > available) toWrite = available;
memcpy(dataBuf + blockOff, buffer + bytesWritten, toWrite);
if (!WriteBlock(self, physBlock, dataBuf)) break;
bytesWritten += toWrite;
}
Memory::g_pfa->Free(dataBuf);
// Update file size if we wrote past the end
uint64_t endPos = offset + bytesWritten;
if (endPos > file.inode.i_size) {
file.inode.i_size = (uint32_t)endPos;
}
// Write updated inode to disk
if (bytesWritten > 0) {
WriteInode(self, file.inodeNum, &file.inode);
}
return (int)bytesWritten;
}
static int CreateImpl(int inst, const char* path) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
auto& self = g_instances[inst];
char parentPath[MaxNameLen];
char fileName[MaxNameLen];
SplitPath(path, parentPath, MaxNameLen, fileName, MaxNameLen);
if (fileName[0] == '\0') return -1;
// Traverse to parent directory
uint32_t parentInodeNum;
Inode parentInode;
if (!TraversePath(self, parentPath, &parentInodeNum, &parentInode)) return -1;
if ((parentInode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return -1;
// Check if file already exists
ParsedEntry existing;
if (FindInDirectory(self, parentInode, fileName, &existing)) {
// If it's a directory, can't truncate
Inode existInode;
if (!ReadInode(self, existing.inodeNum, &existInode)) return -1;
if ((existInode.i_mode & IMODE_TYPE_MASK) == IMODE_DIR) return -1;
// Truncate: free all blocks and reset size
FreeInodeBlocks(self, existInode);
existInode.i_size = 0;
existInode.i_blocks = 0;
WriteInode(self, existing.inodeNum, &existInode);
// Open a handle
for (int i = 0; i < MaxFilesPerInstance; i++) {
if (!self.files[i].inUse) {
self.files[i].inUse = true;
self.files[i].inodeNum = existing.inodeNum;
self.files[i].inode = existInode;
self.files[i].isDirectory = false;
return i;
}
}
return -1;
}
// Allocate a new inode
uint32_t group = (parentInodeNum - 1) / self.inodesPerGroup;
uint32_t newInodeNum = AllocateInode(self, group);
if (newInodeNum == 0) return -1;
// Initialize the new inode
Inode newInode;
memset(&newInode, 0, sizeof(Inode));
newInode.i_mode = IMODE_REG | 0644; // regular file, rw-r--r--
newInode.i_links_count = 1;
WriteInode(self, newInodeNum, &newInode);
// Add directory entry
if (!AddDirEntry(self, parentInodeNum, parentInode,
newInodeNum, fileName, EXT2_FT_REG_FILE)) {
FreeInode(self, newInodeNum);
return -1;
}
// Open a handle
for (int i = 0; i < MaxFilesPerInstance; i++) {
if (!self.files[i].inUse) {
self.files[i].inUse = true;
self.files[i].inodeNum = newInodeNum;
self.files[i].inode = newInode;
self.files[i].isDirectory = false;
return i;
}
}
return -1;
}
static int DeleteImpl(int inst, const char* path) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
auto& self = g_instances[inst];
char parentPath[MaxNameLen];
char fileName[MaxNameLen];
SplitPath(path, parentPath, MaxNameLen, fileName, MaxNameLen);
if (fileName[0] == '\0') return -1;
uint32_t parentInodeNum;
Inode parentInode;
if (!TraversePath(self, parentPath, &parentInodeNum, &parentInode)) return -1;
if ((parentInode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return -1;
ParsedEntry existing;
if (!FindInDirectory(self, parentInode, fileName, &existing)) return -1;
Inode targetInode;
if (!ReadInode(self, existing.inodeNum, &targetInode)) return -1;
// Don't delete directories through this call
if ((targetInode.i_mode & IMODE_TYPE_MASK) == IMODE_DIR) return -1;
// Remove directory entry
if (!RemoveDirEntry(self, parentInode, fileName)) return -1;
// Decrement link count
targetInode.i_links_count--;
if (targetInode.i_links_count == 0) {
// Free all blocks and the inode
FreeInodeBlocks(self, targetInode);
targetInode.i_mode = 0;
WriteInode(self, existing.inodeNum, &targetInode);
FreeInode(self, existing.inodeNum);
} else {
WriteInode(self, existing.inodeNum, &targetInode);
}
return 0;
}
static int MkdirImpl(int inst, const char* path) {
if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1;
auto& self = g_instances[inst];
char parentPath[MaxNameLen];
char dirName[MaxNameLen];
SplitPath(path, parentPath, MaxNameLen, dirName, MaxNameLen);
if (dirName[0] == '\0') return -1;
uint32_t parentInodeNum;
Inode parentInode;
if (!TraversePath(self, parentPath, &parentInodeNum, &parentInode)) return -1;
if ((parentInode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return -1;
// If directory already exists, return success
ParsedEntry existing;
if (FindInDirectory(self, parentInode, dirName, &existing)) {
Inode existInode;
if (ReadInode(self, existing.inodeNum, &existInode) &&
(existInode.i_mode & IMODE_TYPE_MASK) == IMODE_DIR) {
return 0;
}
return -1; // exists as a file
}
// Allocate inode for new directory
uint32_t group = (parentInodeNum - 1) / self.inodesPerGroup;
uint32_t newInodeNum = AllocateInode(self, group);
if (newInodeNum == 0) return -1;
// Allocate a block for the directory data
uint32_t dirBlock = AllocateBlock(self, group);
if (dirBlock == 0) {
FreeInode(self, newInodeNum);
return -1;
}
// Initialize the directory block with . and .. entries
uint32_t blockSize = self.blockSize;
uint8_t* dirBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (!dirBuf) {
FreeBlock(self, dirBlock);
FreeInode(self, newInodeNum);
return -1;
}
memset(dirBuf, 0, blockSize);
// "." entry
DirEntry* dot = (DirEntry*)dirBuf;
dot->inode = newInodeNum;
dot->rec_len = 12; // minimum size for "." (8 header + 1 name + 3 padding)
dot->name_len = 1;
dot->file_type = EXT2_FT_DIR;
dirBuf[sizeof(DirEntry)] = '.';
// ".." entry — takes remaining space in the block
DirEntry* dotdot = (DirEntry*)(dirBuf + 12);
dotdot->inode = parentInodeNum;
dotdot->rec_len = (uint16_t)(blockSize - 12);
dotdot->name_len = 2;
dotdot->file_type = EXT2_FT_DIR;
dirBuf[12 + sizeof(DirEntry)] = '.';
dirBuf[12 + sizeof(DirEntry) + 1] = '.';
if (!WriteBlock(self, dirBlock, dirBuf)) {
Memory::g_pfa->Free(dirBuf);
FreeBlock(self, dirBlock);
FreeInode(self, newInodeNum);
return -1;
}
Memory::g_pfa->Free(dirBuf);
// Initialize the new directory inode
Inode newInode;
memset(&newInode, 0, sizeof(Inode));
newInode.i_mode = IMODE_DIR | 0755; // directory, rwxr-xr-x
newInode.i_size = blockSize;
newInode.i_blocks = blockSize / 512;
newInode.i_links_count = 2; // . and parent's entry
newInode.i_block[0] = dirBlock;
WriteInode(self, newInodeNum, &newInode);
// Add entry to parent directory
// Re-read parent inode since AddDirEntry may modify it
if (!ReadInode(self, parentInodeNum, &parentInode)) return -1;
if (!AddDirEntry(self, parentInodeNum, parentInode,
newInodeNum, dirName, EXT2_FT_DIR)) {
FreeBlock(self, dirBlock);
FreeInode(self, newInodeNum);
return -1;
}
// Increment parent's link count (for ".." in the new dir)
parentInode.i_links_count++;
WriteInode(self, parentInodeNum, &parentInode);
// Update used_dirs_count in block group descriptor
uint32_t newGroup = (newInodeNum - 1) / self.inodesPerGroup;
if (newGroup < self.groupCount) {
self.bgdt[newGroup].bg_used_dirs_count++;
uint32_t bgdtBlock = self.firstDataBlock + 1;
uint32_t bgdtOffset = newGroup * sizeof(BlockGroupDescriptor);
uint32_t bgdtBlockIdx = bgdtBlock + bgdtOffset / self.blockSize;
uint32_t bgdtOffInBlock = bgdtOffset % self.blockSize;
uint8_t* tmpBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
if (tmpBuf) {
if (ReadBlock(self, bgdtBlockIdx, tmpBuf)) {
memcpy(tmpBuf + bgdtOffInBlock, &self.bgdt[newGroup],
sizeof(BlockGroupDescriptor));
WriteBlock(self, bgdtBlockIdx, tmpBuf);
}
Memory::g_pfa->Free(tmpBuf);
}
}
return 0;
}
// =========================================================================
// Template thunks — generate unique function pointers per instance
// =========================================================================
template<int N> struct Thunks {
static int Open(const char* p) { return OpenImpl(N, p); }
static int Read(int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(N, h, b, o, s); }
static uint64_t GetSize(int h) { return GetSizeImpl(N, h); }
static void Close(int h) { CloseImpl(N, h); }
static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); }
static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); }
static int Create(const char* p) { return CreateImpl(N, p); }
static int Delete(const char* p) { return DeleteImpl(N, p); }
static int Mkdir(const char* p) { return MkdirImpl(N, p); }
};
template<int N>
static Vfs::FsDriver MakeDriver() {
return {
Thunks<N>::Open,
Thunks<N>::Read,
Thunks<N>::GetSize,
Thunks<N>::Close,
Thunks<N>::ReadDir,
Thunks<N>::Write,
Thunks<N>::Create,
Thunks<N>::Delete,
Thunks<N>::Mkdir,
};
}
static Vfs::FsDriver g_drivers[] = {
MakeDriver<0>(), MakeDriver<1>(), MakeDriver<2>(), MakeDriver<3>(),
MakeDriver<4>(), MakeDriver<5>(), MakeDriver<6>(), MakeDriver<7>(),
};
// =========================================================================
// Superblock validation and mount
// =========================================================================
Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) {
if (g_instanceCount >= MaxInstances) return nullptr;
auto* dev = Drivers::Storage::GetBlockDevice(blockDevIndex);
if (!dev) return nullptr;
// ext2 superblock is at byte offset 1024 from partition start (sector 2)
uint8_t sbBuf[1024];
if (!dev->ReadSectors(dev->Ctx, startLba + 2, 2, sbBuf)) return nullptr;
Superblock* sb = (Superblock*)sbBuf;
// Validate magic number
if (sb->s_magic != EXT2_MAGIC) return nullptr;
// Validate basic fields
if (sb->s_inodes_count == 0 || sb->s_blocks_count == 0) return nullptr;
if (sb->s_inodes_per_group == 0 || sb->s_blocks_per_group == 0) return nullptr;
uint32_t blockSize = 1024 << sb->s_log_block_size;
if (blockSize < 1024 || blockSize > 65536) return nullptr;
// Determine inode size
uint16_t inodeSize = 128; // default for rev 0
if (sb->s_rev_level >= 1) {
inodeSize = sb->s_inode_size;
if (inodeSize < 128 || inodeSize > blockSize) return nullptr;
}
// Check for incompatible features we don't support
// Bit 1 = compression, bit 2 = filetype (we support), bit 3 = journal needed,
// bit 4 = meta_bg
uint32_t incompat = sb->s_feature_incompat;
// We support filetype (0x02). Reject anything else.
uint32_t unsupported = incompat & ~(uint32_t)0x02;
if (unsupported) return nullptr;
// Compute group count
uint32_t groupCount = (sb->s_blocks_count + sb->s_blocks_per_group - 1)
/ sb->s_blocks_per_group;
if (groupCount == 0) return nullptr;
// Success — initialize instance
int idx = g_instanceCount;
auto& inst = g_instances[idx];
inst.active = true;
inst.blockDevIndex = blockDevIndex;
inst.partStartLba = startLba;
inst.blockSize = blockSize;
inst.inodeSize = inodeSize;
inst.inodesPerGroup = sb->s_inodes_per_group;
inst.blocksPerGroup = sb->s_blocks_per_group;
inst.totalInodes = sb->s_inodes_count;
inst.totalBlocks = sb->s_blocks_count;
inst.firstDataBlock = sb->s_first_data_block;
inst.groupCount = groupCount;
// Volume label
memcpy(inst.volumeLabel, sb->s_volume_name, 16);
inst.volumeLabel[16] = '\0';
// Trim trailing nulls/spaces
for (int i = 15; i >= 0; i--) {
if (inst.volumeLabel[i] == '\0' || inst.volumeLabel[i] == ' ')
inst.volumeLabel[i] = '\0';
else break;
}
// Allocate block buffer
inst.blockBufPages = ((int)blockSize + 0xFFF) / 0x1000;
if (inst.blockBufPages == 1) {
inst.blockBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
} else {
inst.blockBuf = (uint8_t*)Memory::g_pfa->ReallocConsecutive(
nullptr, inst.blockBufPages);
}
if (!inst.blockBuf) {
inst.active = false;
return nullptr;
}
// Load block group descriptor table
// BGDT starts at the block after the superblock
uint32_t bgdtStartBlock = inst.firstDataBlock + 1;
uint32_t bgdtBytes = groupCount * sizeof(BlockGroupDescriptor);
inst.bgdtPages = ((int)bgdtBytes + 0xFFF) / 0x1000;
if (inst.bgdtPages == 1) {
inst.bgdt = (BlockGroupDescriptor*)Memory::g_pfa->AllocateZeroed();
} else {
inst.bgdt = (BlockGroupDescriptor*)Memory::g_pfa->ReallocConsecutive(
nullptr, inst.bgdtPages);
}
if (!inst.bgdt) {
inst.active = false;
return nullptr;
}
// Read BGDT blocks
uint32_t bgdtBlocks = (bgdtBytes + blockSize - 1) / blockSize;
uint8_t* dst = (uint8_t*)inst.bgdt;
for (uint32_t b = 0; b < bgdtBlocks; b++) {
if (!ReadBlock(inst, bgdtStartBlock + b, inst.blockBuf)) {
inst.active = false;
return nullptr;
}
uint32_t copyLen = bgdtBytes - b * blockSize;
if (copyLen > blockSize) copyLen = blockSize;
memcpy(dst + b * blockSize, inst.blockBuf, copyLen);
}
// Clear file handles
for (int i = 0; i < MaxFilesPerInstance; i++) {
inst.files[i].inUse = false;
}
g_instanceCount++;
KernelLogStream(OK, "Ext2") << "Mounted volume \""
<< inst.volumeLabel << "\" (" << inst.totalBlocks << " blocks, "
<< blockSize << " bytes/block, " << groupCount << " groups)";
return &g_drivers[idx];
}
void RegisterProbe() {
FsProbe::Register(Mount);
}
// =========================================================================
// Format
// =========================================================================
int Format(int blockDevIndex, uint64_t startLba, uint64_t sectorCount,
const char* volumeLabel) {
using namespace Drivers::Storage;
auto* dev = GetBlockDevice(blockDevIndex);
if (!dev) return -1;
if (sectorCount < 8192) {
KernelLogStream(ERROR, "Ext2") << "Partition too small for ext2";
return -1;
}
constexpr uint32_t blockSize = 4096;
constexpr uint32_t logBlockSize = 2; // 1024 << 2 = 4096
constexpr uint32_t sectorsPerBlock = blockSize / 512;
constexpr uint32_t inodeSize = 128;
constexpr uint32_t inodesPerBlock = blockSize / inodeSize; // 32
constexpr uint32_t blocksPerGroup = 8 * blockSize; // 32768
constexpr uint32_t reservedInodeCount = 10;
uint32_t totalBlocks = (uint32_t)(sectorCount / sectorsPerBlock);
uint32_t groupCount = (totalBlocks + blocksPerGroup - 1) / blocksPerGroup;
if (groupCount == 0) groupCount = 1;
// Inodes per group: ~1 per 16K, rounded to fill inode table blocks
uint32_t inodesPerGroup = blocksPerGroup / 4; // 8192
inodesPerGroup = (inodesPerGroup / inodesPerBlock) * inodesPerBlock;
if (inodesPerGroup < inodesPerBlock) inodesPerGroup = inodesPerBlock;
// Cap for small partitions
uint32_t maxInodes = totalBlocks / 4;
if (maxInodes < reservedInodeCount + inodesPerBlock)
maxInodes = reservedInodeCount + inodesPerBlock;
uint32_t maxIpg = ((maxInodes + groupCount - 1) / groupCount);
maxIpg = ((maxIpg + inodesPerBlock - 1) / inodesPerBlock) * inodesPerBlock;
if (inodesPerGroup > maxIpg) inodesPerGroup = maxIpg;
uint32_t inodeTableBlocks = (inodesPerGroup * inodeSize) / blockSize;
uint32_t totalInodes = inodesPerGroup * groupCount;
// BGDT
uint32_t bgdtBytes = groupCount * sizeof(BlockGroupDescriptor);
uint32_t bgdtBlocks = (bgdtBytes + blockSize - 1) / blockSize;
// Allocate a block-sized buffer (1 page = 4K = blockSize)
uint8_t* buf = (uint8_t*)Memory::g_pfa->ReallocConsecutive(nullptr, 1);
if (!buf) return -1;
auto writeBlock = [&](uint32_t blockNum) -> bool {
uint64_t sector = startLba + (uint64_t)blockNum * sectorsPerBlock;
for (uint32_t s = 0; s < sectorsPerBlock; s++) {
if (!dev->WriteSectors(dev->Ctx, sector + s, 1, buf + s * 512))
return false;
}
return true;
};
// ---- Superblock (block 0, at byte offset 1024) ----
memset(buf, 0, blockSize);
Superblock* sb = (Superblock*)(buf + 1024);
sb->s_inodes_count = totalInodes;
sb->s_blocks_count = totalBlocks;
sb->s_r_blocks_count = totalBlocks / 20;
// Calculate total used blocks
uint32_t usedBlocks = 0;
for (uint32_t g = 0; g < groupCount; g++) {
uint32_t overhead = 2 + inodeTableBlocks; // bitmaps + itable
if (g == 0) overhead += 1 + bgdtBlocks + 1; // sb + bgdt + root data
usedBlocks += overhead;
}
sb->s_free_blocks_count = totalBlocks - usedBlocks;
sb->s_free_inodes_count = totalInodes - reservedInodeCount;
sb->s_first_data_block = 0; // 4K blocks
sb->s_log_block_size = logBlockSize;
sb->s_log_frag_size = logBlockSize;
sb->s_blocks_per_group = blocksPerGroup;
sb->s_frags_per_group = blocksPerGroup;
sb->s_inodes_per_group = inodesPerGroup;
sb->s_max_mnt_count = 20;
sb->s_magic = EXT2_MAGIC;
sb->s_state = 1; // clean
sb->s_errors = 1; // continue
sb->s_rev_level = 1;
sb->s_first_ino = 11;
sb->s_inode_size = inodeSize;
sb->s_block_group_nr = 0;
sb->s_feature_incompat = 0x0002; // FILETYPE
// Generate UUID from RDTSC
uint32_t lo, hi;
asm volatile ("rdtsc" : "=a"(lo), "=d"(hi));
uint32_t seed = lo ^ hi;
for (int i = 0; i < 16; i++) {
seed = seed * 1103515245 + 12345;
sb->s_uuid[i] = (uint8_t)(seed >> 16);
}
memset(sb->s_volume_name, 0, 16);
if (volumeLabel) {
for (int i = 0; i < 16 && volumeLabel[i]; i++)
sb->s_volume_name[i] = volumeLabel[i];
}
if (!writeBlock(0)) { Memory::g_pfa->Free(buf, 1); return -1; }
// ---- BGDT (block 1..) ----
for (uint32_t b = 0; b < bgdtBlocks; b++) {
memset(buf, 0, blockSize);
uint32_t entriesPerBlock = blockSize / sizeof(BlockGroupDescriptor);
uint32_t startEntry = b * entriesPerBlock;
for (uint32_t e = 0; e < entriesPerBlock && (startEntry + e) < groupCount; e++) {
uint32_t g = startEntry + e;
BlockGroupDescriptor* bgd = (BlockGroupDescriptor*)(buf + e * sizeof(BlockGroupDescriptor));
uint32_t groupBase = g * blocksPerGroup;
uint32_t metaStart = (g == 0) ? groupBase + 1 + bgdtBlocks : groupBase;
bgd->bg_block_bitmap = metaStart;
bgd->bg_inode_bitmap = metaStart + 1;
bgd->bg_inode_table = metaStart + 2;
uint32_t groupBlocks = (g < groupCount - 1) ? blocksPerGroup
: (totalBlocks - g * blocksPerGroup);
uint32_t overhead = 2 + inodeTableBlocks;
if (g == 0) overhead += 1 + bgdtBlocks + 1; // sb+bgdt + root data
bgd->bg_free_blocks_count = (uint16_t)(groupBlocks - overhead);
if (g == 0) {
bgd->bg_free_inodes_count = (uint16_t)(inodesPerGroup - reservedInodeCount);
bgd->bg_used_dirs_count = 1;
} else {
bgd->bg_free_inodes_count = (uint16_t)inodesPerGroup;
bgd->bg_used_dirs_count = 0;
}
}
if (!writeBlock(1 + b)) { Memory::g_pfa->Free(buf, 1); return -1; }
}
// ---- Per-group bitmaps and inode tables ----
for (uint32_t g = 0; g < groupCount; g++) {
uint32_t groupBase = g * blocksPerGroup;
uint32_t metaStart = (g == 0) ? groupBase + 1 + bgdtBlocks : groupBase;
uint32_t groupBlocks = (g < groupCount - 1) ? blocksPerGroup
: (totalBlocks - g * blocksPerGroup);
// Block bitmap
memset(buf, 0, blockSize);
uint32_t overhead = 2 + inodeTableBlocks;
if (g == 0) overhead += 1 + bgdtBlocks;
for (uint32_t bit = 0; bit < overhead; bit++)
buf[bit / 8] |= (1 << (bit % 8));
if (g == 0) {
// Root dir data block
buf[overhead / 8] |= (1 << (overhead % 8));
}
// Mark blocks beyond group end as used (last group)
for (uint32_t bit = groupBlocks; bit < blocksPerGroup; bit++)
buf[bit / 8] |= (1 << (bit % 8));
if (!writeBlock(metaStart)) { Memory::g_pfa->Free(buf, 1); return -1; }
// Inode bitmap
memset(buf, 0, blockSize);
if (g == 0) {
for (uint32_t bit = 0; bit < reservedInodeCount; bit++)
buf[bit / 8] |= (1 << (bit % 8));
}
if (!writeBlock(metaStart + 1)) { Memory::g_pfa->Free(buf, 1); return -1; }
// Inode table
for (uint32_t tb = 0; tb < inodeTableBlocks; tb++) {
memset(buf, 0, blockSize);
if (g == 0 && tb == 0) {
// Root directory inode (inode 2 = index 1)
uint32_t rootDataBlock = 1 + bgdtBlocks + 2 + inodeTableBlocks;
Inode* ri = (Inode*)(buf + 1 * inodeSize);
ri->i_mode = IMODE_DIR | 0x01FF;
ri->i_size = blockSize;
ri->i_links_count = 2;
ri->i_blocks = blockSize / 512;
ri->i_block[0] = rootDataBlock;
}
if (!writeBlock(metaStart + 2 + tb)) { Memory::g_pfa->Free(buf, 1); return -1; }
}
// Root directory data block (group 0 only)
if (g == 0) {
uint32_t rootDataBlock = 1 + bgdtBlocks + 2 + inodeTableBlocks;
memset(buf, 0, blockSize);
// "."
DirEntry* dot = (DirEntry*)buf;
dot->inode = EXT2_ROOT_INODE;
dot->rec_len = 12;
dot->name_len = 1;
dot->file_type = EXT2_FT_DIR;
buf[sizeof(DirEntry)] = '.';
// ".."
DirEntry* dotdot = (DirEntry*)(buf + 12);
dotdot->inode = EXT2_ROOT_INODE;
dotdot->rec_len = blockSize - 12;
dotdot->name_len = 2;
dotdot->file_type = EXT2_FT_DIR;
buf[12 + sizeof(DirEntry)] = '.';
buf[12 + sizeof(DirEntry) + 1] = '.';
if (!writeBlock(rootDataBlock)) { Memory::g_pfa->Free(buf, 1); return -1; }
}
}
Memory::g_pfa->Free(buf, 1);
KernelLogStream(OK, "Ext2") << "Formatted: " << (uint64_t)totalBlocks
<< " blocks (4K), " << (uint64_t)groupCount
<< " groups, " << (uint64_t)totalInodes << " inodes";
return 0;
}
};