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:02x} ({})",
194 byte as char
195 ))
196 .into());
197 }
198 };
199
200 let mut bnum = 0u8;
202 let mut bcom = 0u8;
203 console.read(std::slice::from_mut(&mut bnum))?;
204 console.read(std::slice::from_mut(&mut bcom))?;
205 let cancel = block != bnum || bnum != 255 - bcom;
206
207 let mut buffer = vec![0; block_len];
209 let mut total = 0;
210 while total < block_len {
211 let n = console.read(&mut buffer[total..])?;
212 total += n;
213 }
214
215 let mut crc1 = 0u8;
217 let mut crc2 = 0u8;
218 console.read(std::slice::from_mut(&mut crc1))?;
219 console.read(std::slice::from_mut(&mut crc2))?;
220 let crc = u16::from_be_bytes([crc1, crc2]);
221
222 if cancel {
224 console.write(&[Self::CAN, Self::CAN])?;
225 return Err(XmodemError::Cancelled.into());
226 }
227 if Self::crc16(&buffer) == crc {
228 console.write(&[Self::ACK])?;
230 data.write_all(&buffer)?;
231 block = block.wrapping_add(1);
232 } else {
233 console.write(&[Self::NAK])?;
234 errors += 1;
235 }
236 if errors >= self.max_errors {
237 return Err(XmodemError::ExhaustedRetries(errors).into());
238 }
239 }
240 Ok(())
241 }
242}
243
244#[cfg(test)]
248mod test {
249 use super::*;
250 use crate::util::testing::{ChildConsole, TransferState};
251 use crate::util::tmpfilename;
252
253 #[rustfmt::skip]
254 const GETTYSBURG: &str =
255r#"Four score and seven years ago our fathers brought forth on this
256continent, a new nation, conceived in Liberty, and dedicated to the
257proposition that all men are created equal.
258Now we are engaged in a great civil war, testing whether that nation,
259or any nation so conceived and so dedicated, can long endure. We are met
260on a great battle-field of that war. We have come to dedicate a portion
261of that field, as a final resting place for those who here gave their
262lives that that nation might live. It is altogether fitting and proper
263that we should do this.
264But, in a larger sense, we can not dedicate -- we can not consecrate --
265we can not hallow -- this ground. The brave men, living and dead, who
266struggled here, have consecrated it, far above our poor power to add or
267detract. The world will little note, nor long remember what we say here,
268but it can never forget what they did here. It is for us the living,
269rather, to be dedicated here to the unfinished work which they who
270fought here have thus far so nobly advanced. It is rather for us to be
271here dedicated to the great task remaining before us -- that from these
272honored dead we take increased devotion to that cause for which they gave
273the last full measure of devotion -- that we here highly resolve that
274these dead shall not have died in vain -- that this nation, under God,
275shall have a new birth of freedom -- and that government of the people,
276by the people, for the people, shall not perish from the earth.
277Abraham Lincoln
278November 19, 1863
279"#;
280
281 #[test]
282 fn test_xmodem_send() -> Result<()> {
283 let filename = tmpfilename("test_xmodem_send");
284 let child = ChildConsole::spawn(&["rx", "--with-crc", &filename])?;
285 let xmodem = Xmodem::new();
286 let gettysburg = GETTYSBURG.as_bytes();
287 xmodem.send(&child, gettysburg)?;
288 assert!(child.wait()?.success());
289 let result = std::fs::read(&filename)?;
290 assert_eq!(result.len() % 1024, 0);
292 assert!(result.len() >= gettysburg.len());
293 assert_eq!(&result[..gettysburg.len()], gettysburg);
294 Ok(())
295 }
296
297 #[test]
298 fn test_xmodem_send_with_errors() -> Result<()> {
299 let filename = tmpfilename("test_xmodem_send_with_errors");
300 let child = ChildConsole::spawn_corrupt(
301 &["rx", "--with-crc", &filename],
302 TransferState::default(),
303 TransferState::new(&[3, 136]),
304 )?;
305 let xmodem = Xmodem {
306 max_errors: 2,
307 pad_byte: 0,
308 block_len: XmodemBlock::Block128,
309 };
310 let gettysburg = GETTYSBURG.as_bytes();
311 let err = xmodem.send(&child, gettysburg);
312 assert!(err.is_err());
313 assert_eq!(err.unwrap_err().to_string(), "Exhausted retries: 2");
314 Ok(())
315 }
316
317 #[test]
318 fn test_xmodem_checksum_mode() -> Result<()> {
319 let filename = tmpfilename("test_xmodem_checksum_mode");
320 let child = ChildConsole::spawn(&["rx", &filename])?;
321 let xmodem = Xmodem::new();
322 let gettysburg = GETTYSBURG.as_bytes();
323 let result = xmodem.send(&child, gettysburg);
324 assert!(!child.wait()?.success());
325 assert!(result.is_err());
326 let err = result.unwrap_err().downcast::<XmodemError>().unwrap();
327 assert_eq!(err.to_string(), "Unsupported mode: standard checksums");
328 Ok(())
329 }
330
331 #[test]
332 fn test_xmodem_recv() -> Result<()> {
333 let filename = tmpfilename("test_xmodem_recv");
334 let gettysburg = GETTYSBURG.as_bytes();
335 std::fs::write(&filename, gettysburg)?;
336 let child = ChildConsole::spawn(&["sx", &filename])?;
337 let xmodem = Xmodem::new();
338 let mut result = Vec::new();
339 xmodem.receive(&child, &mut result)?;
340 assert!(child.wait()?.success());
341 assert_eq!(result.len() % 128, 0);
343 assert!(result.len() >= gettysburg.len());
344 assert_eq!(&result[..gettysburg.len()], gettysburg);
345 Ok(())
346 }
347
348 #[test]
349 fn test_xmodem1k_recv() -> Result<()> {
350 let filename = tmpfilename("test_xmodem1k_recv");
351 let gettysburg = GETTYSBURG.as_bytes();
352 std::fs::write(&filename, gettysburg)?;
353 let child = ChildConsole::spawn(&["sx", "--1k", &filename])?;
354 let xmodem = Xmodem::new();
355 let mut result = Vec::new();
356 xmodem.receive(&child, &mut result)?;
357 assert!(child.wait()?.success());
358 assert_eq!(result.len() % 128, 0);
362 assert!(result.len() >= gettysburg.len());
363 assert_eq!(&result[..gettysburg.len()], gettysburg);
364 Ok(())
365 }
366
367 #[test]
368 fn test_xmodem_recv_with_errors() -> Result<()> {
369 let filename = tmpfilename("test_xmodem_recv_with_errors");
370 let gettysburg = GETTYSBURG.as_bytes();
371 std::fs::write(&filename, gettysburg)?;
372 let child = ChildConsole::spawn_corrupt(
373 &["sx", &filename],
374 TransferState::new(&[3, 136]),
375 TransferState::default(),
376 )?;
377 let xmodem = Xmodem {
378 max_errors: 2,
379 pad_byte: 0,
380 block_len: XmodemBlock::Block128,
381 };
382 let mut result = Vec::new();
383 let err = xmodem.receive(&child, &mut result);
384 assert!(err.is_err());
385 assert_eq!(err.unwrap_err().to_string(), "Exhausted retries: 2");
386 Ok(())
387 }
388
389 #[test]
390 fn test_xmodem_recv_with_cancel() -> Result<()> {
391 let filename = tmpfilename("test_xmodem_recv_with_cancel");
392 let gettysburg = GETTYSBURG.as_bytes();
393 std::fs::write(&filename, gettysburg)?;
394 let child = ChildConsole::spawn_corrupt(
395 &["sx", &filename],
396 TransferState::new(&[1, 134]),
397 TransferState::default(),
398 )?;
399 let xmodem = Xmodem {
400 max_errors: 2,
401 pad_byte: 0,
402 block_len: XmodemBlock::Block128,
403 };
404 let mut result = Vec::new();
405 let err = xmodem.receive(&child, &mut result);
406 assert!(err.is_err());
407 assert_eq!(err.unwrap_err().to_string(), "Cancelled");
408 Ok(())
409 }
410}