Viewing: ext2.c
📄 ext2.c (Read Only) ⬅ To go back
#include "ext2.h"
#include "ata_pio.h"
#include "heap.h"
#include "utils.h"
#include "console.h"
#include "errno.h"

#include <stddef.h>

/* ---- ext2 on-disk structures ---- */

#define EXT2_SUPER_MAGIC   0xEF53
#define EXT2_SUPER_OFFSET  1024   /* superblock is at byte offset 1024 */

#define EXT2_ROOT_INO      2

#define EXT2_S_IFREG  0x8000
#define EXT2_S_IFDIR  0x4000
#define EXT2_S_IFLNK  0xA000

#define EXT2_FT_UNKNOWN  0
#define EXT2_FT_REG_FILE 1
#define EXT2_FT_DIR      2
#define EXT2_FT_SYMLINK  7

#define EXT2_NDIR_BLOCKS  12
#define EXT2_IND_BLOCK    12
#define EXT2_DIND_BLOCK   13
#define EXT2_TIND_BLOCK   14
#define EXT2_N_BLOCKS     15

struct ext2_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;
    /* EXT2_DYNAMIC_REV 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];
    char     s_last_mounted[64];
    uint32_t s_algo_bitmap;
    /* ... more fields follow but we don't need them */
} __attribute__((packed));

struct ext2_group_desc {
    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 ext2_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;       /* 512-byte blocks */
    uint32_t i_flags;
    uint32_t i_osd1;
    uint32_t i_block[EXT2_N_BLOCKS];
    uint32_t i_generation;
    uint32_t i_file_acl;
    uint32_t i_dir_acl;      /* i_size_high for regular files in rev1 */
    uint32_t i_faddr;
    uint8_t  i_osd2[12];
} __attribute__((packed));

struct ext2_dir_entry {
    uint32_t inode;
    uint16_t rec_len;
    uint8_t  name_len;
    uint8_t  file_type;
    char     name[];          /* variable length, NOT null-terminated */
} __attribute__((packed));

/* ---- In-memory filesystem state ---- */

#define EXT2_SECTOR_SIZE 512

struct ext2_state {
    int      drive;
    uint32_t part_lba;        /* partition start LBA */
    uint32_t block_size;      /* bytes per block (1024, 2048, or 4096) */
    uint32_t sectors_per_block;
    uint32_t inodes_per_group;
    uint32_t blocks_per_group;
    uint32_t inode_size;      /* on-disk inode size (128 or 256) */
    uint32_t num_groups;
    uint32_t first_data_block;
    uint32_t total_blocks;
    uint32_t total_inodes;
    struct ext2_group_desc* gdt; /* group descriptor table (heap-allocated) */
    uint32_t gdt_blocks;      /* number of blocks occupied by GDT */
};

struct ext2_node {
    fs_node_t vfs;
    uint32_t ino;             /* inode number */
};

static struct ext2_state g_ext2;
static struct ext2_node g_ext2_root;
static int g_ext2_ready = 0;

/* ---- Block I/O ---- */

static int ext2_read_block(uint32_t block, void* buf) {
    uint32_t lba = g_ext2.part_lba + block * g_ext2.sectors_per_block;
    uint8_t* p = (uint8_t*)buf;
    for (uint32_t s = 0; s < g_ext2.sectors_per_block; s++) {
        if (ata_pio_read28(g_ext2.drive, lba + s, p + s * EXT2_SECTOR_SIZE) < 0)
            return -EIO;
    }
    return 0;
}

static int ext2_write_block(uint32_t block, const void* buf) {
    uint32_t lba = g_ext2.part_lba + block * g_ext2.sectors_per_block;
    const uint8_t* p = (const uint8_t*)buf;
    for (uint32_t s = 0; s < g_ext2.sectors_per_block; s++) {
        if (ata_pio_write28(g_ext2.drive, lba + s, p + s * EXT2_SECTOR_SIZE) < 0)
            return -EIO;
    }
    return 0;
}

/* ---- Superblock I/O ---- */

static int ext2_read_superblock(struct ext2_superblock* sb) {
    /* Superblock is at byte offset 1024, which is LBA 2-3 relative to partition */
    uint8_t sec[EXT2_SECTOR_SIZE];
    uint32_t sb_lba = g_ext2.part_lba + EXT2_SUPER_OFFSET / EXT2_SECTOR_SIZE;

    uint8_t raw[1024];
    for (uint32_t i = 0; i < 1024 / EXT2_SECTOR_SIZE; i++) {
        if (ata_pio_read28(g_ext2.drive, sb_lba + i, sec) < 0) return -EIO;
        memcpy(raw + i * EXT2_SECTOR_SIZE, sec, EXT2_SECTOR_SIZE);
    }
    memcpy(sb, raw, sizeof(*sb));
    return 0;
}

static int ext2_write_superblock(const struct ext2_superblock* sb) __attribute__((unused));
static int ext2_write_superblock(const struct ext2_superblock* sb) {
    uint32_t sb_lba = g_ext2.part_lba + EXT2_SUPER_OFFSET / EXT2_SECTOR_SIZE;
    uint8_t raw[1024];
    memset(raw, 0, sizeof(raw));
    memcpy(raw, sb, sizeof(*sb));

    for (uint32_t i = 0; i < 1024 / EXT2_SECTOR_SIZE; i++) {
        if (ata_pio_write28(g_ext2.drive, sb_lba + i, raw + i * EXT2_SECTOR_SIZE) < 0)
            return -EIO;
    }
    return 0;
}

/* ---- GDT I/O ---- */

static int ext2_write_gdt(void) {
    /* GDT starts at block after superblock (block 1 for 1KB blocks, block 1 for others too
     * since superblock is in block 0 or 1 depending on block_size) */
    uint32_t gdt_block = g_ext2.first_data_block + 1;
    uint8_t* p = (uint8_t*)g_ext2.gdt;

    for (uint32_t b = 0; b < g_ext2.gdt_blocks; b++) {
        uint8_t blk_buf[4096]; /* max block size */
        memset(blk_buf, 0, g_ext2.block_size);
        uint32_t bytes = g_ext2.block_size;
        uint32_t remain = g_ext2.num_groups * (uint32_t)sizeof(struct ext2_group_desc) - b * g_ext2.block_size;
        if (bytes > remain) bytes = remain;
        memcpy(blk_buf, p + b * g_ext2.block_size, bytes);
        if (ext2_write_block(gdt_block + b, blk_buf) < 0) return -EIO;
    }
    return 0;
}

/* ---- Inode I/O ---- */

static int ext2_read_inode(uint32_t ino, struct ext2_inode* out) {
    if (ino == 0 || !out) return -EINVAL;
    uint32_t group = (ino - 1) / g_ext2.inodes_per_group;
    uint32_t index = (ino - 1) % g_ext2.inodes_per_group;

    if (group >= g_ext2.num_groups) return -EINVAL;

    uint32_t inode_table_block = g_ext2.gdt[group].bg_inode_table;
    uint32_t byte_offset = index * g_ext2.inode_size;
    uint32_t block = inode_table_block + byte_offset / g_ext2.block_size;
    uint32_t offset_in_block = byte_offset % g_ext2.block_size;

    uint8_t blk_buf[4096];
    if (ext2_read_block(block, blk_buf) < 0) return -EIO;
    memcpy(out, blk_buf + offset_in_block, sizeof(*out));
    return 0;
}

