mirror of
https://github.com/Ap0dexMe0/unixtract.git
synced 2026-07-15 21:00:03 +02:00
ubifs
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# Cross-compilation settings.
|
||||
#
|
||||
# When building the Windows GNU target from Linux, point Cargo at the
|
||||
# mingw-w64 toolchain (install via `apt install gcc-mingw-w64-x86-64`).
|
||||
# These settings are ignored on hosts that build natively.
|
||||
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "x86_64-w64-mingw32-gcc"
|
||||
ar = "x86_64-w64-mingw32-ar"
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
@@ -0,0 +1,61 @@
|
||||
name: rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.target }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
artifact: unixtract-linux
|
||||
bin: unixtract
|
||||
- target: x86_64-pc-windows-gnu
|
||||
artifact: unixtract-windows.exe
|
||||
bin: unixtract.exe
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install mingw-w64 (Windows target)
|
||||
if: matrix.target == 'x86_64-pc-windows-gnu'
|
||||
run: sudo apt-get update && sudo apt-get install -y gcc-mingw-w64-x86-64
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Rename binary
|
||||
run: cp "target/${{ matrix.target }}/release/${{ matrix.bin }}" "${{ matrix.artifact }}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: ${{ matrix.artifact }}
|
||||
if-no-files-found: error
|
||||
+4
-1
@@ -6,4 +6,7 @@ wvenv/
|
||||
dist/
|
||||
firmware/
|
||||
device/
|
||||
dist/
|
||||
dist/
|
||||
3rd_party/
|
||||
.vscode/
|
||||
*.exe
|
||||
@@ -1,232 +1,183 @@
|
||||
# unixtract
|
||||
Extractor for various file formats.
|
||||
This project is a fork of [theubusu/unixtract](https://github.com/theubusu/unixtract), adding extra features and improvements.
|
||||
This is a tool that is able to extract the contents of various firmware package formats, mostly from TVs and AV devices.
|
||||
Built in Rust, and made to not depend on any external dependancies, only Rust crates. This way it can work on Windows, Linux and MacOS and even Android.
|
||||
Please note that this project is still under active development and errors may occur. Feel free to make an issue in that case, or if you have any feature request.
|
||||
|
||||
**PLEASE NOTE** - this program is NOT, and will never be designed for re-packing the extracted files!
|
||||
|
||||
## About this fork
|
||||
This fork extends the upstream project with additional features and bug fixes. For the original README and format support details, see the [upstream repository](https://github.com/theubusu/unixtract).
|
||||
> A fast, dependency-free firmware extractor for TVs, Blu-ray players, set-top boxes, and other AV devices.
|
||||
|
||||
# Installation
|
||||
You can download the latest auto build for Windows and Linux x86-64 from [here](https://nightly.link/theubusu/unixtract/workflows/rust/main).
|
||||
Or, build from source, by downloading the code or cloning the respository and running `cargo build --release`. The binary will be saved in `target/release`.
|
||||
`unixtract` analyzes and unpacks a wide range of proprietary firmware package formats, with built-in decryption and decompression. It is written entirely in Rust with no external runtime dependencies, so a single binary runs on **Windows, Linux, macOS, and Android**.
|
||||
|
||||
# Usage
|
||||
`unixtract [OPTIONS] <INPUT_TARGET> [OUTPUT_FOLDER]`
|
||||
Arguments:
|
||||
`<INPUT_TARGET>` - The target to analyze/extract.
|
||||
`[OUTPUT_FOLDER]` - Folder to save extracted files to.
|
||||
If an output folder is not provided, extracted files will be saved in folder `_<INPUT_TARGET>`.
|
||||
Options:
|
||||
`-o, --options <OPTIONS>` - Format specific or global(for all formats that implement it) options, see the list below for format specific options. You can use this multiple times to activate multiple options.
|
||||
This project is a fork of [theubusu/unixtract](https://github.com/theubusu/unixtract), adding extra formats, features, and fixes.
|
||||
|
||||
## Global options
|
||||
`dump_dec_hdrs` - For formats with an encrypted header - dump the decrypted header(s).
|
||||
> [!NOTE]
|
||||
> This project is under active development — errors may occur. Bug reports and feature requests are welcome via the issue tracker.
|
||||
|
||||
# Supported formats
|
||||
## Amlogic burning image
|
||||
**Used in:** Android TVs and Boxes
|
||||
**Notes:** V1 format is not supported because of the lack of sample file.
|
||||
**Thanks to:** https://github.com/7Ji/ampack
|
||||
> [!IMPORTANT]
|
||||
> `unixtract` is an **extraction** tool only. It is **not**, and will never be, designed to re-pack extracted files.
|
||||
|
||||
## Android OTA payload.bin
|
||||
**Used in:** Android devices, smartphones, TVs
|
||||
**Notes:** Some compression methods may not be supported.
|
||||
**Thanks to:** https://android.googlesource.com/platform/system/update_engine/+/HEAD/update_metadata.proto
|
||||
---
|
||||
|
||||
## BDL
|
||||
**Used in:** Enterprise HP Printers
|
||||
**Notes:** None, all files should be supported
|
||||
## Table of Contents
|
||||
|
||||
## CD5
|
||||
**Used in:** Some Samsung TV tuners, and possibly other Irdeto(?)-based tuners
|
||||
**Notes:** Decryption is not supported.
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Supported Formats](#supported-formats)
|
||||
- [Format Options](#format-options)
|
||||
- [Notes on Keys](#notes-on-keys)
|
||||
- [License](#license)
|
||||
|
||||
## EPK v1
|
||||
**Used in:** LG TVs before ~2010
|
||||
**Notes:** None, all files should be supported
|
||||
**Thanks to:** https://github.com/openlgtv/epk2extract
|
||||
---
|
||||
|
||||
## EPK v2
|
||||
**Used in:** LG TVs since ~2010
|
||||
**Notes:** **Depends on keys** - see keys.rs (most common keys should be included)
|
||||
**Thanks to:** https://github.com/openlgtv/epk2extract
|
||||
**Options:**
|
||||
※ Support `dump_dec_hdrs` option
|
||||
## Features
|
||||
|
||||
## EPK v3
|
||||
**Used in:** LG webOS-based TVs
|
||||
**Notes:** **Depends on keys** - see keys.rs
|
||||
**Thanks to:** https://github.com/openlgtv/epk2extract
|
||||
**Options:**
|
||||
※ Support `dump_dec_hdrs` option
|
||||
- **35+ firmware formats** across major TV/AV silicon vendors (MStar, MediaTek, Novatek, Amlogic, Broadcom…).
|
||||
- **Built-in decryption** (AES, DES, ECB/CBC, RSA) and **decompression** (LZO, LZ4, LZMA/XZ, zlib/gzip, bzip2, zstd, LZHS, sparse).
|
||||
- **Recursive extraction** — container formats automatically unpack their inner payloads.
|
||||
- **NAND-aware** — handles raw NAND dumps, OOB/spare stripping, and UBI/UBIFS rootfs images.
|
||||
- **Single static binary** — no interpreters, no system libraries.
|
||||
- **Bulk mode** — process an entire directory of firmware in one run.
|
||||
|
||||
## Funai BDP
|
||||
**Used in:** Funai & Funai-made Philips Blu-Ray player/HTS (USA market)
|
||||
**Notes:** N/A
|
||||
## Installation
|
||||
|
||||
## Funai MStar
|
||||
**Used in:** MStar-based Funai & Funai-made Philips TVs (USA market)
|
||||
**Notes:** Inner SoC part is extracted with mstar_secure_old
|
||||
### Prebuilt binaries
|
||||
|
||||
## Funai UPG
|
||||
**Used in:** Some Funai TVs
|
||||
**Notes:** **Depends on keys** - see keys.rs.
|
||||
Download the latest automated build for Windows and Linux x86-64 from the [nightly builds](https://nightly.link/Ap0dexMe0/unixtract/workflows/rust/main).
|
||||
|
||||
## Funai UPG PHL
|
||||
**Used in:** Funai & Funai-made Philips TVs (USA market)
|
||||
**Notes:** **Depends on keys** - see keys.rs (most common keys should be included).
|
||||
### From source
|
||||
|
||||
## GX DVB
|
||||
**Used in:** Cheap NationalChip GX-based DVB tuners
|
||||
**Notes:** None, all files should be supported
|
||||
```sh
|
||||
git clone https://github.com/Ap0dexMe0/unixtract
|
||||
cd unixtract
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## INVINCIBLE_IMAGE
|
||||
**Used in:** LG Broadcom-based Blu-Ray players
|
||||
**Notes:** Key ID 1 (<2010) is not supported.
|
||||
Tip: if you have split ROM (.ROM-00 and .ROM-01), extract both into the same folder so they get combined.
|
||||
The resulting binary is written to `target/release/unixtract`.
|
||||
|
||||
## MSD 1.0
|
||||
**Used in:** Samsung TVs 2013-2015
|
||||
**Notes:** **Depends on keys** - see keys.rs
|
||||
**Thanks to:** https://github.com/bugficks/msddecrypt
|
||||
**Options:**
|
||||
`msd10:save_cmac` - Save CMAC data for files that is skipped by default.
|
||||
`msd:print_ouith` - Prints the entire parsed OUITH header.
|
||||
※ Support `dump_dec_hdrs` option
|
||||
## Usage
|
||||
|
||||
## MSD 1.1
|
||||
**Used in:** Samsung TVs 2016+
|
||||
**Notes:** **Depends on keys** - see keys.rs (keys 2015-2018, 2020 included)
|
||||
**Thanks to:** https://github.com/bugficks/msddecrypt
|
||||
**Options:**
|
||||
`msd:print_ouith` - Prints the entire parsed OUITH header.
|
||||
※ Support `dump_dec_hdrs` option
|
||||
```sh
|
||||
unixtract [OPTIONS]
|
||||
```
|
||||
|
||||
## MStar upgrade bin
|
||||
**Used in:** Many MStar-based TVs (Hisense, Toshiba...)
|
||||
**Notes:** All files should be supported, includes lzop, lz4, lzma, sparse_write support
|
||||
**Options:**
|
||||
`mstar:keep_unknown` - Save data with unknown destination.
|
||||
※ Support `dump_dec_hdrs` option (will save the script)
|
||||
### Input modes (mutually exclusive)
|
||||
|
||||
## MStar upgrade bin (Secure, old)
|
||||
**Used in:** Older MStar-based TVs with Secure upgrade mode (encrypted+signed)
|
||||
**Notes:** Only default upgrade key is supported. This use the extractor above after decrypting.
|
||||
**Options:**
|
||||
`mstar_secure_old:keep_decrypted` - Keep decrypted file (it will be deleted by default).
|
||||
| Flag | Description |
|
||||
| --- | --- |
|
||||
| `--file-input <FILE>` | Extract a single firmware binary |
|
||||
| `--dir-input <DIR>` | Bulk-process every firmware binary in a directory |
|
||||
|
||||
## MediaTek BDP
|
||||
**Used in:** Many MediaTek-based Blu-Ray players (LG, Samsung, Philips, Panasonic...)
|
||||
**Notes:** Some older files may fail to extract
|
||||
### Options
|
||||
|
||||
## MediaTek PKG (New)
|
||||
**Used in:** Newer MediaTek-based TVs (TCL, Hisense, Sony, Philips, CVT...)
|
||||
**Notes:** **Depends on keys** - see keys.rs (Keys for Philips and Sony included)
|
||||
**Thanks to:** https://github.com/theubusu/mtkdec
|
||||
**Options:**
|
||||
`mtk_pkg:no_del_comp` - Don't delete LZHS compressed partition file after decompressing.
|
||||
※ Support `dump_dec_hdrs` option
|
||||
| Flag | Description |
|
||||
| --- | --- |
|
||||
| `--output <PATH>` | Output path for extracted data (default: `_<INPUT>`) |
|
||||
| `--lazy-run` | Detect/scan only — skip extraction (fast analysis) |
|
||||
| `--build-prop` | Extract and display firmware build properties and metadata |
|
||||
| `--dump-keys` | Dump the built-in decryption keys |
|
||||
| `--list-formats` | List all supported formats and exit |
|
||||
| `-v, --verbose` | Increase verbosity (repeat for more detail) |
|
||||
| `-h, --help` | Print help information |
|
||||
| `-V, --version` | Print version information |
|
||||
|
||||
## MediaTek PKG (Old)
|
||||
**Used in:** Older MediaTek-based TVs (Philips, Sony, Hisense...)
|
||||
**Notes:** All files should be supported, decryption + decompression
|
||||
**Options:**
|
||||
`mtk_pkg:no_del_comp` - Don't delete LZHS compressed partition file after decompressing.
|
||||
※ Support `dump_dec_hdrs` option
|
||||
### Examples
|
||||
|
||||
## Novatek PKG (NFWB)
|
||||
**Used in:** Some older Novatek-based TVs (LG, Philips)
|
||||
**Notes:** None, all files should be supported.
|
||||
```sh
|
||||
# Single file
|
||||
unixtract --file-input firmware.bin
|
||||
unixtract --file-input firmware.bin --output extracted/ --verbose
|
||||
|
||||
## Novatek TIMG
|
||||
**Used in:** Newer Novatek-based TVs (Philips(TPVision), Hisense, TCL...)
|
||||
**Notes:** None, all files should be supported.
|
||||
# Bulk directory
|
||||
unixtract --dir-input firmware_dump/ --output out/
|
||||
|
||||
## Onkyo
|
||||
**Used in:** Onkyo AVRs and other AV devices
|
||||
**Notes:** Newer files seem to use a different encryption and are not (yet) supported.
|
||||
**Thanks to:** http://divideoverflow.com/2014/04/decrypting-onkyo-firmware-files/
|
||||
**Options:**
|
||||
※ Support `dump_dec_hdrs` option
|
||||
# Quick analysis without extracting
|
||||
unixtract --file-input firmware.bin --lazy-run
|
||||
```
|
||||
|
||||
## Panasonic Blu-Ray (PANA_DVD.FRM, PANA_ESD.FRM, PANAEUSB.FRM)
|
||||
**Used in:** Panasonic Blu-Ray Players and Recorders
|
||||
**Notes:** **Depends on keys** - see keys.rs (Included keys should work for 99% of players released in and before 2014, and some released in 2018), Note that there is currently an issue with MAIN in some very ancient files not extracting correctly.
|
||||
**Options:**
|
||||
`pana_dvd:split_main` - Automatically split the MAIN module into seperate partitions.
|
||||
※ Support `dump_dec_hdrs` option
|
||||
## Supported Formats
|
||||
|
||||
## Philips UPG (Autorun.upg, 2SWU3TXV)
|
||||
**Used in:** Philips pre-TPVision TVs 200?-2013 and some Sony TVs
|
||||
**Notes:** **Depends on keys** - see keys.rs
|
||||
**Thanks to:** https://github.com/frederic/pflupg-tool
|
||||
**Options:**
|
||||
`pfl_upg:no_extract_inner_upg` - Do not automatically extract inner UPGs. (Warning: this can cause file collisions sometimes!)
|
||||
> Entries marked **keys** depend on decryption keys — see [`keys.rs`](src/keys.rs). Most common keys are bundled.
|
||||
> Entries marked **hdrs** support the `dump_dec_hdrs` option.
|
||||
|
||||
## Philips BDP
|
||||
**Used in:** Philips MediaTek-based Blu-ray players/Home theatre systems
|
||||
**Notes:** The main partition (ID 0) can be sometimes encrypted, and there is no good way to detect that. So if MTK BDP extraction fails, try running with `philips_bdp:decrypt` option.
|
||||
**Options:**
|
||||
`philips_bdp:decrypt` - Decrypt main partition
|
||||
### TV / SoC firmware
|
||||
|
||||
## PUP
|
||||
**Used in:** Sony PlayStation 4/5
|
||||
**Notes:** File has to be decrypted.
|
||||
**Thanks to:** https://github.com/Zer0xFF/ps4-pup-unpacker
|
||||
| Format | Used in | Notes |
|
||||
| --- | --- | --- |
|
||||
| **MStar upgrade bin** | Many MStar-based TVs (Hisense, Toshiba…) | LZOP, LZ4, LZMA, sparse-write support · hdrs |
|
||||
| **MStar upgrade bin (Secure, old)** | Older MStar TVs with secure (encrypted+signed) upgrades | Default upgrade key only |
|
||||
| **MStar UNFD NAND dump** | Raw MStar NAND dumps prefixed with `MSTARSEMIUNFDCIS` (e.g. `TH58NVG2S3HTA00.bin`) | Derives NAND geometry; carves boot banks, `rootfs_ubi.bin`, and `nand_data.bin`; recurses into inner formats; writes `cis_info.txt` |
|
||||
| **UBI / UBIFS NAND rootfs** | NAND Linux rootfs images (`UBI#` header), e.g. carved `rootfs_ubi.bin` | Strips interleaved NAND OOB, tolerates vendor header quirks, reconstructs volumes, CRC-validated linear UBIFS scan (LZO/zlib/zstd) |
|
||||
| **MediaTek PKG (New)** | Newer MediaTek TVs (TCL, Hisense, Sony, Philips, CVT…) | keys (Philips, Sony) · hdrs |
|
||||
| **MediaTek PKG (Old)** | Older MediaTek TVs (Philips, Sony, Hisense…) | Full decrypt + decompress · hdrs |
|
||||
| **Novatek PKG (NFWB)** | Older Novatek TVs (LG, Philips) | All files supported |
|
||||
| **Novatek TIMG** | Newer Novatek TVs (TPVision, Hisense, TCL…) | All files supported |
|
||||
| **Amlogic burning image** | Android TVs and boxes | V1 not supported (no sample) · thanks to [ampack](https://github.com/7Ji/ampack) |
|
||||
| **EPK v1** | LG TVs before ~2010 | thanks to [epk2extract](https://github.com/openlgtv/epk2extract) |
|
||||
| **EPK v2** | LG TVs since ~2010 | keys · hdrs · thanks to [epk2extract](https://github.com/openlgtv/epk2extract) |
|
||||
| **EPK v3** | LG webOS TVs | keys · hdrs · thanks to [epk2extract](https://github.com/openlgtv/epk2extract) |
|
||||
| **MSD 1.0** | Samsung TVs 2013–2015 | keys · hdrs · thanks to [msddecrypt](https://github.com/bugficks/msddecrypt) |
|
||||
| **MSD 1.1** | Samsung TVs 2016+ | keys (2015–2018, 2020) · hdrs · thanks to [msddecrypt](https://github.com/bugficks/msddecrypt) |
|
||||
| **Samsung (`*.img.sec` folder)** | Samsung TVs pre-2013 | keys · thanks to [samygo-patcher](https://github.com/george-hopkins/samygo-patcher) |
|
||||
| **Philips UPG** (`Autorun.upg`, `2SWU3TXV`) | Philips pre-TPVision TVs 200?–2013, some Sony TVs | keys · thanks to [pflupg-tool](https://github.com/frederic/pflupg-tool) |
|
||||
| **Roku** | Roku TVs / players | Some inner images remain encrypted |
|
||||
| **GX DVB** | Cheap NationalChip GX-based DVB tuners | All files supported |
|
||||
| **CD5** | Some Samsung TV tuners / Irdeto-based tuners | Decryption not supported |
|
||||
| **TSB Bin** | Older Toshiba TVs | hdrs |
|
||||
|
||||
## Roku
|
||||
**Used in:** Roku TV's/players
|
||||
**Notes:** The contents of the update file can be extracted, but some firmware images contained inside are additionally encrypted, and they cannot be decrypted as of now.
|
||||
### Blu-ray / AV device firmware
|
||||
|
||||
## RUF
|
||||
**Used in:** Samsung Broadcom-based Blu-Ray players
|
||||
**Notes:** **Depends on keys** - see keys.rs
|
||||
| Format | Used in | Notes |
|
||||
| --- | --- | --- |
|
||||
| **MediaTek BDP** | MediaTek Blu-ray players (LG, Samsung, Philips, Panasonic…) | Some older files may fail |
|
||||
| **Philips BDP** | Philips MediaTek-based Blu-ray players / HTS | Main partition may be encrypted — try `philips_bdp:decrypt` |
|
||||
| **Sony BDP** | Sony MediaTek-based Blu-ray players | keys (up to MSB29) · thanks to [s390-firmware](http://malcolmstagg.com/bdp/s390-firmware.html) |
|
||||
| **Funai BDP** | Funai / Funai-made Philips Blu-ray & HTS (USA) | — |
|
||||
| **Funai MStar** | MStar-based Funai / Philips TVs (USA) | Inner SoC part via `mstar_secure_old` |
|
||||
| **Funai UPG** | Some Funai TVs | keys |
|
||||
| **Funai UPG PHL** | Funai / Philips TVs (USA) | keys |
|
||||
| **Panasonic Blu-ray** (`PANA_DVD/ESD/EUSB.FRM`) | Panasonic Blu-ray players & recorders | keys (≤2014, some 2018) · hdrs |
|
||||
| **INVINCIBLE_IMAGE** | LG Broadcom Blu-ray players | Key ID 1 (<2010) unsupported; extract split `.ROM-00/.ROM-01` together |
|
||||
| **RUF** | Samsung Broadcom Blu-ray players | keys |
|
||||
| **RVP / MVP** | Sharp Blu-ray players / recorders | Older XOR-encrypted types only |
|
||||
| **Onkyo** | Onkyo AVRs and AV devices | Newer encryption unsupported · hdrs · thanks to [divideoverflow](http://divideoverflow.com/2014/04/decrypting-onkyo-firmware-files/) |
|
||||
|
||||
## RVP/MVP
|
||||
**Used in:** Sharp Blu-Ray players/recorders
|
||||
**Notes:** Only the older types of files are supported (XOR-encrypted)
|
||||
### Panasonic TV firmware
|
||||
|
||||
## Samsung (Folder with ***.img.sec)
|
||||
**Used in:** Samsung TVs pre 2013
|
||||
**Notes:** **Depends on keys** - see keys.rs
|
||||
**Thanks to:** https://github.com/george-hopkins/samygo-patcher
|
||||
| Format | Used in | Notes |
|
||||
| --- | --- | --- |
|
||||
| **SDDL.SEC** | Panasonic TVs | Based on [sddl_dec](https://github.com/theubusu/sddl_dec) |
|
||||
| **SDBoot** | Panasonic TVs (SD boot) | Single known sample — support may vary |
|
||||
| **SDImage** (`SDImage.bin`) | Some 2010 USA Panasonic TVs | Decryption not yet supported |
|
||||
|
||||
## SDBoot
|
||||
**Used in:** Panasonic TVs SD boot
|
||||
**Notes:** There is only one known sample, so support may vary.
|
||||
**Base:** https://github.com/theubusu/sddl_dec
|
||||
### Generic / other
|
||||
|
||||
## SDDL.SEC
|
||||
**Used in:** Panasonic TVs
|
||||
**Notes:** None, all files should be supported.
|
||||
**Options:**
|
||||
`sddl_sec:save_extra` - Save SDIT.FDI and .TXT files that are not extracted by default.
|
||||
`sddl_sec:split_peaks` - Split PEAKS module into partitions (only on older files). This will also automatically decompress compressed partitions.
|
||||
`sddl_sec:no_decomp_peaks` - Do not automatically decompress partitions when splitting PEAKS with above option.
|
||||
**Base:** https://github.com/theubusu/sddl_dec
|
||||
| Format | Used in | Notes |
|
||||
| --- | --- | --- |
|
||||
| **Android OTA `payload.bin`** | Android devices, phones, TVs | Some compression methods unsupported |
|
||||
| **Raw eMMC user area** | eMMC dumps with `0x5840` partition descriptors | Dumps `<partition>.bin` + `partition_map.txt` |
|
||||
| **BDL** | Enterprise HP printers | All files supported |
|
||||
| **PUP** | Sony PlayStation 4/5 | Requires a decrypted file · thanks to [ps4-pup-unpacker](https://github.com/Zer0xFF/ps4-pup-unpacker) |
|
||||
| **SLP** | Samsung Tizen-based NX cameras | All files supported |
|
||||
|
||||
## SDImage (SDImage.bin)
|
||||
**Used in:** Some 2010 USA Panasonic TVs
|
||||
**Notes:** Decryption is not yet supported.
|
||||
## Format Options
|
||||
|
||||
## SLP
|
||||
**Used in:** Samsung Tizen-based NX series cameras
|
||||
**Notes:** None, all files should be supported.
|
||||
Format-specific options are passed by name. Example: `mstar:keep_unknown`.
|
||||
|
||||
## Sony BDP
|
||||
**Used in:** Sony MediaTek-based Blu-Ray players
|
||||
**Notes:** **Depends on keys** - see keys.rs (Platforms up to MSB29 are supported)
|
||||
**Thanks to:** http://malcolmstagg.com/bdp/s390-firmware.html
|
||||
| Option | Effect |
|
||||
| --- | --- |
|
||||
| `mstar:keep_unknown` | Save data with unknown destination |
|
||||
| `mstar_secure_old:keep_decrypted` | Keep the decrypted file (deleted by default) |
|
||||
| `msd10:save_cmac` | Save CMAC data skipped by default |
|
||||
| `msd:print_ouith` | Print the entire parsed OUITH header |
|
||||
| `mtk_pkg:no_del_comp` | Keep LZHS compressed partition after decompressing |
|
||||
| `pana_dvd:split_main` | Split the MAIN module into separate partitions |
|
||||
| `pfl_upg:no_extract_inner_upg` | Do not auto-extract inner UPGs (may avoid collisions) |
|
||||
| `philips_bdp:decrypt` | Decrypt the main partition |
|
||||
| `sddl_sec:save_extra` | Save `SDIT.FDI` and `.TXT` files skipped by default |
|
||||
| `sddl_sec:split_peaks` | Split the PEAKS module into partitions (older files) |
|
||||
| `sddl_sec:no_decomp_peaks` | Do not auto-decompress when splitting PEAKS |
|
||||
|
||||
## TSB Bin
|
||||
**Used in:** Older Toshiba TVs
|
||||
**Notes:** None, all files should be supported.
|
||||
**Options:**
|
||||
※ Support `dump_dec_hdrs` option
|
||||
## Notes on Keys
|
||||
|
||||
# License
|
||||
Licensed under GNU GPL v3.
|
||||
Many formats require decryption keys stored in [`src/keys.rs`](src/keys.rs). The most common publicly known keys are bundled. Use `--dump-keys` to list what is available. If extraction of a key-dependent format fails, the required key may simply not be present.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the **GNU General Public License v3.0**.
|
||||
|
||||
@@ -1,35 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Cross-compile unixtract for multiple targets and collect the binaries into
|
||||
# ./dist. Run from the repository root:
|
||||
#
|
||||
# ./build.sh # build the default target set
|
||||
# ./build.sh <target> ... # build only the given rustc target triples
|
||||
#
|
||||
# Requirements (Debian/Ubuntu example):
|
||||
# rustup # toolchain manager
|
||||
# sudo apt install gcc-mingw-w64-x86-64 # for x86_64-pc-windows-gnu
|
||||
# sudo apt install gcc-aarch64-linux-gnu # for aarch64-unknown-linux-gnu (optional)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
echo "Building unixtract (Linux + Windows)..."
|
||||
# --- Target set ------------------------------------------------------------
|
||||
# Override by passing target triples as arguments.
|
||||
DEFAULT_TARGETS=(
|
||||
x86_64-unknown-linux-gnu
|
||||
x86_64-pc-windows-gnu
|
||||
)
|
||||
|
||||
# Clean first
|
||||
cargo clean
|
||||
|
||||
# Build Linux
|
||||
cargo build --target x86_64-unknown-linux-gnu --release
|
||||
|
||||
# Build Windows (GNU)
|
||||
cargo build --target x86_64-pc-windows-gnu --release
|
||||
|
||||
# Prepare dist folder
|
||||
mkdir -p dist
|
||||
|
||||
echo "Collecting binaries..."
|
||||
|
||||
# Linux binary
|
||||
LINUX_BIN="target/x86_64-unknown-linux-gnu/release/unixtract"
|
||||
if [ -f "$LINUX_BIN" ]; then
|
||||
cp "$LINUX_BIN" "dist/unixtract-linux"
|
||||
if [ "$#" -gt 0 ]; then
|
||||
TARGETS=("$@")
|
||||
else
|
||||
TARGETS=("${DEFAULT_TARGETS[@]}")
|
||||
fi
|
||||
|
||||
# Windows binary
|
||||
WINDOWS_BIN="target/x86_64-pc-windows-gnu/release/unixtract.exe"
|
||||
if [ -f "$WINDOWS_BIN" ]; then
|
||||
cp "$WINDOWS_BIN" "dist/unixtract-windows.exe"
|
||||
fi
|
||||
BIN_NAME="unixtract"
|
||||
DIST_DIR="dist"
|
||||
|
||||
echo "Cleaning target directory..."
|
||||
rm -rf target
|
||||
echo "==> Building ${BIN_NAME} for: ${TARGETS[*]}"
|
||||
|
||||
echo "Done. Binaries are in ./dist"
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
for target in "${TARGETS[@]}"; do
|
||||
echo
|
||||
echo "==> Target: $target"
|
||||
|
||||
# Ensure the target's std library is installed.
|
||||
if ! rustup target list --installed | grep -qx "$target"; then
|
||||
echo " Installing rust std for $target ..."
|
||||
rustup target add "$target"
|
||||
fi
|
||||
|
||||
cargo build --release --target "$target"
|
||||
|
||||
# Work out the produced binary name (Windows targets get a .exe suffix).
|
||||
case "$target" in
|
||||
*windows*) src="target/$target/release/${BIN_NAME}.exe" ;;
|
||||
*) src="target/$target/release/${BIN_NAME}" ;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$src" ]; then
|
||||
echo " !! Expected binary not found: $src" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Friendly output name, e.g. unixtract-x86_64-pc-windows-gnu.exe
|
||||
case "$target" in
|
||||
*windows*) dst="$DIST_DIR/${BIN_NAME}-${target}.exe" ;;
|
||||
*) dst="$DIST_DIR/${BIN_NAME}-${target}" ;;
|
||||
esac
|
||||
|
||||
cp "$src" "$dst"
|
||||
echo " -> $dst"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "==> Done. Binaries are in ./${DIST_DIR}"
|
||||
ls -la "$DIST_DIR"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
435552455f53544f52455f46494c455f
|
||||
fafe67a1b53bbb8b931fecbaac0f0c0e
|
||||
0007FF4154534D92FC55AA0FFF0110E0
|
||||
BC1197CA30AA0FC84F7FE62E09FD3D9F
|
||||
8981D083B3D53B3DF1AC529A70F244C0
|
||||
3503B1CDE3401EC06030C12A4311F4A5
|
||||
E33AB4C45C2570B8AD15A921F752DEB6
|
||||
214BF3C129547AF31D32A5ECB4742192
|
||||
E01001FF0FAA55FC924D535441FF0700
|
||||
1F1E1D1C1B1A19180706050403020100 # BCM35230/early MTK5369 and LG1152
|
||||
7184C9C428D03C445188234D5A827196 # mtk5369 - Mediatek GP4 - HE_DTV_GP4I_AFAAATAA
|
||||
385A992430196A8C44F1985823C01440 # mtk5398 (a2) - Mediatek NetCast 4/4.5 - HE_LCD_NC5M_AAAAAIAA
|
||||
8E32E4608871ECE9B6301999D5155A07 # mtka5lr (mt5882) - Mediatek webOS 2 (2015) - HE_DTV_W15L_AFAAABAA
|
||||
6856A0482475A8B41728A35474810203 # new BCM35230
|
||||
2F2E2D2C2B2A29281716151413121110 # Saturn7/BCM3556
|
||||
212D3B2A5C2D3A2C4D5B234B1A321D2A # new Saturn7/old LM1
|
||||
4F836AAEB4301F26172A9B0E1120EAF4 # LM1 PDP
|
||||
4EE662C7A2C0917F7328DE73A0836C6B # LM1 LCD
|
||||
2F534ABE34801A36B7DA6B3EB1C04AD4 # m1 - MStar non-webOS - LN45*, LN53*, LN54*, LN565*, LA61*, LA643*, MA53*, PN45*, PN65*
|
||||
7B2CA5943D2E752CF58606228C5B2DAD # m1a - MStar non-webOS (L15 signage) - BS_LCD_LE15_AAAAAIAM
|
||||
D55C6864035A8C8A2B35A6D6C4565596 # m2 - MStar SimpleSmart - HE_LCD_SS1A_AFAAAIAA
|
||||
ADB92D9E23035522F4708CC259B31EA2 # m2 - MStar webOS 3.0 (2016) - HE_DTV_W16R_AFAAABAA
|
||||
D2E6EE17639DFE2F81D3840FA0BC334A # m2r - MStar webOS 3.5 (2017) - HE_DTV_W17R_AFAAABAA
|
||||
4F6DE80C0362FD562464BC2073D15567 # m3 - MStar webOS 4.0 (2018) - HE_DTV_W18R_AFAAABAA
|
||||
88723D91920712D0BAFE87A25E6E8EC7 # m3r - MStar webOS 4.5 (2019) - HE_DTV_W19R_AFAAATAA
|
||||
68A284B4953CAD15024BED2C4F852A09 # lm14 - MStar NetCast 4.5 (2014/2015) - HE_LCD_NC5U_AAADABAA
|
||||
19F51EE9B949C89E41AE136F48BB405C # lm14a - MStar webOS 2 (2015) - HE_DTV_W15A_AFADABAA
|
||||
F8F6BD1AA24506C2759E1BE1D51BB43C # lm14alite - MStar webOS 2 (2015) - HE_DTV_W15B_AFADABAA
|
||||
96F464CB29CDFF5441FD87D47D084FF8 # lm15u - MStar webOS 2 (2015) - HE_DTV_W15U_AFADABAA
|
||||
6FCCC4AA3389B614BABE462498D2020A # lm18a - MStar webOS 4.0 (2018) - HE_DTV_W18A_AFADATAA
|
||||
806B982279521809DBAD9E2E6BF377763903565A7EB4604BAB1E1503DBFC4326 # lm21a - MStar webOS 6 (2021) - HE_DTV_W21A_AFADATAA
|
||||
B65119E0E6CB5DB19C69B4CC78FAC3A87C747E5AEFDE8FF58F2CD47128D9E16D # lm21u (mt5889) - MStar webOS 6 (2021) - HE_DTV_W21U_AFADATAA
|
||||
FC9D81DEC206BA62614C949C43D2DA91D23E9FF3DF9674D69A444D13277BDF96 # lm21ut - MStar webOS 6 StanbyME (2022) - HE_DTV_N21D_AFAAATAA
|
||||
3435663331313732316538383063663538306161643131653335323334613034 # lm21an - MStar webOS 7 (2022) - HE_DTV_W22A_AFADATAA
|
||||
1FB2C3B789D5EA48ED16E79A0343986C691DACEC872BB07787D0F722AF5D1E2C # lm21ann - MStar webOS 8 (2023) - HE_DTV_W23A_AFADATAA
|
||||
4813B5B63C998A2874EF3320684AC8D9 # lg1152 - LX GP4 - HE_DTV_GP4H_AFAAATAA
|
||||
14B3623488212250C7C992AACD537447 # lg115x - LX NC4 - HE_LCD_NC4H_AAADABAA
|
||||
12C344FDD2871C983CD0FBBC25143974 # lg1154 (h13, goldfinger) - LX webOS 1 (2014) - HE_DTV_WT1H_AFAAABAA
|
||||
34CC219D3AFC102433109BBC1DA44095 # m14 (m14tv) - LX webOS 1 (2014) - HE_DTV_WT1M_AFAAATAA
|
||||
5A167D8C342EF094800E7CFA2D10F2D0 # m14 (m14tv) - LX webOS 2 (2015) - HE_DTV_W15M_AFAAATAA
|
||||
13F56BE4B4A0829598DB8F74065A263B # h15 - LX webOS 2 (2015) - HE_DTV_W15H_AFADATAA
|
||||
3679EF1840B7FDEBC1FBF95A0CAFCE3E # m16 - LX webOS 3.0 (2016) - HE_DTV_W16M_AFADABAA
|
||||
5804DF78CB8DC6A71C05DAB0F1EDE3E1 # m16lite - LX webOS 3.0 (2016) - HE_DTV_W16N_AFADABAA
|
||||
1B3C76ADD3F5EE6B089DB253747A8CD4 # m16p - LX webOS 3.5 (2017) - HE_DTV_W17H_AFADABAA
|
||||
3C9D30DF3A95C1AA41928813292BD947 # m16plite - LX webOS 3.5 (2017) - HE_DTV_W17M_AFADATAA
|
||||
89E11D498392F5A521145738EF036AE5 # m16pstb - LX webOS 3.5 (2017) - HE_DTV_W17S_AFADATAA
|
||||
437C02F0DF99F2072D1A64EEBBD2953B # m16pp - LX webOS 4.0 (2018) - HE_DTV_W18H_AFADABAA
|
||||
3471D9BFC5F4B34A8997D56932F34D94 # m16pplite - LX webOS 4.0 (2018) - HE_DTV_W18M_AFADATAA
|
||||
3FE1CBE11BD658BB37813E05052D5FE5 # m16p3 - LX webOS 4.5 (2019) - HE_DTV_W19H_AFADABAA
|
||||
A2FA48FCC1A22FD2F1944BEFA8403765EF178D4F4AB0E81AC7B5B267ACBDF14D # m23 - LX webOS 8 (2023) - HE_DTV_W23M_AFADATAA
|
||||
E529BCDEDF8E49667C0FA3A81174B65E # o18 - LX webOS 4.0 (2018) - HE_DTV_W18O_AFABABAA
|
||||
4ACD2CA8425BBA6C49FD03A174300239 # o18 - LX webOS 4.5 (2019) - HE_DTV_W19O_AFABABAA
|
||||
C9EF645424A625BBAE7521394564025EC6252658FB650D33633111BD40C76011 # o20 - LX webOS 5 (2020) - HE_DTV_W20O_AFABATAA
|
||||
944288798A122C6130B661BEE52DF4FE42120F60A61E312DCFC1411E300A29AE # o20n - LX webOS 6 (2021) - HE_DTV_W21O_AFABATAA
|
||||
3861333237633238613136633438663239623238623037656335623433353862 # o22 - LX webOS 7 (2022) - HE_DTV_W22O_AFABATAA
|
||||
CF5D6DC934F18618B968382368E17BA971DEAA2ECDFC906874B327D87076E228 # o22n - LX webOS 8 (2023) - HE_DTV_W23O_AFABATAA
|
||||
53D6DC79418C1A2371DC9F926CD3A3A06F4E7E4396464B5F41248083C2C65637 # o22n2 - LX webOS 9 (2024) - HE_DTV_W24G_AFABATAA
|
||||
B7724DBBF2AEA073131E8E7D62D114E2AA02F99D17CD7350C14466624528ED79 # o24 - LX webOS 9 (2024) - HE_DTV_W24O_AFABATAA
|
||||
FE90B0C1BE8CC28A9738333F95AC2C58777BDE7D4E8CABABA73B24FB7D1781C7 # o22n3 - LX webOS 10 (2025) - HE_DTV_W25G_AFABATAA
|
||||
52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1 # o24n - LX webOS 10 (2025) - HE_DTV_W25O_AFABATAA
|
||||
CD4171FC9C06869627A67EA7B66D739D
|
||||
0EE52A12A2EB5DE2E13999187B14913F6D3367A79B39AC35979A51E5C12A4FDF # o208k - LX webOS 5 (2020) - HE_DTV_W20K_AFADATAA
|
||||
E27E6AFE44B7866D60C24ED27904ECB296CA69B4251478B5248C03851F08ECF5 # e60n - LX webOS 6 (2021) - HE_DTV_W21K_AFADATAA
|
||||
F2B78AEBAD6D86A17B4742B2B84B60F4
|
||||
C2FBBC5DDD9D366B7FD6CAEB90F86039
|
||||
1C966DFA0E5AE9946AAF8D2EC06B9E18
|
||||
AD17A5923B525FD21DB765A5B6822FBD
|
||||
388BE4B04BD98E7C3CA45A4C6CA346DD2EB32BDCD05DC28FC4A87C9625294A5E # k6lp - Realtek webOS 5 (2020) - HE_DTV_W20P_AFADATAA
|
||||
377050F9B9D91CD803ACAACCEA4046DD99B01CFBB0010451F4F87A1620C4BAEF # k6lpfhd - Realtek webOS 5 (2020) - HE_DTV_W20L_AFAAJAAA
|
||||
395324AD369A529EABAC71FE1E72C25CE25594294D47303BCB2629241AFA4C98 # k6hp - Realtek webOS 5 (2020) - HE_DTV_W20H_AFADABAA
|
||||
74514676D68B9A72A0093CEF56D3067484E1F4D5CF7D4B4ED389BED030FA1B09 # k7lp - Realtek webOS 6 (2021) - HE_DTV_W21P_AFADATAA
|
||||
6A42D2485B716B25AE5C9921176588D167C25B902D4EF2903AF5C1FCC61D34C9 # k8lp - Realtek webOS 7 (2022) - HE_DTV_W22P_AFADATAA
|
||||
A35A57DFDD8266F7CE1AF991EC67BABF6723653ABB9A7D48A4B8AB2A2485BCFE # k8hp - Realtek webOS 7 (2022) - HE_DTV_W22H_AFADATAA
|
||||
703373367638792F423F4528482B4D6251655468576D5A7134743777217A2443 # k8lp - Realtek webOS 7 hospitality (2022) - HE_IDD_H22P_AHAAATAA
|
||||
6251655468576D5A7133743677397A24432646294A404E635266556A586E3272 # k8hp - Realtek webOS 7 hospitality (2022) - HE_IDD_H22H_AHAAATAA
|
||||
3764336361633437326166373639383663353863363039316332383031626637 # k8ap - Realtek webOS 7 (2022) - HE_DTV_W22L_AFAAATAA
|
||||
DFFD1E4F093E305451D4F3752E63BA9A3E6A6404922D986DF36C00818F5595C1 # k8hpp - Realtek webOS 8 (2023) - HE_DTV_W23H_AFADATAA
|
||||
EC2C89B4AF45B5EB7EA9A83DD2387810C0815BD31BBFE1D17C809E7D68339112 # k8lpn - Realtek webOS 8 (2023) - HE_DTV_W23P_AFADATAA
|
||||
F322A9CA1D523C358DD2FD97D5660E25386C9C60E423632AEC9723D282BE971D # kf23f - Realtek webOS 8 smart monitors (2023) - HE_MNT_S23Y_AAAAGLAA
|
||||
6252A0816884997B2FCA30662561A721A4BCC40B18CBEA5D363FA844F17D7DE9 # kid23q - Realtek webOS 8 ultrawide monitors (2023) HE_MNT_S23Z_AAACGLAA
|
||||
7638792F423F4528482B4D6251655368566D597133743677397A24432646294A # k8lpn - Realtek webOS 8 hospitality (2023) - HE_IDD_H23P_AHAAATAA
|
||||
6B5AD1BE81D7A1A494F58EB659431850C1B681826EE4428394D4897052691756 # k8lpn2 - Realtek webOS 9 (2024) - HE_DTV_W24P_AFADATAA
|
||||
FA9EBB838B7BAFBA75EFE8D5A3560374EB0699A113411CA924051B4ADB52E10D # k24 - Realtek webOS 9 (2024) - HE_DTV_W24H_AFADATAA
|
||||
1A8ADAB21D9FF995677DB32BCE2E0CD559AE86840EBF4A696872076E37DFFE8F # k24t - Realtek webOS 9 StanbyME 2 (2024) - HE_DTV_N24D_AFADATAA
|
||||
25DF24166745B52EAB661455BED43DE376320FAA1F7824877B938DB869308B18 # k25lp - Realtek webOS 10 (2025) - HE_DTV_W25P_AFADATAA
|
||||
B2E8C3E214F044B823916E48FA074E606C7C5CD5E6902B6F99BD903DAC0C792F # k24n - Realtek webOS 10 (2025) - HE_DTV_W25H_AFADATAA
|
||||
B23981FD3642CDF401E7A0C2FADBDA4399B6AAF9600B802144933B4F4E5855EA # k6lpwee - Realtek webOS 5 (2020) - HE_DTV_C20P_AFADATAA
|
||||
3263653764623932376235323637653637633035363066353833303235383466 # k8lpwee - Realtek webOS 7 (2022) - HE_DTV_C22P_AFADATAA
|
||||
46715DFBDE23C2D8CBA7EC4F36BA41AAD28E7EC00FEC51F2843F24654DB09BD3 # k8hpwee - Realtek webOS 7 (2022) - HE_DTV_C22H_AFABATAA
|
||||
6238323334663232396632613762316537333731333832306664666136333564 # k8apwee - Realtek webOS 7 (2022) - HE_DTV_C22L_AFAAATAA
|
||||
C43BEC5BAF907800A4DD908628793FCE
|
||||
F72F0FE303CC80E1BC3406E3B11095E1
|
||||
A76C33E840C4E776F4568524FA0B9D86
|
||||
E24424020DAD7B75C0CEDFB6180E4671
|
||||
1234567890abcdef1234567890abcdef
|
||||
3503B1CDE3401EC06030C12A4311F4A500000000000000000000000000000000 # Sansui MST135B_10AMVL_M5621_KNIGHT (MBOOTBAK.bin)
|
||||
@@ -25,6 +25,8 @@ pub struct Format {
|
||||
}
|
||||
|
||||
pub mod mstar;
|
||||
pub mod mstar_unfd;
|
||||
pub mod ubi;
|
||||
pub mod android_ota_zip;
|
||||
pub mod mstar_secure_old;
|
||||
pub mod samsung_old;
|
||||
@@ -86,11 +88,21 @@ pub fn get_registry() -> Vec<Format> {
|
||||
detector_func: crate::formats::android_ota_zip::is_android_ota_zip_file,
|
||||
extractor_func: crate::formats::android_ota_zip::extract_android_ota_zip,
|
||||
},
|
||||
Format {
|
||||
name: "mstar_unfd",
|
||||
detector_func: crate::formats::mstar_unfd::is_mstar_unfd_file,
|
||||
extractor_func: crate::formats::mstar_unfd::extract_mstar_unfd,
|
||||
},
|
||||
Format {
|
||||
name: "mstar",
|
||||
detector_func: crate::formats::mstar::is_mstar_file,
|
||||
extractor_func: crate::formats::mstar::extract_mstar,
|
||||
},
|
||||
Format {
|
||||
name: "ubi",
|
||||
detector_func: crate::formats::ubi::is_ubi_file,
|
||||
extractor_func: crate::formats::ubi::extract_ubi,
|
||||
},
|
||||
Format {
|
||||
name: "samsung_old",
|
||||
detector_func: crate::formats::samsung_old::is_samsung_old_dir,
|
||||
|
||||
@@ -74,6 +74,9 @@ pub fn extract_funai_mstar(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<()
|
||||
output_dir: app_ctx.output_dir.join("SoC"),
|
||||
options: app_ctx.options.clone(),
|
||||
dry_run: app_ctx.dry_run,
|
||||
lazy_run: app_ctx.lazy_run,
|
||||
build_prop: app_ctx.build_prop,
|
||||
dump_keys: app_ctx.dump_keys,
|
||||
quiet: app_ctx.quiet,
|
||||
verbose: app_ctx.verbose,
|
||||
};
|
||||
|
||||
@@ -58,6 +58,9 @@ pub fn extract_mstar_secure_old(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Resu
|
||||
output_dir: app_ctx.output_dir.clone(),
|
||||
options: app_ctx.options.clone(),
|
||||
dry_run: app_ctx.dry_run,
|
||||
lazy_run: app_ctx.lazy_run,
|
||||
build_prop: app_ctx.build_prop,
|
||||
dump_keys: app_ctx.dump_keys,
|
||||
quiet: app_ctx.quiet,
|
||||
verbose: app_ctx.verbose,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
use std::any::Any;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
use crate::{AppContext, InputTarget};
|
||||
use crate::utils::common;
|
||||
|
||||
/// Magic of the MStar UNFD (Universal Nand Flash Driver) Card Information
|
||||
/// Structure (CIS) that prefixes a raw NAND dump produced by MStar-based
|
||||
/// tools (common on Toshiba/Winbond/... SLC NAND used in TVs/STBs).
|
||||
const CIS_MAGIC: &[u8; 16] = b"MSTARSEMIUNFDCIS";
|
||||
|
||||
/// Marker that precedes each firmware "bank" inside the dump (a run of
|
||||
/// `0x02` bytes followed by `0x10 0x10 0x10 0x10`). Used to locate
|
||||
/// where the real NAND payload begins and how many boot banks are present.
|
||||
const BANK_MARKER: &[u8] = b"\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x10\x10\x10\x10";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnfdCisCtx {
|
||||
/// Raw CIS signature dword right after the magic (e.g. 0x91dc9805).
|
||||
pub cis_signature: u32,
|
||||
/// NAND vendor string (e.g. "TOSHIBA").
|
||||
pub vendor: String,
|
||||
/// NAND part number string (e.g. "TH58NVG2S3HTA00").
|
||||
pub part_number: String,
|
||||
/// Logical page data size in bytes (no OOB included).
|
||||
pub page_size: u32,
|
||||
/// Spare/OOB size per page in bytes.
|
||||
pub spare_size: u32,
|
||||
/// Number of pages per erase block.
|
||||
pub pages_per_block: u32,
|
||||
/// Erase block size in bytes (page_size * pages_per_block).
|
||||
pub block_size: u32,
|
||||
/// Total number of erase blocks in the dump.
|
||||
pub block_count: u32,
|
||||
/// Total image size in bytes.
|
||||
pub total_size: u64,
|
||||
/// Offsets of every `MSTARSEMIUNFDCIS` copy found in the image.
|
||||
pub cis_copies: Vec<usize>,
|
||||
/// Offsets of every firmware bank marker found in the image.
|
||||
pub bank_markers: Vec<usize>,
|
||||
/// Offset where the actual NAND payload starts (first bank marker).
|
||||
pub data_start: usize,
|
||||
}
|
||||
|
||||
pub fn is_mstar_unfd_file(
|
||||
app_ctx: &AppContext,
|
||||
) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
||||
let file = match app_ctx.file() {
|
||||
Some(f) => f,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Only the leading CIS header is needed for detection (keep it cheap).
|
||||
let header = common::read_file(&file, 0, 512)?;
|
||||
if header.len() < 16 || &header[0..16] != CIS_MAGIC {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let file_size = file.metadata()?.len();
|
||||
let cis_signature = u32::from_le_bytes(header[0x10..0x14].try_into().unwrap());
|
||||
let vendor = cstr(&header[0x40..0x50]);
|
||||
let part_number = cstr(&header[0x50..0x64]);
|
||||
let (page_size, spare_size, pages_per_block, block_size, block_count) =
|
||||
derive_geometry(file_size);
|
||||
|
||||
info!(
|
||||
"- Detected MStar UNFD NAND dump (CIS signature 0x{:08X})",
|
||||
cis_signature
|
||||
);
|
||||
info!(" Vendor: {}, Part: {}", vendor, part_number);
|
||||
info!(
|
||||
" NAND geometry: page={}B, spare={}B, pages/block={}, block={}B, blocks={}, total={}B",
|
||||
page_size, spare_size, pages_per_block, block_size, block_count, file_size
|
||||
);
|
||||
|
||||
Ok(Some(Box::new(UnfdCisCtx {
|
||||
cis_signature,
|
||||
vendor,
|
||||
part_number,
|
||||
page_size,
|
||||
spare_size,
|
||||
pages_per_block,
|
||||
block_size,
|
||||
block_count,
|
||||
total_size: file_size,
|
||||
cis_copies: Vec::new(),
|
||||
bank_markers: Vec::new(),
|
||||
data_start: 0,
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn extract_mstar_unfd(
|
||||
app_ctx: &AppContext,
|
||||
ctx: Box<dyn Any>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ctx = ctx
|
||||
.downcast::<UnfdCisCtx>()
|
||||
.map_err(|_| "Invalid MStar UNFD context")?;
|
||||
|
||||
fs::create_dir_all(&app_ctx.output_dir)?;
|
||||
|
||||
let input_path = app_ctx
|
||||
.input_path()
|
||||
.ok_or("MStar UNFD extractor requires an input path")?;
|
||||
let mut file = File::open(input_path)?;
|
||||
|
||||
// --- Locate CIS copies and firmware bank markers (full scan) ---
|
||||
ctx.cis_copies = scan_offsets(&mut file, CIS_MAGIC)?;
|
||||
ctx.bank_markers = scan_offsets(&mut file, BANK_MARKER)?;
|
||||
ctx.data_start = *ctx.bank_markers.first().unwrap_or(&0);
|
||||
|
||||
info!(
|
||||
" CIS copies: {:?}",
|
||||
ctx.cis_copies
|
||||
.iter()
|
||||
.map(|o| format!("0x{o:X}"))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
info!(
|
||||
" Bank markers: {:?}",
|
||||
ctx.bank_markers
|
||||
.iter()
|
||||
.map(|o| format!("0x{o:X}"))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
info!(" NAND payload starts at 0x{:X}", ctx.data_start);
|
||||
|
||||
// --- Locate the UBI/rootfs region (first UBI EC header) ---
|
||||
let ubi_start = scan_offsets(&mut file, b"UBI#")?
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or(ctx.total_size as usize);
|
||||
|
||||
// Boot banks are the markers that sit *before* the UBI region.
|
||||
let mut boot_markers: Vec<usize> = ctx
|
||||
.bank_markers
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|&m| m < ubi_start)
|
||||
.collect();
|
||||
boot_markers.sort_unstable();
|
||||
boot_markers.dedup();
|
||||
info!(" Boot banks before UBI: {}", boot_markers.len());
|
||||
|
||||
// --- Write a human readable CIS info file ---
|
||||
write_cis_info(&app_ctx.output_dir, &ctx, &boot_markers, ubi_start)?;
|
||||
|
||||
// --- Carve each boot bank (marker -> next marker, or UBI start) ---
|
||||
let mut carved_names = Vec::new();
|
||||
for (i, &start) in boot_markers.iter().enumerate() {
|
||||
let end = if i + 1 < boot_markers.len() {
|
||||
boot_markers[i + 1]
|
||||
} else {
|
||||
ubi_start
|
||||
};
|
||||
if end <= start {
|
||||
continue;
|
||||
}
|
||||
carve_region(
|
||||
&mut file,
|
||||
&app_ctx.output_dir,
|
||||
&format!("boot_bank{i}"),
|
||||
start,
|
||||
end - start,
|
||||
)?;
|
||||
carved_names.push(format!("boot_bank{i}"));
|
||||
}
|
||||
|
||||
// --- Carve the UBI/rootfs region (if present) ---
|
||||
if ubi_start < ctx.total_size as usize {
|
||||
carve_region(
|
||||
&mut file,
|
||||
&app_ctx.output_dir,
|
||||
"rootfs_ubi",
|
||||
ubi_start,
|
||||
ctx.total_size as usize - ubi_start,
|
||||
)?;
|
||||
carved_names.push("rootfs_ubi".to_string());
|
||||
}
|
||||
|
||||
// --- Carve the raw NAND payload (everything after the CIS header) ---
|
||||
let payload_len = ctx.total_size.saturating_sub(ctx.data_start as u64);
|
||||
if payload_len > 0 {
|
||||
file.seek(SeekFrom::Start(ctx.data_start as u64))?;
|
||||
let out_path = Path::new(&app_ctx.output_dir).join("nand_data.bin");
|
||||
let mut out = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&out_path)?;
|
||||
let copied = io::copy(&mut file.take(payload_len), &mut out)?;
|
||||
info!(
|
||||
" Carved raw NAND payload -> nand_data.bin ({} bytes, 0x{:X}..0x{:X})",
|
||||
copied,
|
||||
ctx.data_start,
|
||||
ctx.data_start as u64 + copied
|
||||
);
|
||||
|
||||
// --- Recursively extract any inner format from the carved payload ---
|
||||
recurse_extract(&out_path, app_ctx)?;
|
||||
} else {
|
||||
warn!(" No NAND payload found after CIS header, skipping carve.");
|
||||
}
|
||||
|
||||
// --- Recurse on the carved boot banks as well ---
|
||||
for name in &carved_names {
|
||||
let p = Path::new(&app_ctx.output_dir).join(format!("{name}.bin"));
|
||||
if p.exists() {
|
||||
if let Err(e) = recurse_extract(&p, app_ctx) {
|
||||
warn!(" Recursion on {name} failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries every registered format against the carved `path` and extracts
|
||||
/// each one that matches. The `mstar` format is intentionally skipped:
|
||||
/// this raw dump's boot script ships as U-Boot `filepartload` format
|
||||
/// strings (filled at runtime), so `mstar` would only produce empty
|
||||
/// stubs. Errors from individual formats are caught so one failure does not
|
||||
/// abort the rest.
|
||||
fn recurse_extract(
|
||||
payload_path: &Path,
|
||||
parent_ctx: &AppContext,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = File::open(payload_path)?;
|
||||
let sub_dir = parent_ctx.output_dir.join("nand_data_extracted");
|
||||
fs::create_dir_all(&sub_dir)?;
|
||||
|
||||
let in_ctx: AppContext = AppContext {
|
||||
input: InputTarget::File(file),
|
||||
input_path: Some(payload_path.to_path_buf()),
|
||||
output_dir: sub_dir,
|
||||
options: parent_ctx.options.clone(),
|
||||
dry_run: parent_ctx.dry_run,
|
||||
lazy_run: parent_ctx.lazy_run,
|
||||
build_prop: parent_ctx.build_prop,
|
||||
dump_keys: parent_ctx.dump_keys,
|
||||
quiet: parent_ctx.quiet,
|
||||
verbose: parent_ctx.verbose,
|
||||
};
|
||||
|
||||
info!(" Scanning carved NAND payload for inner formats...");
|
||||
for format in crate::formats::get_registry() {
|
||||
if format.name == "mstar" {
|
||||
continue;
|
||||
}
|
||||
match (format.detector_func)(&in_ctx) {
|
||||
Ok(Some(ctx)) => {
|
||||
info!(" - inner format detected: {}", format.name);
|
||||
if let Err(e) = (format.extractor_func)(&in_ctx, ctx) {
|
||||
warn!(" inner extraction ({}) failed: {}", format.name, e);
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
warn!(" inner detection ({}) error: {}", format.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Derive the NAND geometry purely from the image size. The image is an
|
||||
/// exact multiple of the page size (no OOB interleaved), which lets us pick
|
||||
/// a sensible page/block layout even when the CIS body is not fully decoded.
|
||||
fn derive_geometry(total: u64) -> (u32, u32, u32, u32, u32) {
|
||||
let page: u32 = 2048;
|
||||
let spare: u32 = if total % (page as u64) != 0 { 64 } else { 0 };
|
||||
|
||||
let mut ppb: u32 = 64;
|
||||
for candidate in [64u32, 128, 32, 256, 512] {
|
||||
if (total as u64) % ((page as u64) * (candidate as u64)) == 0 {
|
||||
ppb = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let block = page * ppb;
|
||||
let blocks = (total / (block as u64)) as u32;
|
||||
(page, spare, ppb, block, blocks)
|
||||
}
|
||||
|
||||
/// Returns the offsets of every occurrence of `pattern` within `file`,
|
||||
/// scanning sequentially in buffered chunks. A small overlap (`keep` bytes
|
||||
/// from the previous chunk) is carried so a match spanning a chunk
|
||||
/// boundary is still found; the absolute offset is reconstructed from the
|
||||
/// known file position of the overlap (`hay_base`).
|
||||
fn scan_offsets(
|
||||
file: &mut File,
|
||||
pattern: &[u8],
|
||||
) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
|
||||
if pattern.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
|
||||
let mut offsets = Vec::new();
|
||||
let mut buf = vec![0u8; 1 << 20];
|
||||
let mut tail: Vec<u8> = Vec::new();
|
||||
let keep = pattern.len() - 1;
|
||||
let mut base: usize = 0; // file offset of the current chunk's buffer region
|
||||
|
||||
loop {
|
||||
let n = file.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let hay_base = base.saturating_sub(keep);
|
||||
let mut hay = tail;
|
||||
hay.extend_from_slice(&buf[..n]);
|
||||
|
||||
let mut start = 0;
|
||||
while let Some(rel) = find_sub(&hay[start..], pattern) {
|
||||
let idx = start + rel;
|
||||
offsets.push(hay_base + idx);
|
||||
start = idx + pattern.len();
|
||||
}
|
||||
|
||||
tail = hay.split_off(hay.len().saturating_sub(keep));
|
||||
base += n;
|
||||
}
|
||||
|
||||
Ok(offsets)
|
||||
}
|
||||
|
||||
/// Naive substring search returning the first relative offset of `needle`.
|
||||
fn find_sub(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||
if needle.len() > haystack.len() {
|
||||
return None;
|
||||
}
|
||||
haystack.windows(needle.len()).position(|w| w == needle)
|
||||
}
|
||||
|
||||
/// Carves `[start, start+len)` of `file` into `<name>.bin` in `output_dir`.
|
||||
fn carve_region(
|
||||
file: &mut File,
|
||||
output_dir: &Path,
|
||||
name: &str,
|
||||
start: usize,
|
||||
len: usize,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if len == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
file.seek(SeekFrom::Start(start as u64))?;
|
||||
let out_path = output_dir.join(format!("{name}.bin"));
|
||||
let mut out = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&out_path)?;
|
||||
let copied = io::copy(&mut file.take(len as u64), &mut out)?;
|
||||
info!(
|
||||
" Carved {} -> {name}.bin ({} bytes, 0x{:X}..0x{:X})",
|
||||
name,
|
||||
copied,
|
||||
start,
|
||||
start as u64 + copied
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_cis_info(
|
||||
output_dir: &Path,
|
||||
ctx: &UnfdCisCtx,
|
||||
boot_markers: &[usize],
|
||||
ubi_start: usize,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = output_dir.join("cis_info.txt");
|
||||
let mut f = File::create(path)?;
|
||||
|
||||
writeln!(f, "MStar UNFD NAND dump - CIS information")?;
|
||||
writeln!(f, "=========================================")?;
|
||||
writeln!(f, "magic: MSTARSEMIUNFDCIS")?;
|
||||
writeln!(f, "cis_signature: 0x{:08X}", ctx.cis_signature)?;
|
||||
writeln!(f, "vendor: {}", ctx.vendor)?;
|
||||
writeln!(f, "part_number: {}", ctx.part_number)?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "Derived NAND geometry:")?;
|
||||
writeln!(f, " page_size: {} bytes", ctx.page_size)?;
|
||||
writeln!(f, " spare_size: {} bytes", ctx.spare_size)?;
|
||||
writeln!(f, " pages_per_block: {}", ctx.pages_per_block)?;
|
||||
writeln!(f, " block_size: {} bytes", ctx.block_size)?;
|
||||
writeln!(f, " block_count: {}", ctx.block_count)?;
|
||||
writeln!(f, " total_size: {} bytes", ctx.total_size)?;
|
||||
writeln!(f)?;
|
||||
writeln!(
|
||||
f,
|
||||
"cis_copies: {:?}",
|
||||
ctx.cis_copies
|
||||
.iter()
|
||||
.map(|o| format!("0x{o:X}"))
|
||||
.collect::<Vec<_>>()
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
"bank_markers: {:?}",
|
||||
ctx.bank_markers
|
||||
.iter()
|
||||
.map(|o| format!("0x{o:X}"))
|
||||
.collect::<Vec<_>>()
|
||||
)?;
|
||||
writeln!(f, "ubi_start: 0x{:X}", ubi_start)?;
|
||||
writeln!(f, "data_start: 0x{:X}", ctx.data_start)?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "Carved partitions:")?;
|
||||
for (i, &m) in boot_markers.iter().enumerate() {
|
||||
let end = if i + 1 < boot_markers.len() {
|
||||
boot_markers[i + 1]
|
||||
} else {
|
||||
ubi_start
|
||||
};
|
||||
writeln!(
|
||||
f,
|
||||
" boot_bank{i}: 0x{:X}..0x{:X} ({} bytes)",
|
||||
m,
|
||||
end,
|
||||
end.saturating_sub(m)
|
||||
)?;
|
||||
}
|
||||
if ubi_start < ctx.total_size as usize {
|
||||
writeln!(
|
||||
f,
|
||||
" rootfs_ubi: 0x{:X}..0x{:X} ({} bytes)",
|
||||
ubi_start,
|
||||
ctx.total_size,
|
||||
ctx.total_size as usize - ubi_start
|
||||
)?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
writeln!(
|
||||
f,
|
||||
"The raw NAND payload (after the CIS header) was carved to nand_data.bin"
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
"and any inner firmware formats were extracted to nand_data_extracted/."
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode a NUL-terminated, space-padded ASCII field.
|
||||
fn cstr(b: &[u8]) -> String {
|
||||
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||
String::from_utf8_lossy(&b[..end]).trim().to_string()
|
||||
}
|
||||
@@ -129,6 +129,9 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), B
|
||||
output_dir: output_path,
|
||||
options: app_ctx.options.clone(),
|
||||
dry_run: app_ctx.dry_run,
|
||||
lazy_run: app_ctx.lazy_run,
|
||||
build_prop: app_ctx.build_prop,
|
||||
dump_keys: app_ctx.dump_keys,
|
||||
quiet: app_ctx.quiet,
|
||||
verbose: app_ctx.verbose,
|
||||
};
|
||||
|
||||
@@ -75,7 +75,7 @@ pub fn extract_philips_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<()
|
||||
info!("Checking if it's also MTK BDP...");
|
||||
|
||||
let new_file = File::open(&output_path)?;
|
||||
let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), input_path: Some(output_path.clone()), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, quiet: app_ctx.quiet, verbose: app_ctx.verbose };
|
||||
let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), input_path: Some(output_path.clone()), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, lazy_run: app_ctx.lazy_run, build_prop: app_ctx.build_prop, dump_keys: app_ctx.dump_keys, quiet: app_ctx.quiet, verbose: app_ctx.verbose };
|
||||
|
||||
if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? {
|
||||
info!("- MTK BDP file detected!\n");
|
||||
|
||||
@@ -129,6 +129,9 @@ pub fn extract_sony_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), B
|
||||
output_dir: mtk_extraction_path,
|
||||
options: app_ctx.options.clone(),
|
||||
dry_run: app_ctx.dry_run,
|
||||
lazy_run: app_ctx.lazy_run,
|
||||
build_prop: app_ctx.build_prop,
|
||||
dump_keys: app_ctx.dump_keys,
|
||||
quiet: app_ctx.quiet,
|
||||
verbose: app_ctx.verbose,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
//! UBI / UBIFS NAND rootfs extractor.
|
||||
//!
|
||||
//! Handles raw UBI images as commonly carved out of MStar/Toshiba NAND dumps
|
||||
//! (e.g. `rootfs_ubi.bin` produced by the `mstar_unfd` format). These images
|
||||
//! frequently still carry the NAND spare/OOB bytes interleaved after every
|
||||
//! page, and use a vendor-quirked UBI layout (EC header `data_offset` field
|
||||
//! reads 0). This module:
|
||||
//!
|
||||
//! 1. Detects the `UBI#` erase-counter header at offset 0.
|
||||
//! 2. Auto-detects and strips interleaved OOB (page + spare geometry).
|
||||
//! 3. Parses per-PEB EC/VID headers (big-endian) and rebuilds each logical
|
||||
//! volume by ordering LEBs and keeping the copy with the highest sqnum.
|
||||
//! 4. Reads the UBI volume table (layout volume) to recover volume names.
|
||||
//! 5. Hands each data volume's reconstructed image to the UBIFS walker
|
||||
//! ([`ubifs`]) which rebuilds the file tree.
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
use crate::AppContext;
|
||||
|
||||
pub mod ubifs;
|
||||
|
||||
/// UBI erase-counter header magic ("UBI#").
|
||||
const EC_MAGIC: &[u8; 4] = b"UBI#";
|
||||
/// UBI volume-identifier header magic ("UBI!").
|
||||
const VID_MAGIC: &[u8; 4] = b"UBI!";
|
||||
/// vol_id of the internal layout volume that holds the volume table.
|
||||
const UBI_LAYOUT_VOLUME_ID: u32 = 0x7FFF_EFFF;
|
||||
/// Size of a single volume-table record.
|
||||
const UBI_VTBL_RECORD_SIZE: usize = 172;
|
||||
/// NAND main page size we assume for OOB detection.
|
||||
const NAND_PAGE_SIZE: usize = 2048;
|
||||
|
||||
/// Geometry / layout information for a UBI image.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UbiCtx {
|
||||
/// Physical erase block size as stored in the file (may include OOB).
|
||||
pub phys_peb_size: usize,
|
||||
/// OOB/spare bytes interleaved after each `NAND_PAGE_SIZE` page (0 = none).
|
||||
pub oob_size: usize,
|
||||
/// Clean PEB size after OOB has been removed.
|
||||
pub clean_peb_size: usize,
|
||||
/// Number of physical erase blocks in the image.
|
||||
pub peb_count: usize,
|
||||
}
|
||||
|
||||
/// Detect a UBI image: `UBI#` at offset 0, and derive PEB geometry (including
|
||||
/// any interleaved NAND OOB) from the spacing of consecutive EC headers.
|
||||
pub fn is_ubi_file(
|
||||
app_ctx: &AppContext,
|
||||
) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
|
||||
let file = match app_ctx.file() {
|
||||
Some(f) => f,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let mut head = [0u8; 4];
|
||||
{
|
||||
let mut f = file;
|
||||
f.seek(SeekFrom::Start(0))?;
|
||||
if f.read_exact(&mut head).is_err() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
if &head != EC_MAGIC {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let file_size = file.metadata()?.len() as usize;
|
||||
|
||||
// Find the spacing between the first two EC headers to learn the physical
|
||||
// PEB size (OOB-interleaved or not). Scan a bounded prefix so detection
|
||||
// stays cheap.
|
||||
let scan_len = file_size.min(4 * 1024 * 1024);
|
||||
let prefix = crate::utils::common::read_file(&file, 0, scan_len)?;
|
||||
let phys_peb_size = match second_magic_offset(&prefix, EC_MAGIC) {
|
||||
Some(d) if d > 0 => d,
|
||||
// Only one EC header visible in the prefix — fall back to a common
|
||||
// clean PEB size guess.
|
||||
_ => guess_single_peb(file_size),
|
||||
};
|
||||
|
||||
let oob_size = detect_oob(phys_peb_size);
|
||||
let pages = phys_peb_size / (NAND_PAGE_SIZE + oob_size);
|
||||
let clean_peb_size = pages * NAND_PAGE_SIZE;
|
||||
let peb_count = file_size / phys_peb_size;
|
||||
|
||||
if clean_peb_size == 0 || peb_count == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
info!("- Detected UBI image");
|
||||
info!(
|
||||
" physical PEB: {} bytes, OOB/page: {} bytes, clean PEB: {} bytes, PEBs: {}",
|
||||
phys_peb_size, oob_size, clean_peb_size, peb_count
|
||||
);
|
||||
|
||||
Ok(Some(Box::new(UbiCtx {
|
||||
phys_peb_size,
|
||||
oob_size,
|
||||
clean_peb_size,
|
||||
peb_count,
|
||||
})))
|
||||
}
|
||||
|
||||
/// A reconstructed logical volume: its LEBs concatenated in `lnum` order.
|
||||
struct Volume {
|
||||
/// lnum -> (sqnum, leb data)
|
||||
lebs: HashMap<u32, (u64, Vec<u8>)>,
|
||||
}
|
||||
|
||||
pub fn extract_ubi(
|
||||
app_ctx: &AppContext,
|
||||
ctx: Box<dyn Any>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let ctx = ctx.downcast::<UbiCtx>().map_err(|_| "Invalid UBI context")?;
|
||||
|
||||
let input_path = app_ctx
|
||||
.input_path()
|
||||
.ok_or("UBI extractor requires an input path")?;
|
||||
let mut file = File::open(input_path)?;
|
||||
|
||||
fs::create_dir_all(&app_ctx.output_dir)?;
|
||||
|
||||
// --- Parse every physical erase block ---
|
||||
let mut volumes: HashMap<u32, Volume> = HashMap::new();
|
||||
let mut buf = vec![0u8; ctx.phys_peb_size];
|
||||
|
||||
for peb in 0..ctx.peb_count {
|
||||
file.seek(SeekFrom::Start((peb * ctx.phys_peb_size) as u64))?;
|
||||
if file.read_exact(&mut buf).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let clean = strip_oob(&buf, ctx.oob_size, ctx.clean_peb_size);
|
||||
|
||||
if &clean[0..4] != EC_MAGIC {
|
||||
continue; // erased / non-UBI block
|
||||
}
|
||||
|
||||
// EC header (big-endian). vid_hdr_offset @0x10, data_offset @0x14.
|
||||
let mut vid_hdr_offset = be32(&clean, 0x10) as usize;
|
||||
let mut data_offset = be32(&clean, 0x14) as usize;
|
||||
|
||||
// Vendor quirk: fields may read 0. Fall back to page-aligned defaults.
|
||||
if vid_hdr_offset == 0 || vid_hdr_offset + 64 > ctx.clean_peb_size {
|
||||
vid_hdr_offset = NAND_PAGE_SIZE;
|
||||
}
|
||||
if data_offset == 0 || data_offset >= ctx.clean_peb_size {
|
||||
data_offset = vid_hdr_offset + NAND_PAGE_SIZE;
|
||||
}
|
||||
|
||||
if vid_hdr_offset + 64 > clean.len() || &clean[vid_hdr_offset..vid_hdr_offset + 4] != VID_MAGIC
|
||||
{
|
||||
continue; // no valid VID header -> unmapped PEB
|
||||
}
|
||||
|
||||
// VID header (big-endian). vol_id @0x08, lnum @0x0C, sqnum @0x28.
|
||||
let vol_id = be32(&clean, vid_hdr_offset + 0x08);
|
||||
let lnum = be32(&clean, vid_hdr_offset + 0x0C);
|
||||
let sqnum = be64(&clean, vid_hdr_offset + 0x28);
|
||||
|
||||
if data_offset >= clean.len() {
|
||||
continue;
|
||||
}
|
||||
let leb_data = clean[data_offset..].to_vec();
|
||||
|
||||
let vol = volumes.entry(vol_id).or_insert_with(|| Volume {
|
||||
lebs: HashMap::new(),
|
||||
});
|
||||
// Keep the newest copy (highest sqnum) of each LEB.
|
||||
match vol.lebs.get(&lnum) {
|
||||
Some((prev_sq, _)) if *prev_sq >= sqnum => {}
|
||||
_ => {
|
||||
vol.lebs.insert(lnum, (sqnum, leb_data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if volumes.is_empty() {
|
||||
warn!(" No valid UBI volumes found");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// --- Recover volume names from the layout volume's volume table ---
|
||||
let names = volumes
|
||||
.get(&UBI_LAYOUT_VOLUME_ID)
|
||||
.map(|v| parse_volume_table(v))
|
||||
.unwrap_or_default();
|
||||
|
||||
info!(" Found {} UBI volume(s)", volumes.len());
|
||||
|
||||
// --- Reconstruct and extract each data volume ---
|
||||
let mut vol_ids: Vec<u32> = volumes.keys().cloned().collect();
|
||||
vol_ids.sort_unstable();
|
||||
|
||||
for vol_id in vol_ids {
|
||||
if vol_id == UBI_LAYOUT_VOLUME_ID {
|
||||
continue; // internal layout volume, not a filesystem
|
||||
}
|
||||
let vol = &volumes[&vol_id];
|
||||
|
||||
let name = names
|
||||
.get(&vol_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("vol_{vol_id}"));
|
||||
|
||||
let image = reconstruct_volume(vol);
|
||||
info!(
|
||||
" Volume '{}' (id {}): {} LEBs, {} bytes",
|
||||
name,
|
||||
vol_id,
|
||||
vol.lebs.len(),
|
||||
image.len()
|
||||
);
|
||||
|
||||
// Always dump the raw reconstructed volume image alongside the tree.
|
||||
let img_path = app_ctx.output_dir.join(format!("{name}.ubifs"));
|
||||
if let Err(e) = fs::write(&img_path, &image) {
|
||||
warn!(" Could not write {}: {}", img_path.display(), e);
|
||||
}
|
||||
|
||||
// Walk the UBIFS filesystem and rebuild files.
|
||||
let out_dir = app_ctx.output_dir.join(&name);
|
||||
match ubifs::extract_ubifs(&image, &out_dir) {
|
||||
Ok(n) => info!(" Extracted {} file(s) from '{}'", n, name),
|
||||
Err(e) => warn!(" UBIFS extraction of '{}' failed: {}", name, e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Concatenate a volume's LEBs in ascending `lnum` order.
|
||||
fn reconstruct_volume(vol: &Volume) -> Vec<u8> {
|
||||
let mut lnums: Vec<u32> = vol.lebs.keys().cloned().collect();
|
||||
lnums.sort_unstable();
|
||||
let mut out = Vec::new();
|
||||
for lnum in lnums {
|
||||
out.extend_from_slice(&vol.lebs[&lnum].1);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Parse the UBI volume table (stored in the layout volume) into a
|
||||
/// `vol_id -> name` map. Each record is `UBI_VTBL_RECORD_SIZE` bytes; the
|
||||
/// record index equals the volume id.
|
||||
fn parse_volume_table(layout: &Volume) -> HashMap<u32, String> {
|
||||
let mut names = HashMap::new();
|
||||
// The layout volume mirrors the table across its LEBs; LEB 0 is enough.
|
||||
let Some((_, data)) = layout.lebs.get(&0).or_else(|| layout.lebs.values().next()) else {
|
||||
return names;
|
||||
};
|
||||
|
||||
let count = data.len() / UBI_VTBL_RECORD_SIZE;
|
||||
for idx in 0..count {
|
||||
let base = idx * UBI_VTBL_RECORD_SIZE;
|
||||
let rec = &data[base..base + UBI_VTBL_RECORD_SIZE];
|
||||
let reserved_pebs = be32(rec, 0x00);
|
||||
if reserved_pebs == 0 {
|
||||
continue; // unused record
|
||||
}
|
||||
let name_len = be16(rec, 0x0E) as usize;
|
||||
if name_len == 0 || name_len > 128 {
|
||||
continue;
|
||||
}
|
||||
let name = String::from_utf8_lossy(&rec[0x10..0x10 + name_len]).to_string();
|
||||
if !name.is_empty() {
|
||||
names.insert(idx as u32, sanitize_name(&name));
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
/// Remove path separators / control characters from a volume name so it is
|
||||
/// safe to use as a directory name.
|
||||
fn sanitize_name(name: &str) -> String {
|
||||
name.chars()
|
||||
.map(|c| {
|
||||
if c.is_control() || c == '/' || c == '\\' || c == ':' {
|
||||
'_'
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Remove interleaved OOB from a physical PEB, returning the clean main data.
|
||||
fn strip_oob(peb: &[u8], oob_size: usize, clean_peb_size: usize) -> Vec<u8> {
|
||||
if oob_size == 0 {
|
||||
return peb[..clean_peb_size.min(peb.len())].to_vec();
|
||||
}
|
||||
let step = NAND_PAGE_SIZE + oob_size;
|
||||
let mut out = Vec::with_capacity(clean_peb_size);
|
||||
let mut pos = 0;
|
||||
while pos + NAND_PAGE_SIZE <= peb.len() && out.len() < clean_peb_size {
|
||||
out.extend_from_slice(&peb[pos..pos + NAND_PAGE_SIZE]);
|
||||
pos += step;
|
||||
}
|
||||
out.truncate(clean_peb_size);
|
||||
out
|
||||
}
|
||||
|
||||
/// Offset of the second occurrence of `magic` (i.e. the distance from the
|
||||
/// first), searched only on page-aligned boundaries for speed.
|
||||
fn second_magic_offset(data: &[u8], magic: &[u8; 4]) -> Option<usize> {
|
||||
let mut off = NAND_PAGE_SIZE;
|
||||
while off + 4 <= data.len() {
|
||||
if &data[off..off + 4] == magic {
|
||||
return Some(off);
|
||||
}
|
||||
off += NAND_PAGE_SIZE;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Determine the OOB size per page given a physical PEB size. Prefers no OOB;
|
||||
/// otherwise picks the first spare size that divides the PEB into a power-of-two
|
||||
/// page count.
|
||||
fn detect_oob(phys_peb_size: usize) -> usize {
|
||||
for oob in [0usize, 16, 32, 64, 128, 218, 224, 256] {
|
||||
let step = NAND_PAGE_SIZE + oob;
|
||||
if phys_peb_size % step != 0 {
|
||||
continue;
|
||||
}
|
||||
let pages = phys_peb_size / step;
|
||||
if pages.is_power_of_two() && (16..=4096).contains(&pages) {
|
||||
return oob;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// Fallback PEB size when only one EC header is present.
|
||||
fn guess_single_peb(file_size: usize) -> usize {
|
||||
for peb in [131072usize, 262144, 126976, 524288, 65536] {
|
||||
if file_size % peb == 0 {
|
||||
return peb;
|
||||
}
|
||||
}
|
||||
131072
|
||||
}
|
||||
|
||||
// --- endian helpers ---------------------------------------------------------
|
||||
|
||||
fn be32(b: &[u8], off: usize) -> u32 {
|
||||
u32::from_be_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
|
||||
}
|
||||
|
||||
fn be16(b: &[u8], off: usize) -> u16 {
|
||||
u16::from_be_bytes([b[off], b[off + 1]])
|
||||
}
|
||||
|
||||
fn be64(b: &[u8], off: usize) -> u64 {
|
||||
u64::from_be_bytes([
|
||||
b[off], b[off + 1], b[off + 2], b[off + 3], b[off + 4], b[off + 5], b[off + 6], b[off + 7],
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
//! Minimal UBIFS reader.
|
||||
//!
|
||||
//! Rather than walking the on-flash B-tree index (which vendor images often
|
||||
//! corrupt or lay out non-standardly), this performs a tolerant *linear* scan
|
||||
//! of every UBIFS node in a reconstructed volume image. It collects inode,
|
||||
//! directory-entry and data nodes, validates each with its CRC-32, then
|
||||
//! rebuilds the directory tree and writes out regular files, directories and
|
||||
//! symlinks.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Seek, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use flate2::Crc;
|
||||
|
||||
/// UBIFS common node header magic (little-endian on disk: 31 18 10 06).
|
||||
const UBIFS_NODE_MAGIC: u32 = 0x0610_1831;
|
||||
|
||||
// Node types.
|
||||
const INO_NODE: u8 = 0;
|
||||
const DATA_NODE: u8 = 1;
|
||||
const DENT_NODE: u8 = 2;
|
||||
|
||||
// Header / node sizes.
|
||||
const CH_SZ: usize = 24; // common header
|
||||
const DATA_NODE_SZ: usize = 48; // header + key + size + compr fields
|
||||
const KEY_OFF: usize = 24;
|
||||
|
||||
// UBIFS logical data block size (uncompressed).
|
||||
const UBIFS_BLOCK_SIZE: u64 = 4096;
|
||||
|
||||
// Compression types.
|
||||
const COMPR_NONE: u16 = 0;
|
||||
const COMPR_LZO: u16 = 1;
|
||||
const COMPR_ZLIB: u16 = 2;
|
||||
const COMPR_ZSTD: u16 = 3;
|
||||
|
||||
// Inode item types stored in directory entries.
|
||||
const ITYPE_REG: u8 = 0;
|
||||
const ITYPE_DIR: u8 = 1;
|
||||
const ITYPE_LNK: u8 = 2;
|
||||
|
||||
/// Root inode number in UBIFS.
|
||||
const ROOT_INO: u32 = 1;
|
||||
|
||||
struct Inode {
|
||||
size: u64,
|
||||
/// Inline data (symlink target for links).
|
||||
inline: Vec<u8>,
|
||||
}
|
||||
|
||||
struct Dent {
|
||||
parent: u32,
|
||||
target: u32,
|
||||
itype: u8,
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// Extract a reconstructed UBIFS volume image into `out_dir`. Returns the
|
||||
/// number of filesystem objects created.
|
||||
pub fn extract_ubifs(image: &[u8], out_dir: &Path) -> Result<usize, Box<dyn std::error::Error>> {
|
||||
let mut inodes: HashMap<u32, Inode> = HashMap::new();
|
||||
let mut dents: Vec<Dent> = Vec::new();
|
||||
// inode -> block_no -> decompressed bytes
|
||||
let mut data: HashMap<u32, HashMap<u64, Vec<u8>>> = HashMap::new();
|
||||
|
||||
scan_nodes(image, &mut inodes, &mut dents, &mut data);
|
||||
|
||||
if inodes.is_empty() && dents.is_empty() {
|
||||
return Err("no UBIFS nodes found".into());
|
||||
}
|
||||
|
||||
// Build parent -> children map.
|
||||
let mut children: HashMap<u32, Vec<&Dent>> = HashMap::new();
|
||||
for d in &dents {
|
||||
children.entry(d.parent).or_default().push(d);
|
||||
}
|
||||
|
||||
fs::create_dir_all(out_dir)?;
|
||||
|
||||
let mut count = 0usize;
|
||||
let mut stack: Vec<(u32, PathBuf)> = vec![(ROOT_INO, out_dir.to_path_buf())];
|
||||
let mut visited: HashMap<u32, ()> = HashMap::new();
|
||||
|
||||
while let Some((ino, dir_path)) = stack.pop() {
|
||||
if visited.insert(ino, ()).is_some() {
|
||||
continue; // guard against cycles
|
||||
}
|
||||
let Some(kids) = children.get(&ino) else {
|
||||
continue;
|
||||
};
|
||||
for d in kids {
|
||||
let child_path = dir_path.join(sanitize_component(&d.name));
|
||||
match d.itype {
|
||||
ITYPE_DIR => {
|
||||
if fs::create_dir_all(&child_path).is_ok() {
|
||||
count += 1;
|
||||
stack.push((d.target, child_path));
|
||||
}
|
||||
}
|
||||
ITYPE_REG => {
|
||||
if write_regular_file(&child_path, d.target, &inodes, &data) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
ITYPE_LNK => {
|
||||
if let Some(ino) = inodes.get(&d.target) {
|
||||
let target = String::from_utf8_lossy(&ino.inline).to_string();
|
||||
// Portable: record symlinks as text files (Windows-safe).
|
||||
if fs::write(&child_path, format!("SYMLINK -> {target}")).is_ok() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Special files (dev/fifo/sock): note as empty placeholder.
|
||||
let _ = File::create(&child_path);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Assemble and write a regular file from its data blocks, truncated to the
|
||||
/// inode size.
|
||||
fn write_regular_file(
|
||||
path: &Path,
|
||||
ino: u32,
|
||||
inodes: &HashMap<u32, Inode>,
|
||||
data: &HashMap<u32, HashMap<u64, Vec<u8>>>,
|
||||
) -> bool {
|
||||
let size = inodes.get(&ino).map(|i| i.size).unwrap_or(0);
|
||||
|
||||
let mut out = match File::create(path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
if let Some(blocks) = data.get(&ino) {
|
||||
let mut block_nos: Vec<u64> = blocks.keys().cloned().collect();
|
||||
block_nos.sort_unstable();
|
||||
for bn in block_nos {
|
||||
let expected_off = bn * UBIFS_BLOCK_SIZE;
|
||||
// Pad holes with zeros to keep byte offsets correct.
|
||||
let cur = out.stream_position().unwrap_or(0);
|
||||
if expected_off > cur {
|
||||
let hole = (expected_off - cur) as usize;
|
||||
let _ = out.write_all(&vec![0u8; hole]);
|
||||
}
|
||||
let _ = out.write_all(&blocks[&bn]);
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate to the exact inode size when known.
|
||||
if size > 0 {
|
||||
let _ = out.flush();
|
||||
if let Ok(f) = File::options().write(true).open(path) {
|
||||
let _ = f.set_len(size);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Linear scan of the volume image collecting UBIFS nodes, validated by CRC-32.
|
||||
fn scan_nodes(
|
||||
image: &[u8],
|
||||
inodes: &mut HashMap<u32, Inode>,
|
||||
dents: &mut Vec<Dent>,
|
||||
data: &mut HashMap<u32, HashMap<u64, Vec<u8>>>,
|
||||
) {
|
||||
let mut off = 0usize;
|
||||
let n = image.len();
|
||||
|
||||
while off + CH_SZ <= n {
|
||||
// UBIFS nodes are 8-byte aligned.
|
||||
if le32(image, off) != UBIFS_NODE_MAGIC {
|
||||
off += 8;
|
||||
continue;
|
||||
}
|
||||
|
||||
let crc = le32(image, off + 4);
|
||||
let len = le32(image, off + 8) as usize;
|
||||
let node_type = image[off + 12];
|
||||
|
||||
if len < CH_SZ || off + len > n {
|
||||
off += 8;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate CRC-32 over everything after the crc field.
|
||||
let mut c = Crc::new();
|
||||
c.update(&image[off + 8..off + len]);
|
||||
if c.sum() != crc {
|
||||
off += 8;
|
||||
continue;
|
||||
}
|
||||
|
||||
let node = &image[off..off + len];
|
||||
match node_type {
|
||||
INO_NODE => parse_ino(node, inodes),
|
||||
DENT_NODE => parse_dent(node, dents),
|
||||
DATA_NODE => parse_data(node, data),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Advance to the next 8-byte-aligned node.
|
||||
off += (len + 7) & !7;
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ino(node: &[u8], inodes: &mut HashMap<u32, Inode>) {
|
||||
if node.len() < 160 {
|
||||
return;
|
||||
}
|
||||
let ino = le32(node, KEY_OFF); // key[0] = inode number
|
||||
let size = le64(node, 48);
|
||||
let mode = le32(node, 104);
|
||||
let data_len = le32(node, 112) as usize;
|
||||
|
||||
let itype = mode_to_itype(mode);
|
||||
let mut inline = Vec::new();
|
||||
if itype == ITYPE_LNK && data_len > 0 && 160 + data_len <= node.len() {
|
||||
inline = node[160..160 + data_len].to_vec();
|
||||
}
|
||||
|
||||
inodes.insert(ino, Inode { size, inline });
|
||||
}
|
||||
|
||||
fn parse_dent(node: &[u8], dents: &mut Vec<Dent>) {
|
||||
if node.len() < 56 {
|
||||
return;
|
||||
}
|
||||
let parent = le32(node, KEY_OFF); // key[0] = parent inode
|
||||
let target = le32(node, 40); // low 32 bits of __le64 inum
|
||||
let itype = node[49];
|
||||
let nlen = le16(node, 50) as usize;
|
||||
if 56 + nlen > node.len() {
|
||||
return;
|
||||
}
|
||||
let name = String::from_utf8_lossy(&node[56..56 + nlen]).to_string();
|
||||
if name.is_empty() {
|
||||
return;
|
||||
}
|
||||
dents.push(Dent {
|
||||
parent,
|
||||
target,
|
||||
itype,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_data(node: &[u8], data: &mut HashMap<u32, HashMap<u64, Vec<u8>>>) {
|
||||
if node.len() < DATA_NODE_SZ {
|
||||
return;
|
||||
}
|
||||
let ino = le32(node, KEY_OFF); // key[0] = inode number
|
||||
let block_no = (le32(node, KEY_OFF + 4) & 0x1FFF_FFFF) as u64; // key[1] low bits
|
||||
let out_len = le32(node, 40) as usize;
|
||||
let compr = le16(node, 44);
|
||||
let payload = &node[DATA_NODE_SZ..];
|
||||
|
||||
let decompressed = match decompress_block(payload, compr, out_len) {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
|
||||
data.entry(ino).or_default().insert(block_no, decompressed);
|
||||
}
|
||||
|
||||
/// Decompress a single UBIFS data block payload.
|
||||
fn decompress_block(payload: &[u8], compr: u16, out_len: usize) -> Option<Vec<u8>> {
|
||||
match compr {
|
||||
COMPR_NONE => {
|
||||
let take = out_len.min(payload.len());
|
||||
Some(payload[..take].to_vec())
|
||||
}
|
||||
COMPR_LZO => match minilzo_rs::LZO::init() {
|
||||
Ok(lzo) => lzo.decompress(payload, out_len).ok(),
|
||||
Err(_) => None,
|
||||
},
|
||||
COMPR_ZLIB => inflate_raw(payload, out_len),
|
||||
COMPR_ZSTD => crate::utils::compression::decompress_zstd(payload).ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw DEFLATE inflate (UBIFS uses headerless zlib streams).
|
||||
fn inflate_raw(payload: &[u8], out_len: usize) -> Option<Vec<u8>> {
|
||||
use flate2::Decompress;
|
||||
use flate2::FlushDecompress;
|
||||
|
||||
let mut d = Decompress::new(false); // false = raw deflate (no zlib header)
|
||||
let mut out = Vec::with_capacity(out_len.max(payload.len()));
|
||||
match d.decompress_vec(payload, &mut out, FlushDecompress::Finish) {
|
||||
Ok(_) => Some(out),
|
||||
Err(_) => {
|
||||
// Retry assuming a zlib header just in case.
|
||||
crate::utils::compression::decompress_zlib(payload).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_to_itype(mode: u32) -> u8 {
|
||||
match mode & 0o170000 {
|
||||
0o040000 => ITYPE_DIR,
|
||||
0o120000 => ITYPE_LNK,
|
||||
0o100000 => ITYPE_REG,
|
||||
_ => 0xFF,
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a single path component safe on the host filesystem.
|
||||
fn sanitize_component(name: &str) -> String {
|
||||
let cleaned: String = name
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_control() || matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') {
|
||||
'_'
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
match cleaned.trim() {
|
||||
"" | "." | ".." => "_".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- little-endian helpers --------------------------------------------------
|
||||
|
||||
fn le16(b: &[u8], off: usize) -> u16 {
|
||||
u16::from_le_bytes([b[off], b[off + 1]])
|
||||
}
|
||||
|
||||
fn le32(b: &[u8], off: usize) -> u32 {
|
||||
u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
|
||||
}
|
||||
|
||||
fn le64(b: &[u8], off: usize) -> u64 {
|
||||
u64::from_le_bytes([
|
||||
b[off], b[off + 1], b[off + 2], b[off + 3], b[off + 4], b[off + 5], b[off + 6], b[off + 7],
|
||||
])
|
||||
}
|
||||
+108
-76
@@ -14,37 +14,66 @@ use crate::error::UnixtractError;
|
||||
#[command(
|
||||
name = "unixtract",
|
||||
version,
|
||||
about = "Firmware extractor for various file formats",
|
||||
long_about = "unixtract analyzes and extracts firmware package formats commonly found \
|
||||
in TVs, Blu-Ray players, and AV devices. It supports 35+ different formats \
|
||||
with built-in decryption and decompression capabilities."
|
||||
about = "Firmware package analyzer and extractor",
|
||||
long_about = "unixtract analyzes and extracts firmware package formats commonly found in\n\
|
||||
TVs, Blu-ray players, and AV devices. It supports 35+ formats with built-in\n\
|
||||
decryption and decompression capabilities.",
|
||||
after_help = "\
|
||||
EXAMPLES:\n\
|
||||
\n\
|
||||
# Single file analysis\n\
|
||||
unixtract --file-input firmware.bin\n\
|
||||
unixtract --file-input firmware.bin --output output/\n\
|
||||
unixtract --file-input firmware.bin --lazy-run\n\
|
||||
unixtract --file-input firmware.bin --build-prop\n\
|
||||
unixtract --file-input firmware.bin --dump-keys\n\
|
||||
unixtract --file-input firmware.bin --verbose\n\
|
||||
\n\
|
||||
# Bulk / directory processing\n\
|
||||
unixtract --dir-input firmware_older/\n\
|
||||
unixtract --dir-input firmware_older/ --output bulk_output/\n\
|
||||
unixtract --dir-input firmware_older/ --lazy-run\n\
|
||||
unixtract --dir-input firmware_older/ --build-prop\n\
|
||||
unixtract --dir-input firmware_older/ --dump-keys\n\
|
||||
unixtract --dir-input firmware_older/ --output bulk_output/ --verbose\n\
|
||||
\n\
|
||||
# Combined usage\n\
|
||||
unixtract --file-input firmware.bin --lazy-run --build-prop --verbose\n\
|
||||
unixtract --dir-input firmware_older/ --output extracted_fw/ --verbose --build-prop\n\
|
||||
unixtract --file-input firmware.bin --dump-keys\n\
|
||||
unixtract --file-input firmware.bin --lazy-run --dump-keys"
|
||||
)]
|
||||
struct Args {
|
||||
/// The target file or directory to analyze/extract
|
||||
#[arg(required_unless_present = "list_formats")]
|
||||
input_target: Option<String>,
|
||||
/// Single firmware binary input
|
||||
#[arg(long = "file-input")]
|
||||
file_input: Option<String>,
|
||||
|
||||
/// Folder to save extracted files to (default: _<INPUT_TARGET>)
|
||||
output_directory: Option<String>,
|
||||
/// Directory containing multiple firmware binaries for bulk processing
|
||||
#[arg(long = "dir-input")]
|
||||
dir_input: Option<String>,
|
||||
|
||||
/// Format-specific or global options (can be used multiple times)
|
||||
#[arg(short, long)]
|
||||
options: Vec<String>,
|
||||
/// Output path for extracted data (default: _<INPUT>)
|
||||
#[arg(long = "output")]
|
||||
output: Option<String>,
|
||||
|
||||
/// Enable lazy-run mode — minimal processing for faster analysis
|
||||
#[arg(long = "lazy-run")]
|
||||
lazy_run: bool,
|
||||
|
||||
/// Extract and display firmware build properties and metadata
|
||||
#[arg(long = "build-prop")]
|
||||
build_prop: bool,
|
||||
|
||||
/// Dump built-in decryption keys
|
||||
#[arg(long = "dump-keys")]
|
||||
dump_keys: bool,
|
||||
|
||||
/// List supported formats and exit
|
||||
#[arg(long)]
|
||||
#[arg(long = "list-formats")]
|
||||
list_formats: bool,
|
||||
|
||||
/// Only detect the format, do not extract
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
|
||||
/// Quiet mode — suppress non-error output
|
||||
#[arg(short, long)]
|
||||
quiet: bool,
|
||||
|
||||
/// Verbose mode — show detailed progress information (use -vv for trace)
|
||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||
/// Increase verbosity level (repeat for more)
|
||||
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
|
||||
verbose: u8,
|
||||
}
|
||||
|
||||
@@ -59,6 +88,9 @@ pub struct AppContext {
|
||||
pub output_dir: PathBuf,
|
||||
pub options: Vec<String>,
|
||||
pub dry_run: bool,
|
||||
pub lazy_run: bool,
|
||||
pub build_prop: bool,
|
||||
pub dump_keys: bool,
|
||||
pub quiet: bool,
|
||||
pub verbose: u8,
|
||||
}
|
||||
@@ -96,25 +128,33 @@ fn clear_terminal() {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
|
||||
// Clear the terminal on execution
|
||||
if !args.quiet {
|
||||
clear_terminal();
|
||||
// --- Handle --list-formats (doesn't require input) ---
|
||||
if args.list_formats {
|
||||
let formats = get_registry();
|
||||
eprintln!("Supported formats ({} total):", formats.len());
|
||||
for (i, fmt) in formats.iter().enumerate() {
|
||||
eprintln!(" {:2}. {}", i + 1, fmt.name);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Initialize logger based on verbosity level
|
||||
// Default (no flags): show info-level output (normal progress)
|
||||
// -q: errors only
|
||||
// -v: debug level (more details)
|
||||
// -vv: trace level (everything)
|
||||
// -vvv: trace level (everything)
|
||||
let log_level = if args.quiet {
|
||||
"error"
|
||||
} else {
|
||||
match args.verbose {
|
||||
0 => "info",
|
||||
1 => "debug",
|
||||
_ => "trace",
|
||||
// --- Validate mutually exclusive input modes ---
|
||||
let (target_path, is_file_mode) = match (&args.file_input, &args.dir_input) {
|
||||
(Some(f), None) => (PathBuf::from(f), true),
|
||||
(None, Some(d)) => (PathBuf::from(d), false),
|
||||
(Some(_), Some(_)) => {
|
||||
return Err("Cannot specify both --file-input and --dir-input at the same time".into());
|
||||
}
|
||||
(None, None) => {
|
||||
return Err("Either --file-input or --dir-input must be provided".into());
|
||||
}
|
||||
};
|
||||
|
||||
// --- Logger initialization ---
|
||||
let log_level = match args.verbose {
|
||||
0 => "info",
|
||||
1 => "debug",
|
||||
_ => "trace",
|
||||
};
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level))
|
||||
@@ -132,34 +172,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
})
|
||||
.init();
|
||||
|
||||
// Handle --list-formats
|
||||
if args.list_formats {
|
||||
let formats = get_registry();
|
||||
eprintln!("Supported formats ({} total):", formats.len());
|
||||
for (i, fmt) in formats.iter().enumerate() {
|
||||
eprintln!(" {:2}. {}", i + 1, fmt.name);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
clear_terminal();
|
||||
|
||||
log::info!("unixtract v{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let target_path_str = args.input_target.as_deref().unwrap_or("");
|
||||
let target_path = PathBuf::from(target_path_str);
|
||||
|
||||
let output_path_str = if let Some(ref out) = args.output_directory {
|
||||
// --- Output directory ---
|
||||
let output_path_str = if let Some(ref out) = args.output {
|
||||
out.clone()
|
||||
} else {
|
||||
format!("_{}", target_path.file_name()
|
||||
let fname = target_path.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.ok_or_else(|| UnixtractError::Other("Invalid input file name".to_string()))?)
|
||||
.ok_or_else(|| UnixtractError::Other("Invalid input file name".to_string()))?;
|
||||
format!("_{}", fname)
|
||||
};
|
||||
let output_directory_path = PathBuf::from(&output_path_str);
|
||||
|
||||
if output_directory_path.exists() {
|
||||
if output_directory_path.is_dir() {
|
||||
let is_empty = fs::read_dir(&output_directory_path)?.next().is_none();
|
||||
if !is_empty && !args.quiet {
|
||||
if !is_empty {
|
||||
log::warn!("Output folder already exists and is NOT empty! Files may be overwritten.");
|
||||
eprintln!("Press Enter if you want to continue...");
|
||||
io::stdin().read_line(&mut String::new())?;
|
||||
@@ -167,33 +198,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
// === DEFAULT MODE (original behavior) ===
|
||||
let app_ctx;
|
||||
|
||||
if target_path.is_file() {
|
||||
// --- Build AppContext ---
|
||||
let app_ctx = if is_file_mode {
|
||||
let file = File::open(&target_path)?;
|
||||
app_ctx = AppContext {
|
||||
AppContext {
|
||||
input: InputTarget::File(file),
|
||||
input_path: Some(target_path.clone()),
|
||||
output_dir: output_directory_path,
|
||||
options: args.options,
|
||||
dry_run: args.dry_run,
|
||||
quiet: args.quiet,
|
||||
options: Vec::new(),
|
||||
dry_run: args.lazy_run,
|
||||
lazy_run: args.lazy_run,
|
||||
build_prop: args.build_prop,
|
||||
dump_keys: args.dump_keys,
|
||||
quiet: args.verbose == 0,
|
||||
verbose: args.verbose,
|
||||
};
|
||||
} else if target_path.is_dir() {
|
||||
app_ctx = AppContext {
|
||||
}
|
||||
} else {
|
||||
AppContext {
|
||||
input: InputTarget::Directory(target_path.clone()),
|
||||
input_path: Some(target_path.clone()),
|
||||
output_dir: output_directory_path,
|
||||
options: args.options,
|
||||
dry_run: args.dry_run,
|
||||
quiet: args.quiet,
|
||||
options: Vec::new(),
|
||||
dry_run: args.lazy_run,
|
||||
lazy_run: args.lazy_run,
|
||||
build_prop: args.build_prop,
|
||||
dump_keys: args.dump_keys,
|
||||
quiet: args.verbose == 0,
|
||||
verbose: args.verbose,
|
||||
};
|
||||
} else {
|
||||
return Err("Invalid input path!".into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let formats: Vec<Format> = get_registry();
|
||||
|
||||
@@ -201,8 +234,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(ctx) = (format.detector_func)(&app_ctx)? {
|
||||
log::info!("\n{} detected!", format.name);
|
||||
|
||||
if app_ctx.dry_run {
|
||||
log::info!("Dry run — skipping extraction.");
|
||||
if app_ctx.lazy_run {
|
||||
log::info!("Lazy-run — skipping extraction.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -213,7 +246,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
(format.extractor_func)(&app_ctx, ctx)?;
|
||||
|
||||
// Extractor returned with no error
|
||||
log::info!("\nExtraction finished! Saved extracted files to {}", output_path_str);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+3070
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user