Skip to main content

mediatek_optical/image/
chip.rs

1//! Chip generation and identity detection from image bytes.
2//!
3//! The authoritative signal for the chip generation (MT1959 vs MT1939) is the
4//! **boot loader banner** at image offset `0x3000` ([`layout::BANNER`], 16
5//! bytes), e.g. `"MT1959 Boot JB8 "` or `"MT1939 Boot Code"`. The family is
6//! decided by [`Chip::from_banner`] over that banner and by nothing else.
7//!
8//! Why the banner and not the `MTEKMT19xx` identity string: across a 119-image
9//! corpus of clean OEM images the banner agrees with the actual code generation
10//! 119/119, whereas the `MTEKMT19xx` descriptor tag is a *stale vendor label*
11//! that is wrong for 55 of them — the JB8/JBC6/JBP6/BUP1 builds (LG WH16NS58,
12//! WH16NS60, BH16NS40..NS50, …) carry the tag `MTEKMT1939` but ship an MT1959
13//! boot loader and MT1959 code. So the tag drives **no logic anywhere**: not the
14//! family, not a fallback, not a gate, not the family hash, not an engine
15//! choice. It is retained only as an optional display field ([`ChipInfo::identity_tag`])
16//! — a curiosity, never a decision.
17//!
18//! Two other things that *look* like family signals are also NOT used to gate
19//! the family:
20//!
21//! * the `MTEKMT19xx` identity string (see above) — display-only;
22//! * the descriptor **marker byte** at `+0x50` takes `0x78/0x58/0x18/0x38`
23//!   within a single family (a variant/region/downgrade flag beside the DE byte
24//!   at `+0x56`) — display-only, never a gate.
25//!
26//! Detection refuses cleanly on an image whose boot banner names no supported
27//! MT19xx generation ([`ChipError::Undetectable`]); the caller then aborts with
28//! a clear message rather than guessing.
29
30use crate::layout;
31use crate::Chip;
32
33const BANNER_OFFSET: usize = layout::BANNER.start;
34const BANNER_LEN: usize = layout::BANNER.len;
35const DESCRIPTOR_OFFSET: usize = layout::DESCRIPTOR.start;
36/// Bytes of the descriptor record consumed by parsing.
37const DESCRIPTOR_LEN: usize = 0x40;
38/// Offset of the descriptor's `MTEKMT19..` family tag within the record.
39const DESCRIPTOR_TAG_OFF: usize = 0x34;
40/// Offset of the display-only marker byte within the descriptor record.
41const DESCRIPTOR_MARKER_OFF: usize = 0x50;
42
43/// Everything the tools could read off the image about its chip identity. The
44/// `banner`, `vendor`, `model`, `rev`, `marker_0x50` and `identity_tag` fields
45/// are display / corroboration only — the `family` is decided by
46/// [`Chip::from_banner`] over the boot banner at [`layout::BANNER`] and nothing
47/// else.
48#[derive(Debug, Clone, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct ChipInfo {
51    /// The chip family, decided by the boot banner ([`Chip::from_banner`]).
52    pub family: Chip,
53    /// The trimmed boot-banner string (e.g. `"MT1959 Boot BU5"`). The authoritative
54    /// source of `family`; also shown for display.
55    pub banner: String,
56    /// Trimmed vendor field from the descriptor (e.g. `"HL-DT-ST"`). May be empty
57    /// if the image has no well-formed identity page.
58    pub vendor: String,
59    /// Trimmed model field from the descriptor (e.g. `"BD-RE BU40N"`).
60    pub model: String,
61    /// Trimmed revision field from the descriptor (e.g. `"1.00"`).
62    pub rev: String,
63    /// Trimmed vendor-specific field from the descriptor (e.g. `"N003600"`) — the
64    /// region/SKU code MakeMKV/LibreDrive key on. Empty if no identity page.
65    pub vendor_specific: String,
66    /// The matched `MTEKMT19xx` identity string, if any. A **stale vendor label**:
67    /// display / audit only, it NEVER influences `family` (see module docs).
68    pub identity_tag: Option<String>,
69    /// The display-only marker byte at descriptor `+0x50`, if present. NEVER
70    /// gates the family (see module docs).
71    pub marker_0x50: Option<u8>,
72    /// Whether the image carries a well-formed MTEK identity page at
73    /// [`layout::DESCRIPTOR`] (tag `MTEKMT19..` present). Gates identity-page
74    /// edits such as the downgrade-enable byte.
75    pub descriptor_present: bool,
76}
77
78impl ChipInfo {
79    /// Structured descriptor identity, preserving an OEM product suffix separately.
80    pub fn product_identity(&self) -> crate::ProductIdentity<'_> {
81        crate::ProductIdentity::from_fields(&self.vendor, &self.model, &self.rev)
82    }
83}
84
85/// Trim NUL padding and surrounding whitespace from a lossily-decoded ASCII
86/// field.
87fn ascii_trim(bytes: &[u8]) -> String {
88    String::from_utf8_lossy(bytes)
89        .trim_matches(|c: char| c == '\0' || c.is_whitespace())
90        .to_string()
91}
92
93/// Scan `image` for the first `MTEKMT19` + two-ASCII-digit occurrence and return
94/// the matched 10-byte string (display / audit only — never used for `family`).
95fn first_mtek_tag(image: &[u8]) -> Option<String> {
96    const PREFIX: &[u8] = b"MTEKMT19";
97    if image.len() < PREFIX.len() + 2 {
98        return None;
99    }
100    let mut i = 0;
101    while i + PREFIX.len() + 2 <= image.len() {
102        if &image[i..i + PREFIX.len()] == PREFIX {
103            let d0 = image[i + PREFIX.len()];
104            let d1 = image[i + PREFIX.len() + 1];
105            if d0.is_ascii_digit() && d1.is_ascii_digit() {
106                return Some(String::from_utf8_lossy(&image[i..i + PREFIX.len() + 2]).into_owned());
107            }
108        }
109        i += 1;
110    }
111    None
112}
113
114/// Why [`detect_chip`] could not name a chip.
115#[derive(Debug, Clone, PartialEq, Eq)]
116#[non_exhaustive]
117pub enum ChipError {
118    /// The boot banner at [`layout::BANNER`] names no supported MT19xx
119    /// generation (MT1959 or MT1939). Unpack packed/truncated dumps first.
120    Undetectable,
121}
122
123impl std::fmt::Display for ChipError {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        match self {
126            Self::Undetectable => f.write_str(
127                "undetectable — the boot loader banner at 0x3000 names no supported MT19xx \
128                 generation (only MT1959 and MT1939 are handled; unpack packed/truncated \
129                 dumps first)",
130            ),
131        }
132    }
133}
134
135impl std::error::Error for ChipError {}
136
137/// Detect the chip family + model/rev from `image`.
138///
139/// Family is decided by the boot loader banner at [`layout::BANNER`] via
140/// [`Chip::from_banner`], and by nothing else — the `MTEKMT19xx` identity string
141/// is a stale vendor label kept only in [`ChipInfo::identity_tag`] for display.
142/// Fails with [`ChipError::Undetectable`] only when the banner names no supported
143/// generation.
144pub fn detect_chip(image: &[u8]) -> Result<ChipInfo, ChipError> {
145    // --- Family: the boot loader banner is the single authoritative source. ---
146    let banner = read_banner(image);
147    let family = banner
148        .as_deref()
149        .and_then(|b: &str| Chip::from_banner(b.as_bytes()))
150        .ok_or(ChipError::Undetectable)?;
151
152    // --- Identity tag (stale vendor label): display / audit only. ---
153    let identity_tag = first_mtek_tag(image);
154
155    // --- Descriptor parse (display-only + DE anchor). ---
156    let (vendor, model, rev, vendor_specific, marker_0x50, descriptor_present) =
157        parse_descriptor(image);
158
159    Ok(ChipInfo {
160        family,
161        banner: banner.unwrap_or_default(),
162        vendor,
163        model,
164        rev,
165        vendor_specific,
166        identity_tag,
167        marker_0x50,
168        descriptor_present,
169    })
170}
171
172/// Read + trim the boot banner at [`layout::BANNER`], or `None` if the image is
173/// too small to contain it.
174fn read_banner(image: &[u8]) -> Option<String> {
175    if image.len() < BANNER_OFFSET + BANNER_LEN {
176        return None;
177    }
178    let banner_bytes = &image[BANNER_OFFSET..BANNER_OFFSET + BANNER_LEN];
179    let nul = banner_bytes
180        .iter()
181        .position(|&b| b == 0)
182        .unwrap_or(banner_bytes.len());
183    let s = ascii_trim(&banner_bytes[..nul]);
184    if s.is_empty() {
185        None
186    } else {
187        Some(s)
188    }
189}
190
191/// Parse the display-only descriptor fields and detect a well-formed identity
192/// page. Returns `(vendor, model, rev, vendor_specific, marker_0x50,
193/// descriptor_present)`; all strings empty and `descriptor_present == false` when
194/// no identity page exists.
195///
196/// Descriptor record layout (mirrors the drive's SCSI INQUIRY): vendor@0x00 (8),
197/// model@0x08 (16), rev@0x18 (4), `vendor_specific`@0x1C (8) — the region/SKU code
198/// MakeMKV/LibreDrive key on (e.g. `N003600`) — then a `UNIQUE<ts>` stamp and the
199/// `MTEKMT19xx` tag @0x34.
200fn parse_descriptor(image: &[u8]) -> (String, String, String, String, Option<u8>, bool) {
201    if image.len() < DESCRIPTOR_OFFSET + DESCRIPTOR_LEN {
202        return (
203            String::new(),
204            String::new(),
205            String::new(),
206            String::new(),
207            None,
208            false,
209        );
210    }
211    let desc = &image[DESCRIPTOR_OFFSET..DESCRIPTOR_OFFSET + DESCRIPTOR_LEN];
212    let tag_present = desc[DESCRIPTOR_TAG_OFF..].starts_with(b"MTEKMT19");
213    let vendor = ascii_trim(&desc[0x00..0x08]);
214    let model = ascii_trim(&desc[0x08..0x18]);
215    let rev = ascii_trim(&desc[0x18..0x1C]);
216    let vendor_specific = ascii_trim(&desc[0x1C..0x24]);
217    // The marker byte lives at descriptor `+0x50`, PAST the 0x40-byte record, so
218    // it is read from the image directly (with a bounds check), not the slice.
219    let marker = image
220        .get(DESCRIPTOR_OFFSET + DESCRIPTOR_MARKER_OFF)
221        .copied();
222    if tag_present {
223        (vendor, model, rev, vendor_specific, marker, true)
224    } else {
225        // No identity page: still expose the marker byte for audit, but report
226        // no descriptor and empty display fields (they'd be garbage).
227        (
228            String::new(),
229            String::new(),
230            String::new(),
231            String::new(),
232            marker,
233            false,
234        )
235    }
236}
237
238#[cfg(test)]
239#[path = "chip_tests.rs"]
240mod tests;