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//! the receiver-bound `receiver::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
39#[cfg(all(test, not(feature = "std")))]
40extern crate std;
41
42mod error_codes;
43/// Common lifecycle observations for generic firmware applications.
44pub mod operation;
45pub use error_codes::CodedError;
46
47mod product_identity;
48pub use product_identity::ProductIdentity;
49
50pub mod cdb;
51#[cfg(feature = "drive")]
52pub mod drive;
53#[cfg(feature = "enc")]
54pub mod enc;
55#[cfg(all(feature = "image", feature = "enc"))]
56pub mod envelope;
57#[cfg(feature = "image")]
58pub mod image;
59pub mod layout;
60#[cfg(feature = "oem")]
61pub mod oem;
62pub mod protocol;
63#[cfg(feature = "drive")]
64pub mod receiver;
65#[cfg(all(feature = "drive", feature = "image", feature = "enc"))]
66pub mod recovery;
67#[cfg(feature = "image")]
68pub mod update;
69
70/// Length of a standard INQUIRY response header, in bytes.
71pub const INQUIRY_LEN: usize = 36;
72
73/// A MediaTek optical controller generation.
74///
75/// Deliberately exhaustive: a new generation is a breaking change, so every
76/// caller that dispatches on the chip must decide how to handle it.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
78pub enum Chip {
79 /// MediaTek MT1959.
80 Mt1959,
81 /// MediaTek MT1939.
82 Mt1939,
83}
84
85impl Chip {
86 /// Short label, `"MT1959"` or `"MT1939"`.
87 pub const fn label(self) -> &'static str {
88 match self {
89 Chip::Mt1959 => "MT1959",
90 Chip::Mt1939 => "MT1939",
91 }
92 }
93
94 /// Map the two trailing digits of an `MTEKMT19xx` or `MT19xx` tag.
95 pub const fn from_digits(d0: u8, d1: u8) -> Option<Self> {
96 match (d0, d1) {
97 (b'5', b'9') => Some(Chip::Mt1959),
98 (b'3', b'9') => Some(Chip::Mt1939),
99 _ => None,
100 }
101 }
102
103 /// The chip named by the first `MTEKMT19xx` identity tag in `bytes`.
104 ///
105 /// This is a label parser, not operational chip detection. OEM identity
106 /// tags can be stale; [`Identity::chip`] and image analysis use the boot
107 /// banner instead. Never use this label to override their result.
108 pub fn from_tag(bytes: &[u8]) -> Option<Self> {
109 const PREFIX: &[u8] = b"MTEKMT19";
110 bytes
111 .windows(PREFIX.len() + 2)
112 .filter(|w| w.starts_with(PREFIX))
113 .find_map(|w| Self::from_digits(w[PREFIX.len()], w[PREFIX.len() + 1]))
114 }
115
116 /// The chip named by a boot banner such as `MT1939 Boot JB7`. This is the
117 /// source used by live identity and image analysis, even when a descriptor
118 /// carries a conflicting [`Chip::from_tag`] label.
119 pub fn from_banner(banner: &[u8]) -> Option<Self> {
120 banner
121 .windows(6)
122 .filter(|w| w.starts_with(b"MT19"))
123 .find_map(|w| Self::from_digits(w[4], w[5]))
124 }
125}
126
127impl core::fmt::Display for Chip {
128 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
129 f.write_str(self.label())
130 }
131}
132
133/// [`Chip::label`] did not name a chip.
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub struct UnknownChip;
136
137impl core::fmt::Display for UnknownChip {
138 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
139 f.write_str("not an MT1959 or MT1939 label")
140 }
141}
142
143#[cfg(feature = "std")]
144impl std::error::Error for UnknownChip {}
145
146impl core::str::FromStr for Chip {
147 type Err = UnknownChip;
148
149 /// Parse a [`Chip::label`], e.g. `MT1939`.
150 fn from_str(label: &str) -> Result<Self, Self::Err> {
151 match label {
152 "MT1959" => Ok(Chip::Mt1959),
153 "MT1939" => Ok(Chip::Mt1939),
154 _ => Err(UnknownChip),
155 }
156 }
157}
158
159/// The update dialect a drive speaks, chosen by [`Identity::class`].
160///
161/// The dialect owns the update session: its command sequence, chunking,
162/// readiness timing after the burn and which bytes read-back verification may
163/// compare.
164#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
165#[non_exhaustive]
166pub enum DriveClass {
167 /// MT1959 and MT1939: the OEM session of 12-byte CDBs. PROBE, TEST UNIT
168 /// READY and PREPARE, the image in [`cdb::CHUNK`] STREAM writes, COMMIT,
169 /// then TEST UNIT READY and REQUEST SENSE. The drive programs when the
170 /// last chunk lands.
171 Mt19,
172}
173
174impl DriveClass {
175 /// Whether a host should offer automatic installed-firmware acquisition.
176 ///
177 /// Stock MT19 firmware does not provide a reliable complete firmware read.
178 /// Hosts should request a Current firmware file instead. Metadata and
179 /// restricted memory reads remain available; this does not forbid them or
180 /// a modified firmware's separate dump command.
181 pub const fn can_read(self) -> bool {
182 match self {
183 Self::Mt19 => false,
184 }
185 }
186
187 /// The dialect of a drive with `chip`.
188 pub const fn of(chip: Chip) -> Self {
189 match chip {
190 Chip::Mt1959 | Chip::Mt1939 => DriveClass::Mt19,
191 }
192 }
193
194 /// Bytes per STREAM write.
195 pub const fn chunk(self) -> usize {
196 cdb::CHUNK
197 }
198
199 /// How long programming may keep the drive from answering TEST UNIT READY
200 /// after COMMIT: 120 s.
201 ///
202 /// The OEM updaters wait for the drive without a total bound and give each
203 /// command a 100 s timeout, so a drive that answers within one OEM command
204 /// window must not be declared failed. 120 s covers that window plus 20 s
205 /// (40 polls) of margin. A shorter bound fails a drive that is still
206 /// programming and invites a power cycle mid-burn; a longer one only costs
207 /// time on a drive that never comes back.
208 pub const fn ready_timeout(self) -> core::time::Duration {
209 core::time::Duration::from_secs(120)
210 }
211
212 /// Delay between TEST UNIT READY polls while the drive programs.
213 pub const fn poll_interval(self) -> core::time::Duration {
214 core::time::Duration::from_millis(500)
215 }
216
217 /// Read-back verification compares no byte below this offset: the drive
218 /// serves its boot area remapped ([`layout::BOOT_AREA`]).
219 pub const fn verify_floor(self) -> usize {
220 layout::BOOT_AREA.end()
221 }
222}
223
224/// Trim trailing ASCII spaces and NULs and decode as UTF-8; `""` if invalid.
225fn field(raw: &[u8]) -> &str {
226 let start = raw.iter().position(|&b| b != b' ').unwrap_or(raw.len());
227 let raw = &raw[start..];
228 let end = raw
229 .iter()
230 .rposition(|&b| b != b' ' && b != 0)
231 .map_or(0, |i| i + 1);
232 core::str::from_utf8(&raw[..end]).unwrap_or("")
233}
234
235/// A drive's identity: the standard INQUIRY response, the boot banner at
236/// [`layout::BANNER`] and the identity descriptor at [`layout::DESCRIPTOR`].
237#[derive(Clone, Debug, PartialEq, Eq)]
238pub struct Identity {
239 inquiry: [u8; INQUIRY_LEN],
240 banner: [u8; layout::BANNER.len],
241 descriptor: [u8; layout::DESCRIPTOR.len],
242}
243
244impl Identity {
245 /// Decode an INQUIRY response (at least [`INQUIRY_LEN`] bytes), a boot
246 /// banner and an identity descriptor. Short banner and descriptor reads
247 /// are zero-padded. Returns `None` if the INQUIRY is short.
248 pub fn parse(inquiry: &[u8], banner: &[u8], descriptor: &[u8]) -> Option<Self> {
249 if inquiry.len() < INQUIRY_LEN {
250 return None;
251 }
252 let mut id = Self {
253 inquiry: [0; INQUIRY_LEN],
254 banner: [0; layout::BANNER.len],
255 descriptor: [0; layout::DESCRIPTOR.len],
256 };
257 id.inquiry.copy_from_slice(&inquiry[..INQUIRY_LEN]);
258 let n = banner.len().min(id.banner.len());
259 id.banner[..n].copy_from_slice(&banner[..n]);
260 let n = descriptor.len().min(id.descriptor.len());
261 id.descriptor[..n].copy_from_slice(&descriptor[..n]);
262 Some(id)
263 }
264
265 /// Peripheral device type; `0x05` for an optical drive.
266 pub fn device_type(&self) -> u8 {
267 self.inquiry[0] & 0x1f
268 }
269 /// Vendor identification, e.g. `HL-DT-ST`.
270 pub fn vendor(&self) -> &str {
271 field(&self.inquiry[8..16])
272 }
273 /// Product identification, e.g. `BD-RE BU40N`.
274 pub fn product(&self) -> &str {
275 field(&self.inquiry[16..32])
276 }
277 /// Structured vendor, drive model, product context and revision.
278 pub fn product_identity(&self) -> ProductIdentity<'_> {
279 ProductIdentity::from_fields(self.vendor(), self.product(), self.revision())
280 }
281 /// Firmware revision, e.g. `1.00`.
282 pub fn revision(&self) -> &str {
283 field(&self.inquiry[32..36])
284 }
285 /// The boot banner, e.g. `MT1959 Boot BU5`, up to its first NUL or
286 /// non-printable byte.
287 pub fn banner(&self) -> &str {
288 let end = self
289 .banner
290 .iter()
291 .position(|&b| !(0x20..0x7f).contains(&b))
292 .unwrap_or(self.banner.len());
293 field(&self.banner[..end])
294 }
295 /// `true` when the descriptor carries an `MTEKMT19xx` identity tag.
296 pub fn has_descriptor(&self) -> bool {
297 Chip::from_tag(&self.descriptor).is_some()
298 }
299 /// Model from the identity descriptor; `""` without a descriptor.
300 pub fn descriptor_model(&self) -> &str {
301 if self.has_descriptor() {
302 field(&self.descriptor[0x08..0x18])
303 } else {
304 ""
305 }
306 }
307 /// Vendor-specific SKU code from the identity descriptor, e.g. `N003600`;
308 /// `""` without a descriptor.
309 pub fn sku(&self) -> &str {
310 if self.has_descriptor() {
311 field(&self.descriptor[0x1C..0x24])
312 } else {
313 ""
314 }
315 }
316 /// The controller generation, decided by the drive's boot banner
317 /// ([`Chip::from_banner`]) and nothing else. The `MTEKMT19xx` identity tag
318 /// is a stale vendor label and is never consulted. `None` for a drive whose
319 /// banner names no supported generation.
320 pub fn chip(&self) -> Option<Chip> {
321 Chip::from_banner(&self.banner)
322 }
323 /// The update dialect this drive speaks: [`DriveClass::Mt19`] for a drive
324 /// that names an MT1959 or MT1939 chip, else `None`.
325 pub fn class(&self) -> Option<DriveClass> {
326 self.chip().map(DriveClass::of)
327 }
328 /// `true` when the banner carries the MT19-family `MT19` signature.
329 pub fn is_mt19(&self) -> bool {
330 self.banner.windows(4).any(|w| w == b"MT19")
331 }
332 /// The raw INQUIRY response.
333 pub fn inquiry_bytes(&self) -> &[u8; INQUIRY_LEN] {
334 &self.inquiry
335 }
336 /// The raw boot banner.
337 pub fn banner_bytes(&self) -> &[u8; layout::BANNER.len] {
338 &self.banner
339 }
340 /// The raw identity descriptor.
341 pub fn descriptor_bytes(&self) -> &[u8; layout::DESCRIPTOR.len] {
342 &self.descriptor
343 }
344}
345
346/// Sense-data parsing and classification.
347pub mod sense {
348 /// Parsed sense: `(key, asc, ascq)`.
349 pub type Triplet = (u8, u8, u8);
350
351 /// Sense key `NOT READY`.
352 pub const NOT_READY: u8 = 0x02;
353 /// Sense key `MEDIUM ERROR`.
354 pub const MEDIUM_ERROR: u8 = 0x03;
355 /// Sense key `HARDWARE ERROR`.
356 pub const HARDWARE_ERROR: u8 = 0x04;
357 /// Sense key `ILLEGAL REQUEST`.
358 pub const ILLEGAL_REQUEST: u8 = 0x05;
359 /// Sense key `ABORTED COMMAND`.
360 pub const ABORTED_COMMAND: u8 = 0x0B;
361
362 /// Parse fixed-format (`0x70`/`0x71`) or descriptor-format (`0x72`/`0x73`)
363 /// sense data into `(key, asc, ascq)`. `None` when short or unrecognized.
364 pub fn parse(data: &[u8]) -> Option<Triplet> {
365 match *data.first()? & 0x7f {
366 0x70 | 0x71 if data.len() >= 14 => Some((data[2] & 0x0f, data[12], data[13])),
367 0x72 | 0x73 if data.len() >= 4 => Some((data[1] & 0x0f, data[2], data[3])),
368 _ => None,
369 }
370 }
371
372 /// `true` for the keys that prove a programming failure after a flash:
373 /// MEDIUM ERROR, HARDWARE ERROR and ABORTED COMMAND. Every other key is a
374 /// transient or unrelated condition; read-back verification decides.
375 pub const fn is_flash_fault(key: u8) -> bool {
376 matches!(key, MEDIUM_ERROR | HARDWARE_ERROR | ABORTED_COMMAND)
377 }
378
379 /// `true` for `02/3A/xx`, the benign "medium not present" of an empty tray.
380 pub const fn is_no_medium(key: u8, asc: u8) -> bool {
381 key == NOT_READY && asc == 0x3A
382 }
383}
384
385#[cfg(all(test, feature = "image"))]
386mod fixtures;
387
388#[cfg(test)]
389#[path = "lib_tests.rs"]
390mod tests;
391
392#[cfg(all(feature = "drive", feature = "image", feature = "enc"))]
393#[doc = include_str!("../README.md")]
394mod readme_examples {}