use crate::{layout, Chip};
pub const FAMILY_SCHEME: &str = "mediatek.hardware.v1";
const CODE: core::ops::Range<usize> = layout::BOOT_AREA.len..layout::UHD_REVOCATION.start;
const TEMPLATE_REACH: usize = 0x800;
const MARKER: [u8; 2] = [0x55, 0xAA];
const RECORD_LEN: usize = 2 + 6 + 10 + 8 + 2;
const RECORD_TAG: usize = 0x10;
const RECORD_PLATFORM: usize = 0x22;
const PLATFORM_LEN: usize = 6;
const RECORD_USED_LEN: usize = MARKER.len() + RECORD_PLATFORM + PLATFORM_LEN;
const DESCRIPTOR_PLATFORM: usize = 0x46;
const PROFILE_LIST: u16 = 0x0000;
const CORE: u16 = 0x0001;
const MEDIUM_FEATURE: u16 = 0x0003;
const BD_WRITE_PROFILES: [u8; 3] = [0x41, 0x42, 0x43];
const LOADER_MAX: usize = 8;
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Family(u64);
impl core::fmt::Display for Family {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:x}", self.0)
}
}
impl core::fmt::Debug for Family {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Family({:x})", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Hardware {
pub chip: Chip,
pub platform: String,
pub loader: String,
pub interface: [u8; 4],
pub medium: u8,
pub bd_write: bool,
}
impl Hardware {
pub fn key(&self) -> String {
let interface: String = self.interface.iter().map(|b| format!("{b:02x}")).collect();
format!(
"chip={}\x1fplatform={}\x1floader={}\x1fiface={interface}\x1fmedium={:02x}\x1fbdwrite={}",
self.chip.label(),
self.platform,
self.loader,
self.medium,
u8::from(self.bd_write)
)
}
pub fn family(&self) -> Family {
Family(fnv1a(self.key().as_bytes()))
}
}
fn fnv1a(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf2_9ce4_8422_2325, |h: u64, &b| {
(h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3)
})
}
pub fn family(image: &[u8]) -> Option<Family> {
hardware(image).map(|h| h.family())
}
pub fn hardware(image: &[u8]) -> Option<Hardware> {
if image.len() != layout::IMAGE_SIZE {
return None;
}
let descriptor = layout::DESCRIPTOR.of(image)?;
let banner = layout::BANNER.of(image)?;
let tagged = Chip::from_tag(descriptor).is_some();
let chip = Chip::from_banner(banner)?;
let loader = loader(banner)?;
let mut records =
(CODE.start..=CODE.end - RECORD_USED_LEN).filter(|&at| is_record(&image[at..]));
let record = records.next()?;
if records.next().is_some() {
return None;
}
let unique = record + MARKER.len();
Chip::from_tag(&image[unique + RECORD_TAG..unique + RECORD_TAG + 10])?;
let platform = trim(&image[unique + RECORD_PLATFORM..unique + RECORD_PLATFORM + PLATFORM_LEN]);
if tagged
&& trim(&descriptor[DESCRIPTOR_PLATFORM..DESCRIPTOR_PLATFORM + PLATFORM_LEN]) != platform
{
return None;
}
if platform.is_empty() || !platform.iter().all(|b| (0x21..0x7F).contains(b)) {
return None;
}
let features = template(image, record)?;
let first = |code: u16| features.iter().find(|(c, _)| *c == code).map(|(_, d)| *d);
let profiles = first(PROFILE_LIST)?;
let core = first(CORE).filter(|d| d.len() >= 4)?;
let medium = *first(MEDIUM_FEATURE)?.first()?;
let bd_write = profiles
.chunks_exact(4)
.any(|p| p[0] == 0 && BD_WRITE_PROFILES.contains(&p[1]));
Some(Hardware {
chip,
platform: String::from_utf8(platform.to_vec()).ok()?,
loader,
interface: [core[0], core[1], core[2], core[3]],
medium,
bd_write,
})
}
fn trim(bytes: &[u8]) -> &[u8] {
let end = bytes
.iter()
.rposition(|&b| b != b' ' && b != 0)
.map_or(0, |i| i + 1);
&bytes[..end]
}
fn loader(banner: &[u8]) -> Option<String> {
let rest = banner.strip_prefix(b"MT19")?;
if rest.len() < 2 || !rest[..2].iter().all(u8::is_ascii_digit) {
return None;
}
let token = rest[2..].strip_prefix(b" Boot ")?;
let len = token
.iter()
.take(LOADER_MAX)
.take_while(|b| (0x21..0x7F).contains(*b))
.count();
if len == 0 {
return None;
}
String::from_utf8(token[..len].to_vec()).ok()
}
fn is_record(bytes: &[u8]) -> bool {
bytes.len() >= RECORD_LEN
&& bytes.starts_with(&MARKER)
&& bytes[2..8] == *b"UNIQUE"
&& bytes[8..18].iter().all(u8::is_ascii_digit)
&& bytes[18..26] == *b"MTEKMT19"
&& bytes[26..28].iter().all(u8::is_ascii_digit)
}
fn template(image: &[u8], end: usize) -> Option<Vec<(u16, &[u8])>> {
let floor = end.saturating_sub(TEMPLATE_REACH).max(CODE.start - 1);
(floor + 1..=end.checked_sub(4)?).rev().find_map(|start| {
let length = image[start + 3];
if image[start..start + 2] != [0, 0] || length == 0 || length % 4 != 0 {
return None;
}
let mut features = Vec::new();
let mut at = start;
while at + 4 <= end {
let code = u16::from_be_bytes([image[at], image[at + 1]]);
let length = usize::from(image[at + 3]);
let data = image.get(at + 4..at + 4 + length)?;
features.push((code, data));
at += 4 + length;
}
(at == end && features.len() > 1 && features[0].0 == PROFILE_LIST && features[1].0 == CORE)
.then_some(features)
})
}
#[cfg(test)]
#[path = "family_tests.rs"]
mod tests;