use super::Transport;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Out,
In,
}
#[derive(Debug, Clone)]
pub struct Step {
pub direction: Direction,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strictness {
Exact,
Lenient,
}
pub struct ReplayTransport {
script: Vec<Step>,
pos: usize,
sent: Vec<Vec<u8>>,
strictness: Strictness,
}
impl ReplayTransport {
pub fn new(script: Vec<Step>) -> Self {
Self {
script,
pos: 0,
sent: Vec::new(),
strictness: Strictness::Exact,
}
}
pub fn lenient(mut self) -> Self {
self.strictness = Strictness::Lenient;
self
}
pub fn sent(&self) -> &[Vec<u8>] {
&self.sent
}
pub fn is_exhausted(&self) -> bool {
self.pos >= self.script.len()
}
pub fn from_script(text: &str) -> Result<Self> {
let mut script = Vec::new();
for (n, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (tag, hex) = line
.split_once(char::is_whitespace)
.ok_or_else(|| Error::Replay(format!("line {}: expected '<O|I> <hex>'", n + 1)))?;
let direction = match tag {
"O" | "o" => Direction::Out,
"I" | "i" => Direction::In,
other => {
return Err(Error::Replay(format!(
"line {}: unknown direction {other:?}, want O or I",
n + 1
)))
}
};
let hex = hex.trim();
if hex.len() % 2 != 0 {
return Err(Error::Replay(format!("line {}: odd-length hex", n + 1)));
}
let bytes = (0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16))
.collect::<std::result::Result<Vec<u8>, _>>()
.map_err(|e| Error::Replay(format!("line {}: {e}", n + 1)))?;
script.push(Step { direction, bytes });
}
Ok(Self::new(script))
}
}
impl Transport for ReplayTransport {
async fn write(&mut self, buf: &[u8]) -> Result<()> {
self.sent.push(buf.to_vec());
let step = self.script.get(self.pos).ok_or_else(|| {
Error::Replay(format!(
"script exhausted; host sent an extra {} bytes",
buf.len()
))
})?;
if step.direction != Direction::Out {
return Err(Error::Replay(
"host wrote, but the script expects the device to speak next".into(),
));
}
if self.strictness == Strictness::Exact && step.bytes != buf {
return Err(Error::Replay(format!(
"sent bytes differ from the script at step {}\n expected {}\n got {}",
self.pos,
hex(&step.bytes),
hex(buf),
)));
}
self.pos += 1;
Ok(())
}
async fn read(&mut self, _max: usize) -> Result<Vec<u8>> {
let step = self
.script
.get(self.pos)
.ok_or_else(|| Error::Replay("script exhausted; host expected a response".into()))?;
if step.direction != Direction::In {
return Err(Error::Replay(
"host read, but the script expects the host to speak next".into(),
));
}
self.pos += 1;
Ok(step.bytes.clone())
}
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}