diff --git a/README.md b/README.md index f093f7b..f1880fb 100755 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ $ python rleapp.py -t -i -o = 200 + and struct.unpack_from(" best[0]: + best = (ver, pack, head, total) + if best is None: + raise F2fsUnreadable("no valid F2FS checkpoint") + self.cp_ver, self.cur_pack, self.ckpt, self.cp_total = best + self.start_cp = self.cp_blkaddr + (self.seg_blocks if self.cur_pack == 2 else 0) + self.cp_flags = struct.unpack_from(" 0: + self.nat_bitmap = self.ckpt[192:] + else: + self.nat_bitmap = self.ckpt[192 + sit_bm:] + self._load_nat_journal() + + def _load_nat_journal(self): + """Recent NAT updates not yet flushed to the on-disk table live in a + journal inside the checkpoint's hot-data summary (fs/f2fs/segment.c + read_normal_summaries / read_compacted_summaries). They override the + on-disk NAT, so a nid found here wins.""" + self.nat_j = {} + sum_bs = 4096 if (self.feature & F2FS_FEAT_PACKED_SSA) else self.bs + sum_entry_size = 7 * (sum_bs // 8) # entries_in_sum summaries of 7 bytes + if self.cp_flags & F2FS_CP_COMPACT_SUM: + blk = self.start_cp + struct.unpack_from(" len(buf): + return + n_nats = struct.unpack_from(" len(buf): + break + nid = struct.unpack_from("> 3 + return (bm[j] >> (i & 7)) & 1 if j < len(bm) else 0 + + def resolve(self, nid): + """The block address holding node nid, or 0 if it is unallocated.""" + if nid in self.nat_j: + return self.nat_j[nid] + start = (nid // self.nepb) * self.nepb + block_off = start // self.nepb + addr = self.nat_blkaddr + (block_off << 1) - (block_off & (self.seg_blocks - 1)) + if self._test_bit(self.nat_bitmap, block_off): + addr += self.seg_blocks + blk = self.block(addr) + eo = (nid - start) * F2FS_NAT_ENTRY_SIZE + if eo + F2FS_NAT_ENTRY_SIZE > len(blk): + return 0 + return struct.unpack_from("= self.bs else None + + def inode(self, nid): + """The parsed inode for nid, or None. A node whose footer nid equals its + footer ino is an inode (f2fs.h RAW_IS_INODE).""" + b = self._node(nid) + if b is None: + return None + if struct.unpack_from(" len(b): + return 0 + return struct.unpack_from(" 255: + bit += 1 + continue + slots = (name_len + 7) // 8 + ns = off + foff + bit * 8 + name = buf[ns:ns + name_len].decode("utf-8", "replace") + bit += slots + if name not in (".", ".."): + out.append((name, child)) + return out + + def listdir(self, nid): + ino = self.inode(nid) + if ino is None or not (ino["mode"] & S_IFDIR): + return [] + if ino["inline"] & F2FS_INLINE_DENTRY: + off = ino["iaddr_off"] + 4 # DEF_INLINE_RESERVED_SIZE + span = 4 * (ino["addrs_in_inode"] - 1) # MAX_INLINE_DATA + return sorted(self._parse_dentry(ino["raw"], off, span)) + out = [] + for L in range((ino["size"] + self.bs - 1) // self.bs): + addr = self._block_of(ino, L) + if addr in (F2FS_NULL_ADDR, F2FS_NEW_ADDR, F2FS_COMPRESS_ADDR): + continue + buf = self.block(addr) + if len(buf) >= self.bs: + out.extend(self._parse_dentry(buf, 0, self.bs)) + return sorted(out) + + def read_file(self, nid, size): + """Yield exactly `size` bytes, holes emitted as zeros so offsets stay + correct. Inline data lives in the inode; encrypted or compressed content + is refused rather than guessed at.""" + ino = self.inode(nid) + if ino is None: + return + if self._encrypted(ino): + raise F2fsUnreadable("the file is encrypted and the volume holds no key") + if self._compressed(ino): + raise F2fsUnreadable("the file is compressed (F2FS compression is not read here)") + if ino["inline"] & F2FS_INLINE_DATA: + off = ino["iaddr_off"] + 4 # skip DEF_INLINE_RESERVED_SIZE + yield ino["raw"][off:off + size] + return + left, L = size, 0 + while left > 0: + addr = self._block_of(ino, L) + if addr in (F2FS_NULL_ADDR, F2FS_NEW_ADDR, F2FS_COMPRESS_ADDR): + buf = bytes(self.bs) + else: + buf = self.block(addr) + if len(buf) < self.bs: + buf = buf + bytes(self.bs - len(buf)) + take = min(self.bs, left) + yield buf[:take] + left -= take + L += 1 + + # --------------------------------------------------------------------------- # FAT32 and exFAT. # @@ -5287,6 +5681,8 @@ def walker_for(kind, fh, base, size=None): return ExfatWalker(fh, base) if kind == "ntfs": return NtfsWalker(fh, base) + if kind == "f2fs": + return F2fsWalker(fh, base) if kind in ("hfs+", "hfsx"): return HfsPlusWalker(fh, base) if kind == "apfs": @@ -5529,6 +5925,66 @@ def identify_fat(fh, base): return None +def identify_f2fs(fh, base): + """Return ("f2fs", lines) for an F2FS volume at base, else None. + + F2FS names itself with a 4-byte magic 1024 bytes into the volume, and the + reserved inode numbers that follow (node 1, meta 2, root 3) are fixed, so + both are required rather than the magic alone (fs/f2fs/super.c + sanity_check_raw_super). Block size and blocks-per-segment must also be the + values the format allows. + """ + b = read_at(fh, base + F2FS_SUPER_OFF, 3072) + if len(b) < 200 or struct.unpack_from("