static int ext2_write_inode(uint32_t ino, const struct ext2_inode* in) {
    if (ino == 0 || !in) return -EINVAL;
    uint32_t group = (ino - 1) / g_ext2.inodes_per_group;
    uint32_t index = (ino - 1) % g_ext2.inodes_per_group;

    if (group >= g_ext2.num_groups) return -EINVAL;

    uint32_t inode_table_block = g_ext2.gdt[group].bg_inode_table;
    uint32_t byte_offset = index * g_ext2.inode_size;
    uint32_t block = inode_table_block + byte_offset / g_ext2.block_size;
    uint32_t offset_in_block = byte_offset % g_ext2.block_size;

    uint8_t blk_buf[4096];
    if (ext2_read_block(block, blk_buf) < 0) return -EIO;
    memcpy(blk_buf + offset_in_block, in, sizeof(*in));
    return ext2_write_block(block, blk_buf);
}

/* ---- Block mapping: logical block → physical block ---- */

/* Resolve logical block number within an inode to physical block number.
 * Handles direct, indirect, doubly-indirect, and triply-indirect blocks. */
static uint32_t ext2_block_map(const struct ext2_inode* inode, uint32_t logical) {
    uint32_t ptrs_per_block = g_ext2.block_size / 4;

    /* Direct blocks (0..11) */
    if (logical < EXT2_NDIR_BLOCKS) {
        return inode->i_block[logical];
    }
    logical -= EXT2_NDIR_BLOCKS;

    /* Singly indirect (12..12+ptrs-1) */
    if (logical < ptrs_per_block) {
        uint32_t ind_block = inode->i_block[EXT2_IND_BLOCK];
        if (ind_block == 0) return 0;
        uint8_t blk_buf[4096];
        if (ext2_read_block(ind_block, blk_buf) < 0) return 0;
        return ((uint32_t*)blk_buf)[logical];
    }
    logical -= ptrs_per_block;

    /* Doubly indirect */
    if (logical < ptrs_per_block * ptrs_per_block) {
        uint32_t dind_block = inode->i_block[EXT2_DIND_BLOCK];
        if (dind_block == 0) return 0;
        uint8_t blk_buf[4096];
        memset(blk_buf, 0, sizeof(blk_buf));
        if (ext2_read_block(dind_block, blk_buf) < 0) return 0;
        uint32_t ind = ((uint32_t*)blk_buf)[logical / ptrs_per_block];
        if (ind == 0) return 0;
        if (ext2_read_block(ind, blk_buf) < 0) return 0;
        return ((uint32_t*)blk_buf)[logical % ptrs_per_block];
    }
    logical -= ptrs_per_block * ptrs_per_block;

    /* Triply indirect */
    {
        uint32_t tind_block = inode->i_block[EXT2_TIND_BLOCK];
        if (tind_block == 0) return 0;
        uint8_t blk_buf[4096];
        memset(blk_buf, 0, sizeof(blk_buf));
        if (ext2_read_block(tind_block, blk_buf) < 0) return 0;
        uint32_t dind = ((uint32_t*)blk_buf)[logical / (ptrs_per_block * ptrs_per_block)];
        if (dind == 0) return 0;
        uint32_t rem = logical % (ptrs_per_block * ptrs_per_block);
        if (ext2_read_block(dind, blk_buf) < 0) return 0;
        uint32_t ind = ((uint32_t*)blk_buf)[rem / ptrs_per_block];
        if (ind == 0) return 0;
        if (ext2_read_block(ind, blk_buf) < 0) return 0;
        return ((uint32_t*)blk_buf)[rem % ptrs_per_block];
    }
}

/* ---- Bitmap helpers (for RW) ---- */

/* Allocate a free block from group, returns block number or 0. */
static uint32_t ext2_alloc_block(void) {
    for (uint32_t g = 0; g < g_ext2.num_groups; g++) {
        if (g_ext2.gdt[g].bg_free_blocks_count == 0) continue;

        uint8_t bmap[4096];
        memset(bmap, 0, sizeof(bmap));
        if (ext2_read_block(g_ext2.gdt[g].bg_block_bitmap, bmap) < 0) continue;

        uint32_t blocks_in_group = g_ext2.blocks_per_group;
        if (g == g_ext2.num_groups - 1) {
            uint32_t rem = g_ext2.total_blocks - g * g_ext2.blocks_per_group;
            if (rem < blocks_in_group) blocks_in_group = rem;
        }

        for (uint32_t bit = 0; bit < blocks_in_group; bit++) {
            if ((bmap[bit / 8] & (1 << (bit % 8))) == 0) {
                bmap[bit / 8] |= (1 << (bit % 8));
                if (ext2_write_block(g_ext2.gdt[g].bg_block_bitmap, bmap) < 0) return 0;
                g_ext2.gdt[g].bg_free_blocks_count--;
                (void)ext2_write_gdt();
                return g * g_ext2.blocks_per_group + bit + g_ext2.first_data_block;
            }
        }
    }
    return 0;
}

static void ext2_free_block(uint32_t block) {
    if (block == 0) return;
    uint32_t adj = block - g_ext2.first_data_block;
    uint32_t g = adj / g_ext2.blocks_per_group;
    uint32_t bit = adj % g_ext2.blocks_per_group;

    if (g >= g_ext2.num_groups) return;

    uint8_t bmap[4096];
    memset(bmap, 0, sizeof(bmap));
    if (ext2_read_block(g_ext2.gdt[g].bg_block_bitmap, bmap) < 0) return;
    bmap[bit / 8] &= ~(1 << (bit % 8));
    (void)ext2_write_block(g_ext2.gdt[g].bg_block_bitmap, bmap);
    g_ext2.gdt[g].bg_free_blocks_count++;
    (void)ext2_write_gdt();
}

/* Allocate a free inode, returns inode number or 0. */
static uint32_t ext2_alloc_inode(void) {
    for (uint32_t g = 0; g < g_ext2.num_groups; g++) {
        if (g_ext2.gdt[g].bg_free_inodes_count == 0) continue;

        uint8_t bmap[4096];
        memset(bmap, 0, sizeof(bmap));
        if (ext2_read_block(g_ext2.gdt[g].bg_inode_bitmap, bmap) < 0) continue;

        for (uint32_t bit = 0; bit < g_ext2.inodes_per_group; bit++) {
            if ((bmap[bit / 8] & (1 << (bit % 8))) == 0) {
                bmap[bit / 8] |= (1 << (bit % 8));
                if (ext2_write_block(g_ext2.gdt[g].bg_inode_bitmap, bmap) < 0) return 0;
                g_ext2.gdt[g].bg_free_inodes_count--;
                (void)ext2_write_gdt();
                return g * g_ext2.inodes_per_group + bit + 1;
            }
        }
    }
    return 0;
}

