Metadata-Version: 2.3
Name: picorescue
Version: 0.2.0
Summary: Inspect and recover LittleFS/FAT partitions from Raspberry Pi Pico flash dumps
Author: Phil Howard
Author-email: Phil Howard <github@gadgetoid.com>
Requires-Dist: click>=8.1
Requires-Dist: littlefs-python>=0.12
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# picorescue

Find, inspect and recover data from Raspberry Pi Pico (RP2040 / RP2350) flash
dumps. Built for Pimoroni-style images that pair a MicroPython firmware with
**LittleFS**, **FAT** and (on RP2350) **ROMFS** partitions, where the rescue
target is usually a handful of accidentally-deleted Python scripts.

It reads the `bi_decl` *binary info* region (the same `BlockDevice` declarations
parsed by [`py_decl`](https://github.com/gadgetoid/py_decl)) to locate
partitions, then reads and recovers data from them. It also signature-scans the
whole dump and merges the results, so it finds filesystems that are missing from
`bi_decl` or *nested* inside a declared device - e.g. the `dir2uf2 --fs-reserve`
hybrid where a LittleFS sits in the tail of a FAT block device.

## Install / run

Uses [uv](https://docs.astral.sh/uv/):

```bash
uv sync
uv run picorescue --help
```

or, from pip:

```bash
uv pip install picorescue
uv run picorescue --help
```

Accepts a raw flash dump (`.bin`, offset 0 = flash base `0x10000000`) or a `.uf2`
(reassembled to the correct flash addresses, gaps filled with `0xFF`).

A full-flash dump is ideal. You can grab one with `picotool save -a flash.bin`
or over SWD with OpenOCD.

## Commands

```bash
picorescue info       DUMP                 # bi_decl info + partitions, with used storage
picorescue partitions DUMP                 # just the partition table
picorescue debug      DUMP                 # report overlapping filesystems & other errors
picorescue ls         DUMP [-p NAME] [-v]  # list live files (-v adds file count + used size)
picorescue extract    DUMP OUTDIR          # extract live (non-deleted) files
picorescue recover    DUMP OUTDIR          # recover deleted / orphaned data
```

`recover` options: `-p/--partition NAME`, `--no-carve`, `--whole-dump` (carve the
entire image, not just known partitions), `--min-score` (carver threshold).

Supported filesystems: **LittleFS** (list, extract, deleted-file recovery),
**FAT12/16/32** (list, extract, `0xE5` undelete), and **MicroPython ROMFS**
(list, extract; read-only, with `.mpy` bytecode surfaced verbatim).

## Examples

File names below are illustrative; the layout and figures match real tool
output.

Inspect a dump - firmware info, discovered partitions, and estimated usage:

```
$ picorescue info flash.bin
Loaded flash.bin: 16.00MB @ base 0x10000000
  ProgramName: MicroPython
  ProgramVersion: v1.25.0
  PicoBoard: pico2
  SDKVersion: 2.1.1
  BinaryEndAddress: 0x100d5138

Partitions (3):
  ROMFS                            0x10200000     1.00MB  romfs     [read] (bi_decl)        used ~ 409.7KB ( 40%)
  MicroPython                      0x10300000    13.00MB  fat       [read,write] (bi_decl)  used ~  1.33MB ( 11%)
      12.02MB FAT volume within the 13.00MB MicroPython bi_decl region
  MicroPython:littlefs@0x10f00000  0x10f00000     1.00MB  littlefs  [-] (scan)              used ~ 100.0KB ( 10%)
```

List a filesystem's live files, with a summary line (`-v`):

```
$ picorescue ls flash.bin -p MicroPython:littlefs@0x10f00000 -v

# MicroPython:littlefs@0x10f00000 (littlefs) @ 0x10f00000
     6212  /main.py
      512  /lib/display.py
       78  /state/clock.json
   307200  /state/screenshot.txt
     ... (12 files)
  -- 12 file(s), used ~100.0KB (10% of 1.00MB)
```

Recover deleted and orphaned data into `./rescued/` (writes a `MANIFEST.json`):

```
$ picorescue recover flash.bin ./rescued

# MicroPython (fat) @ 0x10300000 - 13.00MB
  FAT12: undeleted 4 file(s)
  carved 21 Python candidate(s)

# MicroPython:littlefs@0x10f00000 (littlefs) @ 0x10f00000 - 1.00MB
  metadata: 12 inline file(s) recovered (1 not in live FS -> likely deleted); 3 name-only hint(s)
  carved 0 Python candidate(s)

Done. 37 item(s) -> ./rescued (see MANIFEST.json)

$ ls ./rescued/MicroPython/undelete/
config.py  main.py  notes.txt  _ecret.py
```

Check a dump for structural problems (exits non-zero when errors are found):

```
$ picorescue debug flash.bin
Loaded flash.bin: 16.00MB @ base 0x10000000
Discovered 3 partition(s).

  [ERROR] Overlap: 'MicroPython' (FAT volume 0x10300000-0x10f04000) and 'MicroPython:littlefs@0x10f00000' (LittleFS 0x10f00000-0x11000000) overlap by 16.0KB

1 issue(s): 1 error(s), 0 warning(s), 0 note(s).
```

## How recovery works

`recover` writes into `OUTDIR/<partition>/` and a top-level `MANIFEST.json`
describing every recovered item and the method used.

- **LittleFS metadata** (`deleted/`, `metadata/`) - LittleFS is a copy-on-write,
  log-structured filesystem. Deleting or overwriting a file leaves the old
  commit (its name and, for small files, its *inline* content) in the metadata
  block until that block is erased and compacted. picorescue threads the
  XOR-delta tag log of every metadata block and pulls out names + inline data
  across **all** commits, not just the live view. Entries whose name is absent
  from the mounted filesystem are flagged as likely-deleted and sorted into
  `deleted/`.
- **FAT undelete** (`undelete/`) - deletion only sets the directory entry's
  first name byte to `0xE5` and frees the cluster chain; the starting cluster
  and size remain, so contiguous files undelete cleanly. Verify integrity of
  anything recovered this way - fragmented files may be partial.
- **ROMFS** (`files/`) - ROMFS is immutable, so there are no deletions to hunt;
  `recover` simply extracts every file (including `.mpy` bytecode) alongside the
  carve pass.
- **Carving** (`carved/`) - filesystem-agnostic. Scans raw blocks for printable
  text runs and scores them for "Python-ness" (`import`/`def`/`class`, indented
  multi-line structure, assignments). Catches scripts whose directory entry or
  metadata is gone but whose content still lingers in flash. Carved content is
  de-duplicated against live files.

Recovered scripts are best-effort: always eyeball them. Carved fragments may
have a stray leading byte or be truncated at a block boundary.

## Layout

```
src/picorescue/
  bidecl.py   vendored py_decl bi_decl parser (BlockDevice discovery)
  dump.py     load .bin/.uf2 -> addressed flash image; partition discovery
  lfs.py      LittleFS mount/list/extract + metadata-log recovery
  fat.py      FAT12/16/32 read + 0xE5 undelete
  romfs.py    MicroPython ROMFS read (list/extract)
  carve.py    Python-source carver
  cli.py      click CLI
```

## Support Me

I work on Pico shinies by day, occasionally cranking out balmy tools to make my job easier and sharing them with the world.

If they help you too, great! If you want to throw me a bone for my troubles, see below:

* Ko-Fi - https://ko-fi.com/gadgetoid
* GitHub - https://github.com/sponsors/Gadgetoid
* Patreon - https://www.patreon.com/c/gadgetoid
* PayPal - https://www.paypal.com/paypalme/gadgetoid

Find some of my other projects below:

* dir2uf2 - Pack a directory and append it to a MicroPython uf2 - https://github.com/gadgetoid/dir2uf2
* py_decl - Python code to read Pico's binary declaration format - https://github.com/gadgetoid/py_decl
* Pico pinouts - https://pico.pinout.xyz 
