use super::{frame_with, indexed};
use std::ops::Range;
const FLASH_OP_READ: u8 = 0x44;
pub const FLASH_PAGE_BASIC_PARAM: u16 = 0x0780;
pub const FLASH_PAGES_PER_CHUNK: u16 = 4;
const FLASH_OP_ERASE: u8 = 0x23;
const FLASH_OP_WRITE: u8 = 0x85;
pub const FLASH_PAGE_BYTES: usize = 256;
pub const PARAM_BLOCK: u8 = 0x07;
pub const FIRMWARE_BLOCKS: Range<u8> = 0x00..0x0b;
pub const GOLDEN_BLOCK: u8 = 0x20;
pub const SCREEN_RECORD_ADDR: u32 = 0x0007_f000;
pub const SCREEN_RECORD_LEN: usize = 256;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FlashMap {
pub param_block: u8,
pub firmware_blocks: Range<u8>,
pub golden_block: u8,
pub screen_record_addr: u32,
}
pub const E120: FlashMap = FlashMap {
param_block: PARAM_BLOCK,
firmware_blocks: FIRMWARE_BLOCKS,
golden_block: GOLDEN_BLOCK,
screen_record_addr: SCREEN_RECORD_ADDR,
};
pub const FLASH_REPLY_TYPE: [u8; 2] = [0x09, 0x01];
pub const FLASH_CHUNK_BYTES: usize = 1024;
#[must_use]
pub fn read_flash(rcv_index: u16, page: u16) -> Vec<u8> {
paged(rcv_index, FLASH_OP_READ, page)
}
fn paged(rcv_index: u16, opcode: u8, page: u16) -> Vec<u8> {
frame_with([0x06, 0x00], 126, |p| {
indexed(p, rcv_index, opcode);
p[4] = 0x01;
p[5..7].copy_from_slice(&page.to_be_bytes());
})
}
#[must_use]
pub fn set_program_writable(rcv_index: u16, writable: bool) -> Vec<u8> {
frame_with([0x23, 0x00], 126, |p| {
indexed(p, rcv_index, if writable { 0xff } else { 0x00 });
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WriteError {
ForbiddenBlock(u8),
WrongPageSize(usize),
ForbiddenAddress(u32),
}
impl std::fmt::Display for WriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ForbiddenBlock(b) => write!(
f,
"refusing to touch flash block 0x{b:02x}; outside this write's allowlist"
),
Self::WrongPageSize(n) => {
write!(f, "page payload is {n} bytes, must be {FLASH_PAGE_BYTES}")
}
Self::ForbiddenAddress(a) => write!(
f,
"refusing linear flash access at 0x{a:08x}; only the screen-size \
record may be reached this way"
),
}
}
}
impl std::error::Error for WriteError {}
impl FlashMap {
fn param_block(&self, block: u8) -> Result<u8, WriteError> {
if block == self.param_block {
Ok(block)
} else {
Err(WriteError::ForbiddenBlock(block))
}
}
fn firmware_block(&self, block: u8) -> Result<u8, WriteError> {
if self.firmware_blocks.contains(&block) {
Ok(block)
} else {
Err(WriteError::ForbiddenBlock(block))
}
}
pub fn erase_block(&self, rcv_index: u16, block: u8) -> Result<Vec<u8>, WriteError> {
Ok(erase_block_unchecked(rcv_index, self.param_block(block)?))
}
pub fn erase_firmware_block(&self, rcv_index: u16, block: u8) -> Result<Vec<u8>, WriteError> {
Ok(erase_block_unchecked(rcv_index, self.firmware_block(block)?))
}
pub fn write_firmware_page(&self, rcv_index: u16, block: u8, page: u8, data: &[u8]) -> Result<Vec<u8>, WriteError> {
Ok(write_page_unchecked(rcv_index, self.firmware_block(block)?, page, one_page(data)?))
}
pub fn write_page(&self, rcv_index: u16, block: u8, page: u8, data: &[u8]) -> Result<Vec<u8>, WriteError> {
Ok(write_page_unchecked(rcv_index, self.param_block(block)?, page, one_page(data)?))
}
pub fn write_screen_record(&self, rcv_index: u16, addr: u32, data: &[u8]) -> Result<Vec<u8>, WriteError> {
if data.len() != SCREEN_RECORD_LEN {
return Err(WriteError::WrongPageSize(data.len()));
}
if addr != self.screen_record_addr {
return Err(WriteError::ForbiddenAddress(addr));
}
Ok(linear(rcv_index, FLASH_OP_WRITE, addr, SCREEN_RECORD_LEN as u32, data, 4))
}
#[must_use]
pub fn read_screen_record(&self, rcv_index: u16) -> Vec<u8> {
read_flash_linear(rcv_index, self.screen_record_addr, SCREEN_RECORD_LEN as u32)
}
}
fn one_page(data: &[u8]) -> Result<&[u8], WriteError> {
if data.len() == FLASH_PAGE_BYTES {
Ok(data)
} else {
Err(WriteError::WrongPageSize(data.len()))
}
}
fn erase_block_unchecked(rcv_index: u16, block: u8) -> Vec<u8> {
paged(rcv_index, FLASH_OP_ERASE, u16::from(block) << 8)
}
fn write_page_unchecked(rcv_index: u16, block: u8, page: u8, data: &[u8]) -> Vec<u8> {
frame_with([0x06, 0x00], 8 + FLASH_PAGE_BYTES, |p| {
indexed(p, rcv_index, FLASH_OP_WRITE);
p[5] = block;
p[6] = page;
p[8..].copy_from_slice(data);
})
}
#[must_use]
pub fn read_flash_linear(rcv_index: u16, addr: u32, len: u32) -> Vec<u8> {
linear(rcv_index, FLASH_OP_READ, addr, len, &[], 0)
}
fn linear(rcv_index: u16, opcode: u8, addr: u32, len: u32, data: &[u8], tail: usize) -> Vec<u8> {
frame_with([0x19, 0x00], 12 + data.len() + tail, |p| {
indexed(p, rcv_index, opcode);
p[4..8].copy_from_slice(&addr.to_be_bytes());
p[8..12].copy_from_slice(&len.to_be_bytes());
p[12..12 + data.len()].copy_from_slice(data);
})
}
pub fn flash_reply_data(eth_frame: &[u8]) -> Option<&[u8]> {
if eth_frame.len() < 15 || eth_frame[12..14] != FLASH_REPLY_TYPE {
return None;
}
let data = ð_frame[15..];
Some(&data[..data.len().min(FLASH_CHUNK_BYTES)])
}
#[cfg(test)]
mod linear_tests {
use super::*;
#[test]
fn screen_record_write_matches_the_documented_layout() {
let data = vec![0xabu8; SCREEN_RECORD_LEN];
let f = E120.write_screen_record(0, SCREEN_RECORD_ADDR, &data).unwrap();
assert_eq!(&f[12..14], &[0x19, 0x00]);
assert_eq!(f[17], FLASH_OP_WRITE);
assert_eq!(&f[18..22], &SCREEN_RECORD_ADDR.to_be_bytes());
assert_eq!(&f[22..26], &(SCREEN_RECORD_LEN as u32).to_be_bytes());
assert_eq!(&f[26..26 + SCREEN_RECORD_LEN], &data[..]);
assert_eq!(f.len(), 286);
}
#[test]
fn linear_frames_refuse_every_address_but_the_screen_record() {
let data = vec![0u8; SCREEN_RECORD_LEN];
for addr in [
0x0000_0000,
0x0007_0000,
0x0007_efff,
0x0007_f001,
0x0008_0000,
0xffff_ffff,
] {
assert_eq!(
E120.write_screen_record(0, addr, &data),
Err(WriteError::ForbiddenAddress(addr)),
"address 0x{addr:08x} must be refused"
);
}
}
#[test]
fn the_screen_record_address_is_allowed() {
let data = vec![0u8; SCREEN_RECORD_LEN];
assert!(E120.write_screen_record(0, SCREEN_RECORD_ADDR, &data).is_ok());
}
#[test]
fn a_wrong_length_payload_is_refused() {
assert_eq!(
E120.write_screen_record(0, SCREEN_RECORD_ADDR, &[0; 128]),
Err(WriteError::WrongPageSize(128))
);
}
#[test]
fn a_linear_read_carries_no_data() {
let f = read_flash_linear(0, SCREEN_RECORD_ADDR, SCREEN_RECORD_LEN as u32);
assert_eq!(&f[12..14], &[0x19, 0x00]);
assert_eq!(f[17], FLASH_OP_READ);
assert_eq!(&f[18..22], &SCREEN_RECORD_ADDR.to_be_bytes());
assert_eq!(&f[22..26], &256u32.to_be_bytes());
assert_eq!(f.len(), 26);
}
}
#[cfg(test)]
mod firmware_tests {
use super::*;
#[test]
fn firmware_writes_are_confined_to_the_primary_image() {
let page = [0u8; FLASH_PAGE_BYTES];
for block in FIRMWARE_BLOCKS {
assert!(E120.erase_firmware_block(0, block).is_ok());
assert!(E120.write_firmware_page(0, block, 0, &page).is_ok());
}
for block in [GOLDEN_BLOCK, 0x0b, 0x0c, 0x21, 0xff] {
assert_eq!(
E120.erase_firmware_block(0, block),
Err(WriteError::ForbiddenBlock(block)),
"block 0x{block:02x} must be refused"
);
assert_eq!(
E120.write_firmware_page(0, block, 0, &page),
Err(WriteError::ForbiddenBlock(block))
);
}
}
#[test]
fn the_golden_bank_is_outside_the_writable_range() {
assert!(!FIRMWARE_BLOCKS.contains(&GOLDEN_BLOCK));
}
#[test]
fn the_parameter_helpers_still_refuse_firmware_blocks() {
assert_eq!(E120.erase_block(0, 0x00), Err(WriteError::ForbiddenBlock(0x00)));
assert_eq!(
E120.write_page(0, 0x00, 0, &[0u8; FLASH_PAGE_BYTES]),
Err(WriteError::ForbiddenBlock(0x00))
);
}
}
#[cfg(test)]
mod writable_tests {
use super::*;
#[test]
fn unlock_uses_the_negated_flag() {
let f = set_program_writable(0, true);
assert_eq!(&f[12..14], &[0x23, 0x00]);
assert_eq!(f[17], 0xff, "enable is 0xff, not 0x01");
assert_eq!(f.len(), 140);
}
#[test]
fn relock_clears_it() {
assert_eq!(set_program_writable(0, false)[17], 0x00);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CARD_MAC, SENDER_MAC};
#[test]
fn read_frame_matches_the_documented_layout() {
let f = read_flash(0, FLASH_PAGE_BASIC_PARAM);
assert_eq!(f.len(), 140);
assert_eq!(&f[0..6], &CARD_MAC);
assert_eq!(&f[6..12], &SENDER_MAC);
assert_eq!(&f[12..14], &[0x06, 0x00]);
assert_eq!(f[17], FLASH_OP_READ);
assert_eq!(&f[19..21], &[0x07, 0x80]);
}
#[test]
fn a_read_frame_carries_no_data() {
let f = read_flash(0, FLASH_PAGE_BASIC_PARAM);
assert!(f[21..].iter().all(|&b| b == 0));
}
#[test]
fn writes_outside_the_parameter_block_are_refused() {
for block in [0x00, 0x01, 0x06, 0x08, 0xff] {
assert_eq!(
E120.erase_block(0, block),
Err(WriteError::ForbiddenBlock(block)),
"block 0x{block:02x} must be refused"
);
assert_eq!(
E120.write_page(0, block, 0, &[0; FLASH_PAGE_BYTES]),
Err(WriteError::ForbiddenBlock(block))
);
}
}
#[test]
fn the_parameter_block_is_allowed() {
assert!(E120.erase_block(0, PARAM_BLOCK).is_ok());
assert!(E120.write_page(0, PARAM_BLOCK, 0x80, &[0; FLASH_PAGE_BYTES]).is_ok());
}
#[test]
fn a_page_write_must_be_exactly_one_page() {
assert_eq!(
E120.write_page(0, PARAM_BLOCK, 0, &[0; 255]),
Err(WriteError::WrongPageSize(255))
);
assert_eq!(
E120.write_page(0, PARAM_BLOCK, 0, &[0; 257]),
Err(WriteError::WrongPageSize(257))
);
}
#[test]
fn write_frame_carries_the_page_data_at_the_documented_offset() {
let data: Vec<u8> = (0..=255u8).collect();
let f = E120.write_page(1, PARAM_BLOCK, 0x81, &data).unwrap();
assert_eq!(f.len(), 278);
assert_eq!(f[17], FLASH_OP_WRITE);
assert_eq!(f[18], 0x00, "flag byte");
assert_eq!(f[19], PARAM_BLOCK);
assert_eq!(f[20], 0x81);
assert_eq!(&f[22..], &data[..]);
}
#[test]
fn erase_frame_carries_the_block_in_the_page_high_byte() {
let f = E120.erase_block(0, PARAM_BLOCK).unwrap();
assert_eq!(f.len(), 140);
assert_eq!(f[17], FLASH_OP_ERASE);
assert_eq!(f[18], 0x01);
assert_eq!(&f[19..21], &[PARAM_BLOCK, 0x00]);
}
}