static void ext2_free_inode(uint32_t ino) {
    if (ino == 0) return;
    uint32_t g = (ino - 1) / g_ext2.inodes_per_group;
    uint32_t bit = (ino - 1) % g_ext2.inodes_per_group;

    if (g >= g_ext2.num_groups) return;

    uint8_t bmap[4096];
    memset(bmap, 0, sizeof(bmap));
    if (ext2_read_block(g_ext2.gdt[g].bg_inode_bitmap, bmap) < 0) return;
    bmap[bit / 8] &= ~(1 << (bit % 8));
    (void)ext2_write_block(g_ext2.gdt[g].bg_inode_bitmap, bmap);
    g_ext2.gdt[g].bg_free_inodes_count++;
    (void)ext2_write_gdt();
}

/* ---- Block mapping write: set logical→physical mapping in inode ---- */

/* Allocate an indirect block if val is zero. Returns the block number (existing or new), or 0 on failure. */
static uint32_t ext2_ensure_indirect(uint32_t val) {
    if (val != 0) return val;
    uint32_t nb = ext2_alloc_block();
    if (nb == 0) return 0;
    /* Zero out the new indirect block */
    uint8_t zero[4096];
    memset(zero, 0, g_ext2.block_size);
    if (ext2_write_block(nb, zero) < 0) {
        ext2_free_block(nb);
        return 0;
    }
    return nb;
}

/* Set the physical block for a logical block in an inode.
 * Allocates indirect blocks as needed. Writes inode back to disk. */
static int ext2_block_map_set(uint32_t ino, struct ext2_inode* inode,
                               uint32_t logical, uint32_t phys_block) {
    uint32_t ptrs_per_block = g_ext2.block_size / 4;

    if (logical < EXT2_NDIR_BLOCKS) {
        inode->i_block[logical] = phys_block;
        return ext2_write_inode(ino, inode);
    }
    logical -= EXT2_NDIR_BLOCKS;

    if (logical < ptrs_per_block) {
        uint32_t ind_blk = ext2_ensure_indirect(inode->i_block[EXT2_IND_BLOCK]);
        if (ind_blk == 0) return -ENOSPC;
        inode->i_block[EXT2_IND_BLOCK] = ind_blk;
        if (ext2_write_inode(ino, inode) < 0) return -EIO;

        uint8_t blk_buf[4096];
        if (ext2_read_block(inode->i_block[EXT2_IND_BLOCK], blk_buf) < 0) return -EIO;
        ((uint32_t*)blk_buf)[logical] = phys_block;
        return ext2_write_block(inode->i_block[EXT2_IND_BLOCK], blk_buf);
    }
    logical -= ptrs_per_block;

    if (logical < ptrs_per_block * ptrs_per_block) {
        uint32_t dind_blk = ext2_ensure_indirect(inode->i_block[EXT2_DIND_BLOCK]);
        if (dind_blk == 0) return -ENOSPC;
        inode->i_block[EXT2_DIND_BLOCK] = dind_blk;
        if (ext2_write_inode(ino, inode) < 0) return -EIO;

        uint8_t blk_buf[4096];
        if (ext2_read_block(inode->i_block[EXT2_DIND_BLOCK], blk_buf) < 0) return -EIO;
        uint32_t idx1 = logical / ptrs_per_block;
        uint32_t idx2 = logical % ptrs_per_block;
        uint32_t ind = ((uint32_t*)blk_buf)[idx1];
        if (ind == 0) {
            ind = ext2_alloc_block();
            if (ind == 0) return -ENOSPC;
            uint8_t zero[4096];
            memset(zero, 0, g_ext2.block_size);
            if (ext2_write_block(ind, zero) < 0) { ext2_free_block(ind); return -EIO; }
            ((uint32_t*)blk_buf)[idx1] = ind;
            if (ext2_write_block(inode->i_block[EXT2_DIND_BLOCK], blk_buf) < 0) return -EIO;
        }

        if (ext2_read_block(ind, blk_buf) < 0) return -EIO;
        ((uint32_t*)blk_buf)[idx2] = phys_block;
        return ext2_write_block(ind, blk_buf);
    }

    /* Triply indirect — not implemented for now */
    return -ENOSPC;
}

/* Free all data blocks referenced by an inode (direct + indirect). */
static void ext2_free_inode_blocks(struct ext2_inode* inode) {
    uint32_t ptrs_per_block = g_ext2.block_size / 4;

    /* Direct */
    for (uint32_t i = 0; i < EXT2_NDIR_BLOCKS; i++) {
        if (inode->i_block[i]) {
            ext2_free_block(inode->i_block[i]);
            inode->i_block[i] = 0;
        }
    }

    /* Singly indirect */
    if (inode->i_block[EXT2_IND_BLOCK]) {
        uint8_t blk_buf[4096];
        if (ext2_read_block(inode->i_block[EXT2_IND_BLOCK], blk_buf) == 0) {
            uint32_t* ptrs = (uint32_t*)blk_buf;
            for (uint32_t i = 0; i < ptrs_per_block; i++) {
                if (ptrs[i]) ext2_free_block(ptrs[i]);
            }
        }
        ext2_free_block(inode->i_block[EXT2_IND_BLOCK]);
        inode->i_block[EXT2_IND_BLOCK] = 0;
    }

    /* Doubly indirect */
    if (inode->i_block[EXT2_DIND_BLOCK]) {
        uint8_t blk_buf[4096];
        if (ext2_read_block(inode->i_block[EXT2_DIND_BLOCK], blk_buf) == 0) {
            uint32_t* l1 = (uint32_t*)blk_buf;
            for (uint32_t i = 0; i < ptrs_per_block; i++) {
                if (l1[i]) {
                    uint8_t blk2[4096];
                    if (ext2_read_block(l1[i], blk2) == 0) {
                        uint32_t* l2 = (uint32_t*)blk2;
                        for (uint32_t j = 0; j < ptrs_per_block; j++) {
                            if (l2[j]) ext2_free_block(l2[j]);
                        }
                    }
                    ext2_free_block(l1[i]);
                }
            }
        }
        ext2_free_block(inode->i_block[EXT2_DIND_BLOCK]);
        inode->i_block[EXT2_DIND_BLOCK] = 0;
    }

    /* Triply indirect — free top level only for safety */
    if (inode->i_block[EXT2_TIND_BLOCK]) {
        ext2_free_block(inode->i_block[EXT2_TIND_BLOCK]);
        inode->i_block[EXT2_TIND_BLOCK] = 0;
    }

    inode->i_blocks = 0;
    inode->i_size = 0;
}

/* ---- Forward declarations ---- */
static fs_node_t* ext2_finddir(fs_node_t* node, const char* name);
static uint32_t ext2_file_read(fs_node_t* node, uint32_t offset, uint32_t size, uint8_t* buffer);
static uint32_t ext2_file_write(fs_node_t* node, uint32_t offset, uint32_t size, const uint8_t* buffer);
static int ext2_readdir_impl(struct fs_node* node, uint32_t* inout_index, void* buf, uint32_t buf_len);
static int ext2_create_impl(struct fs_node* dir, const char* name, uint32_t flags, struct fs_node** out);
static int ext2_mkdir_impl(struct fs_node* dir, const char* name);
static int ext2_unlink_impl(struct fs_node* dir, const char* name);
static int ext2_rmdir_impl(struct fs_node* dir, const char* name);
static int ext2_rename_impl(struct fs_node* old_dir, const char* old_name,
                             struct fs_node* new_dir, const char* new_name);
