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 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        // Wait for the XMODEM CRC start sequence.
86        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        // Send the byte indicating the protocol we want (Xmodem-CRC).
175        console.write(&[Self::CRC])?;
176
177        let mut block = 1u8;
178        let mut errors = 0usize;
179        loop {
180            // The first byte of the packet is the packet type which indicates the block size.
181            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                    // End of file.  Send an ACK.
188                    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            // The next two bytes are the block number and its complement.
200            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            // The next `block_len` bytes are the packet itself.
207            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            // The final two bytes are the CRC16.
215            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 we should cancel, do it now.
222            if cancel {
223                console.write(&[Self::CAN, Self::CAN])?;
224                return Err(XmodemError::Cancelled.into());
225            }
226            if Self::crc16(&buffer) == crc {
227                // CRC was good; send an ACK and keep the data.
228                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// The xmodem tests depend on the lrzsz package which contains the classic
244// XMODEM/YMODEM/ZMODEM file transfer programs dating back to the 1980s and
245// 1990s.
246#[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        // The file should be a multiple of the block size.
290        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        // The received data should be a multiple of the block size.
341        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        // The received data should be a multiple of the block size.
358        // Even though we're using 1K blocks, the lrzsz programs use
359        // shorter blocks for the last bit of the data.
360        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}