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