#!/usr/bin/env python3 """ EXT4 Filesystem Libraries """ import struct, os, sys, stat from pathlib import Path BLOCK=4096 def read_at(f, offset, size): f.seek(offset) return f.read(size) def parse_superblock(data): sb = {} sb['inodes_count'] = struct.unpack_from('= 64: hi = struct.unpack_from(' len(data): break l_block = struct.unpack_from(' 0: extents.append((l_block, phys, ee_len & 0x7FFF)) else: # Depth > 0: extent index node - follow first child # (handles large dirs gracefully) o = base + 12 ei_leaf_lo = struct.unpack_from(' 0: for b in range(ee_len & 0x7FFF): result.append((l_block + b, phys + b)) else: # Index node - recurse into each child for i in range(entries): o = base + 12 + i * 12 leaf_lo = struct.unpack_from(' 0: # Inconsistent - probably corruption return 'corrupt' if dtime == 0 and links_count == 0: # Unallocated inode - should not appear in dir entries return 'unallocated' return 'active' # dtime=0, links_count>0 - normal live inode def read_dir_entries(f, sb, gdt_data, inum): """Return dict of name -> (child_inum, ftype).""" idata, slot = read_inode(f, sb, gdt_data, inum) entries = {} for logical, phys in sorted(read_extent_tree_blocks(f, idata, slot)): 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 dump_file(f, sb, gdt_data, inum, dest_path): """Extract a regular file by inode to dest_path.""" 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): try: idata, slot = read_inode(f, sb, gdt_data, inum) size = struct.unpack_from(' {target!r}", file=sys.stderr) return False if os.path.lexists(dest_path): return True # already exists from a previous run os.symlink(target, dest_path) return True except (OSError, IndexError) as e: print(f" WARN symlink {dest_path}: {e}", file=sys.stderr) return False # ── recursive dumper ───────────────────────────────────────────────────────── FTYPE_REG = 1 FTYPE_DIR = 2 FTYPE_SYM = 7 def dump_tree(f, sb, gdt_data, inum, dest_dir, depth=0, visited=None): if visited is None: visited = set() if inum in visited: return visited.add(inum) try: entries = read_dir_entries(f, sb, gdt_data, inum) except Exception: return os.makedirs(dest_dir, exist_ok=True) for name, (child_inum, ftype) in entries.items(): if name in ('.', '..'): continue safe_name = name.replace('/', '_').replace('\x00', '') dest = os.path.join(dest_dir, safe_name) try: # If ftype unknown, derive from inode mode if ftype == 0: idata, slot = read_inode(f, sb, gdt_data, child_inum) mode = struct.unpack_from('