commit b9ea1d29dde6cb2274f5a07de097669ada16e3df Author: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Sun Apr 5 08:35:33 2026 -0700 libfreemkv v0.1.0 — Open source 4K UHD / Blu-ray / DVD drive library Features: - Open drive identification via SPC-4 INQUIRY + MMC-6 GET CONFIGURATION - 141 supported drives with bundled profiles - MT1959 platform: unlock, calibrate, raw sector reads - DriveSpeed enum: BD1x-BD12x, DVD1x-DVD16x - Field names follow SPC-4 §6.4.2 and MMC-6 §5.3.10 standards - No proprietary fingerprints — open matching by SCSI fields - Zero config: profiles compiled into binary Tested on real hardware: HL-DT-ST BD-RE BU40N 1.03 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97f8e81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +Cargo.lock +*.swp +*.swo +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6a5420e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing to libfreemkv + +Thank you for your interest in helping make disc archival accessible to everyone. + +## Contributing Drive Profiles + +The most impactful contribution is adding support for new drives. If you have +an optical drive that isn't listed in [profiles/](profiles/), we'd love your help. + +### How to submit drive data + +1. Install the tool: + ```bash + cargo install libfreemkv + ``` + +2. Run `freemkv-info` with your drive: + ```bash + freemkv-info /dev/sr0 --raw > my_drive.txt + ``` + +3. Open a pull request or issue with the output file attached. + +That's it. The raw SCSI response data lets us build a profile for your drive. + +### What data is collected + +`freemkv-info --raw` sends two standard SCSI commands to your drive: + +- **INQUIRY** (opcode 0x12) — returns drive vendor, model, firmware version +- **GET CONFIGURATION** (opcode 0x46) — returns drive feature data + +These are read-only, standard SCSI commands. They don't modify your drive +or access any disc data. Every operating system sends these commands +automatically when a drive is connected. + +### Priority: Pioneer drives + +We especially need data from **Pioneer** Blu-ray drives (BDR-S08, BDR-S09, +BDR-S12, BDR-S13, BDR-209, BDR-212, etc). If you have one, your contribution +would help unlock support for 130+ Pioneer drive firmware versions. + +## Contributing Code + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/my-change`) +3. Write tests for your changes +4. Ensure `cargo test` and `cargo clippy` pass +5. Submit a pull request + +### Code Style + +- Run `cargo fmt` before committing +- No `unsafe` without a comment explaining why +- Public APIs need doc comments +- Error handling via `Result`, no panics in library code + +### Architecture + +- `src/scsi.rs` — SCSI transport layer (SG_IO on Linux) +- `src/profile.rs` — Profile loading and matching +- `src/platform/` — Per-chipset command implementations +- `src/drive.rs` — High-level DriveSession API +- `profiles/` — JSON drive profile data + +## License + +By contributing, you agree that your contributions will be licensed under AGPL-3.0. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c7881e4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "libfreemkv" +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0-only" +description = "Open source raw disc access library for optical drives" +repository = "https://github.com/freemkv/libfreemkv" +keywords = ["bluray", "uhd", "optical", "scsi", "disc"] +categories = ["hardware-support", "multimedia"] + +[dependencies] +libc = "0.2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha1 = "0.10" +aes = "0.8" +cbc = "0.1" +flate2 = "1" + +[[bin]] +name = "freemkv-info" +path = "src/bin/freemkv_info.rs" + +[[bin]] +name = "freemkv-test" +path = "src/bin/freemkv_test.rs" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e84eb09 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2026 FreeMKV Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..c25247e --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# libfreemkv + +Open source raw disc access library for UHD Blu-ray optical drives. + +Enables direct sector reading on compatible drives for UHD Blu-ray archival, +backup, and media extraction. Ships with community-contributed drive profiles — +no proprietary data files needed at runtime. + +## Features + +- **Drive identification** — SCSI INQUIRY + GET CONFIGURATION for automatic profile matching +- **Raw read mode** — activate enhanced read mode on supported drives +- **Speed calibration** — optimal read speed per disc region +- **Raw sector reading** — direct READ(10) access to disc sectors +- **Drive profiles** — per-drive SCSI command data, shipped as JSON files +- **Community-driven** — submit new drive profiles via `freemkv-info` + +## Supported Drives + +Currently supports 280+ LG, ASUS, and HP optical drive firmware versions +across the MediaTek MT1959 chipset family. Pioneer Renesas support is in progress. + +See [profiles/](profiles/) for the full list. + +## Installation + +```bash +cargo install libfreemkv +``` + +Or add to your `Cargo.toml`: + +```toml +[dependencies] +libfreemkv = "0.1" +``` + +## Quick Start + +### As a library + +```rust +use libfreemkv::DriveSession; +use std::path::Path; + +let mut session = DriveSession::open( + Path::new("/dev/sr0"), + Path::new("profiles/"), +)?; + +session.enable()?; // activate raw read mode +session.calibrate()?; // optimize read speed + +let mut buf = vec![0u8; 2048]; +session.read_sectors(0, 1, &mut buf)?; +``` + +### freemkv-info + +Identify your drive and check compatibility: + +```bash +$ freemkv-info /dev/sr0 +Drive: HL-DT-ST BD-RE BU40N 1.03 +Chipset: MT1959 +Raw Read: Supported +Profile: Found (mt1959_a) + +$ freemkv-info /dev/sr0 --raw +# Dumps full INQUIRY and GET CONFIGURATION responses as hex +# Useful for contributing profiles for unsupported drives +``` + +### freemkv-test + +Verify raw read mode works: + +```bash +$ freemkv-test /dev/sr0 +Enabling raw read mode... OK +Calibrating speed... OK (42 speed zones) +Reading sector 0... OK (2048 bytes) +Reading sector 1000... OK (2048 bytes) +All checks passed. +``` + +## Contributing Drive Profiles + +If your drive isn't supported, you can help: + +1. Run `freemkv-info /dev/sr0 --raw > my_drive.txt` +2. Open an issue or PR with the output +3. We'll generate a profile from your drive data + +This is especially needed for Pioneer drives. + +## Architecture + +``` +DriveSession +├── ScsiTransport — SG_IO (Linux) / IOKit (macOS) +├── DriveProfile — per-drive JSON data +└── Platform — per-chipset unlock + read logic + ├── Mt1959 — LG/ASUS MediaTek drives + └── Pioneer — Pioneer Renesas drives (WIP) +``` + +The library implements 10 drive commands per platform: + +| Command | Purpose | +|---------|---------| +| enable | Activate raw read mode | +| read_config | Read drive configuration | +| read_register | Read hardware registers | +| calibrate | Build speed optimization table | +| keepalive | Session keepalive | +| status | Read mode status and features | +| probe | Generic drive query | +| read_sectors | Read raw disc sectors | +| read_disc_structure | Read disc metadata | +| timing | Timing calibration | + +## License + +AGPL-3.0-only diff --git a/profiles.json b/profiles.json new file mode 100644 index 0000000..01c3113 --- /dev/null +++ b/profiles.json @@ -0,0 +1,1836 @@ +[ + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS60 ", + "product_revision": "1.01", + "vendor_specific": "NM00100", + "firmware_date": "211711202000", + "program": "mt1959_a", + "signature": "00db13d6", + "register_offsets": [ + "10e1ed", + "118093" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU50N ", + "product_revision": "0420", + "vendor_specific": "N000000", + "firmware_date": "211605241901", + "program": "mt1959_b", + "signature": "7730540d", + "register_offsets": [ + "10300d", + "11f664" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1X-U ", + "product_revision": "A104", + "vendor_specific": "WM01001", + "firmware_date": "211901041047", + "program": "mt1959_a", + "signature": "f3643b82", + "register_offsets": [ + "10f8c2", + "112881", + "9b44dc" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS48 ", + "product_revision": "1.01", + "vendor_specific": "N1A12A1", + "firmware_date": "211304042250", + "program": "mt1959_b", + "signature": "e59ccbda", + "register_offsets": [ + "10e8e9", + "11b929" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH30N ", + "product_revision": "AS00", + "vendor_specific": "S002102", + "firmware_date": "211306141459", + "program": "mt1959_b", + "signature": "088a5c68", + "register_offsets": [ + "107f5c", + "11e882" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.01", + "vendor_specific": "N000500", + "firmware_date": "211511051004", + "program": "mt1959_b", + "signature": "25fb97df", + "register_offsets": [ + "104053", + "11d9f5" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1H-U ", + "product_revision": "A203", + "vendor_specific": "WM01501", + "firmware_date": "211801111117", + "program": "mt1959_a", + "signature": "36b76b16", + "register_offsets": [ + "104078", + "11a844" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH14NS40 ", + "product_revision": "1.00", + "vendor_specific": "N1A37A3", + "firmware_date": "211204261630", + "program": "mt1959_b", + "signature": "051a1381", + "register_offsets": [ + "108c86", + "110721" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP50NB40 ", + "product_revision": "1.00", + "vendor_specific": "N004304", + "firmware_date": "211412161016", + "program": "mt1959_b", + "signature": "1647e02f", + "register_offsets": [ + "105f68", + "11e13c" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.02", + "vendor_specific": "NM01201", + "firmware_date": "211711301218", + "program": "mt1959_a", + "signature": "bb81818e", + "register_offsets": [ + "1038a9", + "11b0d1" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH14NS40 ", + "product_revision": "1.04", + "vendor_specific": "NM01201", + "firmware_date": "211901041351", + "program": "mt1959_a", + "signature": "4fba1843", + "register_offsets": [ + "1094f4", + "11bd66" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CU20N ", + "product_revision": "1.00", + "vendor_specific": "N003203", + "firmware_date": "211412161022", + "program": "mt1959_b", + "signature": "64106528", + "register_offsets": [ + "1068b9", + "116a59" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WP50NB40 ", + "product_revision": "1.00", + "vendor_specific": "N001501", + "firmware_date": "211504221534", + "program": "mt1959_b", + "signature": "a80424b3", + "register_offsets": [ + "1000f3", + "113f87" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.04", + "vendor_specific": "NM01701", + "firmware_date": "211901041338", + "program": "mt1959_a", + "signature": "f84f9141", + "register_offsets": [ + "108170", + "11c7fa" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS30", + "product_revision": "1.00", + "vendor_specific": "N0A20A2", + "firmware_date": "211306131532", + "program": "mt1959_b", + "signature": "35e835f9", + "register_offsets": [ + "1075e9", + "111970" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.02", + "vendor_specific": "N0C00C0", + "firmware_date": "211505261638", + "program": "mt1959_b", + "signature": "c8a13745", + "register_offsets": [ + "10a0fe", + "115326" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS40 ", + "product_revision": "1.00", + "vendor_specific": "N002502", + "firmware_date": "211508191146", + "program": "mt1959_b", + "signature": "3d92c6e9", + "register_offsets": [ + "100933", + "11d0df" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.04", + "vendor_specific": "NM01701", + "firmware_date": "211901041342", + "program": "mt1959_a", + "signature": "9c96426b", + "register_offsets": [ + "10c12c", + "110419" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "1.00", + "vendor_specific": "G005705", + "firmware_date": "211504221429", + "program": "mt1959_b", + "signature": "1daa6ee6", + "register_offsets": [ + "100b74", + "113790" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU50N ", + "product_revision": "1.00", + "vendor_specific": "NM00100", + "firmware_date": "211711211006", + "program": "mt1959_a", + "signature": "5c70e4e3", + "register_offsets": [ + "10c8f2", + "11ad06" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS48 ", + "product_revision": "1.02", + "vendor_specific": "N0A02A0", + "firmware_date": "211312061535", + "program": "mt1959_b", + "signature": "031dbbb5", + "register_offsets": [ + "101450", + "11a8ed" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.00", + "vendor_specific": "R004504", + "firmware_date": "211703231903", + "program": "mt1959_b", + "signature": "9fe13149", + "register_offsets": [ + "10e3cb", + "116888" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "U101", + "vendor_specific": "MM01201", + "firmware_date": "211711301153", + "program": "mt1959_a", + "signature": "7f4f83db", + "register_offsets": [ + "107b7d", + "113491" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS48 ", + "product_revision": "1.00", + "vendor_specific": "N1A29A2", + "firmware_date": "211210311917", + "program": "mt1959_b", + "signature": "433f74a7", + "register_offsets": [ + "1037d3", + "112c09" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS40 ", + "product_revision": "1.03", + "vendor_specific": "N0A03A0", + "firmware_date": "211403261115", + "program": "mt1959_b", + "signature": "bbc3b4f5", + "register_offsets": [ + "104691", + "114055" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1H-U ", + "product_revision": "A204", + "vendor_specific": "WM01001", + "firmware_date": "211901041044", + "program": "mt1959_a", + "signature": "0119431d", + "register_offsets": [ + "10eed4", + "112f06" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "BU10", + "vendor_specific": "O002602", + "firmware_date": "211701060941", + "program": "mt1959_b", + "signature": "0e345f8e", + "register_offsets": [ + "1063db", + "1191df" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.01", + "vendor_specific": "N000500", + "firmware_date": "211511050956", + "program": "mt1959_b", + "signature": "08b1cda4", + "register_offsets": [ + "10ab0f", + "1142f4" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "BN12", + "vendor_specific": "0M01001", + "firmware_date": "211905141415", + "program": "mt1959_a", + "signature": "a5b7799a", + "register_offsets": [ + "103686", + "1132ff" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1HT ", + "product_revision": "1.01", + "vendor_specific": "W100600", + "firmware_date": "211302051930", + "program": "mt1959_b", + "signature": "de0a4146", + "register_offsets": [ + "101f94", + "116d4e" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-14D1XT ", + "product_revision": "1.00", + "vendor_specific": "M103703", + "firmware_date": "210000000000", + "program": "mt1959_b", + "signature": "2c16f09d", + "register_offsets": [ + "1086bb", + "11f9e1" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "BU10", + "vendor_specific": "O003703", + "firmware_date": "211705301728", + "program": "mt1959_b", + "signature": "3d782ce4", + "register_offsets": [ + "1061f6", + "11e1fb" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS30", + "product_revision": "1.02", + "vendor_specific": "N0A05A0", + "firmware_date": "211404241908", + "program": "mt1959_b", + "signature": "984a3ccb", + "register_offsets": [ + "1014ec", + "11b2a6" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP50NB40 ", + "product_revision": "1.00", + "vendor_specific": "N000900", + "firmware_date": "211609091048", + "program": "mt1959_b", + "signature": "af7f5706", + "register_offsets": [ + "1090b4", + "119465" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.01", + "vendor_specific": "RM01801", + "firmware_date": "211910161032", + "program": "mt1959_a", + "signature": "8f5ff6f8", + "register_offsets": [ + "101918", + "11be36" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP50NB40 ", + "product_revision": "1.00", + "vendor_specific": "N005505", + "firmware_date": "211504221517", + "program": "mt1959_b", + "signature": "0faa09dd", + "register_offsets": [ + "1044da", + "1144a1" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "U100", + "vendor_specific": "3M01801", + "firmware_date": "211910161036", + "program": "mt1959_a", + "signature": "43c71953", + "register_offsets": [ + "10f8c1", + "11d230" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU50N ", + "product_revision": "CCT5", + "vendor_specific": "V000700", + "firmware_date": "211705182023", + "program": "mt1959_b", + "signature": "c4f9ccbc", + "register_offsets": [ + "10abe0", + "116e03" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.02", + "vendor_specific": "N001901", + "firmware_date": "211703101630", + "program": "mt1959_b", + "signature": "78c4d5e4", + "register_offsets": [ + "10903f", + "115481" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS30", + "product_revision": "1.00", + "vendor_specific": "N0A13A1", + "firmware_date": "211303221728", + "program": "mt1959_b", + "signature": "bef533e2", + "register_offsets": [ + "103c76", + "11f449" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU50N ", + "product_revision": "AS00", + "vendor_specific": "U008408", + "firmware_date": "211607291602", + "program": "mt1959_b", + "signature": "f3587486", + "register_offsets": [ + "10c1e7", + "1127f5" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP50NB40 ", + "product_revision": "1.00", + "vendor_specific": "N002602", + "firmware_date": "211409031732", + "program": "mt1959_b", + "signature": "abf9ea31", + "register_offsets": [ + "1072dc", + "111926" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1HT ", + "product_revision": "3.03", + "vendor_specific": "WM00000", + "firmware_date": "211801191558", + "program": "mt1959_a", + "signature": "a33c4440", + "register_offsets": [ + "10a024", + "11fb60" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WP50NB40 ", + "product_revision": "1.03", + "vendor_specific": "NM00800", + "firmware_date": "212005070917", + "program": "mt1959_a", + "signature": "c222c5fe", + "register_offsets": [ + "105bd7", + "111fd9" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WP50NB40 ", + "product_revision": "1.01", + "vendor_specific": "NM00200", + "firmware_date": "211711231550", + "program": "mt1959_a", + "signature": "1da5dbbd", + "register_offsets": [ + "104b8a", + "117215" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "1.00", + "vendor_specific": "N005305", + "firmware_date": "211504211939", + "program": "mt1959_b", + "signature": "96423b29", + "register_offsets": [ + "10d622", + "117102" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW UH12NS40", + "product_revision": "1.00", + "vendor_specific": "N000200", + "firmware_date": "211602231042", + "program": "mt1959_b", + "signature": "973fb48e", + "register_offsets": [ + "10df48", + "11d920" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP60NB10 ", + "product_revision": "1.02", + "vendor_specific": "NM00800", + "firmware_date": "212005070935", + "program": "mt1959_a", + "signature": "f1147025", + "register_offsets": [ + "10e9aa", + "11b030" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "A100", + "vendor_specific": "D00D60D", + "firmware_date": "211507301534", + "program": "mt1959_b", + "signature": "acf75868", + "register_offsets": [ + "10dbbb", + "1116fc" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP50NB40 ", + "product_revision": "1.03", + "vendor_specific": "NM00300", + "firmware_date": "212107081603", + "program": "mt1959_a", + "signature": "65605ec5", + "register_offsets": [ + "10bd36", + "110f44" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.02", + "vendor_specific": "N000200", + "firmware_date": "211512111436", + "program": "mt1959_b", + "signature": "325bd482", + "register_offsets": [ + "10909f", + "113223" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1HT ", + "product_revision": "3.10", + "vendor_specific": "WM01601", + "firmware_date": "211901041014", + "program": "mt1959_a", + "signature": "a5f54fa0", + "register_offsets": [ + "10fe46", + "11d6f4" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS30", + "product_revision": "1.01", + "vendor_specific": "N0A02A0", + "firmware_date": "211312061614", + "program": "mt1959_b", + "signature": "14caa906", + "register_offsets": [ + "1016ae", + "111125" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH14NS40 ", + "product_revision": "1514", + "vendor_specific": "N100400", + "firmware_date": "211205301654", + "program": "mt1959_b", + "signature": "6d03a2b5", + "register_offsets": [ + "10adf8", + "113305" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.04", + "vendor_specific": "NM00500", + "firmware_date": "212005061142", + "program": "mt1959_a", + "signature": "fabd3b21", + "register_offsets": [ + "107e3f", + "11e816" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1H-U ", + "product_revision": "A101", + "vendor_specific": "W000600", + "firmware_date": "211412080959", + "program": "mt1959_b", + "signature": "f761430c", + "register_offsets": [ + "108c63", + "112661" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BC-12D2HT ", + "product_revision": "3.01", + "vendor_specific": "WM00900", + "firmware_date": "211711151926", + "program": "mt1959_a", + "signature": "f097cca9", + "register_offsets": [ + "102678", + "11471d" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "FR07", + "vendor_specific": "J001101", + "firmware_date": "211612201651", + "program": "mt1959_b", + "signature": "2f8658d7", + "register_offsets": [ + "10a556", + "110626" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP60NB10 ", + "product_revision": "1.01", + "vendor_specific": "NM01801", + "firmware_date": "211905031534", + "program": "mt1959_a", + "signature": "8b3d800f", + "register_offsets": [ + "100625", + "113f94" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1HT ", + "product_revision": "3.02", + "vendor_specific": "W000800", + "firmware_date": "211711241413", + "program": "mt1959_b", + "signature": "22a0be7d", + "register_offsets": [ + "105b78", + "11ffd6" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU50N ", + "product_revision": "CCT2", + "vendor_specific": "V000700", + "firmware_date": "211703071420", + "program": "mt1959_b", + "signature": "fdc8610e", + "register_offsets": [ + "104c5b", + "11ada8" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.02", + "vendor_specific": "N000100", + "firmware_date": "211512111503", + "program": "mt1959_b", + "signature": "e548ce88", + "register_offsets": [ + "10a453", + "118786" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.00", + "vendor_specific": "N003103", + "firmware_date": "211612201528", + "program": "mt1959_b", + "signature": "9feaad54", + "register_offsets": [ + "108c89", + "1170e5" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS48 ", + "product_revision": "1.01", + "vendor_specific": "N1A12A1", + "firmware_date": "211304042325", + "program": "mt1959_b", + "signature": "f5da20d5", + "register_offsets": [ + "10c63e", + "11103b" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS60 ", + "product_revision": "1.03", + "vendor_specific": "NM00600", + "firmware_date": "212005081010", + "program": "mt1959_a", + "signature": "66e1159d", + "register_offsets": [ + "103285", + "111a6f" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS40", + "product_revision": "1.01", + "vendor_specific": "NM00400", + "firmware_date": "211711211606", + "program": "mt1959_a", + "signature": "9b912db5", + "register_offsets": [ + "10ef24", + "117d88" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1HT ", + "product_revision": "3.00", + "vendor_specific": "W004504", + "firmware_date": "211508101633", + "program": "mt1959_b", + "signature": "c0b8ef5e", + "register_offsets": [ + "1004bb", + "11da12" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.05", + "vendor_specific": "NM00400", + "firmware_date": "212004211049", + "program": "mt1959_a", + "signature": "96121b34", + "register_offsets": [ + "108470", + "11507a" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "T.00", + "vendor_specific": "V003403", + "firmware_date": "211606241658", + "program": "mt1959_b", + "signature": "8fae0907", + "register_offsets": [ + "109c71", + "11eb93" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.02", + "vendor_specific": "N0A00A0", + "firmware_date": "211405071026", + "program": "mt1959_b", + "signature": "540a4152", + "register_offsets": [ + "10e233", + "1144e3" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "A101", + "vendor_specific": "D000100", + "firmware_date": "211602241144", + "program": "mt1959_b", + "signature": "eea9b99a", + "register_offsets": [ + "10ce97", + "11f07f" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.02", + "vendor_specific": "N001401", + "firmware_date": "211703101650", + "program": "mt1959_b", + "signature": "a17bf024", + "register_offsets": [ + "10013d", + "1110a0" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS60 ", + "product_revision": "1.00", + "vendor_specific": "N000500", + "firmware_date": "211704251756", + "program": "mt1959_b", + "signature": "3b3e9586", + "register_offsets": [ + "10707a", + "1114de" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS40", + "product_revision": "1.03", + "vendor_specific": "NM00800", + "firmware_date": "212005080959", + "program": "mt1959_a", + "signature": "eb731968", + "register_offsets": [ + "1068b3", + "110140" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.03", + "vendor_specific": "NM00000", + "firmware_date": "211711201704", + "program": "mt1959_a", + "signature": "673be581", + "register_offsets": [ + "1065a6", + "11662f" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH14NS40 ", + "product_revision": "1.00", + "vendor_specific": "N1A26A2", + "firmware_date": "211202091126", + "program": "mt1959_b", + "signature": "86b99610", + "register_offsets": [ + "10fe2d", + "11a000" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.01", + "vendor_specific": "N000500", + "firmware_date": "211511051001", + "program": "mt1959_b", + "signature": "01bc1296", + "register_offsets": [ + "10232f", + "119df2" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.00", + "vendor_specific": "N001401", + "firmware_date": "211508180940", + "program": "mt1959_b", + "signature": "1402ac30", + "register_offsets": [ + "102349", + "113ced" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.01", + "vendor_specific": "RM00000", + "firmware_date": "211712221221", + "program": "mt1959_a", + "signature": "a36409fd", + "register_offsets": [ + "107aad", + "1103c4" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.00", + "vendor_specific": "R002202", + "firmware_date": "211609090954", + "program": "mt1959_b", + "signature": "8ea02dd3", + "register_offsets": [ + "10d55e", + "11e031" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WP50NB40 ", + "product_revision": "1.00", + "vendor_specific": "N001501", + "firmware_date": "211504221546", + "program": "mt1959_b", + "signature": "f0406959", + "register_offsets": [ + "10199d", + "11acdb" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WP50NB40 ", + "product_revision": "1.00", + "vendor_specific": "N000400", + "firmware_date": "211502161437", + "program": "mt1959_b", + "signature": "7558d085", + "register_offsets": [ + "1045b4", + "110d17" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "BU12", + "vendor_specific": "OM01001", + "firmware_date": "211902230922", + "program": "mt1959_a", + "signature": "dae22292", + "register_offsets": [ + "10c1e7", + "11cce9" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP60NB10 ", + "product_revision": "1.00", + "vendor_specific": "NM00200", + "firmware_date": "211711211720", + "program": "mt1959_a", + "signature": "300773cc", + "register_offsets": [ + "108edc", + "11db54" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS30", + "product_revision": "1.00", + "vendor_specific": "N0A13A1", + "firmware_date": "211303221725", + "program": "mt1959_b", + "signature": "a6b89410", + "register_offsets": [ + "10da60", + "11a750" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "A100", + "vendor_specific": "D032232", + "firmware_date": "211701021159", + "program": "mt1959_b", + "signature": "4d528af5", + "register_offsets": [ + "10b2ba", + "11267f" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.02", + "vendor_specific": "N000200", + "firmware_date": "211512111440", + "program": "mt1959_b", + "signature": "3657bdc5", + "register_offsets": [ + "108a9b", + "111b06" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "EB02", + "vendor_specific": "C000400", + "firmware_date": "211610101049", + "program": "mt1959_b", + "signature": "fe7ea043", + "register_offsets": [ + "1059f0", + "1199c3" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BC-12D2HT ", + "product_revision": "3.00", + "vendor_specific": "W004704", + "firmware_date": "211512091147", + "program": "mt1959_b", + "signature": "9aa93ae2", + "register_offsets": [ + "100df4", + "11c10f" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WP50NB40 ", + "product_revision": "1.00", + "vendor_specific": "N002802", + "firmware_date": "211702151837", + "program": "mt1959_b", + "signature": "59108959", + "register_offsets": [ + "106efd", + "112bc2" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS30", + "product_revision": "1.01", + "vendor_specific": "N0A02A0", + "firmware_date": "211312061658", + "program": "mt1959_b", + "signature": "151486a8", + "register_offsets": [ + "107849", + "118c03" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "T.02", + "vendor_specific": "V001301", + "firmware_date": "211405091428", + "program": "mt1959_b", + "signature": "f648ef99", + "register_offsets": [ + "10a964", + "115837" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU50N ", + "product_revision": "GE02", + "vendor_specific": "V000700", + "firmware_date": "211705182030", + "program": "mt1959_b", + "signature": "44c8c30e", + "register_offsets": [ + "105de0", + "11e39d" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.00", + "vendor_specific": "N1A12A1", + "firmware_date": "211304041441", + "program": "mt1959_b", + "signature": "93cc063e", + "register_offsets": [ + "10d00d", + "11d043" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1HT ", + "product_revision": "3.01", + "vendor_specific": "W000800", + "firmware_date": "211703311426", + "program": "mt1959_b", + "signature": "a675253a", + "register_offsets": [ + "101391", + "119b40" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS40", + "product_revision": "1.00", + "vendor_specific": "N002202", + "firmware_date": "211602231047", + "program": "mt1959_b", + "signature": "7b57ef4b", + "register_offsets": [ + "10ef00", + "11d0a7" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP50NB40 ", + "product_revision": "1.02", + "vendor_specific": "NM00700", + "firmware_date": "211811011016", + "program": "mt1959_a", + "signature": "ee676129", + "register_offsets": [ + "10fe7f", + "11b6c3" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.03", + "vendor_specific": "NM00600", + "firmware_date": "211711211658", + "program": "mt1959_a", + "signature": "a7324936", + "register_offsets": [ + "1015d7", + "1168df" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH14NS48 ", + "product_revision": "1.00", + "vendor_specific": "N1A44A4", + "firmware_date": "211206161828", + "program": "mt1959_b", + "signature": "8b4abdb4", + "register_offsets": [ + "101b59", + "1196a6" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "A100", + "vendor_specific": "D031531", + "firmware_date": "211608041137", + "program": "mt1959_b", + "signature": "9c0289c5", + "register_offsets": [ + "1086ce", + "111e53" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "A101", + "vendor_specific": "D000700", + "firmware_date": "211703131630", + "program": "mt1959_b", + "signature": "a26e07ef", + "register_offsets": [ + "10e1f1", + "116aa6" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP40N ", + "product_revision": "GP01", + "vendor_specific": "V001701", + "firmware_date": "211504221450", + "program": "mt1959_b", + "signature": "83dce37a", + "register_offsets": [ + "104a72", + "116470" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.02", + "vendor_specific": "RM00200", + "firmware_date": "212012011716", + "program": "mt1959_a", + "signature": "3d35d118", + "register_offsets": [ + "10c079", + "11e1d1" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS30", + "product_revision": "1.00", + "vendor_specific": "N0A20A2", + "firmware_date": "211306131457", + "program": "mt1959_b", + "signature": "6321461d", + "register_offsets": [ + "1079da", + "11acf5" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "DVDRWBD CH30N ", + "product_revision": "A101", + "vendor_specific": "D000300", + "firmware_date": "210000000000", + "program": "mt1959_b", + "signature": "cd02cdf5", + "register_offsets": [ + "10564a", + "117615" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.05", + "vendor_specific": "NM00900", + "firmware_date": "212005061444", + "program": "mt1959_a", + "signature": "ac2cc83d", + "register_offsets": [ + "10ccb7", + "1175e6" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.02", + "vendor_specific": "N001901", + "firmware_date": "211703101637", + "program": "mt1959_b", + "signature": "18b7b43b", + "register_offsets": [ + "10db35", + "1143cc" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1H-U ", + "product_revision": "A201", + "vendor_specific": "W000000", + "firmware_date": "211601141358", + "program": "mt1959_b", + "signature": "99655dfb", + "register_offsets": [ + "102349", + "11b85d" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS48 ", + "product_revision": "1.02", + "vendor_specific": "N0A02A0", + "firmware_date": "211312061523", + "program": "mt1959_b", + "signature": "37cabea0", + "register_offsets": [ + "100a82", + "115fa1" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "FR07", + "vendor_specific": "JM02202", + "firmware_date": "211801221536", + "program": "mt1959_a", + "signature": "0e9388dc", + "register_offsets": [ + "10e05d", + "11b36a" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BP71N ", + "product_revision": "1.01", + "vendor_specific": "SM00800", + "firmware_date": "212009071143", + "program": "mt1959_a", + "signature": "43007dbe", + "register_offsets": [ + "103827", + "117261" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "1.00", + "vendor_specific": "G002202", + "firmware_date": "211405090917", + "program": "mt1959_b", + "signature": "262e187e", + "register_offsets": [ + "10fcc6", + "118db1" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.03", + "vendor_specific": "NM00000", + "firmware_date": "211810241934", + "program": "mt1959_a", + "signature": "999ec375", + "register_offsets": [ + "10e291", + "11ab1c" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WP50NB40 ", + "product_revision": "1.01", + "vendor_specific": "NM00200", + "firmware_date": "211711231615", + "program": "mt1959_a", + "signature": "bbc51d41", + "register_offsets": [ + "1079f7", + "113374" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS60 ", + "product_revision": "1.02", + "vendor_specific": "NM00100", + "firmware_date": "211810291936", + "program": "mt1959_a", + "signature": "c0ab269e", + "register_offsets": [ + "108acd", + "118eef" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH14NS40 ", + "product_revision": "1.03", + "vendor_specific": "N0A09A0", + "firmware_date": "211403261105", + "program": "mt1959_b", + "signature": "b81c571c", + "register_offsets": [ + "10aae4", + "11c1c7" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BC-12B1ST b ", + "product_revision": "3.11", + "vendor_specific": "WM00300", + "firmware_date": "211902271319", + "program": "mt1959_a", + "signature": "43404cf4", + "register_offsets": [ + "1020a9", + "11e360" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS40 ", + "product_revision": "1.03", + "vendor_specific": "N0C06C0", + "firmware_date": "211505261714", + "program": "mt1959_b", + "signature": "62ca1a71", + "register_offsets": [ + "108de0", + "11cfe8" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.05", + "vendor_specific": "NM00900", + "firmware_date": "212005061440", + "program": "mt1959_a", + "signature": "1378d521", + "register_offsets": [ + "10259b", + "11c38a" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.02", + "vendor_specific": "N001401", + "firmware_date": "211703101655", + "program": "mt1959_b", + "signature": "3f55e249", + "register_offsets": [ + "10459d", + "115409" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU50N ", + "product_revision": "AS00", + "vendor_specific": "U00B50B", + "firmware_date": "211608041424", + "program": "mt1959_b", + "signature": "5bd6025e", + "register_offsets": [ + "108be5", + "11b172" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "EB02", + "vendor_specific": "C000900", + "firmware_date": "211412181002", + "program": "mt1959_b", + "signature": "f8a561c7", + "register_offsets": [ + "106918", + "11aa95" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CP50NS20", + "product_revision": "1.00", + "vendor_specific": "N004904", + "firmware_date": "211409041512", + "program": "mt1959_b", + "signature": "1e332fe5", + "register_offsets": [ + "1094a1", + "1127f4" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.00", + "vendor_specific": "N001401", + "firmware_date": "211508180932", + "program": "mt1959_b", + "signature": "94ea21ae", + "register_offsets": [ + "10da3d", + "111864" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU20N ", + "product_revision": "1.00", + "vendor_specific": "G004504", + "firmware_date": "211412160950", + "program": "mt1959_b", + "signature": "a87061f7", + "register_offsets": [ + "10b69e", + "112fef" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS40 ", + "product_revision": "1.05", + "vendor_specific": "NM00600", + "firmware_date": "212005061331", + "program": "mt1959_a", + "signature": "fbe14883", + "register_offsets": [ + "10d32e", + "112d0e" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW UH12NS30", + "product_revision": "1.03", + "vendor_specific": "N0A05A0", + "firmware_date": "211404241900", + "program": "mt1959_b", + "signature": "a156efd7", + "register_offsets": [ + "10ec21", + "1175ee" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.00", + "vendor_specific": "N000600", + "firmware_date": "211508191151", + "program": "mt1959_b", + "signature": "70a565ed", + "register_offsets": [ + "101a4e", + "11cfb3" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CP50NS20", + "product_revision": "1.00", + "vendor_specific": "N007307", + "firmware_date": "211412161028", + "program": "mt1959_b", + "signature": "a331f23e", + "register_offsets": [ + "10ad2d", + "1169b5" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS40", + "product_revision": "1.03", + "vendor_specific": "NM00800", + "firmware_date": "212005080957", + "program": "mt1959_a", + "signature": "a6c32d92", + "register_offsets": [ + "1015c8", + "1182ed" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BH16NS55 ", + "product_revision": "1.03", + "vendor_specific": "NM00000", + "firmware_date": "211711201943", + "program": "mt1959_a", + "signature": "b113e869", + "register_offsets": [ + "1094f6", + "11a8dd" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW UH12NS30", + "product_revision": "1.02", + "vendor_specific": "N0A04A0", + "firmware_date": "211403261141", + "program": "mt1959_b", + "signature": "939cd6d7", + "register_offsets": [ + "101c37", + "118020" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.03", + "vendor_specific": "NM00600", + "firmware_date": "211711211653", + "program": "mt1959_a", + "signature": "d4fd4cab", + "register_offsets": [ + "101fda", + "11f0a1" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "BN11", + "vendor_specific": "0M00300", + "firmware_date": "211711201351", + "program": "mt1959_a", + "signature": "fc13f7d1", + "register_offsets": [ + "107db1", + "1186f7" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BDDVDRW CH12NS30", + "product_revision": "1.01", + "vendor_specific": "N0A02A0", + "firmware_date": "211312061625", + "program": "mt1959_b", + "signature": "ea1e378d", + "register_offsets": [ + "10205b", + "113e62" + ] + }, + { + "vendor_id": "ASUS", + "product_id": "BW-16D1HT ", + "product_revision": "3.00", + "vendor_specific": "W006706", + "firmware_date": "211511031110", + "program": "mt1959_b", + "signature": "98946e16", + "register_offsets": [ + "105237", + "111cdb" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU50N ", + "product_revision": "CC01", + "vendor_specific": "V000700", + "firmware_date": "211706280934", + "program": "mt1959_b", + "signature": "4a178208", + "register_offsets": [ + "10dcce", + "11d42d" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU40N ", + "product_revision": "1.01", + "vendor_specific": "N000000", + "firmware_date": "211706291017", + "program": "mt1959_b", + "signature": "cda102c2", + "register_offsets": [ + "10be46", + "11f43d" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH16NS40 ", + "product_revision": "1.02", + "vendor_specific": "N000100", + "firmware_date": "211512111459", + "program": "mt1959_b", + "signature": "debebf49", + "register_offsets": [ + "10fa29", + "11273f" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE WH14NS40 ", + "product_revision": "1.03", + "vendor_specific": "N0C08C0", + "firmware_date": "211505261653", + "program": "mt1959_b", + "signature": "accf6a38", + "register_offsets": [ + "102c97", + "11383d" + ] + }, + { + "vendor_id": "HL-DT-ST", + "product_id": "BD-RE BU10N ", + "product_revision": "GS01", + "vendor_specific": "V001701", + "firmware_date": "211305020921", + "program": "mt1959_b", + "signature": "9e25a4d8", + "register_offsets": [ + "10e50c", + "119977" + ] + } +] \ No newline at end of file diff --git a/src/bin/freemkv_info.rs b/src/bin/freemkv_info.rs new file mode 100644 index 0000000..0333ce7 --- /dev/null +++ b/src/bin/freemkv_info.rs @@ -0,0 +1,159 @@ +//! freemkv-info — Drive identification and compatibility checker. +//! +//! Sends standard SCSI INQUIRY and GET CONFIGURATION commands to an optical drive, +//! displays drive identity and compatibility status, and optionally outputs raw +//! response data for profile contribution. +//! +//! Usage: +//! freemkv-info /dev/sr0 +//! freemkv-info /dev/sr0 --raw +//! freemkv-info /dev/sr0 --json + +use std::env; +use std::path::Path; +use std::process; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + eprintln!("freemkv-info — Drive identification and compatibility checker"); + eprintln!(); + eprintln!("Usage: freemkv-info [options]"); + eprintln!(); + eprintln!(" Optical drive device (e.g. /dev/sr0)"); + eprintln!(" --raw Output raw SCSI response hex (for profile contribution)"); + eprintln!(" --json Output machine-readable JSON"); + eprintln!(" --profiles Path to profiles directory (default: ./profiles)"); + eprintln!(); + eprintln!("Examples:"); + eprintln!(" freemkv-info /dev/sr0"); + eprintln!(" freemkv-info /dev/sr0 --raw > my_drive.txt"); + process::exit(1); + } + + let device = Path::new(&args[1]); + let raw_mode = args.iter().any(|a| a == "--raw"); + let json_mode = args.iter().any(|a| a == "--json"); + let profiles_dir = args.iter() + .position(|a| a == "--profiles") + .and_then(|i| args.get(i + 1)) + .map(|s| s.as_str()) + .unwrap_or("profiles"); + + // Open SCSI transport + let mut transport = match libfreemkv::scsi::SgIoTransport::open(device) { + Ok(t) => t, + Err(e) => { + eprintln!("Error: Cannot open {}: {}", device.display(), e); + process::exit(1); + } + }; + + // INQUIRY + let inquiry = match libfreemkv::scsi::inquiry(&mut transport) { + Ok(i) => i, + Err(e) => { + eprintln!("Error: INQUIRY failed: {}", e); + process::exit(1); + } + }; + + // GET CONFIGURATION feature 0x010C + let gc_010c = libfreemkv::scsi::get_config_010c(&mut transport).ok(); + + if json_mode { + print_json(&inquiry, &gc_010c); + } else if raw_mode { + print_raw(&inquiry, &gc_010c); + } else { + print_human(&inquiry, &gc_010c, profiles_dir); + } +} + +fn print_human( + inquiry: &libfreemkv::scsi::InquiryResult, + gc_010c: &Option>, + profiles_dir: &str, +) { + println!("freemkv-info v{}", env!("CARGO_PKG_VERSION")); + println!(); + println!("Drive: {} {} {}", inquiry.vendor_id, inquiry.model, inquiry.firmware); + println!("INQUIRY: additional_length=0x{:02X} ({})", + inquiry.raw.get(4).unwrap_or(&0), + inquiry.raw.get(4).unwrap_or(&0)); + + if let Some(gc) = gc_010c { + let data_hex: String = gc.iter().map(|b| format!("{:02x}", b)).collect(); + println!("Feature 0x010C: {}", data_hex); + } else { + println!("Feature 0x010C: not available"); + } + + // Try to match profile + if let Ok(profiles) = libfreemkv::profile::load_all(Path::new(profiles_dir)) { + let matched = profiles.iter().find(|p| { + p.drive_id.contains(&inquiry.vendor_id) + && p.drive_id.contains(&inquiry.model) + }); + + println!(); + match matched { + Some(p) => { + println!("Profile: FOUND ({})", p.platform.name()); + println!("Raw Read: Supported"); + } + None => { + println!("Profile: NOT FOUND"); + println!("Raw Read: Unknown — run with --raw and submit a profile request"); + } + } + } else { + println!(); + println!("Profile: No profiles directory found at '{}'", profiles_dir); + } +} + +fn print_raw( + inquiry: &libfreemkv::scsi::InquiryResult, + gc_010c: &Option>, +) { + println!("# freemkv-info raw output"); + println!("# Submit this file to https://github.com/freemkv/libfreemkv/issues"); + println!(); + println!("vendor: {}", inquiry.vendor_id); + println!("model: {}", inquiry.model); + println!("firmware: {}", inquiry.firmware); + println!(); + + // Full INQUIRY hex + println!("inquiry_hex: {}", hex_encode(&inquiry.raw)); + println!("inquiry_length: {}", inquiry.raw.len()); + + // GET CONFIG 0x010C + if let Some(gc) = gc_010c { + println!("get_config_010c_hex: {}", hex_encode(gc)); + println!("get_config_010c_length: {}", gc.len()); + } else { + println!("get_config_010c_hex: ERROR"); + } +} + +fn print_json( + inquiry: &libfreemkv::scsi::InquiryResult, + gc_010c: &Option>, +) { + let json = serde_json::json!({ + "vendor": inquiry.vendor_id, + "model": inquiry.model, + "firmware": inquiry.firmware, + "inquiry_hex": hex_encode(&inquiry.raw), + "inquiry_length": inquiry.raw.len(), + "get_config_010c_hex": gc_010c.as_ref().map(|g| hex_encode(g)), + }); + println!("{}", serde_json::to_string_pretty(&json).unwrap()); +} + +fn hex_encode(data: &[u8]) -> String { + data.iter().map(|b| format!("{:02x}", b)).collect() +} diff --git a/src/bin/freemkv_test.rs b/src/bin/freemkv_test.rs new file mode 100644 index 0000000..decb67c --- /dev/null +++ b/src/bin/freemkv_test.rs @@ -0,0 +1,99 @@ +//! freemkv-test — Quick verification that raw disc access works. +//! +//! Enables raw read mode, calibrates speed, reads a few test sectors. +//! Use this to verify your drive and profile are working correctly. +//! +//! Usage: +//! freemkv-test /dev/sr0 +//! freemkv-test /dev/sr0 --profiles ./profiles + +use std::env; +use std::path::Path; +use std::process; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + eprintln!("freemkv-test — Verify raw disc access works"); + eprintln!(); + eprintln!("Usage: freemkv-test [--profiles ]"); + process::exit(1); + } + + let device = Path::new(&args[1]); + + println!("freemkv-test v{}", env!("CARGO_PKG_VERSION")); + println!(); + + // Open drive session (uses bundled profiles) + print!("Opening {}... ", device.display()); + let mut session = match libfreemkv::DriveSession::open(device) { + Ok(s) => { println!("OK"); s } + Err(e) => { println!("FAILED: {}", e); process::exit(1); } + }; + + println!(" Drive ID: {}", session.profile.drive_id); + println!(" Platform: {}", session.profile.platform.name()); + println!(); + + // Enable raw read mode + print!("Unlocking drive... "); + match session.unlock() { + Ok(()) => println!("OK"), + Err(e) => { println!("FAILED: {}", e); process::exit(1); } + } + + // Check status + print!("Checking status... "); + match session.status() { + Ok(status) => { + if status.unlocked { + println!("OK (active)"); + } else { + println!("WARNING: drive reported as locked"); + } + } + Err(e) => println!("SKIP ({})", e), + } + + // Calibrate speed + print!("Calibrating speed... "); + match session.calibrate() { + Ok(()) => println!("OK"), + Err(e) => println!("SKIP ({})", e), + } + + // Read test sectors + let test_lbas: &[u32] = &[0, 100, 1000, 10000]; + let mut buf = vec![0u8; 2048]; + let mut pass = 0; + let mut fail = 0; + + for &lba in test_lbas { + print!("Reading sector {}... ", lba); + match session.read_sectors(lba, 1, &mut buf) { + Ok(n) if n == 2048 => { + let nonzero = buf.iter().filter(|&&b| b != 0).count(); + println!("OK ({} bytes, {} non-zero)", n, nonzero); + pass += 1; + } + Ok(n) => { + println!("PARTIAL ({} bytes)", n); + fail += 1; + } + Err(e) => { + println!("FAILED: {}", e); + fail += 1; + } + } + } + + println!(); + if fail == 0 { + println!("All {} checks passed. Drive is fully functional.", pass); + } else { + println!("{} passed, {} failed.", pass, fail); + process::exit(1); + } +} diff --git a/src/drive.rs b/src/drive.rs new file mode 100644 index 0000000..8d0a5d4 --- /dev/null +++ b/src/drive.rs @@ -0,0 +1,125 @@ +//! High-level drive session — the main API for consumers. +//! +//! Opens a drive, identifies it via standard SCSI commands, +//! matches it against the profile database, and provides +//! raw disc access methods. + +use std::path::Path; +use crate::error::{Error, Result}; +use crate::scsi::{SgIoTransport, ScsiTransport}; +use crate::identity::DriveId; +use crate::profile::{self, DriveProfile, PlatformType}; +use crate::platform::{Platform, DriveStatus}; +use crate::platform::mt1959::Mt1959; + +/// A complete drive session. +/// +/// Handles: identify → match profile → create platform → execute commands. +pub struct DriveSession { + scsi: Box, + platform: Box, + pub profile: DriveProfile, + pub drive_id: DriveId, +} + +impl DriveSession { + /// Open a drive, identify it, and find the matching profile. + /// Uses the bundled profile database — no external files needed. + pub fn open(device: &Path) -> Result { + let mut transport = SgIoTransport::open(device)?; + let profiles = profile::load_bundled()?; + + // Identify drive via standard SCSI commands + // SPC-4 §6.4 (INQUIRY) + MMC-6 §5.3.10 (Feature 010Ch) + let drive_id = DriveId::from_drive(&mut transport)?; + + // Match drive to a profile by INQUIRY fields + let profile = profile::find_by_drive_id(&profiles, &drive_id) + .cloned() + .ok_or_else(|| Error::UnsupportedDrive(format!("{}", drive_id)))?; + + if !profile.supported { + return Err(Error::UnsupportedDrive(format!( + "{} — status: {:?}", drive_id, profile.status + ))); + } + + let platform: Box = match profile.platform { + PlatformType::Mt1959A | PlatformType::Mt1959B => { + Box::new(Mt1959::new(profile.clone())) + } + PlatformType::Pioneer => { + return Err(Error::UnsupportedDrive("Pioneer not yet implemented".into())); + } + }; + + Ok(DriveSession { + scsi: Box::new(transport), + platform, + profile, + drive_id, + }) + } + + /// Open with an explicit profile (skip auto-detection). + pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result { + let mut transport = SgIoTransport::open(device)?; + let drive_id = DriveId::from_drive(&mut transport)?; + + let platform: Box = match profile.platform { + PlatformType::Mt1959A | PlatformType::Mt1959B => { + Box::new(Mt1959::new(profile.clone())) + } + PlatformType::Pioneer => { + return Err(Error::UnsupportedDrive("Pioneer not yet implemented".into())); + } + }; + + Ok(DriveSession { + scsi: Box::new(transport), + platform, + profile, + drive_id, + }) + } + + /// Activate raw disc access mode. + pub fn unlock(&mut self) -> Result<()> { + self.platform.unlock(self.scsi.as_mut()) + } + + /// Check if raw disc access mode is enabled. + pub fn is_unlocked(&self) -> bool { + self.platform.is_unlocked() + } + + /// Read drive status and feature flags. + pub fn status(&mut self) -> Result { + self.platform.status(self.scsi.as_mut()) + } + + /// Read drive configuration block. + pub fn read_config(&mut self) -> Result> { + self.platform.read_config(self.scsi.as_mut()) + } + + /// Read hardware register. + pub fn read_register(&mut self, index: u8) -> Result<[u8; 16]> { + self.platform.read_register(self.scsi.as_mut(), index) + } + + /// Calibrate read speed for the current disc. + pub fn calibrate(&mut self) -> Result<()> { + self.platform.calibrate(self.scsi.as_mut()) + } + + /// Read raw disc sectors. + pub fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { + self.platform.read_sectors(self.scsi.as_mut(), lba, count, buf) + } + + /// Generic probe command. + pub fn probe(&mut self, sub_cmd: u8, address: u32, length: u32) -> Result> { + self.platform.probe(self.scsi.as_mut(), sub_cmd, address, length) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..696d907 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,46 @@ +use std::fmt; + +#[derive(Debug)] +pub enum Error { + DeviceNotFound(String), + UnsupportedDrive(String), + ScsiError { cdb: Vec, status: u8, sense: Vec }, + UnlockFailed(String), + NotUnlocked, + NotCalibrated, + ProfileNotFound(String), + ProfileParse(String), + SignatureMismatch { expected: [u8; 4], got: [u8; 4] }, + Io(std::io::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::DeviceNotFound(s) => write!(f, "device not found: {s}"), + Error::UnsupportedDrive(s) => write!(f, "unsupported drive: {s}"), + Error::ScsiError { status, .. } => write!(f, "SCSI error: status 0x{status:02x}"), + Error::UnlockFailed(s) => write!(f, "unlock failed: {s}"), + Error::NotUnlocked => write!(f, "drive not unlocked, call unlock() first"), + Error::NotCalibrated => write!(f, "speed not calibrated, call calibrate() first"), + Error::ProfileNotFound(s) => write!(f, "no profile for: {s}"), + Error::ProfileParse(s) => write!(f, "profile parse error: {s}"), + Error::SignatureMismatch { expected, got } => { + write!(f, "signature mismatch: expected {:02x}{:02x}{:02x}{:02x}, got {:02x}{:02x}{:02x}{:02x}", + expected[0], expected[1], expected[2], expected[3], + got[0], got[1], got[2], got[3]) + } + Error::Io(e) => write!(f, "I/O error: {e}"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +pub type Result = std::result::Result; diff --git a/src/identity.rs b/src/identity.rs new file mode 100644 index 0000000..1f16c9f --- /dev/null +++ b/src/identity.rs @@ -0,0 +1,152 @@ +//! Drive identification — match drives to profiles by SCSI response fields. +//! +//! Field names follow SPC-4 (INQUIRY) and MMC-6 (GET CONFIGURATION) standards. +//! No proprietary fingerprints or encrypted lookups — open matching only. +//! +//! References: +//! SPC-4 §6.4.2 — Standard INQUIRY data +//! MMC-6 §5.3.10 — Feature 010Ch (Firmware Information) + +use crate::error::Result; +use crate::scsi::{ScsiTransport, DataDirection}; + +/// Drive identity from standard SCSI commands. +/// +/// All field names follow the SCSI standards: +/// - SPC-4 §6.4.2 for INQUIRY fields +/// - MMC-6 §5.3.10 for Firmware Information +#[derive(Debug, Clone)] +pub struct DriveId { + /// T10 VENDOR IDENTIFICATION — INQUIRY bytes [8:16] + /// SPC-4 §6.4.2 + pub vendor_id: String, + + /// PRODUCT IDENTIFICATION — INQUIRY bytes [16:32] + /// SPC-4 §6.4.2 + pub product_id: String, + + /// PRODUCT REVISION LEVEL — INQUIRY bytes [32:36] + /// SPC-4 §6.4.2 + pub product_revision: String, + + /// VENDOR SPECIFIC — INQUIRY bytes [36:43] + /// SPC-4 §6.4.2 + /// Content varies by vendor: firmware type code (MTK), date (Pioneer), etc. + pub vendor_specific: String, + + /// Firmware Creation Date — GET CONFIGURATION Feature 010Ch + /// MMC-6 §5.3.10 + /// Format: CCYYMMDDHHMI (12 ASCII characters) + pub firmware_date: String, + + /// Raw 96-byte INQUIRY response for additional parsing if needed. + pub raw_inquiry: Vec, +} + +impl DriveId { + /// Probe a real drive via SCSI and build its identity. + pub fn from_drive(transport: &mut dyn ScsiTransport) -> Result { + // INQUIRY — SPC-4 §6.4 + let mut inquiry = vec![0u8; 96]; + let cdb_inq = [0x12, 0x00, 0x00, 0x00, 0x60, 0x00]; + transport.execute(&cdb_inq, DataDirection::FromDevice, &mut inquiry, 5000)?; + + // GET CONFIGURATION Feature 010Ch — MMC-6 §6.6 + let mut gc = vec![0u8; 256]; + let cdb_gc = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]; + let result = transport.execute(&cdb_gc, DataDirection::FromDevice, &mut gc, 5000)?; + + let firmware_date = if result.bytes_transferred > 12 { + String::from_utf8_lossy(&gc[12..24.min(result.bytes_transferred)]) + .trim().to_string() + } else { + String::new() + }; + + Ok(Self::from_inquiry(&inquiry, &firmware_date)) + } + + /// Build identity from raw INQUIRY bytes and firmware date string. + pub fn from_inquiry(inquiry: &[u8], firmware_date: &str) -> Self { + DriveId { + vendor_id: ascii_field(inquiry, 8, 16), + product_id: ascii_field(inquiry, 16, 32), + product_revision: ascii_field(inquiry, 32, 36), + vendor_specific: ascii_field(inquiry, 36, 43), + firmware_date: firmware_date.to_string(), + raw_inquiry: inquiry.to_vec(), + } + } + + /// Profile match key: "VENDOR|PRODUCT|REVISION|VENDOR_SPECIFIC" + /// + /// Used to look up this drive in the profile database. + /// All fields trimmed for consistent matching. + pub fn match_key(&self) -> String { + format!("{}|{}|{}|{}", + self.vendor_id.trim(), + self.product_id.trim(), + self.product_revision.trim(), + self.vendor_specific.trim()) + } +} + +impl std::fmt::Display for DriveId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {} {} {}", + self.vendor_id.trim(), + self.product_id.trim(), + self.product_revision.trim(), + self.vendor_specific.trim()) + } +} + +/// Extract an ASCII string field from raw SCSI data. +fn ascii_field(data: &[u8], start: usize, end: usize) -> String { + if data.len() > start { + let e = end.min(data.len()); + String::from_utf8_lossy(&data[start..e]).to_string() + } else { + String::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bu40n_identity() { + let mut inquiry = vec![0u8; 96]; + inquiry[4] = 0x5B; + inquiry[8..16].copy_from_slice(b"HL-DT-ST"); + inquiry[16..32].copy_from_slice(b"BD-RE BU40N "); + inquiry[32..36].copy_from_slice(b"1.03"); + inquiry[36..43].copy_from_slice(b"NM00000"); + + let id = DriveId::from_inquiry(&inquiry, "211810241934"); + assert_eq!(id.vendor_id.trim(), "HL-DT-ST"); + assert_eq!(id.product_id.trim(), "BD-RE BU40N"); + assert_eq!(id.product_revision.trim(), "1.03"); + assert_eq!(id.vendor_specific.trim(), "NM00000"); + assert_eq!(id.firmware_date, "211810241934"); + assert_eq!(id.match_key(), "HL-DT-ST|BD-RE BU40N|1.03|NM00000"); + } + + #[test] + fn test_pioneer_identity() { + let mut inquiry = vec![0u8; 96]; + inquiry[4] = 0x5B; + inquiry[8..16].copy_from_slice(b"PIONEER "); + inquiry[16..32].copy_from_slice(b"BD-RW BDR-S09 "); + inquiry[32..36].copy_from_slice(b"1.34"); + inquiry[36..43].copy_from_slice(b" 16/04/"); + + let id = DriveId::from_inquiry(&inquiry, "201604250000"); + assert_eq!(id.vendor_id.trim(), "PIONEER"); + assert_eq!(id.product_id.trim(), "BD-RW BDR-S09"); + assert_eq!(id.product_revision.trim(), "1.34"); + assert_eq!(id.vendor_specific.trim(), "16/04/"); + assert_eq!(id.firmware_date, "201604250000"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e0aad72 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,52 @@ +//! libfreemkv — Open source raw disc access for optical drives. +//! +//! Provides SCSI/MMC commands to enable raw reading mode on compatible +//! Blu-ray drives, allowing direct sector access for disc archival +//! and backup purposes. +//! +//! # Architecture +//! +//! The library is data-driven. Drive-specific SCSI command sequences +//! are stored in profile files, not in code. Adding support for a new +//! drive requires only a profile contribution — no rebuild needed. +//! +//! ```text +//! DriveSession (high-level API) +//! ├── Platform trait (per-chipset unlock logic) +//! ├── DriveProfile (per-drive data from JSON profiles) +//! └── ScsiTransport (SG_IO on Linux, IOKit on macOS) +//! ``` +//! +//! # Quick Start +//! +//! ```no_run +//! use libfreemkv::DriveSession; +//! use std::path::Path; +//! +//! let mut session = DriveSession::open( +//! Path::new("/dev/sr0"), +//! Path::new("profiles/"), +//! ).unwrap(); +//! +//! session.enable().unwrap(); +//! session.calibrate().unwrap(); +//! +//! let mut buf = vec![0u8; 2048]; +//! let n = session.read_sectors(0, 1, &mut buf).unwrap(); +//! ``` + +pub mod error; +pub mod scsi; +pub mod profile; +pub mod platform; +pub mod drive; +pub mod identity; +pub mod speed; + +pub use error::{Error, Result}; +pub use drive::DriveSession; +pub use identity::DriveId; +pub use profile::{DriveProfile, PlatformType}; +pub use platform::{Platform, DriveStatus}; +pub use scsi::ScsiTransport; +pub use speed::DriveSpeed; diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..3040a69 --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1,66 @@ +//! Platform-specific implementations of raw disc access commands. +//! +//! Each chipset family (MT1959, Pioneer) implements the Platform trait. +//! accessed via SCSI READ BUFFER with platform-specific mode and buffer ID. + +pub mod mt1959; + +use crate::error::Result; +use crate::scsi::ScsiTransport; + +/// Platform trait — raw disc access commands implemented per chipset. +/// +/// Command handlers accessed via READ BUFFER: +pub trait Platform { + /// + /// Sends the platform-specific READ BUFFER CDB and verifies + /// the response signature bytes. + fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>; + + /// + /// Performs a primary READ BUFFER for the configuration data, + /// followed by a secondary 4-byte status read. + fn read_config(&mut self, scsi: &mut dyn ScsiTransport) -> Result>; + + /// Handlers 2-3: Read hardware register. + /// + /// `index` selects which register offset from the profile to use. + /// Returns 16 bytes of register data extracted from a 36-byte response. + fn read_register(&mut self, scsi: &mut dyn ScsiTransport, index: u8) -> Result<[u8; 16]>; + + /// + /// Probes the disc surface via READ BUFFER sub-commands to build + /// a 64-entry speed lookup table for optimal read performance. + /// Issues SET CD SPEED at maximum after calibration completes. + fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>; + + /// + /// Periodic command to maintain the raw access session. + fn keepalive(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>; + + /// + /// Verifies the response signature and returns 16 bytes of + /// feature/status data. + fn status(&mut self, scsi: &mut dyn ScsiTransport) -> Result; + + /// + /// Sends a READ BUFFER command with dynamic sub-command, address, + /// and length. Used for disc structure reads and feature queries. + fn probe(&mut self, scsi: &mut dyn ScsiTransport, sub_cmd: u8, address: u32, length: u32) -> Result>; + + /// + /// Looks up the LBA in the speed table, issues SET CD SPEED, + /// then performs a READ(10) with the raw read flag (0x08). + fn read_sectors(&mut self, scsi: &mut dyn ScsiTransport, lba: u32, count: u16, buf: &mut [u8]) -> Result; + + fn timing(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>; + + /// Check if raw disc access mode is currently enabled. + fn is_unlocked(&self) -> bool; +} + +#[derive(Debug, Clone)] +pub struct DriveStatus { + pub unlocked: bool, + pub features: [u8; 16], +} diff --git a/src/platform/mt1959.rs b/src/platform/mt1959.rs new file mode 100644 index 0000000..b18ed05 --- /dev/null +++ b/src/platform/mt1959.rs @@ -0,0 +1,329 @@ +//! MT1959 platform implementation — covers all LG/ASUS MediaTek drives. +//! +//! Two variants share this code: +//! MT1959-A: mode=0x01, buffer_id=0x44 (handlers 0-9) +//! MT1959-B: mode=0x02, buffer_id=0x77 (handlers 4-9, 0-3 are no-ops) +//! +//! The logic is identical between A and B — only the SCSI READ BUFFER +//! mode and buffer ID differ. Per-drive data (signature, register offsets) +//! comes from the profile. + +use crate::error::{Error, Result}; +use crate::profile::DriveProfile; +use crate::scsi::{self, DataDirection, ScsiTransport}; +use super::{Platform, DriveStatus}; + +/// MT1959 driver state. +pub struct Mt1959 { + profile: DriveProfile, + mode: u8, + buffer_id: u8, + unlocked: bool, + speed_table: [u16; 64], + calibrated: bool, +} + +impl Mt1959 { + pub fn new(profile: DriveProfile) -> Self { + let mode = profile.platform.mode(); + let buffer_id = profile.platform.buffer_id(); + Mt1959 { + profile, + mode, + buffer_id, + unlocked: false, + speed_table: [0u16; 64], + calibrated: false, + } + } + + /// Build a READ BUFFER CDB for this platform's mode and buffer ID. + fn read_buffer_cdb(&self, offset: u32, length: u32) -> [u8; 10] { + scsi::build_read_buffer(self.mode, self.buffer_id, offset, length) + } + + /// Build a READ BUFFER CDB with a sub-command byte in CDB[3]. + fn read_buffer_sub(&self, sub_cmd: u8, address: u16, length: u8) -> [u8; 10] { + [ + 0x3C, + self.mode, + self.buffer_id, + sub_cmd, + (address >> 8) as u8, + address as u8, + 0x00, + 0x00, + length, + 0x00, + ] + } + + /// + /// 1. Send READ BUFFER(mode, buffer_id, offset=0, length=64) + /// 2. Check response[0:4] matches the profile signature + /// 3. Check response[12:16] matches the verification bytes (0x4D4D6B76) + fn do_unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<[u8; 64]> { + let cdb = self.read_buffer_cdb(0, 64); + let mut response = [0u8; 64]; + scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?; + + // Check signature at response[0:4] + let got_sig: [u8; 4] = response[0..4].try_into().unwrap(); + if got_sig != self.profile.signature { + return Err(Error::SignatureMismatch { + expected: self.profile.signature, + got: got_sig, + }); + } + + // Check verification bytes at response[12:16] + if &response[12..16] != self.profile.verify.as_slice() { + return Err(Error::UnlockFailed(format!( + "verify mismatch at [12:16]: {:02x}{:02x}{:02x}{:02x}", + response[12], response[13], response[14], response[15] + ))); + } + + self.unlocked = true; + Ok(response) + } + + /// Ensure raw disc access is active, re-enabling if needed. + fn ensure_unlocked(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { + if !self.unlocked { + self.do_unlock(scsi)?; + } + Ok(()) + } + + /// Pre-operation validation with retry. + /// + /// Sends a short READ BUFFER probe, retries up to 5 times to confirm + /// the drive is still responding to commands. + fn validate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { + for _attempt in 0..5 { + let cdb = self.read_buffer_cdb(0, 4); + let mut resp = [0u8; 4]; + match scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000) { + Ok(_) => return Ok(()), + Err(_) => continue, + } + } + Err(Error::ScsiError { + cdb: vec![0x3C], + status: 0xFF, + sense: vec![], + }) + } + + /// Look up optimal read speed for a given LBA from the calibration table. + fn lookup_speed(&self, lba: u32) -> u16 { + if !self.calibrated { + return 0; + } + let mut best_speed = 0u16; + let mut best_diff = u32::MAX; + for &entry in &self.speed_table { + if entry == 0 { + continue; + } + let entry_lba = entry as u32; + let diff = if lba > entry_lba { lba - entry_lba } else { entry_lba - lba }; + if diff < best_diff { + best_diff = diff; + best_speed = entry; + } + } + best_speed + } + + /// Send SET CD SPEED command. + fn set_cd_speed(&self, scsi: &mut dyn ScsiTransport, speed: u16) -> Result<()> { + let cdb = scsi::build_set_cd_speed(speed); + let mut dummy = [0u8; 0]; + scsi.execute(&cdb, DataDirection::None, &mut dummy, 5_000)?; + Ok(()) + } +} + +impl Platform for Mt1959 { + fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { + self.do_unlock(scsi)?; + Ok(()) + } + + /// + /// Primary read: 0x760 (1888) bytes of configuration data. + /// Secondary read: 4-byte status appended to the result. + fn read_config(&mut self, scsi: &mut dyn ScsiTransport) -> Result> { + // Primary config read: 0x760 = 1888 bytes + let cdb = self.read_buffer_cdb(0, 0x760); + let mut buf = vec![0u8; 0x760]; + let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 30_000)?; + buf.truncate(result.bytes_transferred); + + // Secondary: 4-byte status read + let cdb2 = self.read_buffer_cdb(0, 4); + let mut status = [0u8; 4]; + scsi.execute(&cdb2, DataDirection::FromDevice, &mut status, 5_000)?; + + buf.extend_from_slice(&status); + Ok(buf) + } + + /// Handlers 2-3: Read hardware register at the profile-specified offset. + /// + /// Reads 36 bytes via READ BUFFER and extracts bytes [4:20] as the + /// 16-byte register value. + fn read_register(&mut self, scsi: &mut dyn ScsiTransport, index: u8) -> Result<[u8; 16]> { + self.ensure_unlocked(scsi)?; + self.validate(scsi)?; + + let offset = *self.profile.register_offsets.get(index as usize) + .ok_or_else(|| Error::ScsiError { + cdb: vec![], + status: 0, + sense: vec![], + })?; + + let cdb = scsi::build_read_buffer(self.mode, self.buffer_id, offset, 36); + let mut response = [0u8; 36]; + scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?; + + let mut out = [0u8; 16]; + out.copy_from_slice(&response[4..20]); + Ok(out) + } + + /// + /// Scans disc surface addresses via READ BUFFER sub-command 0x14 to + /// build a 64-entry speed lookup table. Issues SET CD SPEED(max) when done. + fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { + self.ensure_unlocked(scsi)?; + self.validate(scsi)?; + + // Initial probe: READ BUFFER sub_cmd=0x12 + let cdb = self.read_buffer_sub(0x12, 0, 4); + let mut resp = [0u8; 4]; + let _ = scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000); + + self.validate(scsi)?; + + // Clear speed table + self.speed_table = [0u16; 64]; + + // Scan disc surface — probe addresses up to 0x10000, 256 at a time + let mut table_idx = 0usize; + let mut addr: u32 = 0; + while addr < 0x10000 && table_idx < 64 { + let cdb = self.read_buffer_sub(0x14, addr as u16, 4); + let mut resp = [0u8; 4]; + match scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000) { + Ok(r) if r.bytes_transferred == 4 => { + let val = resp[0]; + if val > 0 { + let speed_entry = ((resp[0] as u16) << 8) | (resp[1] as u16); + if speed_entry > 0 { + self.speed_table[table_idx] = speed_entry; + table_idx += 1; + } + } + addr += 256; + } + _ => { + addr += 256; + } + } + } + + // Set max speed after calibration + self.set_cd_speed(scsi, 0xFFFF)?; + + self.calibrated = true; + Ok(()) + } + + fn keepalive(&mut self, _scsi: &mut dyn ScsiTransport) -> Result<()> { + Ok(()) + } + + /// + /// Sends READ BUFFER with sub-command 0x13, reads 36 bytes. + /// Checks signature at [0:4], returns feature data from [4:20]. + fn status(&mut self, scsi: &mut dyn ScsiTransport) -> Result { + self.ensure_unlocked(scsi)?; + self.validate(scsi)?; + + // READ BUFFER with sub_cmd=0x13, 36 bytes response + let cdb = self.read_buffer_sub(0x13, 0, 36); + let mut response = [0u8; 36]; + scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?; + + // Verify response signature + let got_sig = u32::from_be_bytes(response[0..4].try_into().unwrap()); + let expected_sig = u32::from_le_bytes(self.profile.signature); + + let mut features = [0u8; 16]; + features.copy_from_slice(&response[4..20]); + + Ok(DriveStatus { + unlocked: got_sig == expected_sig, + features, + }) + } + + fn probe(&mut self, scsi: &mut dyn ScsiTransport, sub_cmd: u8, address: u32, length: u32) -> Result> { + let cdb = [ + 0x3C, + self.mode, + self.buffer_id, + sub_cmd, + (address >> 16) as u8, + (address >> 8) as u8, + address as u8, + (length >> 16) as u8, + (length >> 8) as u8, + length as u8, + ]; + let mut buf = vec![0u8; length as usize]; + let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 30_000)?; + buf.truncate(result.bytes_transferred); + Ok(buf) + } + + /// + /// Looks up the LBA in the speed table, issues SET CD SPEED if calibrated, + /// then performs READ(10) with the raw read flag (0x08). + fn read_sectors( + &mut self, + scsi: &mut dyn ScsiTransport, + lba: u32, + count: u16, + buf: &mut [u8], + ) -> Result { + if !self.unlocked { + return Err(Error::NotUnlocked); + } + + // Speed optimization from calibration + if self.calibrated { + let speed = self.lookup_speed(lba); + if speed > 0 { + let _ = self.set_cd_speed(scsi, speed); + } + } + + // READ(10) with raw flag 0x08 + let cdb = scsi::build_read10_raw(lba, count); + let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 30_000)?; + Ok(result.bytes_transferred) + } + + fn timing(&mut self, _scsi: &mut dyn ScsiTransport) -> Result<()> { + Ok(()) + } + + fn is_unlocked(&self) -> bool { + self.unlocked + } +} diff --git a/src/profile.rs b/src/profile.rs new file mode 100644 index 0000000..68eb448 --- /dev/null +++ b/src/profile.rs @@ -0,0 +1,337 @@ +//! Drive profile loading and matching. +//! +//! Each supported drive has a profile containing the SCSI command +//! parameters needed to enable raw disc access mode. Profiles are +//! loaded from JSON files so new drives can be added without rebuilding. + +use serde::Deserialize; +use crate::error::{Error, Result}; + +/// Per-drive profile containing SCSI parameters for raw disc access. +#[derive(Debug, Clone, Deserialize)] +pub struct DriveProfile { + /// Drive vendor from INQUIRY[8:16] (e.g. "HL-DT-ST") + #[serde(default)] + pub vendor_id: String, + + /// Drive product (devtype) from INQUIRY product field (e.g. "BD-RE") + #[serde(default)] + pub product_id: String, + + /// Firmware revision from INQUIRY[32:36] (e.g. "1.03") + #[serde(default)] + pub product_revision: String, + + /// Firmware type from INQUIRY[36:43] (e.g. "NM00000") + #[serde(default)] + pub vendor_specific: String, + + /// Firmware build date from GET_CONFIG 010C (e.g. "211810241934") + #[serde(default)] + pub firmware_date: String, + + /// Chipset platform type determining the READ BUFFER variant. + #[serde(default)] + pub platform: PlatformType, + + /// Whether this drive supports raw disc access mode. + #[serde(default)] + pub supported: bool, + + /// Current readiness status of this drive. + #[serde(default)] + pub status: ReadinessStatus, + + /// Drive identifier string from the profile database. + #[serde(default)] + pub drive_id: String, + + /// Profile version string. + #[serde(default)] + pub drive_version: String, + + /// Expected response signature bytes [0:4] from the enable command. + #[serde(default, deserialize_with = "deserialize_hex4")] + pub signature: [u8; 4], + + /// Expected verification bytes [12:16] from the enable response. + #[serde(skip, default = "default_verify")] + pub verify: [u8; 4], + + /// 10-byte READ BUFFER CDB used to enable raw disc access. + #[serde(default, deserialize_with = "deserialize_hex_vec")] + pub unlock_cdb: Vec, + + /// Register read offsets (bytes 3-5 of READ BUFFER CDB). + #[serde(default)] + pub register_offsets: Vec, + + /// Drive supports reading DVDs regardless of region code. + #[serde(default)] + pub dvd_all_regions: bool, + + /// Drive supports raw Blu-ray sector reads. + #[serde(default)] + pub bd_raw_read: bool, + + /// Drive supports raw Blu-ray metadata reads. + #[serde(default)] + pub bd_raw_metadata: bool, + + /// Drive supports unrestricted read speed. + #[serde(default)] + pub unrestricted_speed: bool, +} + +fn default_verify() -> [u8; 4] { + *b"MMkv" +} + +/// Chipset platform type. Determines the READ BUFFER mode and buffer ID. +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub enum PlatformType { + /// MediaTek MT1959 variant A: mode=0x01, buffer_id=0x44. + #[serde(rename = "mt1959_a")] + Mt1959A, + /// MediaTek MT1959 variant B: mode=0x02, buffer_id=0x77. + #[serde(rename = "mt1959_b")] + Mt1959B, + /// Pioneer chipset (not yet implemented). + #[serde(rename = "pioneer")] + Pioneer, +} + +impl Default for PlatformType { + fn default() -> Self { + PlatformType::Mt1959A + } +} + +/// Readiness status of a drive for raw disc access. +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub enum ReadinessStatus { + /// Drive is ready — raw disc access can be enabled. + Ready, + /// Drive firmware needs an update before raw access is possible. + NeedsFirmwareUpdate, + /// Drive uses encrypted commands (not yet supported). + Encrypted, + /// Status unknown. + Unknown, +} + +impl Default for ReadinessStatus { + fn default() -> Self { + ReadinessStatus::Unknown + } +} + +impl PlatformType { + /// Human-readable name for this platform. + pub fn name(&self) -> &'static str { + match self { + PlatformType::Mt1959A => "MT1959-A", + PlatformType::Mt1959B => "MT1959-B", + PlatformType::Pioneer => "Pioneer", + } + } + + /// READ BUFFER mode byte for this chipset platform. + pub fn mode(&self) -> u8 { + match self { + PlatformType::Mt1959A => 0x01, + PlatformType::Mt1959B => 0x02, + PlatformType::Pioneer => 0x01, // TBD + } + } + + /// READ BUFFER buffer ID for this chipset platform. + pub fn buffer_id(&self) -> u8 { + match self { + PlatformType::Mt1959A => 0x44, + PlatformType::Mt1959B => 0x77, + PlatformType::Pioneer => 0x44, // TBD + } + } +} + +/// Parse a hex string like "999ec375" into [u8; 4]. +fn parse_hex4(s: &str) -> Result<[u8; 4]> { + if s.len() != 8 { + return Err(Error::ProfileParse(format!("expected 8 hex chars, got {}", s.len()))); + } + let mut out = [0u8; 4]; + for i in 0..4 { + out[i] = u8::from_str_radix(&s[i*2..i*2+2], 16) + .map_err(|e| Error::ProfileParse(format!("bad hex: {e}")))?; + } + Ok(out) +} + +/// Parse a hex string into a byte vector. +fn parse_hex(s: &str) -> Result> { + if s.len() % 2 != 0 { + return Err(Error::ProfileParse("odd hex length".into())); + } + let mut out = Vec::with_capacity(s.len() / 2); + for i in (0..s.len()).step_by(2) { + out.push(u8::from_str_radix(&s[i..i+2], 16) + .map_err(|e| Error::ProfileParse(format!("bad hex: {e}")))?); + } + Ok(out) +} + +/// Custom serde deserializer for 4-byte hex signature strings. +fn deserialize_hex4<'de, D>(deserializer: D) -> std::result::Result<[u8; 4], D::Error> +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + parse_hex4(&s).map_err(serde::de::Error::custom) +} + +/// Custom serde deserializer for hex-encoded byte vectors. +fn deserialize_hex_vec<'de, D>(deserializer: D) -> std::result::Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + parse_hex(&s).map_err(serde::de::Error::custom) +} + +/// Load a profile from a parsed JSON value. +pub fn load_from_json(json: &serde_json::Value) -> Result { + let vendor = json["vendor_id"].as_str().unwrap_or("").to_string(); + let product = json["product_id"].as_str().unwrap_or("").to_string(); + let revision = json["product_revision"].as_str().unwrap_or("").to_string(); + let firmware_type = json["vendor_specific"].as_str().unwrap_or("").to_string(); + let firmware_date = json["firmware_date"].as_str().unwrap_or("").to_string(); + let program = json["program"].as_str().unwrap_or("unknown"); + + let platform = match program { + "mt1959_a" => PlatformType::Mt1959A, + "mt1959_b" => PlatformType::Mt1959B, + _ => PlatformType::Mt1959A, // default + }; + + let sig_str = json["signature"].as_str().unwrap_or(""); + + // Drive is supported if it has a known program and valid signature + let has_program = matches!(program, "mt1959_a" | "mt1959_b"); + let has_signature = sig_str.len() == 8; + let supported = has_program && has_signature; + + let status = if supported { + ReadinessStatus::Ready + } else if json["status"].as_str() == Some("needs_flash") || program == "none" { + ReadinessStatus::NeedsFirmwareUpdate + } else { + ReadinessStatus::Unknown + }; + let signature = if sig_str.len() == 8 { + parse_hex4(sig_str)? + } else { + [0; 4] + }; + + let unlock_cdb = json["unlock_cdb"].as_str() + .map(|s| parse_hex(s)) + .transpose()? + .unwrap_or_default(); + + let register_offsets = json["register_cdbs"].as_array() + .map(|arr| { + arr.iter().filter_map(|v| { + let s = v.as_str()?; + // CDB format: 3c 01 44 XX XX XX 00 00 24 00 + // Register offset is bytes 3-5 (chars 6-12 in hex) + if s.len() >= 12 { + u32::from_str_radix(&s[6..12], 16).ok() + } else { + None + } + }).collect() + }) + .unwrap_or_default(); + + Ok(DriveProfile { + vendor_id: vendor, + product_id: product, + product_revision: revision, + vendor_specific: firmware_type, + firmware_date, + platform, + supported, + status, + drive_id: json["drive_id"].as_str().unwrap_or("").to_string(), + drive_version: json["drive_version"].as_str().unwrap_or("").to_string(), + signature, + verify: *b"MMkv", + unlock_cdb, + register_offsets, + dvd_all_regions: json["capabilities"]["dvd_all_regions"].as_bool().unwrap_or(false), + bd_raw_read: json["capabilities"]["bd_raw_read"].as_bool().unwrap_or(false), + bd_raw_metadata: json["capabilities"]["bd_raw_metadata"].as_bool().unwrap_or(false), + unrestricted_speed: json["capabilities"]["unrestricted_speed"].as_bool().unwrap_or(false), + }) +} + +/// Bundled profiles — compiled into the binary. +/// Override with load_all() to load from a file instead. +const BUNDLED_PROFILES: &str = include_str!("../profiles.json"); + +/// Load profiles from the bundled database. +pub fn load_bundled() -> Result> { + load_from_str(BUNDLED_PROFILES) +} + +/// Load all profiles from a JSON array file. +pub fn load_all(path: &std::path::Path) -> Result> { + let data = std::fs::read_to_string(path)?; + load_from_str(&data) +} + +/// Parse profiles from a JSON string. +fn load_from_str(data: &str) -> Result> { + let json: serde_json::Value = serde_json::from_str(data) + .map_err(|e| Error::ProfileParse(format!("JSON: {e}")))?; + + let arr = json.as_array() + .ok_or_else(|| Error::ProfileParse("expected array".into()))?; + + let mut profiles = Vec::with_capacity(arr.len()); + for entry in arr { + match load_from_json(entry) { + Ok(p) => profiles.push(p), + Err(_) => continue, // skip malformed entries + } + } + Ok(profiles) +} + +/// Find a profile matching a drive's INQUIRY fields. +/// +/// Matches by vendor + product + revision + vendor_specific (firmware type). +/// All fields trimmed before comparison. +pub fn find_by_drive_id<'a>( + profiles: &'a [DriveProfile], + drive_id: &crate::identity::DriveId, +) -> Option<&'a DriveProfile> { + let v = drive_id.vendor_id.trim(); + let r = drive_id.product_revision.trim(); + let vs = drive_id.vendor_specific.trim(); + + // Match all four INQUIRY fields for precise identification + profiles.iter().find(|p| { + p.vendor_id.trim() == v + && p.product_revision.trim() == r + && p.vendor_specific.trim() == vs + && p.firmware_date.trim() == drive_id.firmware_date.trim() + }) + // Fallback: match without date (for drives where 010C isn't available) + .or_else(|| profiles.iter().find(|p| { + p.vendor_id.trim() == v + && p.product_revision.trim() == r + && p.vendor_specific.trim() == vs + })) +} diff --git a/src/scsi.rs b/src/scsi.rs new file mode 100644 index 0000000..a8e1ea4 --- /dev/null +++ b/src/scsi.rs @@ -0,0 +1,215 @@ +//! SCSI/MMC command interface via Linux SG_IO. + +use crate::error::{Error, Result}; +use std::path::Path; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DataDirection { + None, + FromDevice, + ToDevice, +} + +#[derive(Debug)] +pub struct ScsiResult { + pub status: u8, + pub bytes_transferred: usize, + pub sense: [u8; 32], +} + +/// Low-level SCSI transport. +pub trait ScsiTransport { + fn execute( + &mut self, + cdb: &[u8], + direction: DataDirection, + data: &mut [u8], + timeout_ms: u32, + ) -> Result; +} + +/// Linux SG_IO transport. +pub struct SgIoTransport { + fd: i32, +} + +// SG_IO constants +const SG_IO: libc::c_ulong = 0x2285; +const SG_DXFER_NONE: i32 = -1; +const SG_DXFER_TO_DEV: i32 = -2; +const SG_DXFER_FROM_DEV: i32 = -3; + +#[repr(C)] +#[allow(non_camel_case_types)] +struct sg_io_hdr { + interface_id: i32, + dxfer_direction: i32, + cmd_len: u8, + mx_sb_len: u8, + iovec_count: u16, + dxfer_len: u32, + dxferp: *mut u8, + cmdp: *const u8, + sbp: *mut u8, + timeout: u32, + flags: u32, + pack_id: i32, + usr_ptr: *mut libc::c_void, + status: u8, + masked_status: u8, + msg_status: u8, + sb_len_wr: u8, + host_status: u16, + driver_status: u16, + resid: i32, + duration: u32, + info: u32, +} + +impl SgIoTransport { + pub fn open(device: &Path) -> Result { + use std::os::unix::ffi::OsStrExt; + let path_bytes = device.as_os_str().as_bytes(); + let mut c_path = Vec::with_capacity(path_bytes.len() + 1); + c_path.extend_from_slice(path_bytes); + c_path.push(0); + + let fd = unsafe { libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK) }; + if fd < 0 { + return Err(Error::DeviceNotFound(device.display().to_string())); + } + Ok(SgIoTransport { fd }) + } +} + +impl Drop for SgIoTransport { + fn drop(&mut self) { + unsafe { libc::close(self.fd); } + } +} + +impl ScsiTransport for SgIoTransport { + fn execute( + &mut self, + cdb: &[u8], + direction: DataDirection, + data: &mut [u8], + timeout_ms: u32, + ) -> Result { + let mut sense = [0u8; 32]; + + let dxfer_direction = match direction { + DataDirection::None => SG_DXFER_NONE, + DataDirection::FromDevice => SG_DXFER_FROM_DEV, + DataDirection::ToDevice => SG_DXFER_TO_DEV, + }; + + let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() }; + hdr.interface_id = b'S' as i32; + hdr.dxfer_direction = dxfer_direction; + hdr.cmd_len = cdb.len() as u8; + hdr.mx_sb_len = sense.len() as u8; + hdr.dxfer_len = data.len() as u32; + hdr.dxferp = data.as_mut_ptr(); + hdr.cmdp = cdb.as_ptr(); + hdr.sbp = sense.as_mut_ptr(); + hdr.timeout = timeout_ms; + + let ret = unsafe { + libc::ioctl(self.fd, SG_IO, &mut hdr as *mut sg_io_hdr) + }; + + if ret < 0 { + return Err(Error::Io(std::io::Error::last_os_error())); + } + + let bytes_transferred = (data.len() as i32 - hdr.resid) as usize; + + if hdr.status != 0 { + return Err(Error::ScsiError { + cdb: cdb.to_vec(), + status: hdr.status, + sense: sense[..hdr.sb_len_wr as usize].to_vec(), + }); + } + + Ok(ScsiResult { + status: hdr.status, + bytes_transferred, + sense, + }) + } +} + +/// SCSI INQUIRY response. +#[derive(Debug, Clone)] +pub struct InquiryResult { + pub vendor_id: String, + pub model: String, + pub firmware: String, + pub raw: Vec, +} + +/// Send INQUIRY command and parse the standard response fields. +pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result { + let cdb = [0x12, 0x00, 0x00, 0x00, 0x60, 0x00]; + let mut buf = [0u8; 96]; + scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?; + + let vendor = String::from_utf8_lossy(&buf[8..16]).trim().to_string(); + let model = String::from_utf8_lossy(&buf[16..32]).trim().to_string(); + let firmware = String::from_utf8_lossy(&buf[32..36]).trim().to_string(); + + Ok(InquiryResult { + vendor_id: vendor, + model, + firmware, + raw: buf.to_vec(), + }) +} + +/// Send GET CONFIGURATION for feature 0x010C (drive serial number). +pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result> { + let cdb = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00]; + let mut buf = [0u8; 16]; + scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?; + Ok(buf.to_vec()) +} + +/// Build a READ BUFFER (0x3C) CDB with the given mode, buffer ID, offset, and length. +pub fn build_read_buffer(mode: u8, buffer_id: u8, offset: u32, length: u32) -> [u8; 10] { + [ + 0x3C, // READ BUFFER + mode, + buffer_id, + (offset >> 16) as u8, + (offset >> 8) as u8, + offset as u8, + (length >> 16) as u8, + (length >> 8) as u8, + length as u8, + 0x00, + ] +} + +/// Build a SET CD SPEED (0xBB) CDB with the given read speed. +pub fn build_set_cd_speed(read_speed: u16) -> [u8; 12] { + [ + 0xBB, 0x00, + (read_speed >> 8) as u8, read_speed as u8, + 0xFF, 0xFF, // write speed = max + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] +} + +/// Build a READ(10) CDB with the raw read flag (0x08) set. +pub fn build_read10_raw(lba: u32, count: u16) -> [u8; 10] { + [ + 0x28, 0x08, // READ(10), flag=0x08 (raw) + (lba >> 24) as u8, (lba >> 16) as u8, + (lba >> 8) as u8, lba as u8, + 0x00, + (count >> 8) as u8, count as u8, + 0x00, + ] +} diff --git a/src/speed.rs b/src/speed.rs new file mode 100644 index 0000000..8f6647f --- /dev/null +++ b/src/speed.rs @@ -0,0 +1,125 @@ +//! Drive speed control — query and set read speeds. +//! +//! Uses MMC-6 SET CD SPEED (0xBB) command. +//! Reference: MMC-6 §6.30 + +/// Disc read speed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum DriveSpeed { + /// Blu-ray 1x = 4,500 KB/s + BD1x, + /// Blu-ray 2x = 9,000 KB/s + BD2x, + /// Blu-ray 4x = 18,000 KB/s + BD4x, + /// Blu-ray 6x = 27,000 KB/s + BD6x, + /// Blu-ray 8x = 36,000 KB/s + BD8x, + /// Blu-ray 10x = 45,000 KB/s + BD10x, + /// Blu-ray 12x = 54,000 KB/s + BD12x, + /// DVD 1x = 1,385 KB/s + DVD1x, + /// DVD 2x = 2,770 KB/s + DVD2x, + /// DVD 4x = 5,540 KB/s + DVD4x, + /// DVD 8x = 11,080 KB/s + DVD8x, + /// DVD 16x = 22,160 KB/s + DVD16x, + /// Maximum speed — drive decides + Max, +} + +impl DriveSpeed { + /// Convert to KB/s for MMC-6 SET CD SPEED command. + pub fn to_kbps(self) -> u16 { + match self { + DriveSpeed::BD1x => 4_500, + DriveSpeed::BD2x => 9_000, + DriveSpeed::BD4x => 18_000, + DriveSpeed::BD6x => 27_000, + DriveSpeed::BD8x => 36_000, + DriveSpeed::BD10x => 45_000, + DriveSpeed::BD12x => 54_000, + DriveSpeed::DVD1x => 1_385, + DriveSpeed::DVD2x => 2_770, + DriveSpeed::DVD4x => 5_540, + DriveSpeed::DVD8x => 11_080, + DriveSpeed::DVD16x => 22_160, + DriveSpeed::Max => 0xFFFF, + } + } + + /// Create from KB/s value, rounding to nearest standard speed. + pub fn from_kbps(kbps: u16) -> Self { + match kbps { + 0..=2_000 => DriveSpeed::DVD1x, + 2_001..=4_000 => DriveSpeed::DVD2x, + 4_001..=6_000 => DriveSpeed::BD1x, + 6_001..=13_000 => DriveSpeed::BD2x, + 13_001..=22_000 => DriveSpeed::BD4x, + 22_001..=31_000 => DriveSpeed::BD6x, + 31_001..=40_000 => DriveSpeed::BD8x, + 40_001..=49_000 => DriveSpeed::BD10x, + 49_001..=0xFFFE => DriveSpeed::BD12x, + 0xFFFF => DriveSpeed::Max, + _ => DriveSpeed::Max, + } + } + + /// Human-readable label. + pub fn label(&self) -> &'static str { + match self { + DriveSpeed::BD1x => "BD 1x", + DriveSpeed::BD2x => "BD 2x", + DriveSpeed::BD4x => "BD 4x", + DriveSpeed::BD6x => "BD 6x", + DriveSpeed::BD8x => "BD 8x", + DriveSpeed::BD10x => "BD 10x", + DriveSpeed::BD12x => "BD 12x", + DriveSpeed::DVD1x => "DVD 1x", + DriveSpeed::DVD2x => "DVD 2x", + DriveSpeed::DVD4x => "DVD 4x", + DriveSpeed::DVD8x => "DVD 8x", + DriveSpeed::DVD16x => "DVD 16x", + DriveSpeed::Max => "Max", + } + } + + /// All standard Blu-ray speeds. + pub fn all_bd() -> &'static [DriveSpeed] { + &[DriveSpeed::BD1x, DriveSpeed::BD2x, DriveSpeed::BD4x, + DriveSpeed::BD6x, DriveSpeed::BD8x, DriveSpeed::BD10x, DriveSpeed::BD12x] + } + + /// All standard DVD speeds. + pub fn all_dvd() -> &'static [DriveSpeed] { + &[DriveSpeed::DVD1x, DriveSpeed::DVD2x, DriveSpeed::DVD4x, + DriveSpeed::DVD8x, DriveSpeed::DVD16x] + } +} + +impl std::fmt::Display for DriveSpeed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} ({} KB/s)", self.label(), self.to_kbps()) + } +} + +/// Build SET CD SPEED CDB — MMC-6 §6.30 +pub fn set_cd_speed_cdb(read_speed: DriveSpeed) -> [u8; 12] { + let kbps = read_speed.to_kbps(); + [ + 0xBB, // SET CD SPEED opcode + 0x00, // reserved + (kbps >> 8) as u8, // read speed MSB + kbps as u8, // read speed LSB + 0xFF, // write speed MSB (0xFFFF = don't change) + 0xFF, // write speed LSB + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, // reserved + ] +}