static int ext2_truncate_impl(struct fs_node* node, uint32_t length);
static int ext2_link_impl(struct fs_node* dir, const char* name, struct fs_node* target);
static void ext2_close_impl(fs_node_t* node);

static const struct file_operations ext2_file_fops = {
    .read     = ext2_file_read,
    .write    = ext2_file_write,
    .close    = ext2_close_impl,
};

static const struct inode_operations ext2_file_iops = {
    .truncate = ext2_truncate_impl,
};

static const struct file_operations ext2_dir_fops = {
    .close   = ext2_close_impl,
};

static const struct inode_operations ext2_dir_iops = {
    .lookup  = ext2_finddir,
    .readdir = ext2_readdir_impl,
    .create  = ext2_create_impl,
    .mkdir   = ext2_mkdir_impl,
    .unlink  = ext2_unlink_impl,
    .rmdir   = ext2_rmdir_impl,
    .rename  = ext2_rename_impl,
    .link    = ext2_link_impl,
};

static void ext2_close_impl(fs_node_t* node) {
    if (!node) return;
    struct ext2_node* en = (struct ext2_node*)node;
    kfree(en);
}

static struct ext2_node* ext2_make_node(uint32_t ino, const struct ext2_inode* inode, const char* name) {
    struct ext2_node* en = (struct ext2_node*)kmalloc(sizeof(struct ext2_node));
    if (!en) return NULL;
    memset(en, 0, sizeof(*en));

    en->ino = ino;

    size_t nlen = strlen(name);
    if (nlen >= sizeof(en->vfs.name)) nlen = sizeof(en->vfs.name) - 1;
    memcpy(en->vfs.name, name, nlen);
    en->vfs.name[nlen] = '\0';
    en->vfs.inode = ino;
    en->vfs.uid = inode->i_uid;
    en->vfs.gid = inode->i_gid;
    en->vfs.mode = inode->i_mode;

    if ((inode->i_mode & 0xF000) == EXT2_S_IFDIR) {
        en->vfs.flags = FS_DIRECTORY;
        en->vfs.length = inode->i_size;
        en->vfs.f_ops = &ext2_dir_fops;
        en->vfs.i_ops = &ext2_dir_iops;
    } else if ((inode->i_mode & 0xF000) == EXT2_S_IFLNK) {
        en->vfs.flags = FS_SYMLINK;
        en->vfs.length = inode->i_size;
        /* For small symlinks, target is stored inline in i_block */
        if (inode->i_size < sizeof(inode->i_block)) {
            memcpy(en->vfs.symlink_target, inode->i_block, inode->i_size);
            en->vfs.symlink_target[inode->i_size] = '\0';
        }
    } else {
        en->vfs.flags = FS_FILE;
        en->vfs.length = inode->i_size;
        en->vfs.f_ops = &ext2_file_fops;
        en->vfs.i_ops = &ext2_file_iops;
    }

    return en;
}

/* ---- File read ---- */

static uint32_t ext2_file_read(fs_node_t* node, uint32_t offset, uint32_t size, uint8_t* buffer) {
    if (!node || !buffer) return 0;
    struct ext2_node* en = (struct ext2_node*)node;

    struct ext2_inode inode;
    if (ext2_read_inode(en->ino, &inode) < 0) return 0;

    uint32_t file_size = inode.i_size;
    if (offset >= file_size) return 0;
    if (offset + size > file_size) size = file_size - offset;
    if (size == 0) return 0;

    uint32_t bs = g_ext2.block_size;
    uint32_t total = 0;

    while (total < size) {
        uint32_t pos = offset + total;
        uint32_t logical_block = pos / bs;
        uint32_t offset_in_block = pos % bs;
        uint32_t chunk = bs - offset_in_block;
        if (chunk > size - total) chunk = size - total;

        uint32_t phys_block = ext2_block_map(&inode, logical_block);
        if (phys_block == 0) break;

        uint8_t blk_buf[4096];
        if (ext2_read_block(phys_block, blk_buf) < 0) break;
        memcpy(buffer + total, blk_buf + offset_in_block, chunk);
        total += chunk;
    }

    return total;
}

/* ---- File write ---- */

static uint32_t ext2_file_write(fs_node_t* node, uint32_t offset, uint32_t size, const uint8_t* buffer) {
    if (!node || !buffer || size == 0) return 0;
    struct ext2_node* en = (struct ext2_node*)node;

    struct ext2_inode inode;
    if (ext2_read_inode(en->ino, &inode) < 0) return 0;

    uint64_t end64 = (uint64_t)offset + (uint64_t)size;
    if (end64 > 0xFFFFFFFFULL) return 0;

    uint32_t bs = g_ext2.block_size;
    uint32_t total = 0;

    while (total < size) {
        uint32_t pos = offset + total;
        uint32_t logical_block = pos / bs;
        uint32_t offset_in_block = pos % bs;
        uint32_t chunk = bs - offset_in_block;
        if (chunk > size - total) chunk = size - total;

        uint32_t phys_block = ext2_block_map(&inode, logical_block);
        if (phys_block == 0) {
            /* Need to allocate a new block */
            phys_block = ext2_alloc_block();
            if (phys_block == 0) break;
            if (ext2_block_map_set(en->ino, &inode, logical_block, phys_block) < 0) {
                ext2_free_block(phys_block);
                break;
            }
            inode.i_blocks += g_ext2.block_size / EXT2_SECTOR_SIZE;
        }

        uint8_t blk_buf[4096];
        if (offset_in_block != 0 || chunk != bs) {
            if (ext2_read_block(phys_block, blk_buf) < 0) break;
        }
        memcpy(blk_buf + offset_in_block, buffer + total, chunk);
        if (ext2_write_block(phys_block, blk_buf) < 0) break;
        total += chunk;
    }

    if (offset + total > inode.i_size) {
        inode.i_size = offset + total;
    }
    (void)ext2_write_inode(en->ino, &inode);
    node->length = inode.i_size;

    return total;
}

/* ---- finddir ---- */

