1use std::io::{Read, Write};
6
7use anyhow::Result;
8use thiserror::Error;
9
10use crate::io::console::{ConsoleDevice, ConsoleExt};
11
12#[derive(Debug, Error)]
13pub enum XmodemError {
14 #[error("Cancelled")]
15 Cancelled,
16 #[error("Exhausted retries: {0}")]
17 ExhaustedRetries(usize),
18 #[error("Unsupported mode: {0}")]
19 UnsupportedMode(String),
20}
21
22#[derive(Debug, Clone, Copy)]
23#[repr(usize)]
24pub enum XmodemBlock {
25 Block128 = 128,
26 Block1k = 1024,
27}
28
29#[derive(Debug)]
30pub struct Xmodem {
31 pub max_errors: usize,
32 pub pad_byte: u8,
33 pub block_len: XmodemBlock,
34}
35
36impl Default for Xmodem {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl Xmodem {
43 const POLYNOMIAL: u16 = 0x1021;
44 const CRC: u8 = 0x43;
45 const SOH: u8 = 0x01;
46 const STX: u8 = 0x02;
47 const EOF: u8 = 0x04;
48 const ACK: u8 = 0x06;
49 const NAK: u8 = 0x15;
50 const CAN: u8 = 0x18;
51
52 pub fn new() -> Self {
53 Xmodem {
54 max_errors: 16,
55 pad_byte: 0xff,
56 block_len: XmodemBlock::Block1k,
57 }
58 }
59
60 fn crc16(buf: &[u8]) -> u16 {
61 let mut crc = 0u16;
62 for byte in buf {
63 crc ^= (*byte as u16) << 8;
64 for _bit in 0..8 {
65 let msb = crc & 0x8000 != 0;
66 crc <<= 1;
67 if msb {
68 crc ^= Self::POLYNOMIAL;
69 }
70 }
71 }
72 crc
73 }
74
75 pub fn send(&self, console: &dyn ConsoleDevice, data: impl Read) -> Result<()> {
76 self.send_start(console)?;
77 self.send_data(console, data)?;
78 self.send_finish(console)?;
79 Ok(())
80 }
81
82 fn send_start(&self, console: &dyn ConsoleDevice) -> Result<()> {
83 let mut ch = 0u8;
84 let mut cancels = 0usize;
85 loop {
87 console.read(std::slice::from_mut(&mut ch))?;
88 match ch {
89 Self::CRC => {
90 return Ok(());
91 }
92 Self::NAK => {
93 return Err(XmodemError::UnsupportedMode("standard checksums".into()).into());
94 }
95 Self::CAN => {
96 cancels += 1;
97 if cancels >= 2 {
98 return Err(XmodemError::Cancelled.into());
99 }
100 }
101 _ => {
102 let p = ch as char;
103 log::info!(
104 "Unknown byte received while waiting for XMODEM start: {p:?} ({ch:#x?})"
105 );
106 }
107 }
108 }
109 }
110
111 fn send_data(&self, console: &dyn ConsoleDevice, mut data: impl Read) -> Result<()> {
112 let mut block = 0usize;
113 let mut errors = 0usize;
114 loop {
115 block += 1;
116 let mut buf = vec![self.pad_byte; self.block_len as usize + 3];
117 let n = data.read(&mut buf[3..])?;
118 if n == 0 {
119 break;
120 }
121
122 buf[0] = match self.block_len {
123 XmodemBlock::Block128 => Self::SOH,
124 XmodemBlock::Block1k => Self::STX,
125 };
126 buf[1] = block as u8;
127 buf[2] = 255 - buf[1];
128 let crc = Self::crc16(&buf[3..]);
129 buf.push((crc >> 8) as u8);
130 buf.push((crc & 0xFF) as u8);
131 log::info!("Sending block {block}");
132
133 let mut cancels = 0usize;
134 loop {
135 console.write(&buf)?;
136 let mut ch = 0u8;
137 console.read(std::slice::from_mut(&mut ch))?;
138 match ch {
139 Self::ACK => break,
140 Self::NAK => {
141 log::info!("XMODEM send got NAK. Retrying.");
142 errors += 1;
143 }
144 Self::CAN => {
145 cancels += 1;
146 if cancels >= 2 {
147 return Err(XmodemError::Cancelled.into());
148 }
149 }
150 _ => {
151 log::info!("Expected ACK. Got {ch:#x}.");
152 errors += 1;
153 }
154 }
155 if errors >= self.max_errors {
156 return Err(XmodemError::ExhaustedRetries(errors).into());
157 }
158 }
159 }
160 Ok(())
161 }
162
163 fn send_finish(&self, console: &dyn ConsoleDevice) -> Result<()> {
164 console.write(&[Self::EOF])?;
165 let mut ch = 0u8;
166 console.read(std::slice::from_mut(&mut ch))?;
167 if ch != Self::ACK {
168 log::info!("Expected ACK. Got {ch:#x}.");
169 }
170 Ok(())
171 }
172
173 pub fn receive(&self, console: &dyn ConsoleDevice, data: &mut impl Write) -> Result<()> {
174 console.write(&[Self::CRC])?;
176
177 let mut block = 1u8;
178 let mut errors = 0usize;
179 loop {
180 let mut byte = 0u8;
182 console.read(std::slice::from_mut(&mut byte))?;
183 let block_len = match byte {
184 Self::SOH => 128,
185 Self::STX => 1024,
186 Self::EOF => {
187 console.write(&[Self::ACK])?;
189 break;
190 }
191 _ => {
192 return Err(XmodemError::UnsupportedMode(format!(
193 "bad start of packet: {byte:?}"
194 ))
195 .into());
196 }
197 };
198
199 let mut bnum = 0u8;
201 let mut bcom = 0u8;
202 console.read(std::slice::from_mut(&mut bnum))?;
203 console.read(std::slice::from_mut(&mut bcom))?;
204 let cancel = block != bnum || bnum != 255 - bcom;
205
206 let mut buffer = vec![0; block_len];
208 let mut total = 0;
209 while total < block_len {
210 let n = console.read(&mut buffer[total..])?;
211 total += n;
212 }
213
214 let mut crc1 = 0u8;
216 let mut crc2 = 0u8;
217 console.read(std::slice::from_mut(&mut crc1))?;
218 console.read(std::slice::from_mut(&mut crc2))?;
219 let crc = u16::from_be_bytes([crc1, crc2]);
220
221 if cancel {
223 console.write(&[Self::CAN, Self::CAN])?;
224 return Err(XmodemError::Cancelled.into());
225 }
226 if Self::crc16(&buffer) == crc {
227 console.write(&[Self::ACK])?;
229 data.write_all(&buffer)?;
230 block = block.wrapping_add(1);
231 } else {
232 console.write(&[Self::NAK])?;
233 errors += 1;
234 }
235 if errors >= self.max_errors {
236 return Err(XmodemError::ExhaustedRetries(errors).into());
237 }
238 }
239 Ok(())
240 }
241}
242
243#[cfg(test)]
247mod test {
248 use super::*;
249 use crate::util::testing::{ChildConsole, TransferState};
250 use crate::util::tmpfilename;
251
252 #[rustfmt::skip]
253 const GETTYSBURG: &str =
254r#"Four score and seven years ago our fathers brought forth on this
255continent, a new nation, conceived in Liberty, and dedicated to the
256proposition that all men are created equal.
257Now we are engaged in a great civil war, testing whether that nation,
258or any nation so conceived and so dedicated, can long endure. We are met
259on a great battle-field of that war. We have come to dedicate a portion
260of that field, as a final resting place for those who here gave their
261lives that that nation might live. It is altogether fitting and proper
262that we should do this.
263But, in a larger sense, we can not dedicate -- we can not consecrate --
264we can not hallow -- this ground. The brave men, living and dead, who
265struggled here, have consecrated it, far above our poor power to add or
266detract. The world will little note, nor long remember what we say here,
267but it can never forget what they did here. It is for us the living,
268rather, to be dedicated here to the unfinished work which they who
269fought here have thus far so nobly advanced. It is rather for us to be
270here dedicated to the great task remaining before us -- that from these
271honored dead we take increased devotion to that cause for which they gave
272the last full measure of devotion -- that we here highly resolve that
273these dead shall not have died in vain -- that this nation, under God,
274shall have a new birth of freedom -- and that government of the people,
275by the people, for the people, shall not perish from the earth.
276Abraham Lincoln
277November 19, 1863
278"#;
279
280 #[test]
281 fn test_xmodem_send() -> Result<()> {
282 let filename = tmpfilename("test_xmodem_send");
283 let child = ChildConsole::spawn(&["rx", "--with-crc", &filename])?;
284 let xmodem = Xmodem::new();
285 let gettysburg = GETTYSBURG.as_bytes();
286 xmodem.send(&child, gettysburg)?;
287 assert!(child.wait()?.success());
288 let result = std::fs::read(&filename)?;
289 assert_eq!(result.len() % 1024, 0);
291 assert!(result.len() >= gettysburg.len());
292 assert_eq!(&result[..gettysburg.len()], gettysburg);
293 Ok(())
294 }
295
296 #[test]
297 fn test_xmodem_send_with_errors() -> Result<()> {
298 let filename = tmpfilename("test_xmodem_send_with_errors");
299 let child = ChildConsole::spawn_corrupt(
300 &["rx", "--with-crc", &filename],
301 TransferState::default(),
302 TransferState::new(&[3, 136]),
303 )?;
304 let xmodem = Xmodem {
305 max_errors: 2,
306 pad_byte: 0,
307 block_len: XmodemBlock::Block128,
308 };
309 let gettysburg = GETTYSBURG.as_bytes();
310 let err = xmodem.send(&child, gettysburg);
311 assert!(err.is_err());
312 assert_eq!(err.unwrap_err().to_string(), "Exhausted retries: 2");
313 Ok(())
314 }
315
316 #[test]
317 fn test_xmodem_checksum_mode() -> Result<()> {
318 let filename = tmpfilename("test_xmodem_checksum_mode");
319 let child = ChildConsole::spawn(&["rx", &filename])?;
320 let xmodem = Xmodem::new();
321 let gettysburg = GETTYSBURG.as_bytes();
322 let result = xmodem.send(&child, gettysburg);
323 assert!(!child.wait()?.success());
324 assert!(result.is_err());
325 let err = result.unwrap_err().downcast::<XmodemError>().unwrap();
326 assert_eq!(err.to_string(), "Unsupported mode: standard checksums");
327 Ok(())
328 }
329
330 #[test]
331 fn test_xmodem_recv() -> Result<()> {
332 let filename = tmpfilename("test_xmodem_recv");
333 let gettysburg = GETTYSBURG.as_bytes();
334 std::fs::write(&filename, gettysburg)?;
335 let child = ChildConsole::spawn(&["sx", &filename])?;
336 let xmodem = Xmodem::new();
337 let mut result = Vec::new();
338 xmodem.receive(&child, &mut result)?;
339 assert!(child.wait()?.success());
340 assert_eq!(result.len() % 128, 0);
342 assert!(result.len() >= gettysburg.len());
343 assert_eq!(&result[..gettysburg.len()], gettysburg);
344 Ok(())
345 }
346
347 #[test]
348 fn test_xmodem1k_recv() -> Result<()> {
349 let filename = tmpfilename("test_xmodem1k_recv");
350 let gettysburg = GETTYSBURG.as_bytes();
351 std::fs::write(&filename, gettysburg)?;
352 let child = ChildConsole::spawn(&["sx", "--1k", &filename])?;
353 let xmodem = Xmodem::new();
354 let mut result = Vec::new();
355 xmodem.receive(&child, &mut result)?;
356 assert!(child.wait()?.success());
357 assert_eq!(result.len() % 128, 0);
361 assert!(result.len() >= gettysburg.len());
362 assert_eq!(&result[..gettysburg.len()], gettysburg);
363 Ok(())
364 }
365
366 #[test]
367 fn test_xmodem_recv_with_errors() -> Result<()> {
368 let filename = tmpfilename("test_xmodem_recv_with_errors");
369 let gettysburg = GETTYSBURG.as_bytes();
370 std::fs::write(&filename, gettysburg)?;
371 let child = ChildConsole::spawn_corrupt(
372 &["sx", &filename],
373 TransferState::new(&[3, 136]),
374 TransferState::default(),
375 )?;
376 let xmodem = Xmodem {
377 max_errors: 2,
378 pad_byte: 0,
379 block_len: XmodemBlock::Block128,
380 };
381 let mut result = Vec::new();
382 let err = xmodem.receive(&child, &mut result);
383 assert!(err.is_err());
384 assert_eq!(err.unwrap_err().to_string(), "Exhausted retries: 2");
385 Ok(())
386 }
387
388 #[test]
389 fn test_xmodem_recv_with_cancel() -> Result<()> {
390 let filename = tmpfilename("test_xmodem_recv_with_cancel");
391 let gettysburg = GETTYSBURG.as_bytes();
392 std::fs::write(&filename, gettysburg)?;
393 let child = ChildConsole::spawn_corrupt(
394 &["sx", &filename],
395 TransferState::new(&[1, 134]),
396 TransferState::default(),
397 )?;
398 let xmodem = Xmodem {
399 max_errors: 2,
400 pad_byte: 0,
401 block_len: XmodemBlock::Block128,
402 };
403 let mut result = Vec::new();
404 let err = xmodem.receive(&child, &mut result);
405 assert!(err.is_err());
406 assert_eq!(err.unwrap_err().to_string(), "Cancelled");
407 Ok(())
408 }
409}