Skip to main content

mediatek_optical/
lib.rs

1//! MediaTek MT1959 / MT1939 optical drive (BD/DVD) protocol: command encoding,
2//! identity decoding, firmware image analysis and OEM image reconstruction.
3//!
4//! The crate root is `no_std`, allocation-free and dependency-free:
5//!
6//! - [`cdb`] — the command descriptor blocks, as byte constructors.
7//! - [`Identity`] — the decoded INQUIRY, boot banner and identity descriptor.
8//! - [`Chip`] — which controller generation a drive or image belongs to.
9//! - [`layout`] — the fixed 2 MiB image geometry and the drive-written regions.
10//! - [`sense`] — sense-data parsing and classification of flash faults.
11//!
12//! Optional features add:
13//!
14//! - `drive` — [`drive`]: the command sequences (identify, memory read, flash
15//!   session) over a caller-supplied [`drive::Transport`].
16//! - `image` — [`image`]: chip detection from image bytes and the AES-CMAC
17//!   integrity table ([`image::cmac`]). Uses `std`.
18//! - `enc` — [`enc`]: the AES-128-ECB transport envelope.
19//! - `oem` — [`oem`]: rebuild the flashable OEM image from a drive read, with
20//!   the provenance of every substituted byte. Implies `image`.
21//! - `std` — links the standard library (implied by `image`).
22//!
23//! ## Drive memory
24//!
25//! READ BUFFER mode 6 exposes the 2 MiB flash at file offsets, with one
26//! exception: the boot page `0x0..0x400` reads back as the image's own page at
27//! [`layout::BOOT_MIRROR`] instead of its stored form. The drive also rewrites
28//! the regions in [`layout::DRIVE_WRITTEN`] at runtime (revocation lists,
29//! settings, calibration), so a read is never byte-identical to the image that
30//! was flashed. [`oem::rebuild`] undoes both.
31#![cfg_attr(not(feature = "std"), no_std)]
32#![deny(missing_docs)]
33#![forbid(unsafe_code)]
34
35pub mod cdb;
36#[cfg(feature = "drive")]
37pub mod drive;
38#[cfg(feature = "enc")]
39pub mod enc;
40#[cfg(feature = "image")]
41pub mod image;
42pub mod layout;
43#[cfg(feature = "oem")]
44pub mod oem;
45
46/// Length of a standard INQUIRY response header, in bytes.
47pub const INQUIRY_LEN: usize = 36;
48
49/// A MediaTek optical controller generation.
50///
51/// Deliberately exhaustive: a new generation is a breaking change, so every
52/// caller that dispatches on the chip must decide how to handle it.
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
54pub enum Chip {
55    /// MediaTek MT1959.
56    Mt1959,
57    /// MediaTek MT1939.
58    Mt1939,
59}
60
61impl Chip {
62    /// Short label, `"MT1959"` or `"MT1939"`.
63    pub const fn label(self) -> &'static str {
64        match self {
65            Chip::Mt1959 => "MT1959",
66            Chip::Mt1939 => "MT1939",
67        }
68    }
69
70    /// Map the two trailing digits of an `MTEKMT19xx` or `MT19xx` tag.
71    pub const fn from_digits(d0: u8, d1: u8) -> Option<Self> {
72        match (d0, d1) {
73            (b'5', b'9') => Some(Chip::Mt1959),
74            (b'3', b'9') => Some(Chip::Mt1939),
75            _ => None,
76        }
77    }
78
79    /// The chip named by the first `MTEKMT19xx` identity tag in `bytes`.
80    ///
81    /// This tag is authoritative. The boot banner names the bootloader
82    /// generation instead: MT1939 parts can carry an `MT1959 Boot` banner.
83    pub fn from_tag(bytes: &[u8]) -> Option<Self> {
84        const PREFIX: &[u8] = b"MTEKMT19";
85        bytes
86            .windows(PREFIX.len() + 2)
87            .filter(|w| w.starts_with(PREFIX))
88            .find_map(|w| Self::from_digits(w[PREFIX.len()], w[PREFIX.len() + 1]))
89    }
90
91    /// The chip named by a boot banner such as `MT1939 Boot JB7`. Weaker than
92    /// [`Chip::from_tag`]; use only when no identity tag exists.
93    pub fn from_banner(banner: &[u8]) -> Option<Self> {
94        banner
95            .windows(6)
96            .filter(|w| w.starts_with(b"MT19"))
97            .find_map(|w| Self::from_digits(w[4], w[5]))
98    }
99}
100
101impl core::fmt::Display for Chip {
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        f.write_str(self.label())
104    }
105}
106
107/// Trim trailing ASCII spaces and NULs and decode as UTF-8; `""` if invalid.
108fn field(raw: &[u8]) -> &str {
109    let start = raw.iter().position(|&b| b != b' ').unwrap_or(raw.len());
110    let raw = &raw[start..];
111    let end = raw
112        .iter()
113        .rposition(|&b| b != b' ' && b != 0)
114        .map_or(0, |i| i + 1);
115    core::str::from_utf8(&raw[..end]).unwrap_or("")
116}
117
118/// A drive's identity: the standard INQUIRY response, the boot banner at
119/// [`layout::BANNER`] and the identity descriptor at [`layout::DESCRIPTOR`].
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct Identity {
122    inquiry: [u8; INQUIRY_LEN],
123    banner: [u8; layout::BANNER.len],
124    descriptor: [u8; layout::DESCRIPTOR.len],
125}
126
127impl Identity {
128    /// Decode an INQUIRY response (at least [`INQUIRY_LEN`] bytes), a boot
129    /// banner and an identity descriptor. Short banner and descriptor reads
130    /// are zero-padded. Returns `None` if the INQUIRY is short.
131    pub fn parse(inquiry: &[u8], banner: &[u8], descriptor: &[u8]) -> Option<Self> {
132        if inquiry.len() < INQUIRY_LEN {
133            return None;
134        }
135        let mut id = Self {
136            inquiry: [0; INQUIRY_LEN],
137            banner: [0; layout::BANNER.len],
138            descriptor: [0; layout::DESCRIPTOR.len],
139        };
140        id.inquiry.copy_from_slice(&inquiry[..INQUIRY_LEN]);
141        let n = banner.len().min(id.banner.len());
142        id.banner[..n].copy_from_slice(&banner[..n]);
143        let n = descriptor.len().min(id.descriptor.len());
144        id.descriptor[..n].copy_from_slice(&descriptor[..n]);
145        Some(id)
146    }
147
148    /// Peripheral device type; `0x05` for an optical drive.
149    pub fn device_type(&self) -> u8 {
150        self.inquiry[0] & 0x1f
151    }
152    /// Vendor identification, e.g. `HL-DT-ST`.
153    pub fn vendor(&self) -> &str {
154        field(&self.inquiry[8..16])
155    }
156    /// Product identification, e.g. `BD-RE BU40N`.
157    pub fn product(&self) -> &str {
158        field(&self.inquiry[16..32])
159    }
160    /// Firmware revision, e.g. `1.00`.
161    pub fn revision(&self) -> &str {
162        field(&self.inquiry[32..36])
163    }
164    /// The boot banner, e.g. `MT1959 Boot BU5`, up to its first NUL or
165    /// non-printable byte.
166    pub fn banner(&self) -> &str {
167        let end = self
168            .banner
169            .iter()
170            .position(|&b| !(0x20..0x7f).contains(&b))
171            .unwrap_or(self.banner.len());
172        field(&self.banner[..end])
173    }
174    /// `true` when the descriptor carries an `MTEKMT19xx` identity tag.
175    pub fn has_descriptor(&self) -> bool {
176        Chip::from_tag(&self.descriptor).is_some()
177    }
178    /// Model from the identity descriptor; `""` without a descriptor.
179    pub fn descriptor_model(&self) -> &str {
180        if self.has_descriptor() {
181            field(&self.descriptor[0x08..0x18])
182        } else {
183            ""
184        }
185    }
186    /// Vendor-specific SKU code from the identity descriptor, e.g. `N003600`;
187    /// `""` without a descriptor.
188    pub fn sku(&self) -> &str {
189        if self.has_descriptor() {
190            field(&self.descriptor[0x1C..0x24])
191        } else {
192            ""
193        }
194    }
195    /// The controller generation: the descriptor's identity tag, else the
196    /// boot banner. `None` for a drive that names neither.
197    pub fn chip(&self) -> Option<Chip> {
198        Chip::from_tag(&self.descriptor).or_else(|| Chip::from_banner(&self.banner))
199    }
200    /// `true` when the banner carries the MT19-family `MT19` signature.
201    pub fn is_mt19(&self) -> bool {
202        self.banner.windows(4).any(|w| w == b"MT19")
203    }
204    /// The raw INQUIRY response.
205    pub fn inquiry_bytes(&self) -> &[u8; INQUIRY_LEN] {
206        &self.inquiry
207    }
208    /// The raw boot banner.
209    pub fn banner_bytes(&self) -> &[u8; layout::BANNER.len] {
210        &self.banner
211    }
212    /// The raw identity descriptor.
213    pub fn descriptor_bytes(&self) -> &[u8; layout::DESCRIPTOR.len] {
214        &self.descriptor
215    }
216}
217
218/// Sense-data parsing and classification.
219pub mod sense {
220    /// Parsed sense: `(key, asc, ascq)`.
221    pub type Triplet = (u8, u8, u8);
222
223    /// Sense key `NOT READY`.
224    pub const NOT_READY: u8 = 0x02;
225    /// Sense key `MEDIUM ERROR`.
226    pub const MEDIUM_ERROR: u8 = 0x03;
227    /// Sense key `HARDWARE ERROR`.
228    pub const HARDWARE_ERROR: u8 = 0x04;
229    /// Sense key `ILLEGAL REQUEST`.
230    pub const ILLEGAL_REQUEST: u8 = 0x05;
231    /// Sense key `ABORTED COMMAND`.
232    pub const ABORTED_COMMAND: u8 = 0x0B;
233
234    /// Parse fixed-format (`0x70`/`0x71`) or descriptor-format (`0x72`/`0x73`)
235    /// sense data into `(key, asc, ascq)`. `None` when short or unrecognized.
236    pub fn parse(data: &[u8]) -> Option<Triplet> {
237        match *data.first()? & 0x7f {
238            0x70 | 0x71 if data.len() >= 14 => Some((data[2] & 0x0f, data[12], data[13])),
239            0x72 | 0x73 if data.len() >= 4 => Some((data[1] & 0x0f, data[2], data[3])),
240            _ => None,
241        }
242    }
243
244    /// `true` for the keys that prove a programming failure after a flash:
245    /// MEDIUM ERROR, HARDWARE ERROR and ABORTED COMMAND. Every other key is a
246    /// transient or unrelated condition; read-back verification decides.
247    pub const fn is_flash_fault(key: u8) -> bool {
248        matches!(key, MEDIUM_ERROR | HARDWARE_ERROR | ABORTED_COMMAND)
249    }
250
251    /// `true` for `02/3A/xx`, the benign "medium not present" of an empty tray.
252    pub const fn is_no_medium(key: u8, asc: u8) -> bool {
253        key == NOT_READY && asc == 0x3A
254    }
255}
256
257#[cfg(test)]
258#[path = "lib_tests.rs"]
259mod tests;