Skip to main content

colorlight/
flash.rs

1//! Flash read, erase and write frames.
2//!
3//! Reads are unrestricted; every write builder is allowlisted by the card's
4//! [`FlashMap`]: page-addressed (type 0x0600) to the parameter block or the
5//! primary firmware blocks, linear (type 0x1900) to the screen-size record only.
6
7use super::{frame_with, indexed};
8use std::ops::Range;
9
10/// A read frame carries no data, so it cannot modify the card wherever it
11/// is pointed.
12const FLASH_OP_READ: u8 = 0x44;
13
14/// Page index (256-byte pages) of the receiver's basic parameters.
15pub const FLASH_PAGE_BASIC_PARAM: u16 = 0x0780;
16
17/// Pages advance by 4 per 1024-byte chunk read.
18pub const FLASH_PAGES_PER_CHUNK: u16 = 4;
19
20const FLASH_OP_ERASE: u8 = 0x23;
21
22/// Page write; the same opcode writes an EEPROM record (`eeprom::write`).
23const FLASH_OP_WRITE: u8 = 0x85;
24
25pub const FLASH_PAGE_BYTES: usize = 256;
26
27/// The E120's 64 KB parameter block (`config/cards/e120.toml`).
28pub const PARAM_BLOCK: u8 = 0x07;
29
30/// The E120's primary firmware blocks; the golden backup at
31/// [`GOLDEN_BLOCK`] is outside every allowlist.
32pub const FIRMWARE_BLOCKS: Range<u8> = 0x00..0x0b;
33
34/// First block of the E120's golden backup image.
35pub const GOLDEN_BLOCK: u8 = 0x20;
36
37/// Flash address of the E120's screen-size record, reachable only through
38/// the linear-address frames.
39pub const SCREEN_RECORD_ADDR: u32 = 0x0007_f000;
40
41pub const SCREEN_RECORD_LEN: usize = 256;
42
43/// The blocks and addresses one card's writes are confined to. Every write
44/// builder is a method here, so no frame can be built outside a map.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct FlashMap {
47    /// The block `erase_block` and `write_page` accept.
48    pub param_block: u8,
49    /// Blocks `erase_firmware_block` and `write_firmware_page` accept.
50    pub firmware_blocks: Range<u8>,
51    /// First block of the golden backup; outside both allowlists.
52    pub golden_block: u8,
53    /// The one linear address `write_screen_record` accepts.
54    pub screen_record_addr: u32,
55}
56
57/// The E120's map, the values the frame tests pin.
58pub const E120: FlashMap = FlashMap {
59    param_block: PARAM_BLOCK,
60    firmware_blocks: FIRMWARE_BLOCKS,
61    golden_block: GOLDEN_BLOCK,
62    screen_record_addr: SCREEN_RECORD_ADDR,
63};
64
65/// Frame type of a flash-read reply.
66pub const FLASH_REPLY_TYPE: [u8; 2] = [0x09, 0x01];
67
68/// Flash bytes per reply frame.
69pub const FLASH_CHUNK_BYTES: usize = 1024;
70
71/// Read 1024 bytes starting at `page` (a 256-byte page index): type 0x0600,
72/// 126-byte payload.
73#[must_use]
74pub fn read_flash(rcv_index: u16, page: u16) -> Vec<u8> {
75    paged(rcv_index, FLASH_OP_READ, page)
76}
77
78/// A 0x0600 read or erase: `[4]` = 0x01, `[5..7]` page BE, no data.
79fn paged(rcv_index: u16, opcode: u8, page: u16) -> Vec<u8> {
80    frame_with([0x06, 0x00], 126, |p| {
81        indexed(p, rcv_index, opcode);
82        p[4] = 0x01;
83        p[5..7].copy_from_slice(&page.to_be_bytes());
84    })
85}
86
87/// Unlock or relock the write-protected program region.
88///
89/// Erases and page writes silently do nothing while locked. The vendor negates
90/// the flag, so enable is 0xff (`unlock_uses_the_negated_flag`). Relock on
91/// every exit path.
92#[must_use]
93pub fn set_program_writable(rcv_index: u16, writable: bool) -> Vec<u8> {
94    frame_with([0x23, 0x00], 126, |p| {
95        indexed(p, rcv_index, if writable { 0xff } else { 0x00 });
96    })
97}
98
99/// A write refused before any frame is built.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum WriteError {
102    ForbiddenBlock(u8),
103    WrongPageSize(usize),
104    ForbiddenAddress(u32),
105}
106
107impl std::fmt::Display for WriteError {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::ForbiddenBlock(b) => write!(
111                f,
112                "refusing to touch flash block 0x{b:02x}; outside this write's allowlist"
113            ),
114            Self::WrongPageSize(n) => {
115                write!(f, "page payload is {n} bytes, must be {FLASH_PAGE_BYTES}")
116            }
117            Self::ForbiddenAddress(a) => write!(
118                f,
119                "refusing linear flash access at 0x{a:08x}; only the screen-size \
120                 record may be reached this way"
121            ),
122        }
123    }
124}
125
126impl std::error::Error for WriteError {}
127
128impl FlashMap {
129    fn param_block(&self, block: u8) -> Result<u8, WriteError> {
130        if block == self.param_block {
131            Ok(block)
132        } else {
133            Err(WriteError::ForbiddenBlock(block))
134        }
135    }
136
137    fn firmware_block(&self, block: u8) -> Result<u8, WriteError> {
138        if self.firmware_blocks.contains(&block) {
139            Ok(block)
140        } else {
141            Err(WriteError::ForbiddenBlock(block))
142        }
143    }
144
145    /// Erase the whole 64 KB parameter block; the caller must hold a full copy.
146    ///
147    /// # Errors
148    /// Refuses any block other than `param_block`.
149    pub fn erase_block(&self, rcv_index: u16, block: u8) -> Result<Vec<u8>, WriteError> {
150        Ok(erase_block_unchecked(rcv_index, self.param_block(block)?))
151    }
152
153    /// Erase a firmware block. Kept apart from [`Self::erase_block`] so a
154    /// firmware write is never reachable through the parameter path.
155    ///
156    /// # Errors
157    /// Refuses any block outside `firmware_blocks`.
158    pub fn erase_firmware_block(&self, rcv_index: u16, block: u8) -> Result<Vec<u8>, WriteError> {
159        Ok(erase_block_unchecked(rcv_index, self.firmware_block(block)?))
160    }
161
162    /// Write one page of a firmware block.
163    ///
164    /// # Errors
165    /// Refuses any block outside `firmware_blocks`, or a payload that is not
166    /// exactly one page.
167    pub fn write_firmware_page(&self, rcv_index: u16, block: u8, page: u8, data: &[u8]) -> Result<Vec<u8>, WriteError> {
168        Ok(write_page_unchecked(rcv_index, self.firmware_block(block)?, page, one_page(data)?))
169    }
170
171    /// Write one 256-byte page within the parameter block.
172    ///
173    /// # Errors
174    /// Refuses any block other than `param_block`, or a payload that is not
175    /// exactly one page.
176    pub fn write_page(&self, rcv_index: u16, block: u8, page: u8, data: &[u8]) -> Result<Vec<u8>, WriteError> {
177        Ok(write_page_unchecked(rcv_index, self.param_block(block)?, page, one_page(data)?))
178    }
179
180    /// Write the screen-size record (linear-address frame, type 0x1900).
181    /// Linear writes can reach firmware, so exactly one address is allowed.
182    ///
183    /// # Errors
184    /// Refuses any address but `screen_record_addr`, or a wrong length.
185    pub fn write_screen_record(&self, rcv_index: u16, addr: u32, data: &[u8]) -> Result<Vec<u8>, WriteError> {
186        if data.len() != SCREEN_RECORD_LEN {
187            return Err(WriteError::WrongPageSize(data.len()));
188        }
189        if addr != self.screen_record_addr {
190            return Err(WriteError::ForbiddenAddress(addr));
191        }
192        Ok(linear(rcv_index, FLASH_OP_WRITE, addr, SCREEN_RECORD_LEN as u32, data, 4))
193    }
194
195    /// Read the screen-size record.
196    #[must_use]
197    pub fn read_screen_record(&self, rcv_index: u16) -> Vec<u8> {
198        read_flash_linear(rcv_index, self.screen_record_addr, SCREEN_RECORD_LEN as u32)
199    }
200}
201
202fn one_page(data: &[u8]) -> Result<&[u8], WriteError> {
203    if data.len() == FLASH_PAGE_BYTES {
204        Ok(data)
205    } else {
206        Err(WriteError::WrongPageSize(data.len()))
207    }
208}
209
210fn erase_block_unchecked(rcv_index: u16, block: u8) -> Vec<u8> {
211    paged(rcv_index, FLASH_OP_ERASE, u16::from(block) << 8)
212}
213
214fn write_page_unchecked(rcv_index: u16, block: u8, page: u8, data: &[u8]) -> Vec<u8> {
215    // [1..3] index, [3] opcode, [4] flag, [5] block, [6] page, data at 8
216    // (`write_frame_carries_the_page_data_at_the_documented_offset`).
217    frame_with([0x06, 0x00], 8 + FLASH_PAGE_BYTES, |p| {
218        indexed(p, rcv_index, FLASH_OP_WRITE);
219        p[5] = block;
220        p[6] = page;
221        p[8..].copy_from_slice(data);
222    })
223}
224
225/// Read `len` bytes at any flash address.
226#[must_use]
227pub fn read_flash_linear(rcv_index: u16, addr: u32, len: u32) -> Vec<u8> {
228    linear(rcv_index, FLASH_OP_READ, addr, len, &[], 0)
229}
230
231/// A linear-address 0x1900 frame: `[4..8]` address, `[8..12]` length, data at
232/// 12, then `tail` zero bytes.
233fn linear(rcv_index: u16, opcode: u8, addr: u32, len: u32, data: &[u8], tail: usize) -> Vec<u8> {
234    frame_with([0x19, 0x00], 12 + data.len() + tail, |p| {
235        indexed(p, rcv_index, opcode);
236        p[4..8].copy_from_slice(&addr.to_be_bytes());
237        p[8..12].copy_from_slice(&len.to_be_bytes());
238        p[12..12 + data.len()].copy_from_slice(data);
239    })
240}
241
242/// The flash bytes of a reply: header, one status byte at 14, then up to
243/// [`FLASH_CHUNK_BYTES`] of data.
244pub fn flash_reply_data(eth_frame: &[u8]) -> Option<&[u8]> {
245    if eth_frame.len() < 15 || eth_frame[12..14] != FLASH_REPLY_TYPE {
246        return None;
247    }
248    let data = &eth_frame[15..];
249    Some(&data[..data.len().min(FLASH_CHUNK_BYTES)])
250}
251
252#[cfg(test)]
253mod linear_tests {
254    use super::*;
255
256    #[test]
257    fn screen_record_write_matches_the_documented_layout() {
258        let data = vec![0xabu8; SCREEN_RECORD_LEN];
259        let f = E120.write_screen_record(0, SCREEN_RECORD_ADDR, &data).unwrap();
260        assert_eq!(&f[12..14], &[0x19, 0x00]);
261        assert_eq!(f[17], FLASH_OP_WRITE);
262        assert_eq!(&f[18..22], &SCREEN_RECORD_ADDR.to_be_bytes());
263        assert_eq!(&f[22..26], &(SCREEN_RECORD_LEN as u32).to_be_bytes());
264        assert_eq!(&f[26..26 + SCREEN_RECORD_LEN], &data[..]);
265        assert_eq!(f.len(), 286);
266    }
267
268    #[test]
269    fn linear_frames_refuse_every_address_but_the_screen_record() {
270        let data = vec![0u8; SCREEN_RECORD_LEN];
271        for addr in [
272            0x0000_0000,
273            0x0007_0000,
274            0x0007_efff,
275            0x0007_f001,
276            0x0008_0000,
277            0xffff_ffff,
278        ] {
279            assert_eq!(
280                E120.write_screen_record(0, addr, &data),
281                Err(WriteError::ForbiddenAddress(addr)),
282                "address 0x{addr:08x} must be refused"
283            );
284        }
285    }
286
287    #[test]
288    fn the_screen_record_address_is_allowed() {
289        let data = vec![0u8; SCREEN_RECORD_LEN];
290        assert!(E120.write_screen_record(0, SCREEN_RECORD_ADDR, &data).is_ok());
291    }
292
293    #[test]
294    fn a_wrong_length_payload_is_refused() {
295        assert_eq!(
296            E120.write_screen_record(0, SCREEN_RECORD_ADDR, &[0; 128]),
297            Err(WriteError::WrongPageSize(128))
298        );
299    }
300
301    #[test]
302    fn a_linear_read_carries_no_data() {
303        let f = read_flash_linear(0, SCREEN_RECORD_ADDR, SCREEN_RECORD_LEN as u32);
304        assert_eq!(&f[12..14], &[0x19, 0x00]);
305        assert_eq!(f[17], FLASH_OP_READ);
306        assert_eq!(&f[18..22], &SCREEN_RECORD_ADDR.to_be_bytes());
307        assert_eq!(&f[22..26], &256u32.to_be_bytes());
308        assert_eq!(f.len(), 26);
309    }
310}
311
312#[cfg(test)]
313mod firmware_tests {
314    use super::*;
315
316    #[test]
317    fn firmware_writes_are_confined_to_the_primary_image() {
318        let page = [0u8; FLASH_PAGE_BYTES];
319        for block in FIRMWARE_BLOCKS {
320            assert!(E120.erase_firmware_block(0, block).is_ok());
321            assert!(E120.write_firmware_page(0, block, 0, &page).is_ok());
322        }
323        for block in [GOLDEN_BLOCK, 0x0b, 0x0c, 0x21, 0xff] {
324            assert_eq!(
325                E120.erase_firmware_block(0, block),
326                Err(WriteError::ForbiddenBlock(block)),
327                "block 0x{block:02x} must be refused"
328            );
329            assert_eq!(
330                E120.write_firmware_page(0, block, 0, &page),
331                Err(WriteError::ForbiddenBlock(block))
332            );
333        }
334    }
335
336    #[test]
337    fn the_golden_bank_is_outside_the_writable_range() {
338        assert!(!FIRMWARE_BLOCKS.contains(&GOLDEN_BLOCK));
339    }
340
341    #[test]
342    fn the_parameter_helpers_still_refuse_firmware_blocks() {
343        assert_eq!(E120.erase_block(0, 0x00), Err(WriteError::ForbiddenBlock(0x00)));
344        assert_eq!(
345            E120.write_page(0, 0x00, 0, &[0u8; FLASH_PAGE_BYTES]),
346            Err(WriteError::ForbiddenBlock(0x00))
347        );
348    }
349}
350
351#[cfg(test)]
352mod writable_tests {
353    use super::*;
354
355    #[test]
356    fn unlock_uses_the_negated_flag() {
357        let f = set_program_writable(0, true);
358        assert_eq!(&f[12..14], &[0x23, 0x00]);
359        assert_eq!(f[17], 0xff, "enable is 0xff, not 0x01");
360        assert_eq!(f.len(), 140);
361    }
362
363    #[test]
364    fn relock_clears_it() {
365        assert_eq!(set_program_writable(0, false)[17], 0x00);
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::{CARD_MAC, SENDER_MAC};
373
374    #[test]
375    fn read_frame_matches_the_documented_layout() {
376        let f = read_flash(0, FLASH_PAGE_BASIC_PARAM);
377        assert_eq!(f.len(), 140);
378        assert_eq!(&f[0..6], &CARD_MAC);
379        assert_eq!(&f[6..12], &SENDER_MAC);
380        assert_eq!(&f[12..14], &[0x06, 0x00]);
381        assert_eq!(f[17], FLASH_OP_READ);
382        assert_eq!(&f[19..21], &[0x07, 0x80]);
383    }
384
385    #[test]
386    fn a_read_frame_carries_no_data() {
387        let f = read_flash(0, FLASH_PAGE_BASIC_PARAM);
388        assert!(f[21..].iter().all(|&b| b == 0));
389    }
390
391    #[test]
392    fn writes_outside_the_parameter_block_are_refused() {
393        for block in [0x00, 0x01, 0x06, 0x08, 0xff] {
394            assert_eq!(
395                E120.erase_block(0, block),
396                Err(WriteError::ForbiddenBlock(block)),
397                "block 0x{block:02x} must be refused"
398            );
399            assert_eq!(
400                E120.write_page(0, block, 0, &[0; FLASH_PAGE_BYTES]),
401                Err(WriteError::ForbiddenBlock(block))
402            );
403        }
404    }
405
406    #[test]
407    fn the_parameter_block_is_allowed() {
408        assert!(E120.erase_block(0, PARAM_BLOCK).is_ok());
409        assert!(E120.write_page(0, PARAM_BLOCK, 0x80, &[0; FLASH_PAGE_BYTES]).is_ok());
410    }
411
412    #[test]
413    fn a_page_write_must_be_exactly_one_page() {
414        assert_eq!(
415            E120.write_page(0, PARAM_BLOCK, 0, &[0; 255]),
416            Err(WriteError::WrongPageSize(255))
417        );
418        assert_eq!(
419            E120.write_page(0, PARAM_BLOCK, 0, &[0; 257]),
420            Err(WriteError::WrongPageSize(257))
421        );
422    }
423
424    #[test]
425    fn write_frame_carries_the_page_data_at_the_documented_offset() {
426        let data: Vec<u8> = (0..=255u8).collect();
427        let f = E120.write_page(1, PARAM_BLOCK, 0x81, &data).unwrap();
428        assert_eq!(f.len(), 278);
429        assert_eq!(f[17], FLASH_OP_WRITE);
430        assert_eq!(f[18], 0x00, "flag byte");
431        assert_eq!(f[19], PARAM_BLOCK);
432        assert_eq!(f[20], 0x81);
433        assert_eq!(&f[22..], &data[..]);
434    }
435
436    #[test]
437    fn erase_frame_carries_the_block_in_the_page_high_byte() {
438        let f = E120.erase_block(0, PARAM_BLOCK).unwrap();
439        assert_eq!(f.len(), 140);
440        assert_eq!(f[17], FLASH_OP_ERASE);
441        assert_eq!(f[18], 0x01);
442        assert_eq!(&f[19..21], &[PARAM_BLOCK, 0x00]);
443    }
444}