#!/usr/bin/env python3 """ EXT4 low-level filesystem primitives. All functions that need a file handle expect it open in 'rb' mode. """ import os, stat, struct, sys BLOCK = 4096 FTYPE_REG = 1 FTYPE_DIR = 2 FTYPE_SYM = 7 ITYPE_REG = 0x8000 ITYPE_DIR = 0x4000 ITYPE_SYM = 0xA000 # ── block I/O ───────────────────────────────────────────────────────────────── def read_at(f, offset, size): f.seek(offset) return f.read(size) # ── superblock / GDT ────────────────────────────────────────────────────────── def parse_superblock(data): sb = {} sb['inodes_count'] = struct.unpack_from('= 64: hi = struct.unpack_from(' 0: for b in range(ee_len & 0x7FFF): result.append((l_block + b, phys + b)) else: for i in range(entries): o = base + 12 + i * 12 leaf_lo = struct.unpack_from('= 256 and len(data) - offset >= 164: size_hi = struct.unpack_from(' 0: return 'corrupt' if dtime == 0 and links == 0: return 'unallocated' return 'active' def get_inode_meta(idata, slot, sb): """Return (permissions, uid, gid, atime, mtime) from a raw inode buffer.""" mode = struct.unpack_from('= 256: atime_extra = struct.unpack_from(' (child_inum, ftype). """ entries = {} for _logical, phys in sorted(read_extent_tree_blocks(f, idata, inode_offset)): try: bdata = read_at(f, phys * BLOCK, BLOCK) offset = 0 while offset < BLOCK - 8: e_ino, rec_len, name_len, ftype = struct.unpack_from(' BLOCK: break if e_ino != 0 and name_len > 0: name = bdata[offset + 8:offset + 8 + name_len].decode('utf-8', errors='replace') entries[name] = (e_ino, ftype) offset += rec_len except OSError: pass return entries def read_dir_entries(f, sb, gdt_data, inum): """Read directory entries for inode inum. Returns dict of name -> (child_inum, ftype).""" idata, slot = read_inode(f, sb, gdt_data, inum) return read_dir_entries_raw(f, idata, slot) # ── file extraction ─────────────────────────────────────────────────────────── def dump_file(f, sb, gdt_data, inum, dest_path): """Extract a regular file by inode number to dest_path. Returns True on success.""" try: idata, slot = read_inode(f, sb, gdt_data, inum) size_lo = struct.unpack_from(' written: out.seek(hole) written = hole remaining = size - written if remaining <= 0: break chunk = read_at(f, phys * BLOCK, BLOCK) out.write(chunk[:min(BLOCK, remaining)]) written += min(BLOCK, remaining) out.truncate(size) return True except OSError: return False def dump_symlink(f, sb, gdt_data, inum, dest_path): """Create a symlink at dest_path from the symlink inode. Returns True on success.""" try: idata, slot = read_inode(f, sb, gdt_data, inum) size = struct.unpack_from('