mediatek_optical/image/
family.rs1use crate::{layout, Chip};
25
26pub const FAMILY_SCHEME: &str = "mediatek.hardware.v1";
30
31const CODE: core::ops::Range<usize> = layout::BOOT_AREA.len..layout::UHD_REVOCATION.start;
34const TEMPLATE_REACH: usize = 0x800;
36const MARKER: [u8; 2] = [0x55, 0xAA];
38const RECORD_LEN: usize = 2 + 6 + 10 + 8 + 2;
40const RECORD_TAG: usize = 0x10;
42const RECORD_PLATFORM: usize = 0x22;
44const PLATFORM_LEN: usize = 6;
46const RECORD_USED_LEN: usize = MARKER.len() + RECORD_PLATFORM + PLATFORM_LEN;
48const DESCRIPTOR_PLATFORM: usize = 0x46;
50const PROFILE_LIST: u16 = 0x0000;
52const CORE: u16 = 0x0001;
53const MEDIUM_FEATURE: u16 = 0x0003;
54const BD_WRITE_PROFILES: [u8; 3] = [0x41, 0x42, 0x43];
56const LOADER_MAX: usize = 8;
58
59#[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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
85#[non_exhaustive]
86pub struct Hardware {
87 pub chip: Chip,
89 pub platform: String,
91 pub loader: String,
93 pub interface: [u8; 4],
95 pub medium: u8,
97 pub bd_write: bool,
99}
100
101impl Hardware {
102 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 pub fn family(&self) -> Family {
118 Family(fnv1a(self.key().as_bytes()))
119 }
120}
121
122fn 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
129pub fn family(image: &[u8]) -> Option<Family> {
132 hardware(image).map(|h| h.family())
133}
134
135pub 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 let tagged = Chip::from_tag(descriptor).is_some();
159 let chip = Chip::from_banner(banner)?;
160 let loader = loader(banner)?;
161
162 let mut at = CODE.start;
163 let mut records = core::iter::from_fn(|| {
164 while let Some(found) = next_marker(image, at) {
165 at = found + 1;
166 if is_record(&image[found..]) {
167 return Some(found);
168 }
169 }
170 None
171 });
172 let record = records.next()?;
173 if records.next().is_some() {
174 return None;
175 }
176 let unique = record + MARKER.len();
177 Chip::from_tag(&image[unique + RECORD_TAG..unique + RECORD_TAG + 10])?;
182 let platform = trim(&image[unique + RECORD_PLATFORM..unique + RECORD_PLATFORM + PLATFORM_LEN]);
183 if tagged
184 && trim(&descriptor[DESCRIPTOR_PLATFORM..DESCRIPTOR_PLATFORM + PLATFORM_LEN]) != platform
185 {
186 return None;
187 }
188 if platform.is_empty() || !platform.iter().all(|b| (0x21..0x7F).contains(b)) {
189 return None;
190 }
191
192 let features = template(image, record)?;
193 let first = |code: u16| features.iter().find(|(c, _)| *c == code).map(|(_, d)| *d);
194 let profiles = first(PROFILE_LIST)?;
195 let core = first(CORE).filter(|d| d.len() >= 4)?;
196 let medium = *first(MEDIUM_FEATURE)?.first()?;
197 let bd_write = profiles
200 .chunks_exact(4)
201 .any(|p| p[0] == 0 && BD_WRITE_PROFILES.contains(&p[1]));
202
203 Some(Hardware {
204 chip,
205 platform: String::from_utf8(platform.to_vec()).ok()?,
206 loader,
207 interface: [core[0], core[1], core[2], core[3]],
208 medium,
209 bd_write,
210 })
211}
212
213fn trim(bytes: &[u8]) -> &[u8] {
215 let end = bytes
216 .iter()
217 .rposition(|&b| b != b' ' && b != 0)
218 .map_or(0, |i| i + 1);
219 &bytes[..end]
220}
221
222fn loader(banner: &[u8]) -> Option<String> {
225 let rest = banner.strip_prefix(b"MT19")?;
226 if rest.len() < 2 || !rest[..2].iter().all(u8::is_ascii_digit) {
227 return None;
228 }
229 let token = rest[2..].strip_prefix(b" Boot ")?;
230 let len = token
231 .iter()
232 .take(LOADER_MAX)
233 .take_while(|b| (0x21..0x7F).contains(*b))
234 .count();
235 if len == 0 {
236 return None;
237 }
238 String::from_utf8(token[..len].to_vec()).ok()
239}
240
241fn next_marker(image: &[u8], from: usize) -> Option<usize> {
246 const LO: u64 = 0x0101_0101_0101_0101;
247 const HI: u64 = 0x8080_8080_8080_8080;
248 let last = CODE.end - RECORD_USED_LEN;
249 if from > last {
250 return None;
251 }
252 let window = &image[from..=last];
253 let mut chunks = window.chunks_exact(8);
254 let mut base = 0;
255 for chunk in &mut chunks {
256 let word = u64::from_le_bytes(chunk.try_into().expect("chunk is eight bytes"))
257 ^ (LO * u64::from(MARKER[0]));
258 if word.wrapping_sub(LO) & !word & HI != 0 {
259 if let Some(i) = chunk.iter().position(|&b| b == MARKER[0]) {
260 return Some(from + base + i);
261 }
262 }
263 base += 8;
264 }
265 chunks
266 .remainder()
267 .iter()
268 .position(|&b| b == MARKER[0])
269 .map(|i| from + base + i)
270}
271
272fn is_record(bytes: &[u8]) -> bool {
274 bytes.len() >= RECORD_LEN
275 && bytes.starts_with(&MARKER)
276 && bytes[2..8] == *b"UNIQUE"
277 && bytes[8..18].iter().all(u8::is_ascii_digit)
278 && bytes[18..26] == *b"MTEKMT19"
279 && bytes[26..28].iter().all(u8::is_ascii_digit)
280}
281
282fn template(image: &[u8], end: usize) -> Option<Vec<(u16, &[u8])>> {
286 let floor = end.saturating_sub(TEMPLATE_REACH).max(CODE.start - 1);
287 (floor + 1..=end.checked_sub(4)?).rev().find_map(|start| {
288 let length = image[start + 3];
289 if image[start..start + 2] != [0, 0] || length == 0 || length % 4 != 0 {
290 return None;
291 }
292 let mut features = Vec::new();
293 let mut at = start;
294 while at + 4 <= end {
295 let code = u16::from_be_bytes([image[at], image[at + 1]]);
296 let length = usize::from(image[at + 3]);
297 let data = image.get(at + 4..at + 4 + length)?;
298 features.push((code, data));
299 at += 4 + length;
300 }
301 (at == end && features.len() > 1 && features[0].0 == PROFILE_LIST && features[1].0 == CORE)
302 .then_some(features)
303 })
304}
305
306#[cfg(test)]
307#[path = "family_tests.rs"]
308mod tests;