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/
|
dist/
|
||||||
firmware/
|
firmware/
|
||||||
device/
|
device/
|
||||||
dist/
|
dist/
|
||||||
|
3rd_party/
|
||||||
|
.vscode/
|
||||||
|
*.exe
|
||||||
@@ -1,232 +1,183 @@
|
|||||||
# unixtract
|
# 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
|
> A fast, dependency-free firmware extractor for TVs, Blu-ray players, set-top boxes, and other AV devices.
|
||||||
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).
|
|
||||||
|
|
||||||
# Installation
|
`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**.
|
||||||
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`.
|
|
||||||
|
|
||||||
# Usage
|
This project is a fork of [theubusu/unixtract](https://github.com/theubusu/unixtract), adding extra formats, features, and fixes.
|
||||||
`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.
|
|
||||||
|
|
||||||
## Global options
|
> [!NOTE]
|
||||||
`dump_dec_hdrs` - For formats with an encrypted header - dump the decrypted header(s).
|
> This project is under active development — errors may occur. Bug reports and feature requests are welcome via the issue tracker.
|
||||||
|
|
||||||
# Supported formats
|
> [!IMPORTANT]
|
||||||
## Amlogic burning image
|
> `unixtract` is an **extraction** tool only. It is **not**, and will never be, designed to re-pack extracted files.
|
||||||
**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
|
|
||||||
|
|
||||||
## 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
|
## Table of Contents
|
||||||
**Used in:** Enterprise HP Printers
|
|
||||||
**Notes:** None, all files should be supported
|
|
||||||
|
|
||||||
## CD5
|
- [Features](#features)
|
||||||
**Used in:** Some Samsung TV tuners, and possibly other Irdeto(?)-based tuners
|
- [Installation](#installation)
|
||||||
**Notes:** Decryption is not supported.
|
- [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
|
## Features
|
||||||
**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
|
|
||||||
|
|
||||||
## EPK v3
|
- **35+ firmware formats** across major TV/AV silicon vendors (MStar, MediaTek, Novatek, Amlogic, Broadcom…).
|
||||||
**Used in:** LG webOS-based TVs
|
- **Built-in decryption** (AES, DES, ECB/CBC, RSA) and **decompression** (LZO, LZ4, LZMA/XZ, zlib/gzip, bzip2, zstd, LZHS, sparse).
|
||||||
**Notes:** **Depends on keys** - see keys.rs
|
- **Recursive extraction** — container formats automatically unpack their inner payloads.
|
||||||
**Thanks to:** https://github.com/openlgtv/epk2extract
|
- **NAND-aware** — handles raw NAND dumps, OOB/spare stripping, and UBI/UBIFS rootfs images.
|
||||||
**Options:**
|
- **Single static binary** — no interpreters, no system libraries.
|
||||||
※ Support `dump_dec_hdrs` option
|
- **Bulk mode** — process an entire directory of firmware in one run.
|
||||||
|
|
||||||
## Funai BDP
|
## Installation
|
||||||
**Used in:** Funai & Funai-made Philips Blu-Ray player/HTS (USA market)
|
|
||||||
**Notes:** N/A
|
|
||||||
|
|
||||||
## Funai MStar
|
### Prebuilt binaries
|
||||||
**Used in:** MStar-based Funai & Funai-made Philips TVs (USA market)
|
|
||||||
**Notes:** Inner SoC part is extracted with mstar_secure_old
|
|
||||||
|
|
||||||
## Funai UPG
|
Download the latest automated build for Windows and Linux x86-64 from the [nightly builds](https://nightly.link/Ap0dexMe0/unixtract/workflows/rust/main).
|
||||||
**Used in:** Some Funai TVs
|
|
||||||
**Notes:** **Depends on keys** - see keys.rs.
|
|
||||||
|
|
||||||
## Funai UPG PHL
|
### From source
|
||||||
**Used in:** Funai & Funai-made Philips TVs (USA market)
|
|
||||||
**Notes:** **Depends on keys** - see keys.rs (most common keys should be included).
|
|
||||||
|
|
||||||
## GX DVB
|
```sh
|
||||||
**Used in:** Cheap NationalChip GX-based DVB tuners
|
git clone https://github.com/Ap0dexMe0/unixtract
|
||||||
**Notes:** None, all files should be supported
|
cd unixtract
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
## INVINCIBLE_IMAGE
|
The resulting binary is written to `target/release/unixtract`.
|
||||||
**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.
|
|
||||||
|
|
||||||
## MSD 1.0
|
## Usage
|
||||||
**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
|
|
||||||
|
|
||||||
## MSD 1.1
|
```sh
|
||||||
**Used in:** Samsung TVs 2016+
|
unixtract [OPTIONS]
|
||||||
**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
|
|
||||||
|
|
||||||
## MStar upgrade bin
|
### Input modes (mutually exclusive)
|
||||||
**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)
|
|
||||||
|
|
||||||
## MStar upgrade bin (Secure, old)
|
| Flag | Description |
|
||||||
**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.
|
| `--file-input <FILE>` | Extract a single firmware binary |
|
||||||
**Options:**
|
| `--dir-input <DIR>` | Bulk-process every firmware binary in a directory |
|
||||||
`mstar_secure_old:keep_decrypted` - Keep decrypted file (it will be deleted by default).
|
|
||||||
|
|
||||||
## MediaTek BDP
|
### Options
|
||||||
**Used in:** Many MediaTek-based Blu-Ray players (LG, Samsung, Philips, Panasonic...)
|
|
||||||
**Notes:** Some older files may fail to extract
|
|
||||||
|
|
||||||
## MediaTek PKG (New)
|
| Flag | Description |
|
||||||
**Used in:** Newer MediaTek-based TVs (TCL, Hisense, Sony, Philips, CVT...)
|
| --- | --- |
|
||||||
**Notes:** **Depends on keys** - see keys.rs (Keys for Philips and Sony included)
|
| `--output <PATH>` | Output path for extracted data (default: `_<INPUT>`) |
|
||||||
**Thanks to:** https://github.com/theubusu/mtkdec
|
| `--lazy-run` | Detect/scan only — skip extraction (fast analysis) |
|
||||||
**Options:**
|
| `--build-prop` | Extract and display firmware build properties and metadata |
|
||||||
`mtk_pkg:no_del_comp` - Don't delete LZHS compressed partition file after decompressing.
|
| `--dump-keys` | Dump the built-in decryption keys |
|
||||||
※ Support `dump_dec_hdrs` option
|
| `--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)
|
### Examples
|
||||||
**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
|
|
||||||
|
|
||||||
## Novatek PKG (NFWB)
|
```sh
|
||||||
**Used in:** Some older Novatek-based TVs (LG, Philips)
|
# Single file
|
||||||
**Notes:** None, all files should be supported.
|
unixtract --file-input firmware.bin
|
||||||
|
unixtract --file-input firmware.bin --output extracted/ --verbose
|
||||||
|
|
||||||
## Novatek TIMG
|
# Bulk directory
|
||||||
**Used in:** Newer Novatek-based TVs (Philips(TPVision), Hisense, TCL...)
|
unixtract --dir-input firmware_dump/ --output out/
|
||||||
**Notes:** None, all files should be supported.
|
|
||||||
|
|
||||||
## Onkyo
|
# Quick analysis without extracting
|
||||||
**Used in:** Onkyo AVRs and other AV devices
|
unixtract --file-input firmware.bin --lazy-run
|
||||||
**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
|
|
||||||
|
|
||||||
## Panasonic Blu-Ray (PANA_DVD.FRM, PANA_ESD.FRM, PANAEUSB.FRM)
|
## Supported Formats
|
||||||
**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
|
|
||||||
|
|
||||||
## Philips UPG (Autorun.upg, 2SWU3TXV)
|
> Entries marked **keys** depend on decryption keys — see [`keys.rs`](src/keys.rs). Most common keys are bundled.
|
||||||
**Used in:** Philips pre-TPVision TVs 200?-2013 and some Sony TVs
|
> Entries marked **hdrs** support the `dump_dec_hdrs` option.
|
||||||
**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!)
|
|
||||||
|
|
||||||
## Philips BDP
|
### TV / SoC firmware
|
||||||
**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
|
|
||||||
|
|
||||||
## PUP
|
| Format | Used in | Notes |
|
||||||
**Used in:** Sony PlayStation 4/5
|
| --- | --- | --- |
|
||||||
**Notes:** File has to be decrypted.
|
| **MStar upgrade bin** | Many MStar-based TVs (Hisense, Toshiba…) | LZOP, LZ4, LZMA, sparse-write support · hdrs |
|
||||||
**Thanks to:** https://github.com/Zer0xFF/ps4-pup-unpacker
|
| **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
|
### Blu-ray / AV device firmware
|
||||||
**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.
|
|
||||||
|
|
||||||
## RUF
|
| Format | Used in | Notes |
|
||||||
**Used in:** Samsung Broadcom-based Blu-Ray players
|
| --- | --- | --- |
|
||||||
**Notes:** **Depends on keys** - see keys.rs
|
| **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
|
### Panasonic TV firmware
|
||||||
**Used in:** Sharp Blu-Ray players/recorders
|
|
||||||
**Notes:** Only the older types of files are supported (XOR-encrypted)
|
|
||||||
|
|
||||||
## Samsung (Folder with ***.img.sec)
|
| Format | Used in | Notes |
|
||||||
**Used in:** Samsung TVs pre 2013
|
| --- | --- | --- |
|
||||||
**Notes:** **Depends on keys** - see keys.rs
|
| **SDDL.SEC** | Panasonic TVs | Based on [sddl_dec](https://github.com/theubusu/sddl_dec) |
|
||||||
**Thanks to:** https://github.com/george-hopkins/samygo-patcher
|
| **SDBoot** | Panasonic TVs (SD boot) | Single known sample — support may vary |
|
||||||
|
| **SDImage** (`SDImage.bin`) | Some 2010 USA Panasonic TVs | Decryption not yet supported |
|
||||||
|
|
||||||
## SDBoot
|
### Generic / other
|
||||||
**Used in:** Panasonic TVs SD boot
|
|
||||||
**Notes:** There is only one known sample, so support may vary.
|
|
||||||
**Base:** https://github.com/theubusu/sddl_dec
|
|
||||||
|
|
||||||
## SDDL.SEC
|
| Format | Used in | Notes |
|
||||||
**Used in:** Panasonic TVs
|
| --- | --- | --- |
|
||||||
**Notes:** None, all files should be supported.
|
| **Android OTA `payload.bin`** | Android devices, phones, TVs | Some compression methods unsupported |
|
||||||
**Options:**
|
| **Raw eMMC user area** | eMMC dumps with `0x5840` partition descriptors | Dumps `<partition>.bin` + `partition_map.txt` |
|
||||||
`sddl_sec:save_extra` - Save SDIT.FDI and .TXT files that are not extracted by default.
|
| **BDL** | Enterprise HP printers | All files supported |
|
||||||
`sddl_sec:split_peaks` - Split PEAKS module into partitions (only on older files). This will also automatically decompress compressed partitions.
|
| **PUP** | Sony PlayStation 4/5 | Requires a decrypted file · thanks to [ps4-pup-unpacker](https://github.com/Zer0xFF/ps4-pup-unpacker) |
|
||||||
`sddl_sec:no_decomp_peaks` - Do not automatically decompress partitions when splitting PEAKS with above option.
|
| **SLP** | Samsung Tizen-based NX cameras | All files supported |
|
||||||
**Base:** https://github.com/theubusu/sddl_dec
|
|
||||||
|
|
||||||
## SDImage (SDImage.bin)
|
## Format Options
|
||||||
**Used in:** Some 2010 USA Panasonic TVs
|
|
||||||
**Notes:** Decryption is not yet supported.
|
|
||||||
|
|
||||||
## SLP
|
Format-specific options are passed by name. Example: `mstar:keep_unknown`.
|
||||||
**Used in:** Samsung Tizen-based NX series cameras
|
|
||||||
**Notes:** None, all files should be supported.
|
|
||||||
|
|
||||||
## Sony BDP
|
| Option | Effect |
|
||||||
**Used in:** Sony MediaTek-based Blu-Ray players
|
| --- | --- |
|
||||||
**Notes:** **Depends on keys** - see keys.rs (Platforms up to MSB29 are supported)
|
| `mstar:keep_unknown` | Save data with unknown destination |
|
||||||
**Thanks to:** http://malcolmstagg.com/bdp/s390-firmware.html
|
| `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
|
## Notes on Keys
|
||||||
**Used in:** Older Toshiba TVs
|
|
||||||
**Notes:** None, all files should be supported.
|
|
||||||
**Options:**
|
|
||||||
※ Support `dump_dec_hdrs` option
|
|
||||||
|
|
||||||
# License
|
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.
|
||||||
Licensed under GNU GPL v3.
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the **GNU General Public License v3.0**.
|
||||||
|
|||||||
@@ -1,35 +1,71 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
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
|
if [ "$#" -gt 0 ]; then
|
||||||
cargo clean
|
TARGETS=("$@")
|
||||||
|
else
|
||||||
# Build Linux
|
TARGETS=("${DEFAULT_TARGETS[@]}")
|
||||||
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"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Windows binary
|
BIN_NAME="unixtract"
|
||||||
WINDOWS_BIN="target/x86_64-pc-windows-gnu/release/unixtract.exe"
|
DIST_DIR="dist"
|
||||||
if [ -f "$WINDOWS_BIN" ]; then
|
|
||||||
cp "$WINDOWS_BIN" "dist/unixtract-windows.exe"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Cleaning target directory..."
|
echo "==> Building ${BIN_NAME} for: ${TARGETS[*]}"
|
||||||
rm -rf target
|
|
||||||
|
|
||||||
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;
|
||||||
|
pub mod mstar_unfd;
|
||||||
|
pub mod ubi;
|
||||||
pub mod android_ota_zip;
|
pub mod android_ota_zip;
|
||||||
pub mod mstar_secure_old;
|
pub mod mstar_secure_old;
|
||||||
pub mod samsung_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,
|
detector_func: crate::formats::android_ota_zip::is_android_ota_zip_file,
|
||||||
extractor_func: crate::formats::android_ota_zip::extract_android_ota_zip,
|
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 {
|
Format {
|
||||||
name: "mstar",
|
name: "mstar",
|
||||||
detector_func: crate::formats::mstar::is_mstar_file,
|
detector_func: crate::formats::mstar::is_mstar_file,
|
||||||
extractor_func: crate::formats::mstar::extract_mstar,
|
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 {
|
Format {
|
||||||
name: "samsung_old",
|
name: "samsung_old",
|
||||||
detector_func: crate::formats::samsung_old::is_samsung_old_dir,
|
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"),
|
output_dir: app_ctx.output_dir.join("SoC"),
|
||||||
options: app_ctx.options.clone(),
|
options: app_ctx.options.clone(),
|
||||||
dry_run: app_ctx.dry_run,
|
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,
|
quiet: app_ctx.quiet,
|
||||||
verbose: app_ctx.verbose,
|
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(),
|
output_dir: app_ctx.output_dir.clone(),
|
||||||
options: app_ctx.options.clone(),
|
options: app_ctx.options.clone(),
|
||||||
dry_run: app_ctx.dry_run,
|
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,
|
quiet: app_ctx.quiet,
|
||||||
verbose: app_ctx.verbose,
|
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,
|
output_dir: output_path,
|
||||||
options: app_ctx.options.clone(),
|
options: app_ctx.options.clone(),
|
||||||
dry_run: app_ctx.dry_run,
|
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,
|
quiet: app_ctx.quiet,
|
||||||
verbose: app_ctx.verbose,
|
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...");
|
info!("Checking if it's also MTK BDP...");
|
||||||
|
|
||||||
let new_file = File::open(&output_path)?;
|
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)? {
|
if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? {
|
||||||
info!("- MTK BDP file detected!\n");
|
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,
|
output_dir: mtk_extraction_path,
|
||||||
options: app_ctx.options.clone(),
|
options: app_ctx.options.clone(),
|
||||||
dry_run: app_ctx.dry_run,
|
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,
|
quiet: app_ctx.quiet,
|
||||||
verbose: app_ctx.verbose,
|
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(
|
#[command(
|
||||||
name = "unixtract",
|
name = "unixtract",
|
||||||
version,
|
version,
|
||||||
about = "Firmware extractor for various file formats",
|
about = "Firmware package analyzer and extractor",
|
||||||
long_about = "unixtract analyzes and extracts firmware package formats commonly found \
|
long_about = "unixtract analyzes and extracts firmware package formats commonly found in\n\
|
||||||
in TVs, Blu-Ray players, and AV devices. It supports 35+ different formats \
|
TVs, Blu-ray players, and AV devices. It supports 35+ formats with built-in\n\
|
||||||
with built-in decryption and decompression capabilities."
|
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 {
|
struct Args {
|
||||||
/// The target file or directory to analyze/extract
|
/// Single firmware binary input
|
||||||
#[arg(required_unless_present = "list_formats")]
|
#[arg(long = "file-input")]
|
||||||
input_target: Option<String>,
|
file_input: Option<String>,
|
||||||
|
|
||||||
/// Folder to save extracted files to (default: _<INPUT_TARGET>)
|
/// Directory containing multiple firmware binaries for bulk processing
|
||||||
output_directory: Option<String>,
|
#[arg(long = "dir-input")]
|
||||||
|
dir_input: Option<String>,
|
||||||
|
|
||||||
/// Format-specific or global options (can be used multiple times)
|
/// Output path for extracted data (default: _<INPUT>)
|
||||||
#[arg(short, long)]
|
#[arg(long = "output")]
|
||||||
options: Vec<String>,
|
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
|
/// List supported formats and exit
|
||||||
#[arg(long)]
|
#[arg(long = "list-formats")]
|
||||||
list_formats: bool,
|
list_formats: bool,
|
||||||
|
|
||||||
/// Only detect the format, do not extract
|
/// Increase verbosity level (repeat for more)
|
||||||
#[arg(long)]
|
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
|
||||||
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)]
|
|
||||||
verbose: u8,
|
verbose: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +88,9 @@ pub struct AppContext {
|
|||||||
pub output_dir: PathBuf,
|
pub output_dir: PathBuf,
|
||||||
pub options: Vec<String>,
|
pub options: Vec<String>,
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
|
pub lazy_run: bool,
|
||||||
|
pub build_prop: bool,
|
||||||
|
pub dump_keys: bool,
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
pub verbose: u8,
|
pub verbose: u8,
|
||||||
}
|
}
|
||||||
@@ -96,25 +128,33 @@ fn clear_terminal() {
|
|||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
// Clear the terminal on execution
|
// --- Handle --list-formats (doesn't require input) ---
|
||||||
if !args.quiet {
|
if args.list_formats {
|
||||||
clear_terminal();
|
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
|
// --- Validate mutually exclusive input modes ---
|
||||||
// Default (no flags): show info-level output (normal progress)
|
let (target_path, is_file_mode) = match (&args.file_input, &args.dir_input) {
|
||||||
// -q: errors only
|
(Some(f), None) => (PathBuf::from(f), true),
|
||||||
// -v: debug level (more details)
|
(None, Some(d)) => (PathBuf::from(d), false),
|
||||||
// -vv: trace level (everything)
|
(Some(_), Some(_)) => {
|
||||||
// -vvv: trace level (everything)
|
return Err("Cannot specify both --file-input and --dir-input at the same time".into());
|
||||||
let log_level = if args.quiet {
|
|
||||||
"error"
|
|
||||||
} else {
|
|
||||||
match args.verbose {
|
|
||||||
0 => "info",
|
|
||||||
1 => "debug",
|
|
||||||
_ => "trace",
|
|
||||||
}
|
}
|
||||||
|
(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))
|
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();
|
.init();
|
||||||
|
|
||||||
// Handle --list-formats
|
clear_terminal();
|
||||||
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(());
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("unixtract v{}", env!("CARGO_PKG_VERSION"));
|
log::info!("unixtract v{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
let target_path_str = args.input_target.as_deref().unwrap_or("");
|
// --- Output directory ---
|
||||||
let target_path = PathBuf::from(target_path_str);
|
let output_path_str = if let Some(ref out) = args.output {
|
||||||
|
|
||||||
let output_path_str = if let Some(ref out) = args.output_directory {
|
|
||||||
out.clone()
|
out.clone()
|
||||||
} else {
|
} else {
|
||||||
format!("_{}", target_path.file_name()
|
let fname = target_path.file_name()
|
||||||
.and_then(|s| s.to_str())
|
.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);
|
let output_directory_path = PathBuf::from(&output_path_str);
|
||||||
|
|
||||||
if output_directory_path.exists() {
|
if output_directory_path.exists() {
|
||||||
if output_directory_path.is_dir() {
|
if output_directory_path.is_dir() {
|
||||||
let is_empty = fs::read_dir(&output_directory_path)?.next().is_none();
|
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.");
|
log::warn!("Output folder already exists and is NOT empty! Files may be overwritten.");
|
||||||
eprintln!("Press Enter if you want to continue...");
|
eprintln!("Press Enter if you want to continue...");
|
||||||
io::stdin().read_line(&mut String::new())?;
|
io::stdin().read_line(&mut String::new())?;
|
||||||
@@ -167,33 +198,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === DEFAULT MODE (original behavior) ===
|
// --- Build AppContext ---
|
||||||
let app_ctx;
|
let app_ctx = if is_file_mode {
|
||||||
|
|
||||||
if target_path.is_file() {
|
|
||||||
let file = File::open(&target_path)?;
|
let file = File::open(&target_path)?;
|
||||||
app_ctx = AppContext {
|
AppContext {
|
||||||
input: InputTarget::File(file),
|
input: InputTarget::File(file),
|
||||||
input_path: Some(target_path.clone()),
|
input_path: Some(target_path.clone()),
|
||||||
output_dir: output_directory_path,
|
output_dir: output_directory_path,
|
||||||
options: args.options,
|
options: Vec::new(),
|
||||||
dry_run: args.dry_run,
|
dry_run: args.lazy_run,
|
||||||
quiet: args.quiet,
|
lazy_run: args.lazy_run,
|
||||||
|
build_prop: args.build_prop,
|
||||||
|
dump_keys: args.dump_keys,
|
||||||
|
quiet: args.verbose == 0,
|
||||||
verbose: args.verbose,
|
verbose: args.verbose,
|
||||||
};
|
}
|
||||||
} else if target_path.is_dir() {
|
} else {
|
||||||
app_ctx = AppContext {
|
AppContext {
|
||||||
input: InputTarget::Directory(target_path.clone()),
|
input: InputTarget::Directory(target_path.clone()),
|
||||||
input_path: Some(target_path.clone()),
|
input_path: Some(target_path.clone()),
|
||||||
output_dir: output_directory_path,
|
output_dir: output_directory_path,
|
||||||
options: args.options,
|
options: Vec::new(),
|
||||||
dry_run: args.dry_run,
|
dry_run: args.lazy_run,
|
||||||
quiet: args.quiet,
|
lazy_run: args.lazy_run,
|
||||||
|
build_prop: args.build_prop,
|
||||||
|
dump_keys: args.dump_keys,
|
||||||
|
quiet: args.verbose == 0,
|
||||||
verbose: args.verbose,
|
verbose: args.verbose,
|
||||||
};
|
}
|
||||||
} else {
|
};
|
||||||
return Err("Invalid input path!".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let formats: Vec<Format> = get_registry();
|
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)? {
|
if let Some(ctx) = (format.detector_func)(&app_ctx)? {
|
||||||
log::info!("\n{} detected!", format.name);
|
log::info!("\n{} detected!", format.name);
|
||||||
|
|
||||||
if app_ctx.dry_run {
|
if app_ctx.lazy_run {
|
||||||
log::info!("Dry run — skipping extraction.");
|
log::info!("Lazy-run — skipping extraction.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +246,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
(format.extractor_func)(&app_ctx, ctx)?;
|
(format.extractor_func)(&app_ctx, ctx)?;
|
||||||
|
|
||||||
// Extractor returned with no error
|
|
||||||
log::info!("\nExtraction finished! Saved extracted files to {}", output_path_str);
|
log::info!("\nExtraction finished! Saved extracted files to {}", output_path_str);
|
||||||
return Ok(());
|
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