static fs_node_t* ext2_finddir(fs_node_t* node, const char* name) {
    if (!node || !name) return NULL;
    struct ext2_node* en = (struct ext2_node*)node;

    struct ext2_inode dir_inode;
    if (ext2_read_inode(en->ino, &dir_inode) < 0) return NULL;
    if ((dir_inode.i_mode & 0xF000) != EXT2_S_IFDIR) return NULL;

    uint32_t dir_size = dir_inode.i_size;
    uint32_t bs = g_ext2.block_size;
    uint32_t name_len = strlen(name);

    for (uint32_t pos = 0; pos < dir_size; ) {
        uint32_t logical = pos / bs;
        uint32_t phys = ext2_block_map(&dir_inode, logical);
        if (phys == 0) break;

        uint8_t blk_buf[4096];
        if (ext2_read_block(phys, blk_buf) < 0) break;

        uint32_t off = pos % bs;
        while (off < bs && pos < dir_size) {
            struct ext2_dir_entry* de = (struct ext2_dir_entry*)(blk_buf + off);
            if (de->rec_len == 0) goto done;

            if (de->inode != 0 && de->name_len == name_len) {
                if (memcmp(de->name, name, name_len) == 0) {
                    struct ext2_inode child_inode;
                    if (ext2_read_inode(de->inode, &child_inode) < 0) return NULL;
                    char child_name[128];
                    if (name_len >= sizeof(child_name)) name_len = sizeof(child_name) - 1;
                    memcpy(child_name, name, name_len);
                    child_name[name_len] = '\0';
                    return (fs_node_t*)ext2_make_node(de->inode, &child_inode, child_name);
                }
            }

            off += de->rec_len;
            pos = (pos / bs) * bs + off;
        }
        if (off >= bs) {
            pos = ((pos / bs) + 1) * bs;
        }
    }

done:
    return NULL;
}

/* ---- readdir ---- */

static int ext2_readdir_impl(struct fs_node* node, uint32_t* inout_index, void* buf, uint32_t buf_len) {
    if (!node || !inout_index || !buf) return -1;
    if (buf_len < sizeof(struct vfs_dirent)) return -1;

    struct ext2_node* en = (struct ext2_node*)node;
    struct ext2_inode dir_inode;
    if (ext2_read_inode(en->ino, &dir_inode) < 0) return -1;

    uint32_t dir_size = dir_inode.i_size;
    uint32_t bs = g_ext2.block_size;
    uint32_t idx = *inout_index;
    uint32_t cap = buf_len / (uint32_t)sizeof(struct vfs_dirent);
    struct vfs_dirent* out = (struct vfs_dirent*)buf;
    uint32_t written = 0;
    uint32_t cur = 0;

    for (uint32_t pos = 0; pos < dir_size && written < cap; ) {
        uint32_t logical = pos / bs;
        uint32_t phys = ext2_block_map(&dir_inode, logical);
        if (phys == 0) break;

        uint8_t blk_buf[4096];
        if (ext2_read_block(phys, blk_buf) < 0) break;

        uint32_t off = pos % bs;
        while (off < bs && pos < dir_size && written < cap) {
            struct ext2_dir_entry* de = (struct ext2_dir_entry*)(blk_buf + off);
            if (de->rec_len == 0) goto done;

            if (de->inode != 0) {
                /* Skip . and .. */
                int skip = 0;
                if (de->name_len == 1 && de->name[0] == '.') skip = 1;
                if (de->name_len == 2 && de->name[0] == '.' && de->name[1] == '.') skip = 1;

                if (!skip) {
                    if (cur >= idx) {
                        memset(&out[written], 0, sizeof(out[written]));
                        out[written].d_ino = de->inode;
                        out[written].d_reclen = (uint16_t)sizeof(struct vfs_dirent);
                        out[written].d_type = de->file_type;
                        uint8_t nlen = de->name_len;
                        if (nlen >= sizeof(out[written].d_name))
                            nlen = sizeof(out[written].d_name) - 1;
                        memcpy(out[written].d_name, de->name, nlen);
                        out[written].d_name[nlen] = '\0';
                        written++;
                    }
                    cur++;
                }
            }

            off += de->rec_len;
            pos = (pos / bs) * bs + off;
        }
        if (off >= bs) {
            pos = ((pos / bs) + 1) * bs;
        }
    }

done:
    *inout_index = cur;
    return (int)(written * (uint32_t)sizeof(struct vfs_dirent));
}

/* ---- Directory entry add/remove helpers ---- */

/* Add a directory entry (name → ino) to a directory inode. */
static int ext2_dir_add_entry(uint32_t dir_ino, const char* name, uint32_t child_ino, uint8_t file_type) {
    struct ext2_inode dir_inode;
    if (ext2_read_inode(dir_ino, &dir_inode) < 0) return -EIO;

    uint32_t bs = g_ext2.block_size;
    uint32_t dir_size = dir_inode.i_size;
    uint32_t name_len = strlen(name);
    uint32_t needed = ((uint32_t)sizeof(struct ext2_dir_entry) + name_len + 3) & ~3U; /* 4-byte aligned */

    /* Scan existing blocks for space */
    for (uint32_t pos = 0; pos < dir_size; ) {
        uint32_t logical = pos / bs;
        uint32_t phys = ext2_block_map(&dir_inode, logical);
        if (phys == 0) break;

        uint8_t blk_buf[4096];
        if (ext2_read_block(phys, blk_buf) < 0) return -EIO;

        uint32_t off = 0;
        while (off < bs) {
            struct ext2_dir_entry* de = (struct ext2_dir_entry*)(blk_buf + off);
            if (de->rec_len == 0) break;

            uint32_t actual_len = ((uint32_t)sizeof(struct ext2_dir_entry) + de->name_len + 3) & ~3U;
            uint32_t free_space = de->rec_len - actual_len;

            if (de->inode == 0 && de->rec_len >= needed) {
                /* Reuse deleted entry */
                de->inode = child_ino;
                de->name_len = (uint8_t)name_len;
                de->file_type = file_type;
                memcpy(de->name, name, name_len);
                return ext2_write_block(phys, blk_buf);
            }

            if (free_space >= needed) {
                /* Split entry */
                de->rec_len = (uint16_t)actual_len;
                struct ext2_dir_entry* new_de = (struct ext2_dir_entry*)(blk_buf + off + actual_len);
                new_de->inode = child_ino;
                new_de->rec_len = (uint16_t)free_space;
                new_de->name_len = (uint8_t)name_len;
                new_de->file_type = file_type;
                memcpy(new_de->name, name, name_len);
                return ext2_write_block(phys, blk_buf);
            }

            off += de->rec_len;
        }
        pos = ((pos / bs) + 1) * bs;
    }

    /* Need a new block for the directory */
    uint32_t new_block = ext2_alloc_block();
    if (new_block == 0) return -ENOSPC;

    uint32_t new_logical = dir_size / bs;
    if (ext2_block_map_set(dir_ino, &dir_inode, new_logical, new_block) < 0) {
        ext2_free_block(new_block);
        return -EIO;
    }
    dir_inode.i_size += bs;
    dir_inode.i_blocks += bs / EXT2_SECTOR_SIZE;
    (void)ext2_write_inode(dir_ino, &dir_inode);

    uint8_t blk_buf[4096];
    memset(blk_buf, 0, bs);
    struct ext2_dir_entry* de = (struct ext2_dir_entry*)blk_buf;
    de->inode = child_ino;
    de->rec_len = (uint16_t)bs;
    de->name_len = (uint8_t)name_len;
    de->file_type = file_type;
    memcpy(de->name, name, name_len);
    return ext2_write_block(new_block, blk_buf);
}

