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:02x} ({})",
194                        byte as char
195                    ))
196                    .into());
197                }
198            };
199
200            // The next two bytes are the block number and its complement.
201            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            // The next `block_len` bytes are the packet itself.
208            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            // The final two bytes are the CRC16.
216            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 we should cancel, do it now.
223            if cancel {
224                console.write(&[Self::CAN, Self::CAN])?;
225                return Err(XmodemError::Cancelled.into());
226            }
227            if Self::crc16(&buffer) == crc {
228                // CRC was good; send an ACK and keep the data.
229                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// The xmodem tests depend on the lrzsz package which contains the classic
245// XMODEM/YMODEM/ZMODEM file transfer programs dating back to the 1980s and
246// 1990s.
247#[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        // The file should be a multiple of the block size.
291        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        // The received data should be a multiple of the block size.
342        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        // The received data should be a multiple of the block size.
359        // Even though we're using 1K blocks, the lrzsz programs use
360        // shorter blocks for the last bit of the data.
361        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}