/* * Ext2.cpp * ext2 filesystem driver * Copyright (c) 2026 Daniel Hammer */ #include "Ext2.hpp" #include "FsProbe.hpp" #include #include #include #include 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 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 static Vfs::FsDriver MakeDriver() { return { Thunks::Open, Thunks::Read, Thunks::GetSize, Thunks::Close, Thunks::ReadDir, Thunks::Write, Thunks::Create, Thunks::Delete, Thunks::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; } };