Skip to main content

mediatek_optical/
update.rs

1//! An update prepared against the drive's installed firmware.
2//!
3//! [`PreparedUpdate::prepare`] takes the image to write and the
4//! [`InstalledFirmware`] (the live [`Identity`] plus a complete read of the
5//! drive, or the image rebuilt from one) and refuses, before any command:
6//!
7//! - an image that is not [`layout::IMAGE_SIZE`] bytes, fails its AES-CMAC
8//!   integrity table, or is a raw drive read;
9//! - an image for another chip than the drive's;
10//! - an image whose hardware family differs from the installed firmware's, or
11//!   where either family is unknown.
12//!
13//! The explicit `prepare_without_installed` path retains target and chip gates
14//! but records that installed-family comparison is unavailable; host consent
15//! is required separately. The prepared update carries the
16//! [`DriveClass`] dialect chosen from the identity, which owns the command
17//! sequence, chunking, readiness timing and read-back verification; with the
18//! `drive` and `enc` features, `crate::receiver::Receiver::prepare` binds it
19//! to the live receiver before executable transfer preparation.
20//!
21//! The caller still owns the drive: opening it, the pre-flash backup, tray and
22//! consent policy, model checks and reporting.
23//!
24//! Requires the `image` feature.
25
26use std::ops::RangeInclusive;
27
28use crate::image::{cmac, detect_chip, family, is_drive_read, Family};
29use crate::{cdb, layout, Chip, DriveClass, Identity};
30
31#[cfg(feature = "drive")]
32mod completion;
33#[cfg(feature = "drive")]
34pub use completion::{Completion, ReadbackQuality, Startup, StartupError};
35#[cfg(feature = "drive")]
36pub(crate) mod flash;
37#[cfg(feature = "drive")]
38pub use flash::{FlashError, FlashRuntime, FlashTransport, Stage, Status, Verified};
39
40/// Why an update was refused before any device command.
41#[derive(Clone, Debug, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum PrepareError {
44    /// No complete live entry profile was identified before preparation.
45    ReceiverEntry,
46    /// Decoder evidence does not yet establish an executable transfer contract.
47    ReceiverContract,
48    /// The image is not [`layout::IMAGE_SIZE`] bytes.
49    Size(usize),
50    /// The image fails its AES-CMAC integrity table.
51    Integrity,
52    /// The image is a raw drive read: its boot page is the read-back mirror,
53    /// and writing it would store that page in place of the boot page.
54    DriveRead,
55    /// The image names no supported chip.
56    UnknownChip,
57    /// The drive's boot banner names no supported chip generation.
58    UnknownDriveChip,
59    /// The image is for another chip than the drive's.
60    ChipMismatch {
61        /// The image's chip.
62        image: Chip,
63        /// The drive's chip.
64        drive: Chip,
65    },
66    /// The installed firmware is not a complete image of the live drive: its
67    /// size, identity descriptor or boot banner differ.
68    InstalledMismatch,
69    /// The installed firmware does not establish a hardware family.
70    UnknownInstalledFamily,
71    /// The image does not establish a hardware family.
72    UnknownTargetFamily,
73    /// The image is for other hardware than the installed firmware.
74    FamilyMismatch {
75        /// Family of the installed firmware.
76        installed: Family,
77        /// Family of the image.
78        target: Family,
79    },
80}
81
82impl core::fmt::Display for PrepareError {
83    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84        match self {
85            Self::ReceiverEntry => f.write_str(
86                "receiver entry profile is unknown; detect the live receiver before preparing",
87            ),
88            Self::ReceiverContract => {
89                f.write_str("receiver transfer contract is incomplete; no firmware was sent")
90            }
91            Self::Size(n) => write!(
92                f,
93                "image is {n} bytes; an update image is exactly {} bytes",
94                layout::IMAGE_SIZE
95            ),
96            Self::Integrity => f.write_str("image fails its AES-CMAC integrity check"),
97            Self::DriveRead => f.write_str(
98                "image is a raw drive read (its boot page is the read-back copy), not a \
99                 flashable image",
100            ),
101            Self::UnknownChip => f.write_str("image names no MT1959 or MT1939 chip"),
102            Self::UnknownDriveChip => f.write_str(
103                "could not read this drive's controller generation (MT1959/MT1939) from its \
104                 boot banner",
105            ),
106            Self::ChipMismatch { image, drive } => write!(
107                f,
108                "image is {image} firmware but this drive is {drive} silicon — refusing to \
109                 flash across chip generations"
110            ),
111            Self::InstalledMismatch => f.write_str(
112                "installed firmware is not a complete image of the live drive (size, identity \
113                 descriptor or boot banner differ)",
114            ),
115            Self::UnknownInstalledFamily => {
116                f.write_str("could not determine the installed firmware family")
117            }
118            Self::UnknownTargetFamily => {
119                f.write_str("could not determine the target firmware family")
120            }
121            Self::FamilyMismatch { installed, target } => write!(
122                f,
123                "family mismatch: installed firmware is family {installed}, target is family \
124                 {target}"
125            ),
126        }
127    }
128}
129
130impl std::error::Error for PrepareError {}
131
132/// Data phase of one [`Step`].
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134pub enum Direction {
135    /// Device to host.
136    In,
137    /// Host to device.
138    Out,
139    /// No data phase.
140    None,
141}
142
143/// One command of an update session.
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub struct Step {
146    /// Short name: `PROBE`, `READY`, `PREPARE`, `STREAM`, `COMMIT` or `STATUS`.
147    pub label: &'static str,
148    /// The CDB.
149    pub cdb: [u8; 12],
150    /// Data phase.
151    pub direction: Direction,
152    /// Bytes transferred (allocation length for [`Direction::In`]).
153    pub length: usize,
154}
155
156impl DriveClass {
157    /// The commands of one update session for a [`layout::IMAGE_SIZE`] image,
158    /// as the OEM tool sends them. Readiness polling and read-back
159    /// verification follow in the receiver-bound executor.
160    pub fn steps(self) -> Vec<Step> {
161        let chunk = self.chunk();
162        let step = |label, cdb, direction, length| Step {
163            label,
164            cdb,
165            direction,
166            length,
167        };
168        let mut steps = vec![
169            step("PROBE", cdb::probe(), Direction::In, cdb::PROBE_LEN),
170            step("READY", cdb::test_unit_ready(), Direction::None, 0),
171            step("PREPARE", cdb::enter_update(), Direction::Out, 0),
172        ];
173        steps.extend((0..layout::IMAGE_SIZE).step_by(chunk).map(|offset| {
174            step(
175                "STREAM",
176                cdb::transfer(offset as u32, chunk as u16),
177                Direction::Out,
178                chunk,
179            )
180        }));
181        steps.extend([
182            step("COMMIT", cdb::finish(), Direction::Out, 0),
183            step("READY", cdb::test_unit_ready(), Direction::None, 0),
184            step(
185                "STATUS",
186                cdb::request_sense(),
187                Direction::In,
188                cdb::REQUEST_SENSE_LEN,
189            ),
190        ]);
191        steps
192    }
193
194    /// Inclusive byte ranges a read-back must match: every active integrity
195    /// entry, clipped to [`DriveClass::verify_floor`]. Per-unit regions are
196    /// outside every entry.
197    pub fn verification_ranges(self, image: &[u8]) -> Vec<RangeInclusive<usize>> {
198        let Ok(entries) = cmac::parse_table(image) else {
199            return Vec::new();
200        };
201        entries
202            .iter()
203            .filter(|entry| entry.is_active())
204            .filter_map(|entry| {
205                let start = (entry.start as usize).max(self.verify_floor());
206                let end = entry.end as usize;
207                (start <= end).then_some(start..=end)
208            })
209            .collect()
210    }
211}
212
213/// The drive's installed firmware, bound to its live identity.
214#[derive(Clone, Debug)]
215pub struct InstalledFirmware {
216    identity: Identity,
217    family: Option<Family>,
218}
219
220impl InstalledFirmware {
221    /// Bind `image`, a complete read of the drive's firmware or the image
222    /// rebuilt from one, to the drive's live `identity`.
223    ///
224    /// Fails with [`PrepareError::InstalledMismatch`] unless the image is
225    /// [`layout::IMAGE_SIZE`] bytes and holds the identity descriptor (when
226    /// the identity has one) and the boot banner (when the identity read one)
227    /// the drive reports; at least one of them must be present.
228    pub fn new(identity: Identity, image: &[u8]) -> Result<Self, PrepareError> {
229        if image.len() != layout::IMAGE_SIZE {
230            return Err(PrepareError::InstalledMismatch);
231        }
232        let descriptor = identity
233            .has_descriptor()
234            .then(|| layout::DESCRIPTOR.of(image) == Some(&identity.descriptor_bytes()[..]));
235        let banner = (!identity.banner().is_empty())
236            .then(|| layout::BANNER.of(image) == Some(&identity.banner_bytes()[..]));
237        match (descriptor, banner) {
238            (None, None) | (Some(false), _) | (_, Some(false)) => {
239                Err(PrepareError::InstalledMismatch)
240            }
241            _ => Ok(Self {
242                family: family(image),
243                identity,
244            }),
245        }
246    }
247
248    /// The live identity.
249    pub fn identity(&self) -> &Identity {
250        &self.identity
251    }
252
253    /// The installed firmware's hardware family, when it has one.
254    pub fn family(&self) -> Option<Family> {
255        self.family
256    }
257}
258
259/// An update validated against the installed firmware, ready to write.
260#[derive(Clone, Debug)]
261pub struct PreparedUpdate {
262    class: DriveClass,
263    family: Family,
264    image: Vec<u8>,
265    transfer: Option<Vec<u8>>,
266}
267
268impl PreparedUpdate {
269    /// Every check that needs no installed firmware, in order: size,
270    /// integrity, raw drive read, chip against the drive and the image's own
271    /// hardware family. Returns that family.
272    ///
273    /// Run it before capturing a backup; [`PreparedUpdate::prepare`] repeats
274    /// it.
275    pub fn check(image: &[u8], drive: &Identity) -> Result<Family, PrepareError> {
276        Self::checked(image, drive).map(|(_, family)| family)
277    }
278
279    /// [`Self::check`], also returning the drive's dialect.
280    fn checked(image: &[u8], drive: &Identity) -> Result<(DriveClass, Family), PrepareError> {
281        if image.len() != layout::IMAGE_SIZE {
282            return Err(PrepareError::Size(image.len()));
283        }
284        if !cmac::verify(image) {
285            return Err(PrepareError::Integrity);
286        }
287        if is_drive_read(image) {
288            return Err(PrepareError::DriveRead);
289        }
290        let image_chip = detect_chip(image).map_err(|_| PrepareError::UnknownChip)?;
291        // The drive's chip generation is decided by its boot banner and nothing
292        // else, exactly as the image's is (see `crate::image::detect_chip`); the
293        // `MTEKMT19xx` descriptor tag is a stale vendor label.
294        let drive_chip =
295            Chip::from_banner(drive.banner_bytes()).ok_or(PrepareError::UnknownDriveChip)?;
296        if image_chip.family != drive_chip {
297            return Err(PrepareError::ChipMismatch {
298                image: image_chip.family,
299                drive: drive_chip,
300            });
301        }
302        let target = family(image).ok_or(PrepareError::UnknownTargetFamily)?;
303        Ok((DriveClass::of(drive_chip), target))
304    }
305
306    /// Prepare `image` for the drive `installed` describes: [`Self::check`]
307    /// against its identity, then the family gate. Both families must be known
308    /// and equal.
309    pub fn prepare(image: &[u8], installed: &InstalledFirmware) -> Result<Self, PrepareError> {
310        let (class, target) = Self::checked(image, installed.identity())?;
311        let installed_family = installed
312            .family()
313            .ok_or(PrepareError::UnknownInstalledFamily)?;
314        if installed_family != target {
315            return Err(PrepareError::FamilyMismatch {
316                installed: installed_family,
317                target,
318            });
319        }
320        Ok(Self {
321            class,
322            family: target,
323            image: image.to_vec(),
324            transfer: None,
325        })
326    }
327
328    /// Prepare after the host explicitly accepted unavailable installed bytes.
329    /// Image integrity, framing, chip and target-family checks still apply.
330    /// Installed-family equality is unavailable, not inferred from the target.
331    /// This method does not confer backup consent or verify installed firmware.
332    pub fn prepare_without_installed(image: &[u8], live: &Identity) -> Result<Self, PrepareError> {
333        let (class, target) = Self::checked(image, live)?;
334        Ok(Self {
335            class,
336            family: target,
337            image: image.to_vec(),
338            transfer: None,
339        })
340    }
341
342    /// Stream the AES-128-ECB transport envelope instead of the plaintext.
343    /// Read-back still compares the plaintext the drive stores.
344    #[cfg(all(feature = "enc", test))]
345    pub(crate) fn encrypted(mut self) -> Self {
346        let mut transfer = self.image.clone();
347        crate::enc::encrypt(&mut transfer).expect("an update image is whole AES blocks");
348        self.transfer = Some(transfer);
349        self
350    }
351
352    /// The update dialect.
353    pub fn class(&self) -> DriveClass {
354        self.class
355    }
356
357    /// The target hardware family. The captured-installed preparation path
358    /// also established equality with the installed family.
359    pub fn family(&self) -> Family {
360        self.family
361    }
362
363    /// The image the drive will store.
364    pub fn image(&self) -> &[u8] {
365        &self.image
366    }
367
368    /// The bytes streamed: the image, or its transport envelope.
369    pub fn transfer(&self) -> &[u8] {
370        self.transfer.as_deref().unwrap_or(&self.image)
371    }
372
373    /// The commands of the update session; see [`DriveClass::steps`].
374    pub fn steps(&self) -> Vec<Step> {
375        self.class.steps()
376    }
377
378    /// The ranges read-back verification compares; see
379    /// [`DriveClass::verification_ranges`].
380    pub fn verification_ranges(&self) -> Vec<RangeInclusive<usize>> {
381        self.class.verification_ranges(&self.image)
382    }
383}
384
385#[cfg(test)]
386#[path = "update_tests.rs"]
387mod tests;