/* Remove a directory entry by name. Returns the removed inode number. */
static int ext2_dir_remove_entry(uint32_t dir_ino, const char* name, uint32_t* removed_ino) {
    struct ext2_inode dir_inode;
    if (ext2_read_inode(dir_ino, &dir_inode) < 0) return -EIO;

    uint32_t bs = g_ext2.block_size;
    uint32_t dir_size = dir_inode.i_size;
    uint32_t name_len = strlen(name);

    for (uint32_t pos = 0; pos < dir_size; ) {
        uint32_t logical = pos / bs;
        uint32_t phys = ext2_block_map(&dir_inode, logical);
        if (phys == 0) break;

        uint8_t blk_buf[4096];
        if (ext2_read_block(phys, blk_buf) < 0) return -EIO;

        uint32_t off = 0;
        uint32_t prev_off = 0;
        int is_first = 1;

        while (off < bs) {
            struct ext2_dir_entry* de = (struct ext2_dir_entry*)(blk_buf + off);
            if (de->rec_len == 0) break;

            if (de->inode != 0 && de->name_len == name_len &&
                memcmp(de->name, name, name_len) == 0) {
                if (removed_ino) *removed_ino = de->inode;

                if (is_first) {
                    /* First entry in block: just zero inode */
                    de->inode = 0;
                } else {
                    /* Merge with previous entry */
                    struct ext2_dir_entry* prev = (struct ext2_dir_entry*)(blk_buf + prev_off);
                    prev->rec_len += de->rec_len;
                }
                return ext2_write_block(phys, blk_buf);
            }

            prev_off = off;
            off += de->rec_len;
            is_first = 0;
        }
        pos = ((pos / bs) + 1) * bs;
    }
    return -ENOENT;
}

/* Find a directory entry by name, return its inode number. */
static int ext2_dir_find(uint32_t dir_ino, const char* name, uint32_t* out_ino) {
    struct ext2_inode dir_inode;
    if (ext2_read_inode(dir_ino, &dir_inode) < 0) return -EIO;

    uint32_t bs = g_ext2.block_size;
    uint32_t dir_size = dir_inode.i_size;
    uint32_t name_len = strlen(name);

    for (uint32_t pos = 0; pos < dir_size; ) {
        uint32_t logical = pos / bs;
        uint32_t phys = ext2_block_map(&dir_inode, logical);
        if (phys == 0) break;

        uint8_t blk_buf[4096];
        if (ext2_read_block(phys, blk_buf) < 0) return -EIO;

        uint32_t off = pos % bs;
        while (off < bs) {
            struct ext2_dir_entry* de = (struct ext2_dir_entry*)(blk_buf + off);
            if (de->rec_len == 0) goto not_found;

            if (de->inode != 0 && de->name_len == name_len &&
                memcmp(de->name, name, name_len) == 0) {
                if (out_ino) *out_ino = de->inode;
                return 0;
            }

            off += de->rec_len;
        }
        pos = ((pos / bs) + 1) * bs;
    }

not_found:
    return -ENOENT;
}

/* Check if a directory is empty (only . and .. entries). */
static int ext2_dir_is_empty(uint32_t dir_ino) {
    struct ext2_inode dir_inode;
    if (ext2_read_inode(dir_ino, &dir_inode) < 0) return 0;

    uint32_t bs = g_ext2.block_size;
    uint32_t dir_size = dir_inode.i_size;

    for (uint32_t pos = 0; pos < dir_size; ) {
        uint32_t logical = pos / bs;
        uint32_t phys = ext2_block_map(&dir_inode, logical);
        if (phys == 0) break;

        uint8_t blk_buf[4096];
        if (ext2_read_block(phys, blk_buf) < 0) return 0;

        uint32_t off = 0;
        while (off < bs) {
            struct ext2_dir_entry* de = (struct ext2_dir_entry*)(blk_buf + off);
            if (de->rec_len == 0) return 1;

            if (de->inode != 0) {
                int is_dot = (de->name_len == 1 && de->name[0] == '.');
                int is_dotdot = (de->name_len == 2 && de->name[0] == '.' && de->name[1] == '.');
                if (!is_dot && !is_dotdot) return 0;
            }

            off += de->rec_len;
        }
        pos = ((pos / bs) + 1) * bs;
    }
    return 1;
}

/* ---- VFS: create ---- */

static int ext2_create_impl(struct fs_node* dir, const char* name, uint32_t flags, struct fs_node** out) {
    if (!dir || !name || !out) return -EINVAL;
    *out = NULL;
    struct ext2_node* parent = (struct ext2_node*)dir;

    /* Check if exists */
    uint32_t existing_ino;
    int rc = ext2_dir_find(parent->ino, name, &existing_ino);
    if (rc == 0) {
        struct ext2_inode existing;
        if (ext2_read_inode(existing_ino, &existing) < 0) return -EIO;
        if ((existing.i_mode & 0xF000) == EXT2_S_IFDIR) return -EISDIR;

        if ((flags & 0x200U) != 0U) { /* O_TRUNC */
            ext2_free_inode_blocks(&existing);
            existing.i_size = 0;
            existing.i_blocks = 0;
            (void)ext2_write_inode(existing_ino, &existing);
        }

        struct ext2_node* en = ext2_make_node(existing_ino, &existing, name);
        if (!en) return -ENOMEM;
        *out = &en->vfs;
        return 0;
    }

    if ((flags & 0x40U) == 0U) return -ENOENT; /* O_CREAT not set */

    /* Allocate new inode */
    uint32_t new_ino = ext2_alloc_inode();
    if (new_ino == 0) return -ENOSPC;

    struct ext2_inode new_inode;
    memset(&new_inode, 0, sizeof(new_inode));
    new_inode.i_mode = EXT2_S_IFREG | 0644;
    new_inode.i_links_count = 1;
    if (ext2_write_inode(new_ino, &new_inode) < 0) {
        ext2_free_inode(new_ino);
        return -EIO;
    }

    rc = ext2_dir_add_entry(parent->ino, name, new_ino, EXT2_FT_REG_FILE);
    if (rc < 0) {
        ext2_free_inode(new_ino);
        return rc;
    }

    struct ext2_node* en = ext2_make_node(new_ino, &new_inode, name);
    if (!en) return -ENOMEM;
    *out = &en->vfs;
    return 0;
}

/* ---- VFS: mkdir ---- */

