From 807c2602feea0fb34303b31dbc01b6948fa41f3d Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sun, 8 Mar 2026 14:41:36 +0100 Subject: [PATCH] feat: ext2 filesystem, installer updates, add update feature, and more --- kernel/src/Api/Filesystem.hpp | 6 +- kernel/src/Api/Graphics.hpp | 6 +- kernel/src/Api/Heap.hpp | 2 +- kernel/src/Api/Storage.hpp | 5 + kernel/src/Api/Syscall.hpp | 1 + kernel/src/Api/Terminal.hpp | 4 + kernel/src/Api/WinServer.cpp | 15 +- kernel/src/Fs/Ext2.cpp | 1825 +++++++++++++++++ kernel/src/Fs/Ext2.hpp | 25 + kernel/src/Fs/Fat32.cpp | 56 + kernel/src/Fs/FsProbe.cpp | 30 + kernel/src/Main.cpp | 2 + kernel/src/Memory/Paging.cpp | 41 +- kernel/src/Memory/Paging.hpp | 4 +- kernel/src/Sched/ElfLoader.cpp | 6 +- kernel/src/Sched/Scheduler.cpp | 10 +- programs/include/Api/Syscall.hpp | 1 + programs/src/desktop/apps/app_filemanager.cpp | 4 + programs/src/init/main.cpp | 18 +- programs/src/installer/actions.cpp | 431 +++- programs/src/installer/installer.h | 26 +- programs/src/installer/main.cpp | 104 +- programs/src/installer/render.cpp | 225 +- 23 files changed, 2717 insertions(+), 130 deletions(-) create mode 100644 kernel/src/Fs/Ext2.cpp create mode 100644 kernel/src/Fs/Ext2.hpp diff --git a/kernel/src/Api/Filesystem.hpp b/kernel/src/Api/Filesystem.hpp index 2fde3b3..65b205c 100644 --- a/kernel/src/Api/Filesystem.hpp +++ b/kernel/src/Api/Filesystem.hpp @@ -32,9 +32,9 @@ namespace Montauk { static int Sys_ReadDir(const char* path, const char** outNames, int maxEntries) { // Get entries from VFS into a kernel-local array - const char* kernelNames[64]; + const char* kernelNames[256]; int max = maxEntries; - if (max > 64) max = 64; + if (max > 256) max = 256; int count = Fs::Vfs::VfsReadDir(path, kernelNames, max); if (count <= 0) return count; @@ -47,7 +47,7 @@ namespace Montauk { uint64_t physAddr = Memory::SubHHDM((uint64_t)page); uint64_t userVa = proc->heapNext; proc->heapNext += 0x1000; - Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa); + if (!Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa)) return -1; // Copy strings into the user page and write pointers to outNames uint64_t offset = 0; diff --git a/kernel/src/Api/Graphics.hpp b/kernel/src/Api/Graphics.hpp index ab17a00..85fd4e6 100644 --- a/kernel/src/Api/Graphics.hpp +++ b/kernel/src/Api/Graphics.hpp @@ -50,11 +50,13 @@ namespace Montauk { constexpr uint64_t userVa = 0x50000000ULL; for (uint64_t i = 0; i < numPages; i++) { - Memory::VMM::Paging::MapUserInWC( + if (!Memory::VMM::Paging::MapUserInWC( proc->pml4Phys, fbPhys + i * 0x1000, userVa + i * 0x1000 - ); + )) { + return 0; + } } return userVa; diff --git a/kernel/src/Api/Heap.hpp b/kernel/src/Api/Heap.hpp index a2ec73d..f09dbfb 100644 --- a/kernel/src/Api/Heap.hpp +++ b/kernel/src/Api/Heap.hpp @@ -48,7 +48,7 @@ namespace Montauk { void* page = Memory::g_pfa->AllocateZeroed(); if (page == nullptr) return 0; uint64_t physAddr = Memory::SubHHDM((uint64_t)page); - Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa + i * 0x1000); + if (!Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa + i * 0x1000)) return 0; } proc->heapNext += size; diff --git a/kernel/src/Api/Storage.hpp b/kernel/src/Api/Storage.hpp index af0fd31..9356d0a 100644 --- a/kernel/src/Api/Storage.hpp +++ b/kernel/src/Api/Storage.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "Syscall.hpp" @@ -115,6 +116,10 @@ namespace Montauk { return (int64_t)Fs::Fat32::Format( part->BlockDevIndex, part->StartLba, part->SectorCount, params->label[0] ? params->label : nullptr); + case FS_TYPE_EXT2: + return (int64_t)Fs::Ext2::Format( + part->BlockDevIndex, part->StartLba, part->SectorCount, + params->label[0] ? params->label : nullptr); default: return -1; // unknown filesystem type } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index c579c87..972c794 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -297,6 +297,7 @@ namespace Montauk { // Filesystem type IDs for SYS_FSFORMAT static constexpr int FS_TYPE_FAT32 = 1; + static constexpr int FS_TYPE_EXT2 = 2; struct FsFormatParams { int32_t partIndex; // global partition index diff --git a/kernel/src/Api/Terminal.hpp b/kernel/src/Api/Terminal.hpp index e7def85..3759b07 100644 --- a/kernel/src/Api/Terminal.hpp +++ b/kernel/src/Api/Terminal.hpp @@ -23,6 +23,8 @@ namespace Montauk { return; } } + // Don't draw over the framebuffer once the GUI is active + if (Kt::g_suppressKernelLog) return; Kt::Print(text); } @@ -35,6 +37,8 @@ namespace Montauk { return; } } + // Don't draw over the framebuffer once the GUI is active + if (Kt::g_suppressKernelLog) return; Kt::Putchar(c); } }; \ No newline at end of file diff --git a/kernel/src/Api/WinServer.cpp b/kernel/src/Api/WinServer.cpp index 4179958..6ebcd8c 100644 --- a/kernel/src/Api/WinServer.cpp +++ b/kernel/src/Api/WinServer.cpp @@ -67,7 +67,10 @@ namespace WinServer { } uint64_t physAddr = Memory::SubHHDM((uint64_t)page); slot.pixelPhysPages[i] = physAddr; - Memory::VMM::Paging::MapUserIn(ownerPml4, physAddr, userVa + (uint64_t)i * 0x1000); + if (!Memory::VMM::Paging::MapUserIn(ownerPml4, physAddr, userVa + (uint64_t)i * 0x1000)) { + slot.used = false; + return -1; + } } slot.ownerVa = userVa; @@ -173,8 +176,10 @@ namespace WinServer { uint64_t userVa = heapNext; for (int i = 0; i < slot.pixelNumPages; i++) { - Memory::VMM::Paging::MapUserIn(callerPml4, slot.pixelPhysPages[i], - userVa + (uint64_t)i * 0x1000); + if (!Memory::VMM::Paging::MapUserIn(callerPml4, slot.pixelPhysPages[i], + userVa + (uint64_t)i * 0x1000)) { + return 0; + } } slot.desktopVa = userVa; @@ -230,7 +235,9 @@ namespace WinServer { if (page == nullptr) return -1; uint64_t physAddr = Memory::SubHHDM((uint64_t)page); slot.pixelPhysPages[i] = physAddr; - Memory::VMM::Paging::MapUserIn(ownerPml4, physAddr, userVa + (uint64_t)i * 0x1000); + if (!Memory::VMM::Paging::MapUserIn(ownerPml4, physAddr, userVa + (uint64_t)i * 0x1000)) { + return -1; + } } slot.width = newW; diff --git a/kernel/src/Fs/Ext2.cpp b/kernel/src/Fs/Ext2.cpp new file mode 100644 index 0000000..5fcadc2 --- /dev/null +++ b/kernel/src/Fs/Ext2.cpp @@ -0,0 +1,1825 @@ +/* + * 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; + } + +}; diff --git a/kernel/src/Fs/Ext2.hpp b/kernel/src/Fs/Ext2.hpp new file mode 100644 index 0000000..e1b7381 --- /dev/null +++ b/kernel/src/Fs/Ext2.hpp @@ -0,0 +1,25 @@ +/* + * Ext2.hpp + * ext2 filesystem driver + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Fs::Ext2 { + + // Try to mount an ext2 filesystem at the given partition range. + // Returns a FsDriver* on success, nullptr if not a valid ext2 volume. + Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount); + + // Register Ext2::Mount as a filesystem probe with FsProbe. + void RegisterProbe(); + + // Format a partition as ext2. + // Returns 0 on success, -1 on error. + int Format(int blockDevIndex, uint64_t startLba, uint64_t sectorCount, + const char* volumeLabel); + +}; diff --git a/kernel/src/Fs/Fat32.cpp b/kernel/src/Fs/Fat32.cpp index 93b127d..584cebf 100644 --- a/kernel/src/Fs/Fat32.cpp +++ b/kernel/src/Fs/Fat32.cpp @@ -79,6 +79,11 @@ namespace Fs::Fat32 { uint8_t* clusterBuf; int clusterBufPages; + // In-memory FAT cache (page-allocated) + uint32_t* fatCache; + int fatCachePages; + uint32_t fatCacheEntries; // number of valid 4-byte entries + // Open file handles Fat32File files[MaxFilesPerInstance]; @@ -127,6 +132,12 @@ namespace Fs::Fat32 { } static uint32_t GetNextCluster(const Fat32Instance& inst, uint32_t cluster) { + // Use in-memory FAT cache if available (avoids disk I/O per lookup) + if (inst.fatCache && cluster < inst.fatCacheEntries) { + return inst.fatCache[cluster] & 0x0FFFFFFF; + } + + // Fallback: read from disk uint32_t fatOffset = cluster * 4; uint32_t fatSector = fatOffset / inst.bytesPerSector; uint32_t entryOffset = fatOffset % inst.bytesPerSector; @@ -183,10 +194,30 @@ namespace Fs::Fat32 { if (!WritePartSectors(inst, fatStart + fatSector, 1, sectorBuf)) return false; } + // Keep in-memory FAT cache in sync + if (inst.fatCache && cluster < inst.fatCacheEntries) { + uint32_t existing = inst.fatCache[cluster]; + inst.fatCache[cluster] = (existing & 0xF0000000) | (value & 0x0FFFFFFF); + } + return true; } static uint32_t AllocateCluster(Fat32Instance& inst) { + // Fast path: scan the in-memory FAT cache + if (inst.fatCache) { + uint32_t limit = inst.clusterCount + 2; + if (limit > inst.fatCacheEntries) limit = inst.fatCacheEntries; + for (uint32_t cluster = 2; cluster < limit; cluster++) { + if ((inst.fatCache[cluster] & 0x0FFFFFFF) == CLUSTER_FREE) { + if (!WriteFatEntry(inst, cluster, 0x0FFFFFFF)) return 0; + return cluster; + } + } + return 0; + } + + // Slow path: read FAT sectors from disk uint8_t sectorBuf[512]; uint32_t entriesPerSector = inst.bytesPerSector / 4; @@ -1575,6 +1606,31 @@ namespace Fs::Fat32 { nullptr, inst.clusterBufPages); } + // Load entire FAT into memory for fast cluster chain lookups. + // fatSize32 is in sectors; each sector is bytesPerSector bytes. + uint64_t fatBytes = (uint64_t)fatSize32 * bytesPerSector; + inst.fatCachePages = (int)((fatBytes + 0xFFF) / 0x1000); + inst.fatCacheEntries = (uint32_t)(fatBytes / 4); + inst.fatCache = (uint32_t*)Memory::g_pfa->ReallocConsecutive( + nullptr, inst.fatCachePages); + if (inst.fatCache) { + uint8_t* dst = (uint8_t*)inst.fatCache; + uint64_t remaining = fatBytes; + uint64_t fatPartSector = reservedSectors; + while (remaining > 0) { + uint32_t chunk = (remaining > 4096) ? 4096 : (uint32_t)remaining; + uint32_t secs = (chunk + bytesPerSector - 1) / bytesPerSector; + if (!ReadPartSectors(inst, fatPartSector, secs, dst)) { + // If read fails, disable cache and fall back to per-lookup reads + inst.fatCache = nullptr; + break; + } + dst += secs * bytesPerSector; + fatPartSector += secs; + remaining -= secs * bytesPerSector; + } + } + // Clear file handles for (int i = 0; i < MaxFilesPerInstance; i++) { inst.files[i].inUse = false; diff --git a/kernel/src/Fs/FsProbe.cpp b/kernel/src/Fs/FsProbe.cpp index be8250f..ea7866d 100644 --- a/kernel/src/Fs/FsProbe.cpp +++ b/kernel/src/Fs/FsProbe.cpp @@ -7,6 +7,7 @@ #include "FsProbe.hpp" #include #include +#include namespace Fs::FsProbe { @@ -19,15 +20,24 @@ namespace Fs::FsProbe { } } + static bool IsEfiPartition(const Drivers::Storage::Gpt::PartitionInfo* part) { + auto& a = part->TypeGuid; + auto& b = Drivers::Storage::Gpt::GUID_EFI_SYSTEM; + return a.Data1 == b.Data1 && a.Data2 == b.Data2 && a.Data3 == b.Data3 && + memcmp(a.Data4, b.Data4, 8) == 0; + } + void MountPartitions(int firstDrive) { int partCount = Drivers::Storage::Gpt::GetPartitionCount(); if (partCount == 0 || g_probeCount == 0) return; int driveNum = firstDrive; + // First pass: mount non-EFI partitions so they get lower drive numbers for (int i = 0; i < partCount && driveNum < Vfs::MaxDrives; i++) { auto* part = Drivers::Storage::Gpt::GetPartition(i); if (!part) continue; + if (IsEfiPartition(part)) continue; for (int p = 0; p < g_probeCount; p++) { Vfs::FsDriver* driver = g_probes[p]( @@ -42,6 +52,26 @@ namespace Fs::FsProbe { } } } + + // Second pass: mount EFI system partitions after all others + for (int i = 0; i < partCount && driveNum < Vfs::MaxDrives; i++) { + auto* part = Drivers::Storage::Gpt::GetPartition(i); + if (!part) continue; + if (!IsEfiPartition(part)) continue; + + for (int p = 0; p < g_probeCount; p++) { + Vfs::FsDriver* driver = g_probes[p]( + part->BlockDevIndex, part->StartLba, part->SectorCount); + + if (driver) { + Vfs::RegisterDrive(driveNum, driver); + Kt::KernelLogStream(Kt::OK, "FsProbe") << "Mounted EFI partition " + << i << " as drive " << driveNum; + driveNum++; + break; + } + } + } } int MountPartition(int partIndex, int driveNum) { diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index 0c5a127..7fb1b31 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -206,6 +207,7 @@ extern "C" void kmain() { // Register filesystem probes and auto-mount partitions. // When no ramdisk, disk partitions start at drive 0 so init.elf is found there. Fs::Fat32::RegisterProbe(); + Fs::Ext2::RegisterProbe(); Fs::FsProbe::MountPartitions(hasRamdisk ? 1 : 0); Hal::LoadTSS(); diff --git a/kernel/src/Memory/Paging.cpp b/kernel/src/Memory/Paging.cpp index fc020c3..79f0eb1 100644 --- a/kernel/src/Memory/Paging.cpp +++ b/kernel/src/Memory/Paging.cpp @@ -49,11 +49,14 @@ namespace Memory::VMM { PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[virtualAddress.GetIndex(level)]); if (!entry->Present) { + void* newPage = Memory::g_pfa->AllocateZeroed(); + if (newPage == nullptr) { + Panic("OOM in Paging::HandleLevel (kernel page table allocation)", nullptr); + } + uint64_t downLevelAddr = Memory::SubHHDM((uint64_t)newPage); + entry->Present = true; entry->Writable = true; - - uint64_t downLevelAddr = Memory::SubHHDM((uint64_t)Memory::g_pfa->AllocateZeroed()); - entry->Address = downLevelAddr >> 12; return (PageTable*)downLevelAddr; @@ -66,12 +69,15 @@ namespace Memory::VMM { PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[virtualAddress.GetIndex(level)]); if (!entry->Present) { + void* newPage = Memory::g_pfa->AllocateZeroed(); + if (newPage == nullptr) { + Panic("OOM in Paging::HandleLevelUser (page table allocation)", nullptr); + } + uint64_t downLevelAddr = Memory::SubHHDM((uint64_t)newPage); + entry->Present = true; entry->Writable = true; entry->Supervisor = 1; // User-accessible - - uint64_t downLevelAddr = Memory::SubHHDM((uint64_t)Memory::g_pfa->AllocateZeroed()); - entry->Address = downLevelAddr >> 12; return (PageTable*)downLevelAddr; @@ -177,21 +183,24 @@ namespace Memory::VMM { return newPml4Phys; } - void Paging::MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) { + bool Paging::MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) { if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) { Panic("Non-aligned address in Paging::MapUserIn!", nullptr); } VirtualAddress va(virtualAddress); - // Walk/create page tables from the given PML4, setting User bit at each level + // Walk/create page tables from the given PML4, setting User bit at each level. + // Returns nullptr on OOM. auto walkLevel = [](PageTable* table, uint64_t index) -> PageTable* { PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[index]); if (!entry->Present) { + void* newPage = Memory::g_pfa->AllocateZeroed(); + if (newPage == nullptr) return nullptr; + uint64_t newPhys = Memory::SubHHDM((uint64_t)newPage); entry->Present = true; entry->Writable = true; entry->Supervisor = 1; // User-accessible - uint64_t newPhys = Memory::SubHHDM((uint64_t)Memory::g_pfa->AllocateZeroed()); entry->Address = newPhys >> 12; return (PageTable*)newPhys; } else { @@ -202,17 +211,21 @@ namespace Memory::VMM { PageTable* pml4 = (PageTable*)pml4Phys; auto pml3 = walkLevel(pml4, va.GetL4Index()); + if (!pml3) return false; auto pml2 = walkLevel(pml3, va.GetL3Index()); + if (!pml2) return false; auto pml1 = walkLevel(pml2, va.GetL2Index()); + if (!pml1) return false; PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]); pageEntry->Present = true; pageEntry->Writable = true; pageEntry->Supervisor = 1; pageEntry->Address = physicalAddress >> 12; + return true; } - void Paging::MapUserInWC(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) { + bool Paging::MapUserInWC(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) { if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) { Panic("Non-aligned address in Paging::MapUserInWC!", nullptr); } @@ -222,10 +235,12 @@ namespace Memory::VMM { auto walkLevel = [](PageTable* table, uint64_t index) -> PageTable* { PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[index]); if (!entry->Present) { + void* newPage = Memory::g_pfa->AllocateZeroed(); + if (newPage == nullptr) return nullptr; + uint64_t newPhys = Memory::SubHHDM((uint64_t)newPage); entry->Present = true; entry->Writable = true; entry->Supervisor = 1; - uint64_t newPhys = Memory::SubHHDM((uint64_t)Memory::g_pfa->AllocateZeroed()); entry->Address = newPhys >> 12; return (PageTable*)newPhys; } else { @@ -236,8 +251,11 @@ namespace Memory::VMM { PageTable* pml4 = (PageTable*)pml4Phys; auto pml3 = walkLevel(pml4, va.GetL4Index()); + if (!pml3) return false; auto pml2 = walkLevel(pml3, va.GetL3Index()); + if (!pml2) return false; auto pml1 = walkLevel(pml2, va.GetL2Index()); + if (!pml1) return false; PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]); pageEntry->Present = true; @@ -245,6 +263,7 @@ namespace Memory::VMM { pageEntry->Supervisor = 1; pageEntry->WriteThrough = true; // PWT=1, PCD=0 → PAT entry 1 = WC pageEntry->Address = physicalAddress >> 12; + return true; } void Paging::UnmapUserIn(std::uint64_t pml4Phys, std::uint64_t virtualAddress) { diff --git a/kernel/src/Memory/Paging.hpp b/kernel/src/Memory/Paging.hpp index 0b17f0b..c2e031f 100644 --- a/kernel/src/Memory/Paging.hpp +++ b/kernel/src/Memory/Paging.hpp @@ -104,10 +104,10 @@ public: static std::uint64_t CreateUserPML4(); // Map a page into an arbitrary PML4 (specified by physical address) with User bit set. - static void MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress); + static bool MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress); // Map a page into an arbitrary PML4 with User + Write-Combining attributes. - static void MapUserInWC(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress); + static bool MapUserInWC(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress); // Unmap a single page from an arbitrary PML4 (clears PTE + invalidates TLB). static void UnmapUserIn(std::uint64_t pml4Phys, std::uint64_t virtualAddress); diff --git a/kernel/src/Sched/ElfLoader.cpp b/kernel/src/Sched/ElfLoader.cpp index f942400..63ec791 100644 --- a/kernel/src/Sched/ElfLoader.cpp +++ b/kernel/src/Sched/ElfLoader.cpp @@ -115,7 +115,11 @@ namespace Sched { uint64_t virtAddr = segBase + p * 0x1000; // Map into the process's PML4 with User bit set - Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, virtAddr); + if (!Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, virtAddr)) { + Kt::KernelLogStream(Kt::ERROR, "ELF") << "Failed to map page"; + Memory::g_heap->Free(fileData); + return 0; + } // Copy file data that overlaps this page (via HHDM) uint64_t pageStart = virtAddr; diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index f4a83be..d1f503d 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -149,7 +149,10 @@ namespace Sched { return -1; } uint64_t physAddr = Memory::SubHHDM((uint64_t)page); - Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, userStackBase + i * 0x1000); + if (!Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, userStackBase + i * 0x1000)) { + Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to map user stack page"; + return -1; + } if (i == UserStackPages - 1) topStackPagePhys = physAddr; } @@ -162,7 +165,10 @@ namespace Sched { return -1; } uint64_t stubPhys = Memory::SubHHDM((uint64_t)stubPage); - Memory::VMM::Paging::MapUserIn(pml4Phys, stubPhys, ExitStubAddr); + if (!Memory::VMM::Paging::MapUserIn(pml4Phys, stubPhys, ExitStubAddr)) { + Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to map exit stub"; + return -1; + } // Write: xor edi, edi; xor eax, eax; syscall uint8_t* stub = (uint8_t*)stubPage; diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index ed96908..e1c0dfd 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -241,6 +241,7 @@ namespace Montauk { // Filesystem type IDs for SYS_FSFORMAT static constexpr int FS_TYPE_FAT32 = 1; + static constexpr int FS_TYPE_EXT2 = 2; struct FsFormatParams { int32_t partIndex; // global partition index diff --git a/programs/src/desktop/apps/app_filemanager.cpp b/programs/src/desktop/apps/app_filemanager.cpp index f589329..7b3d158 100644 --- a/programs/src/desktop/apps/app_filemanager.cpp +++ b/programs/src/desktop/apps/app_filemanager.cpp @@ -695,6 +695,8 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath); } else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) { montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath); + } else if (str_ends_with(fm->entry_names[clicked_idx], ".elf")) { + montauk::spawn(fullpath); } else if (fm->desktop) { open_texteditor_with_file(fm->desktop, fullpath); } @@ -751,6 +753,8 @@ static void filemanager_on_mouse(Window* win, MouseEvent& ev) { montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath); } else if (is_spreadsheet_file(fm->entry_names[clicked_idx])) { montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath); + } else if (str_ends_with(fm->entry_names[clicked_idx], ".elf")) { + montauk::spawn(fullpath); } else if (fm->desktop) { open_texteditor_with_file(fm->desktop, fullpath); } diff --git a/programs/src/init/main.cpp b/programs/src/init/main.cpp index 7bb7e33..f4e571d 100644 --- a/programs/src/init/main.cpp +++ b/programs/src/init/main.cpp @@ -124,7 +124,7 @@ static void log_err(const char* msg) { log(LOG_ERR, msg); } // ---- Service runner ---- -static bool run_service(const char* path, const char* name) { +static bool run_service(const char* path, const char* name, bool wait = true) { char msg[128]; snprintf(msg, sizeof(msg), "Starting %s", name); @@ -137,10 +137,14 @@ static bool run_service(const char* path, const char* name) { return false; } - montauk::waitpid(pid); - - snprintf(msg, sizeof(msg), "%s finished (pid %d)", name, pid); - log_ok(msg); + if (wait) { + montauk::waitpid(pid); + snprintf(msg, sizeof(msg), "%s finished (pid %d)", name, pid); + log_ok(msg); + } else { + snprintf(msg, sizeof(msg), "%s spawned in background (pid %d)", name, pid); + log_ok(msg); + } return true; } @@ -150,8 +154,8 @@ extern "C" void _start() { log_info("The Montauk Operating System"); - // ---- Stage 1: Network configuration ---- - run_service("0:/os/dhcp.elf", "dhcp"); + // ---- Stage 1: Network configuration (non-blocking) ---- + run_service("0:/os/dhcp.elf", "dhcp", false); // ---- Stage 2: Desktop environment (falls back to shell) ---- if (!run_service("0:/os/desktop.elf", "desktop")) { diff --git a/programs/src/installer/actions.cpp b/programs/src/installer/actions.cpp index 40bc0e7..7a662a1 100644 --- a/programs/src/installer/actions.cpp +++ b/programs/src/installer/actions.cpp @@ -7,9 +7,10 @@ #include "installer.h" // ============================================================================ -// EFI System Partition type GUID: C12A7328-F81F-11D2-BA4B-00A0C93EC93B +// Partition type GUIDs // ============================================================================ +// EFI System Partition: C12A7328-F81F-11D2-BA4B-00A0C93EC93B static Montauk::PartGuid esp_type_guid() { Montauk::PartGuid g; g.Data1 = 0xC12A7328; @@ -22,10 +23,31 @@ static Montauk::PartGuid esp_type_guid() { return g; } +// Linux Filesystem: 0FC63DAF-8483-4772-8E79-3D69D8477DE4 +static Montauk::PartGuid linux_fs_type_guid() { + Montauk::PartGuid g; + g.Data1 = 0x0FC63DAF; + g.Data2 = 0x8483; + g.Data3 = 0x4772; + g.Data4[0] = 0x8E; g.Data4[1] = 0x79; + g.Data4[2] = 0x3D; g.Data4[3] = 0x69; + g.Data4[4] = 0xD8; g.Data4[5] = 0x47; + g.Data4[6] = 0x7D; g.Data4[7] = 0xE4; + return g; +} + // ============================================================================ // Refresh disk list // ============================================================================ +void installer_refresh_parts() { + auto& st = g_state; + st.part_count = montauk::partlist(st.parts, MAX_PARTS); + if (st.part_count < 0) st.part_count = 0; + if (st.selected_part < 0 || st.selected_part >= st.part_count) + st.selected_part = st.part_count > 0 ? 0 : -1; +} + void installer_refresh_disks() { auto& st = g_state; st.disk_count = 0; @@ -138,11 +160,11 @@ static int g_files_copied; static int g_dirs_created; // Copy all entries from src_dir (on ramdisk) to dst_dir (on target drive). -// src_dir: e.g. "0:/" or "0:/os" -// dst_dir: e.g. "15:/" or "15:/os" -static bool copy_recursive(const char* src_dir, const char* dst_dir) { - const char* names[64]; - int count = montauk::readdir(src_dir, names, 64); +// If skip_toplevel is non-null, skip that directory name at the top level. +static bool copy_recursive(const char* src_dir, const char* dst_dir, + const char* skip_toplevel = nullptr) { + const char* names[256]; + int count = montauk::readdir(src_dir, names, 256); if (count < 0) return true; // not a directory or empty, skip // Ramdisk readdir returns names relative to the drive root with the @@ -188,6 +210,13 @@ static bool copy_recursive(const char* src_dir, const char* dst_dir) { for (; j < blen - 1 && j < 255; j++) dir_name[j] = basename[j]; dir_name[j] = '\0'; + // Skip the installer app — no need on the installed system + if (strcmp(dir_name, "installer") == 0 && + strcmp(src_local, "apps") == 0) continue; + + // Skip requested top-level directory + if (skip_toplevel && strcmp(dir_name, skip_toplevel) == 0) continue; + // Create directory on target char target_path[256]; path_join(target_path, sizeof(target_path), dst_dir, dir_name); @@ -237,32 +266,245 @@ static bool copy_recursive(const char* src_dir, const char* dst_dir) { } // ============================================================================ -// Install MontaukOS to the selected disk +// GPT + partition helper // ============================================================================ -void do_install() { +static void set_gpt_name(Montauk::GptAddParams& params, const char* name) { + int i = 0; + for (; name[i] && i < 71; i++) params.name[i] = name[i]; + params.name[i] = '\0'; +} + +// ============================================================================ +// Install: EFI + ext2 scheme +// ============================================================================ + +static void install_efi_ext2(int disk) { auto& st = g_state; - int disk = st.selected_disk; - - if (disk < 0 || disk >= st.disk_count) { - st.step = STEP_ERROR; - add_log("No disk selected"); - flush_ui(); - return; - } - - g_files_copied = 0; - g_dirs_created = 0; + char path_buf[64]; + int r; // Step 1: Initialize GPT add_log("Initializing GPT..."); flush_ui(); - int r = montauk::gpt_init(disk); + r = montauk::gpt_init(disk); if (r < 0) { add_log("ERROR: Failed to initialize GPT"); - flush_ui(); - st.step = STEP_ERROR; - return; + flush_ui(); st.step = STEP_ERROR; return; + } + add_log(" GPT initialized"); + flush_ui(); + + // Step 2: Create EFI System Partition (128 MB) + // After gpt_init, FirstUsableLba = 34. + static constexpr uint64_t ESP_SECTORS = 128 * 1024 * 2; // 128 MB in 512-byte sectors + static constexpr uint64_t FIRST_USABLE_LBA = 34; + + add_log("Creating EFI partition (128 MB)..."); + flush_ui(); + { + Montauk::GptAddParams params; + montauk::memset(¶ms, 0, sizeof(params)); + params.blockDev = disk; + params.startLba = FIRST_USABLE_LBA; + params.endLba = FIRST_USABLE_LBA + ESP_SECTORS - 1; + params.typeGuid = esp_type_guid(); + set_gpt_name(params, "EFI System"); + + r = montauk::gpt_add(¶ms); + if (r < 0) { + add_log("ERROR: Failed to create EFI partition"); + flush_ui(); st.step = STEP_ERROR; return; + } + } + add_log(" EFI partition created"); + flush_ui(); + + // Step 3: Create Linux partition (remaining space) + add_log("Creating ext2 partition..."); + flush_ui(); + { + Montauk::GptAddParams params; + montauk::memset(¶ms, 0, sizeof(params)); + params.blockDev = disk; + params.startLba = 0; // auto-fill largest free region + params.endLba = 0; + params.typeGuid = linux_fs_type_guid(); + set_gpt_name(params, "MontaukOS"); + + r = montauk::gpt_add(¶ms); + if (r < 0) { + add_log("ERROR: Failed to create Linux partition"); + flush_ui(); st.step = STEP_ERROR; return; + } + } + add_log(" ext2 partition created"); + flush_ui(); + + // Step 4: Find partition indices + Montauk::PartInfo parts[MAX_PARTS]; + int part_count = montauk::partlist(parts, MAX_PARTS); + int efi_idx = -1, linux_idx = -1; + for (int p = 0; p < part_count; p++) { + if (parts[p].blockDev != disk) continue; + // EFI partition starts at FIRST_USABLE_LBA + if (parts[p].startLba == FIRST_USABLE_LBA) + efi_idx = p; + else + linux_idx = p; + } + if (efi_idx < 0 || linux_idx < 0) { + add_log("ERROR: Could not find partitions"); + flush_ui(); st.step = STEP_ERROR; return; + } + + // Step 5: Format EFI partition as FAT32 + add_log("Formatting EFI partition (FAT32)..."); + flush_ui(); + { + Montauk::FsFormatParams fmt; + montauk::memset(&fmt, 0, sizeof(fmt)); + fmt.partIndex = efi_idx; + fmt.fsType = Montauk::FS_TYPE_FAT32; + r = montauk::fs_format(&fmt); + if (r < 0) { + add_log("ERROR: FAT32 format failed"); + flush_ui(); st.step = STEP_ERROR; return; + } + } + add_log(" FAT32 formatted"); + flush_ui(); + + // Step 6: Format Linux partition as ext2 + add_log("Formatting root partition (ext2)..."); + flush_ui(); + { + Montauk::FsFormatParams fmt; + montauk::memset(&fmt, 0, sizeof(fmt)); + fmt.partIndex = linux_idx; + fmt.fsType = Montauk::FS_TYPE_EXT2; + r = montauk::fs_format(&fmt); + if (r < 0) { + add_log("ERROR: ext2 format failed"); + flush_ui(); st.step = STEP_ERROR; return; + } + } + add_log(" ext2 formatted"); + flush_ui(); + + // Step 7: Mount both partitions + add_log("Mounting partitions..."); + flush_ui(); + int efi_drive = 14, root_drive = 15; + r = montauk::fs_mount(efi_idx, efi_drive); + if (r < 0) { + add_log("ERROR: EFI mount failed"); + flush_ui(); st.step = STEP_ERROR; return; + } + r = montauk::fs_mount(linux_idx, root_drive); + if (r < 0) { + add_log("ERROR: ext2 mount failed"); + flush_ui(); st.step = STEP_ERROR; return; + } + add_log(" Mounted"); + flush_ui(); + + char efi_root[8], ext2_root[8]; + snprintf(efi_root, sizeof(efi_root), "%d:/", efi_drive); + snprintf(ext2_root, sizeof(ext2_root), "%d:/", root_drive); + + // Step 8: Create EFI boot directory structure + add_log("Creating boot directories..."); + flush_ui(); + snprintf(path_buf, sizeof(path_buf), "%d:/EFI", efi_drive); + montauk::fmkdir(path_buf); + snprintf(path_buf, sizeof(path_buf), "%d:/EFI/BOOT", efi_drive); + montauk::fmkdir(path_buf); + snprintf(path_buf, sizeof(path_buf), "%d:/boot", efi_drive); + montauk::fmkdir(path_buf); + + // Step 9: Copy boot files to EFI partition + add_log("Copying boot files to EFI partition..."); + flush_ui(); + + char efi_boot[16]; + snprintf(efi_boot, sizeof(efi_boot), "%d:/boot", efi_drive); + if (!copy_recursive("0:/boot", efi_boot)) { + add_log("ERROR: Boot file copy failed"); + flush_ui(); st.step = STEP_ERROR; return; + } + + // Step 10: Copy root filesystem to ext2 partition (skip boot/) + add_log("Copying root filesystem to ext2..."); + flush_ui(); + if (!copy_recursive("0:/", ext2_root, "boot")) { + add_log("ERROR: Root filesystem copy failed"); + flush_ui(); st.step = STEP_ERROR; return; + } + + char copy_msg[64]; + snprintf(copy_msg, sizeof(copy_msg), " %d files, %d directories copied", + g_files_copied, g_dirs_created); + add_log(copy_msg); + flush_ui(); + + // Step 11: Write limine.conf to EFI partition + add_log("Writing boot configuration..."); + flush_ui(); + snprintf(path_buf, sizeof(path_buf), "%d:/boot/limine/limine.conf", efi_drive); + { + static const char limine_conf[] = + "timeout: 0\n" + "\n" + "/montaukos\n" + " protocol: limine\n" + " path: boot():/boot/kernel\n"; + + int conf_fd = montauk::fcreate(path_buf); + if (conf_fd < 0) { + add_log("ERROR: Failed to write limine.conf"); + flush_ui(); st.step = STEP_ERROR; return; + } + montauk::fwrite(conf_fd, (const uint8_t*)limine_conf, 0, sizeof(limine_conf) - 1); + montauk::close(conf_fd); + } + add_log(" limine.conf written"); + flush_ui(); + + // Step 12: Copy BOOTX64.EFI to EFI/BOOT/ + add_log("Installing EFI bootloader..."); + flush_ui(); + snprintf(path_buf, sizeof(path_buf), "%d:/EFI/BOOT/BOOTX64.EFI", efi_drive); + if (!copy_file("0:/boot/limine/BOOTX64.EFI", path_buf)) { + add_log("ERROR: Failed to install BOOTX64.EFI"); + flush_ui(); st.step = STEP_ERROR; return; + } + add_log(" BOOTX64.EFI installed"); + flush_ui(); + + // Done + add_log("Installation complete!"); + st.step = STEP_DONE; + set_status("MontaukOS installed successfully"); + flush_ui(); +} + +// ============================================================================ +// Install: Single FAT32 scheme (legacy) +// ============================================================================ + +static void install_single_fat32(int disk) { + auto& st = g_state; + char path_buf[64]; + int r; + + // Step 1: Initialize GPT + add_log("Initializing GPT..."); + flush_ui(); + r = montauk::gpt_init(disk); + if (r < 0) { + add_log("ERROR: Failed to initialize GPT"); + flush_ui(); st.step = STEP_ERROR; return; } add_log(" GPT initialized"); flush_ui(); @@ -276,18 +518,12 @@ void do_install() { params.startLba = 0; params.endLba = 0; params.typeGuid = esp_type_guid(); - - const char* pname = "EFI System"; - int i = 0; - for (; pname[i] && i < 71; i++) params.name[i] = pname[i]; - params.name[i] = '\0'; + set_gpt_name(params, "EFI System"); r = montauk::gpt_add(¶ms); if (r < 0) { add_log("ERROR: Failed to create partition"); - flush_ui(); - st.step = STEP_ERROR; - return; + flush_ui(); st.step = STEP_ERROR; return; } add_log(" Partition created"); flush_ui(); @@ -308,9 +544,7 @@ void do_install() { if (esp_index < 0) { add_log("ERROR: Could not find partition"); - flush_ui(); - st.step = STEP_ERROR; - return; + flush_ui(); st.step = STEP_ERROR; return; } Montauk::FsFormatParams fmt; @@ -321,9 +555,7 @@ void do_install() { r = montauk::fs_format(&fmt); if (r < 0) { add_log("ERROR: FAT32 format failed"); - flush_ui(); - st.step = STEP_ERROR; - return; + flush_ui(); st.step = STEP_ERROR; return; } add_log(" FAT32 formatted"); flush_ui(); @@ -335,9 +567,7 @@ void do_install() { r = montauk::fs_mount(esp_index, drive_num); if (r < 0) { add_log("ERROR: Mount failed"); - flush_ui(); - st.step = STEP_ERROR; - return; + flush_ui(); st.step = STEP_ERROR; return; } char drive_root[8]; @@ -349,7 +579,6 @@ void do_install() { add_log("Creating boot directories..."); flush_ui(); - char path_buf[64]; snprintf(path_buf, sizeof(path_buf), "%d:/EFI", drive_num); montauk::fmkdir(path_buf); snprintf(path_buf, sizeof(path_buf), "%d:/EFI/BOOT", drive_num); @@ -361,9 +590,7 @@ void do_install() { if (!copy_recursive("0:/", drive_root)) { add_log("ERROR: File copy failed"); - flush_ui(); - st.step = STEP_ERROR; - return; + flush_ui(); st.step = STEP_ERROR; return; } char copy_msg[64]; @@ -387,9 +614,7 @@ void do_install() { int conf_fd = montauk::fcreate(path_buf); if (conf_fd < 0) { add_log("ERROR: Failed to write limine.conf"); - flush_ui(); - st.step = STEP_ERROR; - return; + flush_ui(); st.step = STEP_ERROR; return; } montauk::fwrite(conf_fd, (const uint8_t*)limine_conf, 0, sizeof(limine_conf) - 1); montauk::close(conf_fd); @@ -398,16 +623,12 @@ void do_install() { flush_ui(); // Step 8: Copy BOOTX64.EFI to EFI/BOOT/ - // (The recursive copy already put it at boot/limine/BOOTX64.EFI, - // but UEFI firmware requires it at /EFI/BOOT/BOOTX64.EFI) add_log("Installing EFI bootloader..."); flush_ui(); snprintf(path_buf, sizeof(path_buf), "%d:/EFI/BOOT/BOOTX64.EFI", drive_num); if (!copy_file("0:/boot/limine/BOOTX64.EFI", path_buf)) { add_log("ERROR: Failed to install BOOTX64.EFI"); - flush_ui(); - st.step = STEP_ERROR; - return; + flush_ui(); st.step = STEP_ERROR; return; } add_log(" BOOTX64.EFI installed"); flush_ui(); @@ -418,3 +639,109 @@ void do_install() { set_status("MontaukOS installed successfully"); flush_ui(); } + +// ============================================================================ +// Install MontaukOS to the selected disk +// ============================================================================ + +void do_install() { + auto& st = g_state; + int disk = st.selected_disk; + + if (disk < 0 || disk >= st.disk_count) { + st.step = STEP_ERROR; + add_log("No disk selected"); + flush_ui(); + return; + } + + g_files_copied = 0; + g_dirs_created = 0; + + switch (st.partition_scheme) { + case SCHEME_EFI_EXT2: + install_efi_ext2(disk); + break; + case SCHEME_SINGLE_FAT32: + install_single_fat32(disk); + break; + default: + st.step = STEP_ERROR; + add_log("Unknown partition scheme"); + flush_ui(); + break; + } +} + +// ============================================================================ +// Update OS and apps on an existing partition +// ============================================================================ + +void do_update() { + auto& st = g_state; + + if (st.selected_part < 0 || st.selected_part >= st.part_count) { + st.step = STEP_ERROR; + add_log("No partition selected"); + flush_ui(); + return; + } + + g_files_copied = 0; + g_dirs_created = 0; + + int part_idx = st.selected_part; + int r; + + // Step 1: Mount the target partition + add_log("Mounting target partition..."); + flush_ui(); + int drive_num = 15; + r = montauk::fs_mount(part_idx, drive_num); + if (r < 0) { + add_log("ERROR: Mount failed"); + flush_ui(); st.step = STEP_ERROR; return; + } + + char drive_root[8]; + snprintf(drive_root, sizeof(drive_root), "%d:/", drive_num); + add_log(" Mounted"); + flush_ui(); + + // Step 2: Update os/ — remove old and copy fresh + add_log("Updating os/..."); + flush_ui(); + + char path_buf[64]; + snprintf(path_buf, sizeof(path_buf), "%d:/os", drive_num); + montauk::fmkdir(path_buf); // ensure it exists + + if (!copy_recursive("0:/os", path_buf)) { + add_log("ERROR: Failed to update os/"); + flush_ui(); st.step = STEP_ERROR; return; + } + + // Step 3: Update apps/ — copy all, overwriting existing and adding new + add_log("Updating apps/..."); + flush_ui(); + + snprintf(path_buf, sizeof(path_buf), "%d:/apps", drive_num); + montauk::fmkdir(path_buf); // ensure it exists + + if (!copy_recursive("0:/apps", path_buf)) { + add_log("ERROR: Failed to update apps/"); + flush_ui(); st.step = STEP_ERROR; return; + } + + char copy_msg[64]; + snprintf(copy_msg, sizeof(copy_msg), " %d files, %d directories updated", + g_files_copied, g_dirs_created); + add_log(copy_msg); + flush_ui(); + + // Done + add_log("Update complete!"); + st.step = STEP_DONE; + set_status("MontaukOS updated successfully"); + flush_ui(); +} diff --git a/programs/src/installer/installer.h b/programs/src/installer/installer.h index 93f026b..39d7a1e 100644 --- a/programs/src/installer/installer.h +++ b/programs/src/installer/installer.h @@ -56,13 +56,23 @@ static constexpr Color DISABLED_BG = Color::from_rgb(0xBB, 0xBB, 0xBB); // Install steps // ============================================================================ +enum InstallerMode { + MODE_INSTALL = 0, + MODE_UPDATE, +}; + enum InstallStep { + STEP_MODE_SELECT, STEP_SELECT_DISK, STEP_PARTITION_SCHEME, STEP_CONFIRM, STEP_INSTALLING, STEP_DONE, STEP_ERROR, + // Update flow + STEP_UPDATE_SELECT_PART, + STEP_UPDATE_CONFIRM, + STEP_UPDATING, }; // ============================================================================ @@ -70,15 +80,18 @@ enum InstallStep { // ============================================================================ enum PartScheme { - SCHEME_SINGLE_FAT32 = 0, + SCHEME_EFI_EXT2 = 0, + SCHEME_SINGLE_FAT32, SCHEME_COUNT, }; static const char* scheme_names[] = { + "EFI + ext2 (recommended)", "Single FAT32 partition (entire disk)", }; static const char* scheme_descs[] = { + "EFI boot partition + ext2 root filesystem.", "One partition for boot, OS, and data. Simple and compatible.", }; @@ -90,11 +103,19 @@ static constexpr int LOG_LINES = 16; static constexpr int LOG_LINE_LEN = 64; struct InstallerState { + int mode; // InstallerMode + + // Install flow Montauk::DiskInfo disks[MAX_DISKS]; int disk_count; int selected_disk; int partition_scheme; + // Update flow + Montauk::PartInfo parts[MAX_PARTS]; + int part_count; + int selected_part; + InstallStep step; char log[LOG_LINES][LOG_LINE_LEN]; int log_count; @@ -111,6 +132,7 @@ extern int g_win_w, g_win_h; extern InstallerState g_state; extern TrueTypeFont* g_font; extern uint32_t* g_pixels; +extern uint32_t* g_backbuf; extern int g_win_id; // ============================================================================ @@ -134,4 +156,6 @@ void render(uint32_t* pixels); // ============================================================================ void installer_refresh_disks(); +void installer_refresh_parts(); void do_install(); +void do_update(); diff --git a/programs/src/installer/main.cpp b/programs/src/installer/main.cpp index 0a3a414..5c102d1 100644 --- a/programs/src/installer/main.cpp +++ b/programs/src/installer/main.cpp @@ -19,6 +19,7 @@ int FONT_SM = 14; InstallerState g_state; TrueTypeFont* g_font = nullptr; uint32_t* g_pixels = nullptr; +uint32_t* g_backbuf = nullptr; int g_win_id = -1; void apply_scale(int scale) { @@ -55,8 +56,9 @@ void add_log(const char* msg) { } void flush_ui() { - if (g_pixels && g_win_id >= 0) { - render(g_pixels); + if (g_pixels && g_backbuf && g_win_id >= 0) { + render(g_backbuf); + montauk::memcpy(g_pixels, g_backbuf, g_win_w * g_win_h * 4); montauk::win_present(g_win_id); } } @@ -84,12 +86,34 @@ void format_disk_size(char* buf, int bufsize, uint64_t sectors, uint16_t sectorS static bool handle_click(int mx, int my) { auto& st = g_state; int fh = g_font ? g_font->get_cache(FONT_SIZE)->ascent - g_font->get_cache(FONT_SIZE)->descent : 16; + int fh_sm = g_font ? g_font->get_cache(FONT_SM)->ascent - g_font->get_cache(FONT_SM)->descent : 12; // Common bottom button area int btn_w = 120, btn_h = 34; int btn_y = g_win_h - STATUS_H - btn_h - 12; - if (st.step == STEP_SELECT_DISK) { + if (st.step == STEP_MODE_SELECT) { + // Two mode cards + int y = CONTENT_TOP + 16 + fh + 4 + fh_sm + 16; + int card_h = 52, card_x = 16, card_w = g_win_w - 32; + + // Install card + if (mx >= card_x && mx < card_x + card_w && my >= y && my < y + card_h) { + st.mode = MODE_INSTALL; + installer_refresh_disks(); + st.step = STEP_SELECT_DISK; + return true; + } + y += card_h + 8; + + // Update card + if (mx >= card_x && mx < card_x + card_w && my >= y && my < y + card_h) { + st.mode = MODE_UPDATE; + installer_refresh_parts(); + st.step = STEP_UPDATE_SELECT_PART; + return true; + } + } else if (st.step == STEP_SELECT_DISK) { // Disk list items int y = CONTENT_TOP + 40 + fh + 12; for (int i = 0; i < st.disk_count; i++) { @@ -171,6 +195,54 @@ static bool handle_click(int mx, int my) { my >= btn_y && my < btn_y + btn_h) { return false; // signal quit } + } else if (st.step == STEP_UPDATE_SELECT_PART) { + // Partition list items + int y = CONTENT_TOP + 16 + fh + 4 + fh_sm + 12; + for (int i = 0; i < st.part_count; i++) { + int item_h = 48; + if (mx >= 16 && mx < g_win_w - 16 && my >= y && my < y + item_h) { + st.selected_part = i; + return true; + } + y += item_h + 4; + } + + // "Next" button + int next_x = g_win_w - btn_w - 16; + if (st.selected_part >= 0 && + mx >= next_x && mx < next_x + btn_w && + my >= btn_y && my < btn_y + btn_h) { + st.step = STEP_UPDATE_CONFIRM; + return true; + } + + // "Back" button + int back_x = next_x - btn_w - 8; + if (mx >= back_x && mx < back_x + btn_w && + my >= btn_y && my < btn_y + btn_h) { + st.step = STEP_MODE_SELECT; + return true; + } + } else if (st.step == STEP_UPDATE_CONFIRM) { + int center_x = g_win_w / 2; + int gap = 16; + + // "Update" button + int upd_x = center_x - btn_w - gap / 2; + if (mx >= upd_x && mx < upd_x + btn_w && + my >= btn_y && my < btn_y + btn_h) { + st.step = STEP_UPDATING; + st.log_count = 0; + return true; + } + + // "Back" button + int back_x = center_x + gap / 2; + if (mx >= back_x && mx < back_x + btn_w && + my >= btn_y && my < btn_y + btn_h) { + st.step = STEP_UPDATE_SELECT_PART; + return true; + } } return true; @@ -183,8 +255,9 @@ static bool handle_click(int mx, int my) { extern "C" void _start() { montauk::memset(&g_state, 0, sizeof(g_state)); g_state.selected_disk = -1; - g_state.partition_scheme = SCHEME_SINGLE_FAT32; - g_state.step = STEP_SELECT_DISK; + g_state.selected_part = -1; + g_state.partition_scheme = SCHEME_EFI_EXT2; + g_state.step = STEP_MODE_SELECT; // Load font { @@ -209,8 +282,10 @@ extern "C" void _start() { uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa; g_pixels = pixels; g_win_id = win_id; + g_backbuf = (uint32_t*)montauk::malloc(g_win_w * g_win_h * 4); - render(pixels); + render(g_backbuf); + montauk::memcpy(pixels, g_backbuf, g_win_w * g_win_h * 4); montauk::win_present(win_id); bool install_triggered = false; @@ -225,11 +300,15 @@ extern "C" void _start() { if (r == 0) { if (g_state.step == STEP_INSTALLING && !install_triggered) { install_triggered = true; - render(pixels); - montauk::win_present(win_id); + flush_ui(); do_install(); - render(pixels); - montauk::win_present(win_id); + flush_ui(); + } + if (g_state.step == STEP_UPDATING && !install_triggered) { + install_triggered = true; + flush_ui(); + do_update(); + flush_ui(); } montauk::sleep_ms(16); continue; @@ -246,6 +325,9 @@ extern "C" void _start() { g_win_w = ev.resize.w; g_win_h = ev.resize.h; pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h); + g_pixels = pixels; + montauk::mfree(g_backbuf); + g_backbuf = (uint32_t*)montauk::malloc(g_win_w * g_win_h * 4); redraw = true; } @@ -263,7 +345,7 @@ extern "C" void _start() { } } - if (redraw) { render(pixels); montauk::win_present(win_id); } + if (redraw) flush_ui(); } montauk::win_destroy(win_id); diff --git a/programs/src/installer/render.cpp b/programs/src/installer/render.cpp index 11eb3f7..22de464 100644 --- a/programs/src/installer/render.cpp +++ b/programs/src/installer/render.cpp @@ -137,6 +137,36 @@ static void px_button_outline(uint32_t* px, int bw, int bh, px_text(px, bw, bh, x + (w - tw) / 2, y + (h - fh) / 2, label, fg); } +// ============================================================================ +// Render: mode selection step +// ============================================================================ + +static void render_mode_select(uint32_t* px) { + int fh = font_h(); + int y = CONTENT_TOP + 16; + + px_text(px, g_win_w, g_win_h, 16, y, "Welcome to MontaukOS Installer", TEXT_COLOR); + y += fh + 4; + px_text(px, g_win_w, g_win_h, 16, y, "Choose an action below.", DIM_TEXT, FONT_SM); + y += font_h(FONT_SM) + 16; + + // Install card + int card_h = 52, card_x = 16, card_w = g_win_w - 32; + px_fill_rounded(px, g_win_w, g_win_h, card_x, y, card_w, card_h, 6, + Color::from_rgb(0xF4, 0xF4, 0xF4)); + px_text(px, g_win_w, g_win_h, card_x + 16, y + 8, "Install", TEXT_COLOR); + px_text(px, g_win_w, g_win_h, card_x + 16, y + 8 + fh + 2, + "Erase a disk and install MontaukOS.", DIM_TEXT, FONT_SM); + y += card_h + 8; + + // Update card + px_fill_rounded(px, g_win_w, g_win_h, card_x, y, card_w, card_h, 6, + Color::from_rgb(0xF4, 0xF4, 0xF4)); + px_text(px, g_win_w, g_win_h, card_x + 16, y + 8, "Update", TEXT_COLOR); + px_text(px, g_win_w, g_win_h, card_x + 16, y + 8 + fh + 2, + "Update OS and apps on an existing partition.", DIM_TEXT, FONT_SM); +} + // ============================================================================ // Render: disk selection step // ============================================================================ @@ -161,22 +191,24 @@ static void render_select_disk(uint32_t* px) { int item_w = g_win_w - 32; bool sel = (i == st.selected_disk); - Color bg = sel - ? Color::from_rgb(0xD8, 0xE8, 0xF8) - : Color::from_rgb(0xF4, 0xF4, 0xF4); - - px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, bg); + px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, + Color::from_rgb(0xF4, 0xF4, 0xF4)); + // Radio indicator + int rx = item_x + 14; + int ry = y + item_h / 2; + px_fill_rounded(px, g_win_w, g_win_h, rx - 6, ry - 6, 12, 12, 6, DIM_TEXT); + px_fill_rounded(px, g_win_w, g_win_h, rx - 5, ry - 5, 10, 10, 5, BG_COLOR); if (sel) - px_fill_rounded(px, g_win_w, g_win_h, item_x, y, 4, item_h, 2, ACCENT); + px_fill_rounded(px, g_win_w, g_win_h, rx - 3, ry - 3, 6, 6, 3, ACCENT); - px_text(px, g_win_w, g_win_h, item_x + 12, y + 6, st.disks[i].model, TEXT_COLOR); + px_text(px, g_win_w, g_win_h, item_x + 32, y + 6, st.disks[i].model, TEXT_COLOR); char info[64], sz[24]; format_disk_size(sz, sizeof(sz), st.disks[i].sectorCount, st.disks[i].sectorSizeLog); const char* dtype = st.disks[i].rpm == 1 ? "SSD" : "HDD"; snprintf(info, sizeof(info), "%s %s (Disk %d)", sz, dtype, i); - px_text(px, g_win_w, g_win_h, item_x + 12, y + 6 + fh + 2, info, DIM_TEXT, FONT_SM); + px_text(px, g_win_w, g_win_h, item_x + 32, y + 6 + fh + 2, info, DIM_TEXT, FONT_SM); y += item_h + 4; } @@ -217,14 +249,9 @@ static void render_partition_scheme(uint32_t* px) { int item_w = g_win_w - 32; bool selected = (i == st.partition_scheme); - Color bg = selected - ? Color::from_rgb(0xD8, 0xE8, 0xF8) - : Color::from_rgb(0xF4, 0xF4, 0xF4); - px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, bg); - - if (selected) - px_fill_rounded(px, g_win_w, g_win_h, item_x, y, 4, item_h, 2, ACCENT); + px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, + Color::from_rgb(0xF4, 0xF4, 0xF4)); // Radio indicator int rx = item_x + 14; @@ -299,6 +326,112 @@ static void render_confirm(uint32_t* px) { "Back", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); } +// ============================================================================ +// Render: update partition selection step +// ============================================================================ + +static void render_update_select_part(uint32_t* px) { + auto& st = g_state; + int fh = font_h(); + int fh_sm = font_h(FONT_SM); + + int y = CONTENT_TOP + 16; + px_text(px, g_win_w, g_win_h, 16, y, "Select a root partition", TEXT_COLOR); + y += fh + 4; + px_text(px, g_win_w, g_win_h, 16, y, + "OS and apps will be updated on this partition.", DIM_TEXT, FONT_SM); + y += fh_sm + 12; + + if (st.part_count == 0) { + px_text(px, g_win_w, g_win_h, 16, y, "No partitions detected", FAINT_TEXT); + } + + for (int i = 0; i < st.part_count; i++) { + int item_h = 48; + int item_x = 16; + int item_w = g_win_w - 32; + bool sel = (i == st.selected_part); + + px_fill_rounded(px, g_win_w, g_win_h, item_x, y, item_w, item_h, 6, + Color::from_rgb(0xF4, 0xF4, 0xF4)); + + // Radio indicator + int rx = item_x + 14; + int ry = y + item_h / 2; + px_fill_rounded(px, g_win_w, g_win_h, rx - 6, ry - 6, 12, 12, 6, DIM_TEXT); + px_fill_rounded(px, g_win_w, g_win_h, rx - 5, ry - 5, 10, 10, 5, BG_COLOR); + if (sel) + px_fill_rounded(px, g_win_w, g_win_h, rx - 3, ry - 3, 6, 6, 3, ACCENT); + + // Partition name + const char* pname = st.parts[i].name[0] ? st.parts[i].name : "Unnamed"; + px_text(px, g_win_w, g_win_h, item_x + 32, y + 6, pname, TEXT_COLOR); + + // Info line: type + size + char info[64], sz[24]; + format_disk_size(sz, sizeof(sz), st.parts[i].sectorCount, 512); + snprintf(info, sizeof(info), "%s %s (Disk %d)", + sz, st.parts[i].typeName, st.parts[i].blockDev); + px_text(px, g_win_w, g_win_h, item_x + 32, y + 6 + fh + 2, info, DIM_TEXT, FONT_SM); + + y += item_h + 4; + } + + // Buttons + int btn_w = 120, btn_h = 34; + int btn_y = g_win_h - STATUS_H - btn_h - 12; + int next_x = g_win_w - btn_w - 16; + + Color next_bg = (st.selected_part >= 0) ? ACCENT : DISABLED_BG; + px_button(px, g_win_w, g_win_h, next_x, btn_y, btn_w, btn_h, + "Next", next_bg, WHITE, TB_BTN_RAD); + + int back_x = next_x - btn_w - 8; + px_button_outline(px, g_win_w, g_win_h, back_x, btn_y, btn_w, btn_h, + "Back", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); +} + +// ============================================================================ +// Render: update confirmation step +// ============================================================================ + +static void render_update_confirm(uint32_t* px) { + auto& st = g_state; + int fh = font_h(); + int y = CONTENT_TOP + 24; + + const char* title = "Confirm Update"; + px_text(px, g_win_w, g_win_h, (g_win_w - text_w(title)) / 2, y, title, TEXT_COLOR); + y += fh + 16; + + const char* warn1 = "This will overwrite os/ and apps/ on:"; + px_text(px, g_win_w, g_win_h, (g_win_w - text_w(warn1)) / 2, y, warn1, ACCENT); + y += fh + 8; + + char desc[128], sz[24]; + format_disk_size(sz, sizeof(sz), st.parts[st.selected_part].sectorCount, 512); + const char* pname = st.parts[st.selected_part].name[0] + ? st.parts[st.selected_part].name : "Unnamed"; + snprintf(desc, sizeof(desc), "%s (%s, Disk %d)", + pname, sz, st.parts[st.selected_part].blockDev); + px_text(px, g_win_w, g_win_h, (g_win_w - text_w(desc)) / 2, y, desc, TEXT_COLOR); + y += fh + 16; + + const char* info = "Existing user data will not be affected."; + px_text(px, g_win_w, g_win_h, (g_win_w - text_w(info, FONT_SM)) / 2, y, info, DIM_TEXT, FONT_SM); + + // Buttons + int btn_w = 120, btn_h = 34; + int center_x = g_win_w / 2; + int btn_y = g_win_h - STATUS_H - btn_h - 12; + int gap = 16; + + px_button(px, g_win_w, g_win_h, center_x - btn_w - gap / 2, btn_y, btn_w, btn_h, + "Update", ACCENT, WHITE, TB_BTN_RAD); + px_button_outline(px, g_win_w, g_win_h, center_x + gap / 2, btn_y, btn_w, btn_h, + "Back", BORDER_COLOR, DIM_TEXT, TB_BTN_RAD); +} + // ============================================================================ // Render: installing / done / error steps // ============================================================================ @@ -311,14 +444,14 @@ static void render_progress(uint32_t* px) { const char* title; Color title_color; - if (st.step == STEP_INSTALLING) { - title = "Installing..."; + if (st.step == STEP_INSTALLING || st.step == STEP_UPDATING) { + title = (st.mode == MODE_UPDATE) ? "Updating..." : "Installing..."; title_color = TEXT_COLOR; } else if (st.step == STEP_DONE) { - title = "Installation Complete"; + title = (st.mode == MODE_UPDATE) ? "Update Complete" : "Installation Complete"; title_color = SUCCESS_COLOR; } else { - title = "Installation Failed"; + title = (st.mode == MODE_UPDATE) ? "Update Failed" : "Installation Failed"; title_color = DANGER; } @@ -375,14 +508,29 @@ static void render_step_bar(uint32_t* px) { Color::from_rgb(0xFA, 0xFA, 0xFA)); px_hline(px, g_win_w, g_win_h, 0, bar_y + STEP_BAR_H - 1, g_win_w, BORDER_COLOR); - static const char* step_labels[] = { "Disk", "Partition", "Confirm", "Install" }; - static constexpr int STEP_COUNT = 4; + // No step bar for mode selection + if (g_state.step == STEP_MODE_SELECT) return; - // Map current state to step index (0-3) + static const char* install_labels[] = { "Disk", "Partition", "Confirm", "Install" }; + static const char* update_labels[] = { "Partition", "Confirm", "Update" }; + + const char** step_labels; + int STEP_COUNT; int cur = 0; - if (g_state.step == STEP_PARTITION_SCHEME) cur = 1; - else if (g_state.step == STEP_CONFIRM) cur = 2; - else if (g_state.step >= STEP_INSTALLING) cur = 3; + + if (g_state.mode == MODE_UPDATE) { + step_labels = update_labels; + STEP_COUNT = 3; + if (g_state.step == STEP_UPDATE_CONFIRM) cur = 1; + else if (g_state.step >= STEP_UPDATING) cur = 2; + else if (g_state.step == STEP_DONE || g_state.step == STEP_ERROR) cur = 2; + } else { + step_labels = install_labels; + STEP_COUNT = 4; + if (g_state.step == STEP_PARTITION_SCHEME) cur = 1; + else if (g_state.step == STEP_CONFIRM) cur = 2; + else if (g_state.step >= STEP_INSTALLING) cur = 3; + } int fh = font_h(); @@ -423,10 +571,17 @@ static void render_status(uint32_t* px) { px_fill(px, g_win_w, g_win_h, 0, sy, g_win_w, STATUS_H, Color::from_rgb(0xF0, 0xF0, 0xF0)); px_hline(px, g_win_w, g_win_h, 0, sy, g_win_w, BORDER_COLOR); if (st.status[0]) { - uint64_t age = montauk::get_milliseconds() - st.status_time; - Color sc = (age < 5000) - ? Color::from_rgb(0x33, 0x33, 0x33) - : Color::from_rgb(0xAA, 0xAA, 0xAA); + Color sc; + if (st.step == STEP_DONE) + sc = TEXT_COLOR; + else if (st.step == STEP_ERROR) + sc = DANGER; + else { + uint64_t age = montauk::get_milliseconds() - st.status_time; + sc = (age < 5000) + ? Color::from_rgb(0x33, 0x33, 0x33) + : Color::from_rgb(0xAA, 0xAA, 0xAA); + } px_text(px, g_win_w, g_win_h, 8, sy + (STATUS_H - fh) / 2, st.status, sc); } } @@ -441,12 +596,16 @@ void render(uint32_t* pixels) { render_step_bar(pixels); switch (g_state.step) { - case STEP_SELECT_DISK: render_select_disk(pixels); break; - case STEP_PARTITION_SCHEME: render_partition_scheme(pixels); break; - case STEP_CONFIRM: render_confirm(pixels); break; + case STEP_MODE_SELECT: render_mode_select(pixels); break; + case STEP_SELECT_DISK: render_select_disk(pixels); break; + case STEP_PARTITION_SCHEME: render_partition_scheme(pixels); break; + case STEP_CONFIRM: render_confirm(pixels); break; + case STEP_UPDATE_SELECT_PART: render_update_select_part(pixels); break; + case STEP_UPDATE_CONFIRM: render_update_confirm(pixels); break; case STEP_INSTALLING: + case STEP_UPDATING: case STEP_DONE: - case STEP_ERROR: render_progress(pixels); break; + case STEP_ERROR: render_progress(pixels); break; } render_status(pixels);