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;