static int ext2_mkdir_impl(struct fs_node* dir, const char* name) {
    if (!dir || !name) return -EINVAL;
    struct ext2_node* parent = (struct ext2_node*)dir;

    if (ext2_dir_find(parent->ino, name, NULL) == 0) return -EEXIST;

    uint32_t new_ino = ext2_alloc_inode();
    if (new_ino == 0) return -ENOSPC;

    /* Allocate one block for . and .. */
    uint32_t new_block = ext2_alloc_block();
    if (new_block == 0) {
        ext2_free_inode(new_ino);
        return -ENOSPC;
    }

    struct ext2_inode new_inode;
    memset(&new_inode, 0, sizeof(new_inode));
    new_inode.i_mode = EXT2_S_IFDIR | 0755;
    new_inode.i_size = g_ext2.block_size;
    new_inode.i_links_count = 2; /* . and parent's entry */
    new_inode.i_blocks = g_ext2.block_size / EXT2_SECTOR_SIZE;
    new_inode.i_block[0] = new_block;
    if (ext2_write_inode(new_ino, &new_inode) < 0) {
        ext2_free_block(new_block);
        ext2_free_inode(new_ino);
        return -EIO;
    }

    /* Write . and .. entries */
    uint8_t blk_buf[4096];
    memset(blk_buf, 0, g_ext2.block_size);

    struct ext2_dir_entry* dot = (struct ext2_dir_entry*)blk_buf;
    dot->inode = new_ino;
    dot->rec_len = 12;
    dot->name_len = 1;
    dot->file_type = EXT2_FT_DIR;
    dot->name[0] = '.';

    struct ext2_dir_entry* dotdot = (struct ext2_dir_entry*)(blk_buf + 12);
    dotdot->inode = parent->ino;
    dotdot->rec_len = (uint16_t)(g_ext2.block_size - 12);
    dotdot->name_len = 2;
    dotdot->file_type = EXT2_FT_DIR;
    dotdot->name[0] = '.';
    dotdot->name[1] = '.';

    if (ext2_write_block(new_block, blk_buf) < 0) {
        ext2_free_block(new_block);
        ext2_free_inode(new_ino);
        return -EIO;
    }

    /* Add entry in parent */
    int rc = ext2_dir_add_entry(parent->ino, name, new_ino, EXT2_FT_DIR);
    if (rc < 0) {
        ext2_free_block(new_block);
        ext2_free_inode(new_ino);
        return rc;
    }

    /* Increment parent link count (for ..) */
    struct ext2_inode parent_inode;
    if (ext2_read_inode(parent->ino, &parent_inode) == 0) {
        parent_inode.i_links_count++;
        (void)ext2_write_inode(parent->ino, &parent_inode);
    }

    /* Update group used_dirs_count */
    uint32_t g = (new_ino - 1) / g_ext2.inodes_per_group;
    if (g < g_ext2.num_groups) {
        g_ext2.gdt[g].bg_used_dirs_count++;
        (void)ext2_write_gdt();
    }

    return 0;
}

/* ---- VFS: unlink ---- */

static int ext2_unlink_impl(struct fs_node* dir, const char* name) {
    if (!dir || !name) return -EINVAL;
    struct ext2_node* parent = (struct ext2_node*)dir;

    uint32_t child_ino;
    int rc = ext2_dir_remove_entry(parent->ino, name, &child_ino);
    if (rc < 0) return rc;

    struct ext2_inode child;
    if (ext2_read_inode(child_ino, &child) < 0) return -EIO;
    if ((child.i_mode & 0xF000) == EXT2_S_IFDIR) return -EISDIR;

    child.i_links_count--;
    if (child.i_links_count == 0) {
        ext2_free_inode_blocks(&child);
        ext2_free_inode(child_ino);
    }
    return ext2_write_inode(child_ino, &child);
}

/* ---- VFS: rmdir ---- */

static int ext2_rmdir_impl(struct fs_node* dir, const char* name) {
    if (!dir || !name) return -EINVAL;
    struct ext2_node* parent = (struct ext2_node*)dir;

    uint32_t child_ino;
    int rc = ext2_dir_find(parent->ino, name, &child_ino);
    if (rc < 0) return rc;

    struct ext2_inode child;
    if (ext2_read_inode(child_ino, &child) < 0) return -EIO;
    if ((child.i_mode & 0xF000) != EXT2_S_IFDIR) return -ENOTDIR;
    if (!ext2_dir_is_empty(child_ino)) return -ENOTEMPTY;

    /* Remove entry from parent */
    rc = ext2_dir_remove_entry(parent->ino, name, NULL);
    if (rc < 0) return rc;

    /* Free directory blocks and inode */
    ext2_free_inode_blocks(&child);
    child.i_links_count = 0;
    (void)ext2_write_inode(child_ino, &child);
    ext2_free_inode(child_ino);

    /* Decrement parent link count (child's ".." pointed to parent) */
    struct ext2_inode parent_inode;
    if (ext2_read_inode(parent->ino, &parent_inode) == 0) {
        if (parent_inode.i_links_count > 0)
            parent_inode.i_links_count--;
        (void)ext2_write_inode(parent->ino, &parent_inode);
    }

    uint32_t g = (child_ino - 1) / g_ext2.inodes_per_group;
    if (g < g_ext2.num_groups && g_ext2.gdt[g].bg_used_dirs_count > 0) {
        g_ext2.gdt[g].bg_used_dirs_count--;
        (void)ext2_write_gdt();
    }

    return 0;
}

/* ---- VFS: rename ---- */

static int ext2_rename_impl(struct fs_node* old_dir, const char* old_name,
                             struct fs_node* new_dir, const char* new_name) {
    if (!old_dir || !old_name || !new_dir || !new_name) return -EINVAL;
    struct ext2_node* odir = (struct ext2_node*)old_dir;
    struct ext2_node* ndir = (struct ext2_node*)new_dir;

    /* Find source */
    uint32_t src_ino;
    int rc = ext2_dir_find(odir->ino, old_name, &src_ino);
    if (rc < 0) return rc;

    struct ext2_inode src_inode;
    if (ext2_read_inode(src_ino, &src_inode) < 0) return -EIO;

    uint8_t ft = ((src_inode.i_mode & 0xF000) == EXT2_S_IFDIR) ? EXT2_FT_DIR : EXT2_FT_REG_FILE;

    /* Remove destination if exists */
    uint32_t dst_ino;
    rc = ext2_dir_find(ndir->ino, new_name, &dst_ino);
    if (rc == 0 && dst_ino != src_ino) {
        struct ext2_inode dst_inode;
        if (ext2_read_inode(dst_ino, &dst_inode) == 0) {
            (void)ext2_dir_remove_entry(ndir->ino, new_name, NULL);
            dst_inode.i_links_count--;
            if (dst_inode.i_links_count == 0) {
                ext2_free_inode_blocks(&dst_inode);
                ext2_free_inode(dst_ino);
            }
            (void)ext2_write_inode(dst_ino, &dst_inode);
        }
    }

    /* Remove from old dir */
    (void)ext2_dir_remove_entry(odir->ino, old_name, NULL);

    /* Add to new dir */
    rc = ext2_dir_add_entry(ndir->ino, new_name, src_ino, ft);
    if (rc < 0) return rc;

    /* If moving a directory, update ".." to point to new parent */
    if (ft == EXT2_FT_DIR && odir->ino != ndir->ino) {
        /* Update ".." entry in moved dir */
        struct ext2_inode moved;
        if (ext2_read_inode(src_ino, &moved) == 0 && moved.i_block[0] != 0) {
            uint8_t blk_buf[4096];
            if (ext2_read_block(moved.i_block[0], blk_buf) == 0) {
                /* ".." is typically the second entry */
                struct ext2_dir_entry* dot = (struct ext2_dir_entry*)blk_buf;
                struct ext2_dir_entry* dotdot = (struct ext2_dir_entry*)(blk_buf + dot->rec_len);
                if (dotdot->name_len == 2 && dotdot->name[0] == '.' && dotdot->name[1] == '.') {
                    dotdot->inode = ndir->ino;
                    (void)ext2_write_block(moved.i_block[0], blk_buf);
                }
            }
        }

        /* Adjust link counts */
        struct ext2_inode old_parent;
        if (ext2_read_inode(odir->ino, &old_parent) == 0) {
            if (old_parent.i_links_count > 0) old_parent.i_links_count--;
            (void)ext2_write_inode(odir->ino, &old_parent);
        }
        struct ext2_inode new_parent;
        if (ext2_read_inode(ndir->ino, &new_parent) == 0) {
            new_parent.i_links_count++;
            (void)ext2_write_inode(ndir->ino, &new_parent);
        }
    }

    return 0;
}

