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//! - [`DriveClass`] — the update dialect a drive speaks ([`Identity::class`]).
10//! - [`layout`] — the fixed 2 MiB image geometry and the drive-written regions.
11//! - [`sense`] — sense-data parsing and classification of flash faults.
12//!
13//! Optional features add:
14//!
15//! - `drive` — [`drive`]: the command sequences (identify, memory read, flash
16//!   session) over a caller-supplied [`drive::Transport`].
17//! - `image` — [`image`]: chip detection from image bytes and the AES-CMAC
18//!   integrity table ([`image::cmac`]). Uses `std`.
19//! - `enc` — [`enc`]: the AES-128-ECB transport envelope.
20//! - `oem` — [`oem`]: rebuild the flashable OEM image from a drive read, with
21//!   the provenance of every substituted byte. Implies `image`.
22//! - `image` also enables [`update`]: an update prepared against the installed
23//!   firmware, refused across chips and hardware families. With `drive`,
24//!   [`update::PreparedUpdate::flash`] runs it.
25//! - `std` — links the standard library (implied by `image`).
26//!
27//! ## Drive memory
28//!
29//! READ BUFFER mode 6 exposes the 2 MiB flash at file offsets, with one
30//! exception: the boot page `0x0..0x400` reads back as the image's own page at
31//! [`layout::BOOT_MIRROR`] instead of its stored form. The drive also rewrites
32//! the regions in [`layout::DRIVE_WRITTEN`] at runtime (revocation lists,
33//! settings, calibration), so a read is never byte-identical to the image that
34//! was flashed. [`oem::rebuild`] undoes both.
35#![cfg_attr(not(feature = "std"), no_std)]
36#![deny(missing_docs)]
37#![forbid(unsafe_code)]
38
39pub mod cdb;
40#[cfg(feature = "drive")]
41pub mod drive;
42#[cfg(feature = "enc")]
43pub mod enc;
44#[cfg(feature = "image")]
45pub mod image;
46pub mod layout;
47#[cfg(feature = "oem")]
48pub mod oem;
49#[cfg(feature = "image")]
50pub mod update;
51
52/// Length of a standard INQUIRY response header, in bytes.
53pub const INQUIRY_LEN: usize = 36;
54
55/// A MediaTek optical controller generation.
56///
57/// Deliberately exhaustive: a new generation is a breaking change, so every
58/// caller that dispatches on the chip must decide how to handle it.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub enum Chip {
61    /// MediaTek MT1959.
62    Mt1959,
63    /// MediaTek MT1939.
64    Mt1939,
65}
66
67impl Chip {
68    /// Short label, `"MT1959"` or `"MT1939"`.
69    pub const fn label(self) -> &'static str {
70        match self {
71            Chip::Mt1959 => "MT1959",
72            Chip::Mt1939 => "MT1939",
73        }
74    }
75
76    /// Map the two trailing digits of an `MTEKMT19xx` or `MT19xx` tag.
77    pub const fn from_digits(d0: u8, d1: u8) -> Option<Self> {
78        match (d0, d1) {
79            (b'5', b'9') => Some(Chip::Mt1959),
80            (b'3', b'9') => Some(Chip::Mt1939),
81            _ => None,
82        }
83    }
84
85    /// The chip named by the first `MTEKMT19xx` identity tag in `bytes`.
86    ///
87    /// This tag is authoritative. The boot banner names the bootloader
88    /// generation instead: MT1939 parts can carry an `MT1959 Boot` banner.
89    pub fn from_tag(bytes: &[u8]) -> Option<Self> {
90        const PREFIX: &[u8] = b"MTEKMT19";
91        bytes
92            .windows(PREFIX.len() + 2)
93            .filter(|w| w.starts_with(PREFIX))
94            .find_map(|w| Self::from_digits(w[PREFIX.len()], w[PREFIX.len() + 1]))
95    }
96
97    /// The chip named by a boot banner such as `MT1939 Boot JB7`. Weaker than
98    /// [`Chip::from_tag`]; use only when no identity tag exists.
99    pub fn from_banner(banner: &[u8]) -> Option<Self> {
100        banner
101            .windows(6)
102            .filter(|w| w.starts_with(b"MT19"))
103            .find_map(|w| Self::from_digits(w[4], w[5]))
104    }
105}
106
107impl core::fmt::Display for Chip {
108    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109        f.write_str(self.label())
110    }
111}
112
113/// [`Chip::label`] did not name a chip.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub struct UnknownChip;
116
117impl core::fmt::Display for UnknownChip {
118    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119        f.write_str("not an MT1959 or MT1939 label")
120    }
121}
122
123impl core::str::FromStr for Chip {
124    type Err = UnknownChip;
125
126    /// Parse a [`Chip::label`], e.g. `MT1939`.
127    fn from_str(label: &str) -> Result<Self, Self::Err> {
128        match label {
129            "MT1959" => Ok(Chip::Mt1959),
130            "MT1939" => Ok(Chip::Mt1939),
131            _ => Err(UnknownChip),
132        }
133    }
134}
135
136/// The update dialect a drive speaks, chosen by [`Identity::class`].
137///
138/// The dialect owns the update session: its command sequence, chunking,
139/// readiness timing after the burn and which bytes read-back verification may
140/// compare.
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
142#[non_exhaustive]
143pub enum DriveClass {
144    /// MT1959 and MT1939: the OEM session of 12-byte CDBs. PROBE, TEST UNIT
145    /// READY and PREPARE, the image in [`cdb::CHUNK`] STREAM writes, COMMIT,
146    /// then TEST UNIT READY and REQUEST SENSE. The drive programs when the
147    /// last chunk lands.
148    Mt19,
149}
150
151impl DriveClass {
152    /// The dialect of a drive with `chip`.
153    pub const fn of(chip: Chip) -> Self {
154        match chip {
155            Chip::Mt1959 | Chip::Mt1939 => DriveClass::Mt19,
156        }
157    }
158
159    /// Bytes per STREAM write.
160    pub const fn chunk(self) -> usize {
161        cdb::CHUNK
162    }
163
164    /// How long programming may keep the drive from answering TEST UNIT READY
165    /// after COMMIT: 120 s.
166    ///
167    /// The OEM updaters wait for the drive without a total bound and give each
168    /// command a 100 s timeout, so a drive that answers within one OEM command
169    /// window must not be declared failed. 120 s covers that window plus 20 s
170    /// (40 polls) of margin. A shorter bound fails a drive that is still
171    /// programming and invites a power cycle mid-burn; a longer one only costs
172    /// time on a drive that never comes back.
173    pub const fn ready_timeout(self) -> core::time::Duration {
174        core::time::Duration::from_secs(120)
175    }
176
177    /// Delay between TEST UNIT READY polls while the drive programs.
178    pub const fn poll_interval(self) -> core::time::Duration {
179        core::time::Duration::from_millis(500)
180    }
181
182    /// Read-back verification compares no byte below this offset: the drive
183    /// serves its boot area remapped ([`layout::BOOT_AREA`]).
184    pub const fn verify_floor(self) -> usize {
185        layout::BOOT_AREA.end()
186    }
187}
188
189/// Trim trailing ASCII spaces and NULs and decode as UTF-8; `""` if invalid.
190fn field(raw: &[u8]) -> &str {
191    let start = raw.iter().position(|&b| b != b' ').unwrap_or(raw.len());
192    let raw = &raw[start..];
193    let end = raw
194        .iter()
195        .rposition(|&b| b != b' ' && b != 0)
196        .map_or(0, |i| i + 1);
197    core::str::from_utf8(&raw[..end]).unwrap_or("")
198}
199
200/// A drive's identity: the standard INQUIRY response, the boot banner at
201/// [`layout::BANNER`] and the identity descriptor at [`layout::DESCRIPTOR`].
202#[derive(Clone, Debug, PartialEq, Eq)]
203pub struct Identity {
204    inquiry: [u8; INQUIRY_LEN],
205    banner: [u8; layout::BANNER.len],
206    descriptor: [u8; layout::DESCRIPTOR.len],
207}
208
209impl Identity {
210    /// Decode an INQUIRY response (at least [`INQUIRY_LEN`] bytes), a boot
211    /// banner and an identity descriptor. Short banner and descriptor reads
212    /// are zero-padded. Returns `None` if the INQUIRY is short.
213    pub fn parse(inquiry: &[u8], banner: &[u8], descriptor: &[u8]) -> Option<Self> {
214        if inquiry.len() < INQUIRY_LEN {
215            return None;
216        }
217        let mut id = Self {
218            inquiry: [0; INQUIRY_LEN],
219            banner: [0; layout::BANNER.len],
220            descriptor: [0; layout::DESCRIPTOR.len],
221        };
222        id.inquiry.copy_from_slice(&inquiry[..INQUIRY_LEN]);
223        let n = banner.len().min(id.banner.len());
224        id.banner[..n].copy_from_slice(&banner[..n]);
225        let n = descriptor.len().min(id.descriptor.len());
226        id.descriptor[..n].copy_from_slice(&descriptor[..n]);
227        Some(id)
228    }
229
230    /// Peripheral device type; `0x05` for an optical drive.
231    pub fn device_type(&self) -> u8 {
232        self.inquiry[0] & 0x1f
233    }
234    /// Vendor identification, e.g. `HL-DT-ST`.
235    pub fn vendor(&self) -> &str {
236        field(&self.inquiry[8..16])
237    }
238    /// Product identification, e.g. `BD-RE BU40N`.
239    pub fn product(&self) -> &str {
240        field(&self.inquiry[16..32])
241    }
242    /// Firmware revision, e.g. `1.00`.
243    pub fn revision(&self) -> &str {
244        field(&self.inquiry[32..36])
245    }
246    /// The boot banner, e.g. `MT1959 Boot BU5`, up to its first NUL or
247    /// non-printable byte.
248    pub fn banner(&self) -> &str {
249        let end = self
250            .banner
251            .iter()
252            .position(|&b| !(0x20..0x7f).contains(&b))
253            .unwrap_or(self.banner.len());
254        field(&self.banner[..end])
255    }
256    /// `true` when the descriptor carries an `MTEKMT19xx` identity tag.
257    pub fn has_descriptor(&self) -> bool {
258        Chip::from_tag(&self.descriptor).is_some()
259    }
260    /// Model from the identity descriptor; `""` without a descriptor.
261    pub fn descriptor_model(&self) -> &str {
262        if self.has_descriptor() {
263            field(&self.descriptor[0x08..0x18])
264        } else {
265            ""
266        }
267    }
268    /// Vendor-specific SKU code from the identity descriptor, e.g. `N003600`;
269    /// `""` without a descriptor.
270    pub fn sku(&self) -> &str {
271        if self.has_descriptor() {
272            field(&self.descriptor[0x1C..0x24])
273        } else {
274            ""
275        }
276    }
277    /// The controller generation, decided by the drive's boot banner
278    /// ([`Chip::from_banner`]) and nothing else. The `MTEKMT19xx` identity tag
279    /// is a stale vendor label and is never consulted. `None` for a drive whose
280    /// banner names no supported generation.
281    pub fn chip(&self) -> Option<Chip> {
282        Chip::from_banner(&self.banner)
283    }
284    /// The update dialect this drive speaks: [`DriveClass::Mt19`] for a drive
285    /// that names an MT1959 or MT1939 chip, else `None`.
286    pub fn class(&self) -> Option<DriveClass> {
287        self.chip().map(DriveClass::of)
288    }
289    /// `true` when the banner carries the MT19-family `MT19` signature.
290    pub fn is_mt19(&self) -> bool {
291        self.banner.windows(4).any(|w| w == b"MT19")
292    }
293    /// The raw INQUIRY response.
294    pub fn inquiry_bytes(&self) -> &[u8; INQUIRY_LEN] {
295        &self.inquiry
296    }
297    /// The raw boot banner.
298    pub fn banner_bytes(&self) -> &[u8; layout::BANNER.len] {
299        &self.banner
300    }
301    /// The raw identity descriptor.
302    pub fn descriptor_bytes(&self) -> &[u8; layout::DESCRIPTOR.len] {
303        &self.descriptor
304    }
305}
306
307/// Sense-data parsing and classification.
308pub mod sense {
309    /// Parsed sense: `(key, asc, ascq)`.
310    pub type Triplet = (u8, u8, u8);
311
312    /// Sense key `NOT READY`.
313    pub const NOT_READY: u8 = 0x02;
314    /// Sense key `MEDIUM ERROR`.
315    pub const MEDIUM_ERROR: u8 = 0x03;
316    /// Sense key `HARDWARE ERROR`.
317    pub const HARDWARE_ERROR: u8 = 0x04;
318    /// Sense key `ILLEGAL REQUEST`.
319    pub const ILLEGAL_REQUEST: u8 = 0x05;
320    /// Sense key `ABORTED COMMAND`.
321    pub const ABORTED_COMMAND: u8 = 0x0B;
322
323    /// Parse fixed-format (`0x70`/`0x71`) or descriptor-format (`0x72`/`0x73`)
324    /// sense data into `(key, asc, ascq)`. `None` when short or unrecognized.
325    pub fn parse(data: &[u8]) -> Option<Triplet> {
326        match *data.first()? & 0x7f {
327            0x70 | 0x71 if data.len() >= 14 => Some((data[2] & 0x0f, data[12], data[13])),
328            0x72 | 0x73 if data.len() >= 4 => Some((data[1] & 0x0f, data[2], data[3])),
329            _ => None,
330        }
331    }
332
333    /// `true` for the keys that prove a programming failure after a flash:
334    /// MEDIUM ERROR, HARDWARE ERROR and ABORTED COMMAND. Every other key is a
335    /// transient or unrelated condition; read-back verification decides.
336    pub const fn is_flash_fault(key: u8) -> bool {
337        matches!(key, MEDIUM_ERROR | HARDWARE_ERROR | ABORTED_COMMAND)
338    }
339
340    /// `true` for `02/3A/xx`, the benign "medium not present" of an empty tray.
341    pub const fn is_no_medium(key: u8, asc: u8) -> bool {
342        key == NOT_READY && asc == 0x3A
343    }
344}
345
346#[cfg(all(test, feature = "image"))]
347mod fixtures;
348
349#[cfg(test)]
350#[path = "lib_tests.rs"]
351mod tests;