Skip to main content

mediatek_optical/image/
family.rs

1//! The hardware family of an MT19xx image.
2//!
3//! Every MT1959 and MT1939 build carries a hardware configuration record in its
4//! code area, right after the MMC feature template the firmware answers GET
5//! CONFIGURATION from:
6//!
7//! ```text
8//! [feature template: [code16 BE][version][length][data]..., Profile List first, then Core]
9//! [55 AA]["UNIQUE" + 10 digits]["MTEKMT19xx"][6 spaces][00 00][platform, 6 bytes]...
10//! ```
11//!
12//! The family ([`FAMILY_SCHEME`]) combines what the board is built around: the
13//! chip, the record's platform code (`JB7`, `JB8`, `BU3`, ...), the bootloader
14//! named by the boot banner, the Core feature's physical interface, the
15//! current profile of feature `0x0003` and whether any BD writing profile is
16//! enabled. Images of one model built for different board lines have different
17//! families; an update is only compatible within a family.
18//!
19//! The family reads only the boot banner, the identity descriptor and the code
20//! area `0x1000..0x1D8000`, none of which a drive rewrites or a drive read
21//! remaps, so a drive read and the image it was flashed from have the same
22//! family.
23
24use crate::{layout, Chip};
25
26/// Identifies the definition [`family`] implements. Recorded alongside a
27/// family wherever one is persisted, so a future definition cannot be confused
28/// with this one.
29pub const FAMILY_SCHEME: &str = "mediatek.hardware.v1";
30
31/// Where the hardware record is searched for: the code area, between the
32/// remapped boot area and the first drive-written region.
33const CODE: core::ops::Range<usize> = layout::BOOT_AREA.len..layout::UHD_REVOCATION.start;
34/// How far before the record the feature template may start.
35const TEMPLATE_REACH: usize = 0x800;
36/// The record's marker.
37const MARKER: [u8; 2] = [0x55, 0xAA];
38/// Record bytes: marker, `UNIQUE`, ten digits, `MTEKMT19`, two digits.
39const RECORD_LEN: usize = 2 + 6 + 10 + 8 + 2;
40/// Offset of the chip tag from the start of `UNIQUE`.
41const RECORD_TAG: usize = 0x10;
42/// Offset of the platform code from the start of `UNIQUE`.
43const RECORD_PLATFORM: usize = 0x22;
44/// Length of a platform code.
45const PLATFORM_LEN: usize = 6;
46/// Complete record prefix consumed when deriving immutable hardware identity.
47const RECORD_USED_LEN: usize = MARKER.len() + RECORD_PLATFORM + PLATFORM_LEN;
48/// Offset of the platform code within the identity descriptor.
49const DESCRIPTOR_PLATFORM: usize = 0x46;
50/// Profile List, Core and the feature whose current profile is keyed.
51const PROFILE_LIST: u16 = 0x0000;
52const CORE: u16 = 0x0001;
53const MEDIUM_FEATURE: u16 = 0x0003;
54/// BD-R SRM, BD-R RRM and BD-RE.
55const BD_WRITE_PROFILES: [u8; 3] = [0x41, 0x42, 0x43];
56/// A boot banner names its bootloader with at most this many characters.
57const LOADER_MAX: usize = 8;
58
59/// A hardware family: equal families identify boards that accept each other's
60/// firmware. Derived from the image's hardware configuration record by
61/// [`family`] under [`FAMILY_SCHEME`].
62///
63/// The value is an opaque hash; only equality and the hex [`Display`] form
64/// are meaningful.
65///
66/// [`Display`]: core::fmt::Display
67#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub struct Family(u64);
69
70impl core::fmt::Display for Family {
71    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
72        write!(f, "{:x}", self.0)
73    }
74}
75
76impl core::fmt::Debug for Family {
77    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
78        write!(f, "Family({:x})", self.0)
79    }
80}
81
82/// The hardware an image is built for, as its configuration record describes
83/// it. [`Hardware::family`] hashes these fields.
84#[derive(Clone, Debug, PartialEq, Eq, Hash)]
85#[non_exhaustive]
86pub struct Hardware {
87    /// The controller generation.
88    pub chip: Chip,
89    /// The record's platform code, e.g. `JB7`.
90    pub platform: String,
91    /// The bootloader named by the boot banner, e.g. `Code` or `JB8`.
92    pub loader: String,
93    /// The first four bytes of the Core feature: its physical interface.
94    pub interface: [u8; 4],
95    /// The first byte of feature `0x0003`.
96    pub medium: u8,
97    /// Whether the Profile List enables a BD writing profile.
98    pub bd_write: bool,
99}
100
101impl Hardware {
102    /// The canonical key [`Hardware::family`] hashes: `name=value` fields
103    /// separated by `0x1F`.
104    pub fn key(&self) -> String {
105        let interface: String = self.interface.iter().map(|b| format!("{b:02x}")).collect();
106        format!(
107            "chip={}\x1fplatform={}\x1floader={}\x1fiface={interface}\x1fmedium={:02x}\x1fbdwrite={}",
108            self.chip.label(),
109            self.platform,
110            self.loader,
111            self.medium,
112            u8::from(self.bd_write)
113        )
114    }
115
116    /// The family: FNV-1a 64 of [`Hardware::key`].
117    pub fn family(&self) -> Family {
118        Family(fnv1a(self.key().as_bytes()))
119    }
120}
121
122/// FNV-1a, 64 bits.
123fn fnv1a(bytes: &[u8]) -> u64 {
124    bytes.iter().fold(0xcbf2_9ce4_8422_2325, |h: u64, &b| {
125        (h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3)
126    })
127}
128
129/// The hardware family of a complete image or drive read, or `None` when it
130/// cannot be established. See [`hardware`].
131pub fn family(image: &[u8]) -> Option<Family> {
132    hardware(image).map(|h| h.family())
133}
134
135/// The hardware an image is built for, or `None` unless every part is present
136/// and consistent:
137///
138/// - the image is exactly [`layout::IMAGE_SIZE`] bytes;
139/// - it names a chip (descriptor tag, else boot banner) and its boot banner
140///   reads `MT19xx Boot <loader>`;
141/// - the code area holds exactly one hardware record, whose tag names the same
142///   chip;
143/// - a tagged descriptor carries the record's platform code at `+0x46`;
144/// - the platform code is printable;
145/// - the feature template ends exactly at the record, starts with the Profile
146///   List and Core features, and holds Core and feature `0x0003`.
147pub fn hardware(image: &[u8]) -> Option<Hardware> {
148    if image.len() != layout::IMAGE_SIZE {
149        return None;
150    }
151    let descriptor = layout::DESCRIPTOR.of(image)?;
152    let banner = layout::BANNER.of(image)?;
153    // The chip generation is decided by the boot loader banner and nothing else;
154    // the `MTEKMT19xx` descriptor tag is a stale vendor label (see
155    // `crate::image::detect_chip`). `tagged` is kept only to gate the descriptor's
156    // platform-code cross-check below (a tagged descriptor must agree on the
157    // platform), never to decide `chip`.
158    let tagged = Chip::from_tag(descriptor).is_some();
159    let chip = Chip::from_banner(banner)?;
160    let loader = loader(banner)?;
161
162    let mut records =
163        (CODE.start..=CODE.end - RECORD_USED_LEN).filter(|&at| is_record(&image[at..]));
164    let record = records.next()?;
165    if records.next().is_some() {
166        return None;
167    }
168    let unique = record + MARKER.len();
169    // Structural corruption guard: the record must carry a parseable MT19xx tag.
170    // We do NOT require it to equal `chip` — the record tag, like the descriptor
171    // tag, is the stale vendor label and legitimately disagrees with the banner
172    // on JB8/JBC6/JBP6/BUP1 builds (tag MT1939, banner/code MT1959).
173    Chip::from_tag(&image[unique + RECORD_TAG..unique + RECORD_TAG + 10])?;
174    let platform = trim(&image[unique + RECORD_PLATFORM..unique + RECORD_PLATFORM + PLATFORM_LEN]);
175    if tagged
176        && trim(&descriptor[DESCRIPTOR_PLATFORM..DESCRIPTOR_PLATFORM + PLATFORM_LEN]) != platform
177    {
178        return None;
179    }
180    if platform.is_empty() || !platform.iter().all(|b| (0x21..0x7F).contains(b)) {
181        return None;
182    }
183
184    let features = template(image, record)?;
185    let first = |code: u16| features.iter().find(|(c, _)| *c == code).map(|(_, d)| *d);
186    let profiles = first(PROFILE_LIST)?;
187    let core = first(CORE).filter(|d| d.len() >= 4)?;
188    let medium = *first(MEDIUM_FEATURE)?.first()?;
189    // Profile descriptors are [number BE][current][reserved]; a high byte of
190    // 0xFE marks a profile the build disables.
191    let bd_write = profiles
192        .chunks_exact(4)
193        .any(|p| p[0] == 0 && BD_WRITE_PROFILES.contains(&p[1]));
194
195    Some(Hardware {
196        chip,
197        platform: String::from_utf8(platform.to_vec()).ok()?,
198        loader,
199        interface: [core[0], core[1], core[2], core[3]],
200        medium,
201        bd_write,
202    })
203}
204
205/// `bytes` with trailing spaces and NULs removed.
206fn trim(bytes: &[u8]) -> &[u8] {
207    let end = bytes
208        .iter()
209        .rposition(|&b| b != b' ' && b != 0)
210        .map_or(0, |i| i + 1);
211    &bytes[..end]
212}
213
214/// The bootloader a banner names: `MT19` + two digits + ` Boot ` + one to
215/// eight printable characters.
216fn loader(banner: &[u8]) -> Option<String> {
217    let rest = banner.strip_prefix(b"MT19")?;
218    if rest.len() < 2 || !rest[..2].iter().all(u8::is_ascii_digit) {
219        return None;
220    }
221    let token = rest[2..].strip_prefix(b" Boot ")?;
222    let len = token
223        .iter()
224        .take(LOADER_MAX)
225        .take_while(|b| (0x21..0x7F).contains(*b))
226        .count();
227    if len == 0 {
228        return None;
229    }
230    String::from_utf8(token[..len].to_vec()).ok()
231}
232
233/// Whether `bytes` starts with a hardware record.
234fn is_record(bytes: &[u8]) -> bool {
235    bytes.len() >= RECORD_LEN
236        && bytes.starts_with(&MARKER)
237        && bytes[2..8] == *b"UNIQUE"
238        && bytes[8..18].iter().all(u8::is_ascii_digit)
239        && bytes[18..26] == *b"MTEKMT19"
240        && bytes[26..28].iter().all(u8::is_ascii_digit)
241}
242
243/// The feature template ending exactly at `end`: the start nearest `end`
244/// (within [`TEMPLATE_REACH`], never inside the boot page) whose walk begins
245/// with the Profile List and Core features and lands exactly on `end`.
246fn template(image: &[u8], end: usize) -> Option<Vec<(u16, &[u8])>> {
247    let floor = end.saturating_sub(TEMPLATE_REACH).max(CODE.start - 1);
248    (floor + 1..=end.checked_sub(4)?).rev().find_map(|start| {
249        let length = image[start + 3];
250        if image[start..start + 2] != [0, 0] || length == 0 || length % 4 != 0 {
251            return None;
252        }
253        let mut features = Vec::new();
254        let mut at = start;
255        while at + 4 <= end {
256            let code = u16::from_be_bytes([image[at], image[at + 1]]);
257            let length = usize::from(image[at + 3]);
258            let data = image.get(at + 4..at + 4 + length)?;
259            features.push((code, data));
260            at += 4 + length;
261        }
262        (at == end && features.len() > 1 && features[0].0 == PROFILE_LIST && features[1].0 == CORE)
263            .then_some(features)
264    })
265}
266
267#[cfg(test)]
268#[path = "family_tests.rs"]
269mod tests;