use std::cell::RefCell;
use std::rc::Rc;
use super::*;
use crate::drive::Error;
use crate::drive::{Data, Transport};
use crate::update::{FlashError, FlashRuntime, FlashTransport, Stage, Status, Verified};
use core::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Via {
Reads,
Writes,
}
#[derive(Default)]
struct Drive {
flash: Vec<u8>,
pending: Vec<u8>,
decrypts: bool,
log: Vec<(Via, Vec<u8>, usize)>,
refuse: Option<Vec<u8>>,
busy: usize,
sense: Vec<Vec<u8>>,
corrupt: Option<usize>,
short_at: Option<usize>,
committed: bool,
}
impl Drive {
fn running(image: &[u8]) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
flash: fixtures::drive_read(image),
pending: vec![0xFF; layout::IMAGE_SIZE],
..Self::default()
}))
}
fn program(&mut self) {
let mut image = self.pending.clone();
if self.decrypts {
crate::enc::decrypt(&mut image).unwrap();
}
let mirror = image[layout::BOOT_MIRROR.start..layout::BOOT_MIRROR.end()].to_vec();
image[..layout::BOOT_PAGE.len].copy_from_slice(&mirror);
for range in layout::DRIVE_WRITTEN {
image[range.start..range.end()].copy_from_slice(&self.flash[range.start..range.end()]);
}
self.flash = image;
}
fn exec(&mut self, via: Via, cdb: &[u8], data: Data<'_>) -> Result<usize, &'static str> {
let len = match &data {
Data::None => 0,
Data::In(buf) => buf.len(),
Data::Out(bytes) => bytes.len(),
};
self.log.push((via, cdb.to_vec(), len));
if self.refuse.as_deref() == Some(cdb) {
return Err("refused");
}
match (cdb[0], data) {
(0x00, _) if self.committed && self.busy > 0 => {
self.busy -= 1;
Err("not ready")
}
(0x3B, Data::Out(bytes)) if cdb[1] == 0x06 => {
let off = u32::from_be_bytes([0, cdb[3], cdb[4], cdb[5]]) as usize;
self.pending[off..off + bytes.len()].copy_from_slice(bytes);
if off + bytes.len() == layout::IMAGE_SIZE {
self.program();
}
Ok(bytes.len())
}
(0x3B, _) if cdb[1] == 0x07 => {
self.committed = true;
Ok(0)
}
(0x3C, Data::In(buf)) => {
let off = u32::from_be_bytes([0, cdb[3], cdb[4], cdb[5]]) as usize;
let n = if self.short_at == Some(off) {
buf.len() - 1
} else {
buf.len()
};
buf[..n].copy_from_slice(&self.flash[off..off + n]);
if let Some(at) = self.corrupt {
if (off..off + n).contains(&at) {
buf[at - off] ^= 0x40;
}
}
Ok(n)
}
(0x03, Data::In(buf)) => {
let reply = if self.sense.is_empty() {
let mut clean = vec![0; 18];
clean[0] = 0x70;
clean
} else {
self.sense.remove(0)
};
let n = reply.len().min(buf.len());
buf[..n].copy_from_slice(&reply[..n]);
Ok(n)
}
(_, Data::Out(bytes)) => Ok(bytes.len()),
_ => Ok(0),
}
}
}
struct Handle(Rc<RefCell<Drive>>, Via);
impl Transport for Handle {
type Error = &'static str;
fn exec(&mut self, cdb: &[u8], data: Data<'_>) -> Result<usize, Self::Error> {
self.0.borrow_mut().exec(self.1, cdb, data)
}
fn sense(&self) -> Option<(u8, u8, u8)> {
None
}
}
#[derive(Default)]
struct Clock {
now: Duration,
events: Vec<String>,
progress: Vec<usize>,
}
impl FlashRuntime<&'static str> for Clock {
fn elapsed(&self) -> Duration {
self.now
}
fn sleep(&mut self, duration: Duration) {
self.now += duration;
}
fn starting(&mut self, bytes: usize) {
self.events.push(format!("starting {bytes}"));
}
fn progress(&mut self, written: usize, total: usize) {
assert_eq!(total, layout::IMAGE_SIZE);
self.progress.push(written);
}
fn commit_failed(&mut self, error: &Error<&'static str>) {
self.events.push(format!("commit failed {error}"));
}
fn commit_not_ready(&mut self, error: &Error<&'static str>) {
self.events.push(format!("commit not ready {error}"));
}
fn status(&mut self, stage: Stage, status: Status<'_, &'static str>) {
let status = match status {
Status::Sense(t) => format!("sense {:02x}/{:02x}/{:02x}", t.0, t.1, t.2),
Status::Unparseable(n) => format!("unparseable {n}"),
Status::Failed(e) => format!("failed {e}"),
};
self.events.push(format!("{stage:?} {status}"));
}
fn uploaded(&mut self, bytes: usize) {
self.events.push(format!("uploaded {bytes}"));
}
fn poll_failed(&mut self, _elapsed: Duration, _error: &Error<&'static str>) {
self.events.push("poll failed".into());
}
fn verifying(&mut self) {
self.events.push("verifying".into());
}
fn unverified(&mut self, offset: usize, length: usize, error: &Error<&'static str>) {
self.events
.push(format!("unverified {offset:#x} {length:#x} {error}"));
}
}
const PROTECTED: usize = (fixtures::SIGNED.1 - fixtures::SIGNED.0 + 1) as usize;
fn run(
drive: &Rc<RefCell<Drive>>,
update: &PreparedUpdate,
clock: &mut Clock,
) -> Result<Verified, FlashError<&'static str>> {
let (mut reads, mut writes) = (
Handle(drive.clone(), Via::Reads),
Handle(drive.clone(), Via::Writes),
);
update.flash(
FlashTransport {
reads: &mut reads,
writes: &mut writes,
},
clock,
)
}
fn setup() -> (Rc<RefCell<Drive>>, PreparedUpdate, Vec<u8>) {
let running = fixtures::image(&JB7, 1);
let target = fixtures::image(&JB7, 2);
let update = prepare(&target, &running).unwrap();
(Drive::running(&running), update, target)
}
fn sense(key: u8, asc: u8) -> Vec<u8> {
let mut s = vec![0; 18];
s[0] = 0x70;
s[2] = key;
s[12] = asc;
s
}
#[test]
fn flash_sends_the_oem_session_then_verifies() {
let (drive, update, target) = setup();
let mut clock = Clock::default();
let verified = run(&drive, &update, &mut clock).unwrap();
assert_eq!(
verified,
Verified {
checked: PROTECTED,
ranges: 1
}
);
let log = drive.borrow().log.clone();
let mut want: Vec<(Via, Vec<u8>, usize)> = vec![
(Via::Writes, cdb::probe().to_vec(), 0x100),
(Via::Writes, cdb::test_unit_ready().to_vec(), 0),
(Via::Writes, cdb::enter_update().to_vec(), 0),
];
for i in 0..128u32 {
want.push((
Via::Writes,
cdb::transfer(i * 0x4000, 0x4000).to_vec(),
0x4000,
));
}
want.extend([
(Via::Writes, cdb::finish().to_vec(), 0),
(Via::Reads, cdb::test_unit_ready().to_vec(), 0),
(Via::Reads, cdb::request_sense().to_vec(), 16),
(Via::Reads, cdb::test_unit_ready().to_vec(), 0),
(Via::Reads, cdb::request_sense().to_vec(), 16),
]);
for i in 0..128u32 {
want.push((
Via::Reads,
cdb::read_memory(i * 0x4000, 0x4000).to_vec(),
0x4000,
));
}
assert_eq!(log, want);
assert_eq!(
clock.events,
[
"starting 2097152",
"Commit sense 00/00/00",
"uploaded 2097152",
"Settled sense 00/00/00",
"verifying"
]
);
assert_eq!(clock.progress.len(), 128);
assert_eq!(clock.progress[0], 0x4000);
assert_eq!(*clock.progress.last().unwrap(), layout::IMAGE_SIZE);
assert_eq!(
drive.borrow().flash[0x1000..0x1D_8000],
target[0x1000..0x1D_8000]
);
}
#[test]
fn a_failed_entry_sends_no_firmware() {
let (drive, update, _) = setup();
drive.borrow_mut().refuse = Some(cdb::enter_update().to_vec());
let error = run(&drive, &update, &mut Clock::default()).unwrap_err();
assert!(matches!(
error,
FlashError::Entry(Error::Transport("refused"))
));
assert!(!drive
.borrow()
.log
.iter()
.any(|(_, c, _)| c[0] == 0x3B && c[1] == 0x06));
}
#[test]
fn a_failed_chunk_stops_before_commit() {
let (drive, update, _) = setup();
drive.borrow_mut().refuse = Some(cdb::transfer(0x8000, 0x4000).to_vec());
let error = run(&drive, &update, &mut Clock::default()).unwrap_err();
assert!(matches!(
error,
FlashError::Transfer {
offset: 0x8000,
length: 0x4000,
..
}
));
let log = &drive.borrow().log;
assert_eq!(log.last().unwrap().1, cdb::transfer(0x8000, 0x4000));
assert!(!log.iter().any(|(_, c, _)| *c == cdb::finish()));
}
#[test]
fn a_commit_error_goes_on_to_the_checks() {
let (drive, update, _) = setup();
drive.borrow_mut().refuse = Some(cdb::finish().to_vec());
let mut clock = Clock::default();
run(&drive, &update, &mut clock).unwrap();
assert_eq!(
clock.events[1],
"commit failed transport error: \"refused\""
);
}
#[test]
fn a_fault_sense_fails_after_commit_and_after_settling() {
let (drive, update, _) = setup();
drive.borrow_mut().sense = vec![sense(0x04, 0x44)];
let error = run(&drive, &update, &mut Clock::default()).unwrap_err();
assert!(matches!(
error,
FlashError::Fault {
stage: Stage::Commit,
sense: (0x04, 0x44, 0)
}
));
let (drive, update, _) = setup();
drive.borrow_mut().sense = vec![sense(0x06, 0x29), sense(0x03, 0x0C)];
let mut clock = Clock::default();
let error = run(&drive, &update, &mut clock).unwrap_err();
assert!(matches!(
error,
FlashError::Fault {
stage: Stage::Settled,
sense: (0x03, 0x0C, 0)
}
));
assert!(clock.events.contains(&"Commit sense 06/29/00".to_string()));
}
#[test]
fn unreadable_sense_is_reported_and_verification_decides() {
let (drive, update, _) = setup();
drive.borrow_mut().sense = vec![vec![0x12; 5]];
let mut clock = Clock::default();
run(&drive, &update, &mut clock).unwrap();
assert!(clock.events.contains(&"Commit unparseable 5".to_string()));
let (drive, update, _) = setup();
drive.borrow_mut().refuse = Some(cdb::request_sense().to_vec());
let mut clock = Clock::default();
run(&drive, &update, &mut clock).unwrap();
let failed = "failed transport error: \"refused\"";
assert!(clock.events.contains(&format!("Commit {failed}")));
assert!(clock.events.contains(&format!("Settled {failed}")));
}
#[test]
fn readiness_waits_through_an_oem_command_window() {
let (drive, update, _) = setup();
drive.borrow_mut().busy = 221;
let mut clock = Clock::default();
run(&drive, &update, &mut clock).unwrap();
assert_eq!(clock.now, Duration::from_secs(110));
assert_eq!(
clock.events[1],
"commit not ready transport error: \"not ready\""
);
assert_eq!(
clock.events.iter().filter(|e| *e == "poll failed").count(),
220
);
}
#[test]
fn readiness_gives_up_after_the_bound() {
let (drive, update, _) = setup();
drive.borrow_mut().busy = usize::MAX;
let mut clock = Clock::default();
let error = run(&drive, &update, &mut clock).unwrap_err();
assert!(matches!(
error,
FlashError::ReadyTimeout(Error::Transport("not ready"))
));
assert_eq!(clock.now, Duration::from_secs(120));
assert_eq!(DriveClass::Mt19.ready_timeout(), Duration::from_secs(120));
assert!(DriveClass::Mt19.ready_timeout() >= Duration::from_secs(100));
assert!(!drive
.borrow()
.log
.iter()
.any(|(_, c, _)| c[..] == cdb::read_memory(0, 0x4000)));
}
#[test]
fn a_protected_difference_fails() {
let (drive, update, _) = setup();
drive.borrow_mut().corrupt = Some(0x2_0010);
let error = run(&drive, &update, &mut Clock::default()).unwrap_err();
assert!(matches!(
error,
FlashError::Mismatch {
address: 0x2_0010,
differing: 1,
checked: PROTECTED,
..
}
));
}
#[test]
fn differences_outside_the_protected_ranges_pass() {
for at in [0x800, 0x5_0000, layout::CALIBRATION.start + 4] {
let (drive, update, _) = setup();
drive.borrow_mut().corrupt = Some(at);
run(&drive, &update, &mut Clock::default()).unwrap();
}
}
#[test]
fn an_unreadable_protected_chunk_is_incomplete() {
let (drive, update, _) = setup();
drive.borrow_mut().short_at = Some(0x1_4000);
let mut clock = Clock::default();
let error = run(&drive, &update, &mut clock).unwrap_err();
assert!(matches!(
error,
FlashError::Incomplete {
unverified: 1,
checked
} if checked == PROTECTED - 0x4000
));
assert!(clock.events.contains(
&"unverified 0x14000 0x4000 short transfer: expected 16384 bytes, got 16383".to_string()
));
let (drive, update, _) = setup();
drive.borrow_mut().short_at = Some(0x4_0000);
run(&drive, &update, &mut Clock::default()).unwrap();
}
#[cfg(feature = "enc")]
#[test]
fn an_encrypted_transfer_verifies_the_plaintext() {
let (drive, update, target) = setup();
let update = update.encrypted();
assert_ne!(update.transfer(), &target[..]);
assert_eq!(update.image(), &target[..]);
drive.borrow_mut().decrypts = true;
run(&drive, &update, &mut Clock::default()).unwrap();
let first = drive.borrow().pending[..16].to_vec();
assert_eq!(first, update.transfer()[..16]);
}