/* ---- VFS: truncate ---- */

static int ext2_truncate_impl(struct fs_node* node, uint32_t length) {
    if (!node) return -EINVAL;
    struct ext2_node* en = (struct ext2_node*)node;

    struct ext2_inode inode;
    if (ext2_read_inode(en->ino, &inode) < 0) return -EIO;

    if (length >= inode.i_size) return 0; /* only shrink */

    uint32_t bs = g_ext2.block_size;
    uint32_t new_blocks = (length + bs - 1) / bs;
    uint32_t old_blocks = (inode.i_size + bs - 1) / bs;

    /* Free blocks beyond new size */
    for (uint32_t b = new_blocks; b < old_blocks; b++) {
        uint32_t phys = ext2_block_map(&inode, b);
        if (phys != 0) {
            ext2_free_block(phys);
            /* Note: we don't zero out the pointer in the inode for simplicity,
             * since the size field prevents access to freed blocks. */
        }
    }

    inode.i_size = length;
    inode.i_blocks = new_blocks * (bs / EXT2_SECTOR_SIZE);
    (void)ext2_write_inode(en->ino, &inode);
    node->length = length;
    return 0;
}

/* ---- VFS: link (hard link) ---- */

static int ext2_link_impl(struct fs_node* dir, const char* name, struct fs_node* target) {
    if (!dir || !name || !target) return -EINVAL;
    struct ext2_node* parent = (struct ext2_node*)dir;
    struct ext2_node* src = (struct ext2_node*)target;

    /* Check doesn't already exist */
    if (ext2_dir_find(parent->ino, name, NULL) == 0) return -EEXIST;

    struct ext2_inode src_inode;
    if (ext2_read_inode(src->ino, &src_inode) < 0) return -EIO;
    if ((src_inode.i_mode & 0xF000) == EXT2_S_IFDIR) return -EPERM;

    int rc = ext2_dir_add_entry(parent->ino, name, src->ino, EXT2_FT_REG_FILE);
    if (rc < 0) return rc;

    src_inode.i_links_count++;
    return ext2_write_inode(src->ino, &src_inode);
}

/* ---- Mount ---- */

fs_node_t* ext2_mount(int drive, uint32_t partition_lba) {
    memset(&g_ext2, 0, sizeof(g_ext2));
    g_ext2.drive = drive;
    g_ext2.part_lba = partition_lba;

    struct ext2_superblock sb;
    if (ext2_read_superblock(&sb) < 0) {
        kprintf("[EXT2] Failed to read superblock\n");
        return NULL;
    }

    if (sb.s_magic != EXT2_SUPER_MAGIC) {
        kprintf("[EXT2] Invalid magic: 0x%x\n", sb.s_magic);
        return NULL;
    }

    g_ext2.block_size = 1024U << sb.s_log_block_size;
    if (g_ext2.block_size > 4096) {
        kprintf("[EXT2] Unsupported block size %u\n", g_ext2.block_size);
        return NULL;
    }
    g_ext2.sectors_per_block = g_ext2.block_size / EXT2_SECTOR_SIZE;
    g_ext2.inodes_per_group = sb.s_inodes_per_group;
    g_ext2.blocks_per_group = sb.s_blocks_per_group;
    g_ext2.first_data_block = sb.s_first_data_block;
    g_ext2.total_blocks = sb.s_blocks_count;
    g_ext2.total_inodes = sb.s_inodes_count;

    if (sb.s_rev_level >= 1 && sb.s_inode_size != 0) {
        g_ext2.inode_size = sb.s_inode_size;
    } else {
        g_ext2.inode_size = 128;
    }

    g_ext2.num_groups = (sb.s_blocks_count + sb.s_blocks_per_group - 1) / sb.s_blocks_per_group;

    /* Read Group Descriptor Table */
    g_ext2.gdt_blocks = (g_ext2.num_groups * (uint32_t)sizeof(struct ext2_group_desc) +
                          g_ext2.block_size - 1) / g_ext2.block_size;
    uint32_t gdt_bytes = g_ext2.num_groups * (uint32_t)sizeof(struct ext2_group_desc);
    g_ext2.gdt = (struct ext2_group_desc*)kmalloc(gdt_bytes);
    if (!g_ext2.gdt) {
        kprintf("[EXT2] Failed to allocate GDT (%u bytes)\n", gdt_bytes);
        return NULL;
    }
    memset(g_ext2.gdt, 0, gdt_bytes);

    uint32_t gdt_block = g_ext2.first_data_block + 1;
    uint8_t* gp = (uint8_t*)g_ext2.gdt;
    for (uint32_t b = 0; b < g_ext2.gdt_blocks; b++) {
        uint8_t blk_buf[4096];
        if (ext2_read_block(gdt_block + b, blk_buf) < 0) {
            kprintf("[EXT2] Failed to read GDT block %u\n", gdt_block + b);
            kfree(g_ext2.gdt);
            g_ext2.gdt = NULL;
            return NULL;
        }
        uint32_t to_copy = g_ext2.block_size;
        if (to_copy > gdt_bytes - b * g_ext2.block_size)
            to_copy = gdt_bytes - b * g_ext2.block_size;
        memcpy(gp + b * g_ext2.block_size, blk_buf, to_copy);
    }

    /* Read root inode */
    struct ext2_inode root_inode;
    if (ext2_read_inode(EXT2_ROOT_INO, &root_inode) < 0) {
        kprintf("[EXT2] Failed to read root inode\n");
        kfree(g_ext2.gdt);
        g_ext2.gdt = NULL;
        return NULL;
    }

    /* Build root node */
    memset(&g_ext2_root, 0, sizeof(g_ext2_root));
    memcpy(g_ext2_root.vfs.name, "ext2", 5);
    g_ext2_root.vfs.flags = FS_DIRECTORY;
    g_ext2_root.vfs.inode = EXT2_ROOT_INO;
    g_ext2_root.vfs.length = root_inode.i_size;
    g_ext2_root.vfs.uid = root_inode.i_uid;
    g_ext2_root.vfs.gid = root_inode.i_gid;
    g_ext2_root.vfs.mode = root_inode.i_mode;
    g_ext2_root.ino = EXT2_ROOT_INO;
    g_ext2_root.vfs.f_ops = &ext2_dir_fops;
    g_ext2_root.vfs.i_ops = &ext2_dir_iops;

    g_ext2_ready = 1;

    kprintf("[EXT2] Mounted at LBA %u (%u blocks, %u inodes, %u groups, %uB/block)\n",
            partition_lba, g_ext2.total_blocks, g_ext2.total_inodes,
            g_ext2.num_groups, g_ext2.block_size);

    return &g_ext2_root.vfs;
}