opentitanlib/rescue/
xmodem.rs

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