Repository: awelkie/xmodem.rs
Pull request #8: Updates from Oxide Computer Company
Author: login: dancrossnyc
Created: 2025-04-03T14:54:40Z | Merged: 2025-04-19T00:35:53Z

This commit encompasses changes introduced by Oxide Computer Company:

* Updated implementation to Rust 2024
* Updated dependencies to most recent versions
* Improved `no_std` support
* Removing the `core_io` dependency
* Keeping track of the number of bytes transferred
* Consistent formatting with `rustfmt`
* Fix clippy lints

All tests pass, we have been using this version internally for several years now, and have confidence in the changes.


Diff:
diff --git a/Cargo.toml b/Cargo.toml
index bbab6d9..363864d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,16 +1,21 @@
 [package]
 name = "xmodem"
-version = "0.1.3"
+version = "0.4.0"
 authors = ["Allen Welkie <allen.welkie@gmail.com>"]
-keywords = ["xmodem", "ymodem", "zmodem", "serial"]
+keywords = ["xmodem", "serial", "no_std"]
 repository = "https://github.com/awelkie/xmodem.rs"
 license = "MIT"
 description = "An implementation of the XMODEM file-transfer protocol."
+edition = "2024"
 
 [dependencies]
-log = "^0.3"
-crc16 = "^0.3"
+log = { version = "0.4", default-features = false }
+crc16 = "0.4"
 
 [dev-dependencies]
-tempfile = "^2.0"
-rand = "^0.3"
+tempfile = "3.0"
+rand = "0.9"
+
+[features]
+std = []
+default = ["std"]
diff --git a/README.md b/README.md
index 49f5225..4a059aa 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,22 @@
 # XMODEM.rs
 
-[![Build Status](https://travis-ci.org/awelkie/xmodem.rs.svg?branch=master)](https://travis-ci.org/awelkie/xmodem.rs)
-[![](https://img.shields.io/crates/v/xmodem.svg)](https://crates.io/crates/xmodem)
-[![](https://img.shields.io/crates/l/xmodem.svg)](https://crates.io/crates/xmodem)
-
 The XMODEM protocol in Rust
 
-# Testing
-The tests require the binaries found in the `lrzsz` package.
+This is derived from https://github.com/awelkie/xmodem.rs.  It has been modified
+primarily to support `no_std` use and for use with the 2018 edition.  All four
+permutations of standard 128-byte and 1024-byte block sizes, classic and CRC16
+variants are supported for send and receive.  YMODEM and ZMODEM are not
+implemented.  In addition, the `send` and `recv` methods return the number of
+bytes of data sent or received.
 
-# Rust channels
-Currently only the beta and nightly channels are supported (stable support is waiting on [#27585](https://github.com/rust-lang/rust/issues/27585))
+For a `no_std` build, it is necessary to request the `core` feature in addition
+to `--no-default-features` or `default-features = false` on account of
+cargo#1839.  Additionally, your compiler must be known to `core_io`.  Changes
+are welcome to allow the use of alternate crates providing `Read` and `Write`
+traits with the same signatures as those in `std::io`; unfortunately this does
+not include (most of?) the Embedded HAL crates, which provide slightly different
+signatures.
+
+# Testing
+The tests require the binaries found in the `lrzsz` package.  There are no tests
+for the `no_std` build.
diff --git a/src/lib.rs b/src/lib.rs
index a0108a6..812d01d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,9 +1,90 @@
-#[macro_use]
-extern crate log;
-extern crate crc16;
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use core::convert::{From, TryFrom};
+
+#[cfg(not(feature = "std"))]
+/// In a `no_std` environment, `std::io` is not available.  We
+/// provide a publically available subset of compatible types
+/// and traits required by the rest of the library that must be
+/// implemented by consumers.
+///
+/// Refer to the `std::io` documentation for details.
+pub mod io {
+    use core::{fmt, mem, result};
+
+    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
+    pub enum ErrorKind {
+        TimedOut,
+        Other,
+    }
+
+    #[derive(Debug)]
+    pub struct Error {
+        kind: ErrorKind,
+        message: &'static str,
+    }
+
+    impl Error {
+        pub fn new(kind: ErrorKind, message: &'static str) -> Error {
+            Error { kind, message }
+        }
+
+        pub fn kind(&self) -> ErrorKind {
+            self.kind
+        }
+    }
+
+    impl fmt::Display for Error {
+        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+            write!(f, "IO error {:?}: {}", self.kind, self.message)
+        }
+    }
 
-use std::io::{self, Read, Write};
-use std::convert::From;
+    pub type Result<T> = result::Result<T, Error>;
+
+    pub trait Read {
+        fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
+        fn read_exact(&mut self, buf: &mut [u8]) -> Result<()>;
+    }
+
+    pub trait Write {
+        fn flush(&mut self) -> Result<()>;
+        fn write(&mut self, buf: &[u8]) -> Result<usize>;
+        fn write_all(&mut self, buf: &[u8]) -> Result<()>;
+    }
+
+    // Credit where due: this is mostly taken from `std::io`.
+    impl Write for &mut [u8] {
+        #[inline]
+        fn write(&mut self, data: &[u8]) -> Result<usize> {
+            let n = usize::min(data.len(), self.len());
+            let dst = mem::replace(self, &mut []);
+            let (a, b) = dst.split_at_mut(n);
+            a.copy_from_slice(&data[..n]);
+            *self = b;
+            Ok(n)
+        }
+
+        #[inline]
+        fn write_all(&mut self, data: &[u8]) -> Result<()> {
+            if self.write(data)? != data.len() {
+                return Err(Error::new(ErrorKind::Other, "failed to write whole buffer"));
+            }
+            Ok(())
+        }
+
+        #[inline]
+        fn flush(&mut self) -> Result<()> {
+            Ok(())
+        }
+    }
+}
+#[cfg(feature = "std")]
+use std::io;
+
+use io::{Read, Write};
+
+use ::log::{debug, error, info, warn};
 
 // TODO: Send CAN byte after too many errors
 // TODO: Handle CAN bytes while sending
@@ -17,18 +98,27 @@ const NAK: u8 = 0x15;
 const CAN: u8 = 0x18;
 const CRC: u8 = 0x43;
 
-pub type Result<T> = std::result::Result<T, Error>;
+pub type Result<T> = core::result::Result<T, Error>;
 
 #[derive(Debug)]
 pub enum Error {
     Io(io::Error),
 
-    /// The number of communications errors exceeded `max_errors` in a single
-    /// transmission.
+    /// The number of communications errors exceeded `max_errors` in a
+    /// single transmission.
     ExhaustedRetries,
 
     /// The transmission was canceled by the other end of the channel.
     Canceled,
+
+    /// Data was received that is not appropriate to the transfer state.
+    Invalid,
+
+    /// A packet was received with mismatched sequence numbers.
+    SequenceMismatch,
+
+    /// A packet was received with an incorrect checksum or CRC16.
+    Checksum,
 }
 
 impl From<io::Error> for Error {
@@ -49,22 +139,136 @@ pub enum BlockLength {
     OneK = 1024,
 }
 
+struct XmodemPacket {
+    pub seqno: u8,
+    data: XmodemBuffer,
+}
+
+#[allow(clippy::large_enum_variant)]
+enum XmodemBuffer {
+    Standard([u8; 128]),
+    OneK([u8; 1024]),
+}
+
+impl XmodemPacket {
+    pub fn new(l: BlockLength, pad: u8) -> Self {
+        match l {
+            BlockLength::Standard => XmodemPacket {
+                seqno: 0,
+                data: XmodemBuffer::Standard([pad; 128]),
+            },
+            BlockLength::OneK => XmodemPacket {
+                seqno: 0,
+                data: XmodemBuffer::OneK([pad; 1024]),
+            },
+        }
+    }
+
+    fn recv_next<R: Read>(r: &mut R, c: Checksum) -> Result<Option<Self>> {
+        let mut ret = match get_byte(r)? {
+            SOH => Self::new(BlockLength::Standard, 0),
+            STX => Self::new(BlockLength::OneK, 0),
+            EOT => return Ok(None),
+            _ => return Err(Error::Invalid),
+        };
+
+        /*
+         * The next two bytes are the packet sequence number mod 256
+         * and the 1's complement of that.  If they don't match we'll
+         * return an error later; we still need to read the packet
+         * to maintain proper transaction state.
+         */
+        let recv_seqno = get_byte(r)?;
+        let recv_seqno1c = get_byte(r)?;
+
+        r.read_exact(ret.as_mut())?;
+
+        let checksum_ok = match c {
+            Checksum::Standard => {
+                let recv_checksum = get_byte(r)?;
+                calc_checksum(ret.as_ref()) == recv_checksum
+            }
+            Checksum::CRC16 => {
+                calc_crc(ret.as_ref()) == u16::from_be_bytes([get_byte(r)?, get_byte(r)?])
+            }
+        };
+
+        if 0xFF - recv_seqno != recv_seqno1c {
+            return Err(Error::SequenceMismatch);
+        }
+
+        if checksum_ok {
+            ret.seqno = recv_seqno;
+            return Ok(Some(ret));
+        }
+
+        Err(Error::Checksum)
+    }
+
+    fn send<W: Write>(&self, w: &mut W, c: Checksum) -> Result<()> {
+        let header: [u8; 3] = [
+            match self.data {
+                XmodemBuffer::Standard(_) => SOH,
+                XmodemBuffer::OneK(_) => STX,
+            },
+            self.seqno,
+            0xFF - self.seqno,
+        ];
+
+        debug!("Sending block {}", self.seqno);
+        w.write_all(&header)?;
+        w.write_all(self.as_ref())?;
+
+        match c {
+            Checksum::Standard => {
+                w.write_all(&[calc_checksum(self.as_ref())])?;
+            }
+            Checksum::CRC16 => {
+                w.write_all(&calc_crc(self.as_ref()).to_be_bytes())?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
+impl AsRef<[u8]> for XmodemPacket {
+    fn as_ref(&self) -> &[u8] {
+        match self.data {
+            XmodemBuffer::Standard(ref b) => b,
+            XmodemBuffer::OneK(ref b) => b,
+        }
+    }
+}
+
+impl AsMut<[u8]> for XmodemPacket {
+    fn as_mut(&mut self) -> &mut [u8] {
+        match self.data {
+            XmodemBuffer::Standard(ref mut b) => b,
+            XmodemBuffer::OneK(ref mut b) => b,
+        }
+    }
+}
+
 /// Configuration for the XMODEM transfer.
 #[derive(Copy, Clone, Debug)]
 pub struct Xmodem {
     /// The number of errors that can occur before the communication is
-    /// considered a failure. Errors include unexpected bytes and timeouts waiting for bytes.
+    /// considered a failure. Errors include unexpected bytes and timeouts
+    /// waiting for bytes.
     pub max_errors: u32,
 
-    /// The byte used to pad the last block. XMODEM can only send blocks of a certain size,
-    /// so if the message is not a multiple of that size the last block needs to be padded.
+    /// The byte used to pad the last block. XMODEM can only send blocks of
+    /// a certain size, so if the message is not a multiple of that size
+    /// the last block needs to be padded.
     pub pad_byte: u8,
 
-    /// The length of each block. There are only two options: 128-byte blocks (standard
-    ///  XMODEM) or 1024-byte blocks (XMODEM-1k).
+    /// The length of each block. There are only two options: 128-byte
+    /// blocks (standard  XMODEM) or 1024-byte blocks (XMODEM-1k).
     pub block_length: BlockLength,
 
-    /// The checksum mode used by XMODEM. This is determined by the receiver.
+    /// The checksum mode used by XMODEM. This is determined by the
+    /// receiver.
     checksum_mode: Checksum,
     errors: u32,
 }
@@ -83,144 +287,155 @@ impl Xmodem {
 
     /// Starts the XMODEM transmission.
     ///
-    /// `dev` should be the serial communication channel (e.g. the serial device).
-    /// `stream` should be the message to send (e.g. a file).
+    /// `dev` should be the serial communication channel (e.g. the serial
+    /// device). `stream` should be the message to send (e.g. a file).
     ///
     /// # Timeouts
-    /// This method has no way of setting the timeout of `dev`, so it's up to the caller
-    /// to set the timeout of the device before calling this method. Timeouts on receiving
-    /// bytes will be counted against `max_errors`, but timeouts on transmitting bytes
-    /// will be considered a fatal error.
-    pub fn send<D: Read + Write, R: Read>(&mut self, dev: &mut D, stream: &mut R) -> Result<()> {
+    /// This method has no way of setting the timeout of `dev`, so it's up
+    /// to the caller to set the timeout of the device before calling this
+    /// method. Timeouts on receiving bytes will be counted against
+    /// `max_errors`, but timeouts on transmitting bytes will be considered
+    /// a fatal error.
+    pub fn send<D: Read + Write, R: Read>(&mut self, dev: &mut D, stream: &mut R) -> Result<usize> {
         self.errors = 0;
 
         debug!("Starting XMODEM transfer");
-        try!(self.start_send(dev));
+        self.start_send(dev)?;
         debug!("First byte received. Sending stream.");
-        try!(self.send_stream(dev, stream));
+        let bytes = self.send_stream(dev, stream)?;
         debug!("Sending EOT");
-        try!(self.finish_send(dev));
+        self.finish_send(dev)?;
 
-        Ok(())
+        Ok(bytes)
     }
 
     /// Receive an XMODEM transmission.
     ///
-    /// `dev` should be the serial communication channel (e.g. the serial device).
-    /// The received data will be written to `outstream`.
-    /// `checksum` indicates which checksum mode should be used; Checksum::Standard is
-    /// a reasonable default.
+    /// `dev` should be the serial communication channel (e.g. the serial
+    /// device). The received data will be written to `outstream`.
+    /// `checksum` indicates which checksum mode should be used;
+    /// Checksum::Standard is the original wrapping 8-bit checksum; you
+    /// probably want CRC16 if the remote supports it (which, in 2021, it's
+    /// all but certain to do).
     ///
     /// # Timeouts
-    /// This method has no way of setting the timeout of `dev`, so it's up to the caller
-    /// to set the timeout of the device before calling this method. Timeouts on receiving
-    /// bytes will be counted against `max_errors`, but timeouts on transmitting bytes
-    /// will be considered a fatal error.
-    pub fn recv<D: Read + Write, W: Write>(&mut self,
-                                           dev: &mut D,
-                                           outstream: &mut W,
-                                           checksum : Checksum) -> Result<()> {
+    /// This method has no way of setting the timeout of `dev`, so it's up
+    /// to the caller to set the timeout of the device before calling this
+    /// method. Timeouts on receiving bytes will be counted against
+    /// `max_errors`, but timeouts on transmitting bytes will be considered
+    /// a fatal error.
+    pub fn recv<D: Read + Write, W: Write>(
+        &mut self,
+        dev: &mut D,
+        outstream: &mut W,
+        checksum: Checksum,
+    ) -> Result<usize> {
         self.errors = 0;
         self.checksum_mode = checksum;
         debug!("Starting XMODEM receive");
-        try!(dev.write(&[match self.checksum_mode {
+        dev.write_all(&[match self.checksum_mode {
             Checksum::Standard => NAK,
-            Checksum::CRC16 => CRC }]));
+            Checksum::CRC16 => CRC,
+        }])?;
         debug!("NCG sent. Receiving stream.");
-        let mut packet_num : u8 = 1;
+        let mut seqno: u32 = 1;
+        let mut bytes: usize = 0;
         loop {
-            match try!(get_byte_timeout(dev)) {
-                bt @ Some(SOH) | bt @ Some(STX) => { // Handle next packet
-                    let packet_size = match bt {
-                        Some(SOH) => 128,
-                        Some(STX) => 1024,
-                        _ => 0, // Why does the compiler need this?
-                    };
-                    let pnum = try!(get_byte(dev)); // specified packet number
-                    let pnum_1c = try!(get_byte(dev)); // same, 1's complemented
-                    // We'll respond with cancel later if the packet number is wrong
-                    let cancel_packet = packet_num != pnum ||  (255-pnum) != pnum_1c;
-                    let mut data : Vec<u8> = Vec::new();
-                    data.resize(packet_size,0);
-                    try!(dev.read_exact(&mut data));
-                    let success = match self.checksum_mode {
-                        Checksum::Standard => {
-                            let recv_checksum = try!(get_byte(dev));
-                            calc_checksum(&data) == recv_checksum },
-                        Checksum::CRC16 => {
-                            let recv_checksum =
-                                ( (try!(get_byte(dev)) as u16) << 8) +
-                                try!(get_byte(dev)) as u16;
-                            calc_crc(&data) == recv_checksum },
-                    };
-
-                    if cancel_packet {
-                        try!(dev.write(&[CAN]));
-                            try!(dev.write(&[CAN]));
-                        return Err(Error::Canceled)
+            if self.errors >= self.max_errors {
+                error!(
+                    "Exhausted max retries ({}) while waiting for data packet {}",
+                    self.max_errors, seqno
+                );
+                dev.write_all(&[CAN, CAN]).unwrap_or_default();
+                return Err(Error::ExhaustedRetries);
+            }
+
+            let packet = match XmodemPacket::recv_next(dev, self.checksum_mode) {
+                Ok(Some(x)) => {
+                    if u32::from(x.seqno) != (seqno & 0xFF) {
+                        dev.write_all(&[CAN, CAN])?;
+                        return Err(Error::Canceled);
                     }
-                    if success {
-                        packet_num = packet_num.wrapping_add(1);
-                        try!(dev.write(&[ACK]));
-                        try!(outstream.write_all(&data));
-                    } else {
-                        try!(dev.write(&[NAK]));
+                    x
+                }
+                Ok(None) => {
+                    dev.write_all(&[ACK])?;
+                    break;
+                }
+                Err(Error::Io(e)) => match e.kind() {
+                    io::ErrorKind::TimedOut => {
                         self.errors += 1;
+                        warn!("Timeout!");
+                        continue;
                     }
+                    _ => return Err(Error::Io(e)),
                 },
-                Some(EOT) => { // End of file
-                    try!(dev.write(&[ACK]));
-                    break
-                },
-                Some(_) => {
+                Err(Error::Checksum) => {
+                    dev.write_all(&[NAK])?;
+                    self.errors += 1;
+                    continue;
+                }
+                Err(Error::SequenceMismatch) => {
+                    dev.write_all(&[CAN, CAN])?;
+
+                    /* XXX Is this the right code? */
+                    return Err(Error::Canceled);
+                }
+                Err(Error::Invalid) => {
                     warn!("Unrecognized symbol!");
-                },
-                None => { self.errors += 1; warn!("Timeout!") },
-            }
-            if self.errors >= self.max_errors {
-                error!("Exhausted max retries ({}) while waiting for ACK for EOT", self.max_errors);
-                return Err(Error::ExhaustedRetries);
-            }
+                    continue;
+                }
+                Err(e) => return Err(e),
+            };
+
+            outstream.write_all(packet.as_ref()).map_err(|e| {
+                dev.write_all(&[CAN, CAN]).unwrap_or_default();
+                Error::Io(e)
+            })?;
+            dev.write_all(&[ACK])?;
+            seqno = seqno.wrapping_add(1);
+            bytes += packet.as_ref().len();
         }
-        Ok(())
+
+        Ok(bytes)
     }
-          
+
     fn start_send<D: Read + Write>(&mut self, dev: &mut D) -> Result<()> {
-        let mut cancels = 0u32;
+        let mut cancels = 0;
         loop {
-            match try!(get_byte_timeout(dev)) {
-                Some(c) => {
-                    match c {
-                        NAK => {
-                            debug!("Standard checksum requested");
-                            self.checksum_mode = Checksum::Standard;
-                            return Ok(());
-                        }
-                        CRC => {
-                            debug!("16-bit CRC requested");
-                            self.checksum_mode = Checksum::CRC16;
-                            return Ok(());
-                        }
-                        CAN => {
-                            warn!("Cancel (CAN) byte received");
-                            cancels += 1;
-                        },
-                        c => warn!("Unknown byte received at start of XMODEM transfer: {}", c),
-                    }
-                },
+            match get_byte_timeout(dev)? {
+                Some(NAK) => {
+                    debug!("Standard checksum requested");
+                    self.checksum_mode = Checksum::Standard;
+                    return Ok(());
+                }
+                Some(CRC) => {
+                    debug!("16-bit CRC requested");
+                    self.checksum_mode = Checksum::CRC16;
+                    return Ok(());
+                }
+                Some(CAN) => {
+                    warn!("Cancel (CAN) byte received");
+                    cancels += 1;
+                }
+                Some(c) => warn!("Unknown byte received at start of XMODEM transfer: {}", c),
                 None => warn!("Timed out waiting for start of XMODEM transfer."),
             }
 
             self.errors += 1;
 
             if cancels >= 2 {
-                error!("Transmission canceled: received two cancel (CAN) bytes \
-                        at start of XMODEM transfer");
+                error!(
+                    "Transmission canceled: received two cancel (CAN) bytes at start of XMODEM transfer"
+                );
                 return Err(Error::Canceled);
             }
 
             if self.errors >= self.max_errors {
-                error!("Exhausted max retries ({}) at start of XMODEM transfer.", self.max_errors);
+                error!(
+                    "Exhausted max retries ({}) at start of XMODEM transfer.",
+                    self.max_errors
+                );
                 if let Err(err) = dev.write_all(&[CAN]) {
                     warn!("Error sending CAN byte: {}", err);
                 }
@@ -229,57 +444,46 @@ impl Xmodem {
         }
     }
 
-    fn send_stream<D: Read + Write, R: Read>(&mut self, dev: &mut D, stream: &mut R) -> Result<()> {
-        let mut block_num = 0u32;
+    fn send_stream<D: Read + Write, R: Read>(
+        &mut self,
+        dev: &mut D,
+        stream: &mut R,
+    ) -> Result<usize> {
+        let mut seqno: u32 = 0;
+        let mut bytes: usize = 0;
         loop {
-            let mut buff = vec![self.pad_byte; self.block_length as usize + 3];
-            let n = try!(stream.read(&mut buff[3..]));
+            let mut packet = XmodemPacket::new(self.block_length, self.pad_byte);
+
+            let n = stream.read(packet.as_mut())?;
             if n == 0 {
                 debug!("Reached EOF");
-                return Ok(());
+                return Ok(bytes);
             }
 
-            block_num += 1;
-            buff[0] = match self.block_length {
-                BlockLength::Standard => SOH,
-                BlockLength::OneK => STX,
-            };
-            buff[1] = (block_num & 0xFF) as u8;
-            buff[2] = 0xFF - buff[1];
+            seqno += 1;
+            packet.seqno = u8::try_from(seqno & 0xFF).unwrap();
+            packet.send(dev, self.checksum_mode)?;
+            bytes += n;
 
-            match self.checksum_mode {
-                Checksum::Standard => {
-                    let checksum = calc_checksum(&buff[3..]);
-                    buff.push(checksum);
-                },
-                Checksum::CRC16 => {
-                    let crc = calc_crc(&buff[3..]);
-                    buff.push(((crc >> 8) & 0xFF) as u8);
-                    buff.push((crc & 0xFF) as u8);
+            match get_byte_timeout(dev)? {
+                Some(ACK) => {
+                    debug!("Received ACK for block {}", seqno);
+                    continue;
                 }
-            }
-
-            debug!("Sending block {}", block_num);
-            try!(dev.write_all(&buff));
-
-            match try!(get_byte_timeout(dev)) {
-                Some(c) => {
-                    if c == ACK {
-                        debug!("Received ACK for block {}", block_num);
-                        continue
-                    } else {
-                        warn!("Expected ACK, got {}", c);
-                    }
-                    // TODO handle CAN bytes
-                },
-                None => warn!("Timeout waiting for ACK for block {}", block_num),
+                // TODO handle CAN bytes
+                Some(b) => {
+                    warn!("Expected ACK, got {}", b);
+                }
+                None => warn!("Timeout waiting for ACK for block {}", seqno),
             }
 
             self.errors += 1;
 
             if self.errors >= self.max_errors {
-                error!("Exhausted max retries ({}) while sending block {} in XMODEM transfer",
-                       self.max_errors, block_num);
+                error!(
+                    "Exhausted max retries ({}) while sending block {} in XMODEM transfer",
+                    self.max_errors, seqno
+                );
                 return Err(Error::ExhaustedRetries);
             }
         }
@@ -287,30 +491,40 @@ impl Xmodem {
 
     fn finish_send<D: Read + Write>(&mut self, dev: &mut D) -> Result<()> {
         loop {
-            try!(dev.write_all(&[EOT]));
-
-            match try!(get_byte_timeout(dev)) {
-                Some(c) => {
-                    if c == ACK {
-                        info!("XMODEM transmission successful");
-                        return Ok(());
-                    } else {
-                        warn!("Expected ACK, got {}", c);
-                    }
-                },
-                None => warn!("Timeout waiting for ACK for EOT"),
+            dev.write_all(&[EOT])?;
+
+            match get_byte_timeout(dev)? {
+                Some(ACK) => {
+                    info!("XMODEM transmission successful");
+                    return Ok(());
+                }
+                Some(b) => {
+                    warn!("Expected ACK, got {}", b);
+                }
+                None => {
+                    warn!("Timeout waiting for ACK for EOT")
+                }
             }
 
             self.errors += 1;
 
             if self.errors >= self.max_errors {
-                error!("Exhausted max retries ({}) while waiting for ACK for EOT", self.max_errors);
+                error!(
+                    "Exhausted max retries ({}) while waiting for ACK for EOT",
+                    self.max_errors
+                );
                 return Err(Error::ExhaustedRetries);
             }
         }
     }
 }
 
+impl Default for Xmodem {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 fn calc_checksum(data: &[u8]) -> u8 {
     data.iter().fold(0, |x, &y| x.wrapping_add(y))
 }
@@ -319,14 +533,14 @@ fn calc_crc(data: &[u8]) -> u16 {
     crc16::State::<crc16::XMODEM>::calculate(data)
 }
 
-fn get_byte<R: Read>(reader: &mut R) -> std::io::Result<u8> {
+fn get_byte<R: Read>(reader: &mut R) -> io::Result<u8> {
     let mut buff = [0];
-    try!(reader.read_exact(&mut buff));
+    reader.read_exact(&mut buff)?;
     Ok(buff[0])
 }
 
 /// Turns timeout errors into `Ok(None)`
-fn get_byte_timeout<R: Read>(reader: &mut R) -> std::io::Result<Option<u8>> {
+fn get_byte_timeout<R: Read>(reader: &mut R) -> io::Result<Option<u8>> {
     match get_byte(reader) {
         Ok(c) => Ok(Some(c)),
         Err(err) => {
diff --git a/tests/external.rs b/tests/external.rs
index ccb16b8..e1d1307 100644
--- a/tests/external.rs
+++ b/tests/external.rs
@@ -1,13 +1,13 @@
 //! Test against the `sx` program, and itself
-extern crate tempfile;
 extern crate rand;
+extern crate tempfile;
 extern crate xmodem;
 
-use std::process::{Command, Stdio, ChildStdin, ChildStdout};
-use std::io::{self, Read, Write, Seek};
+use rand::{RngCore, rng};
+use std::io::{self, Read, Seek, Write};
+use std::process::{ChildStdin, ChildStdout, Command, Stdio};
 use tempfile::NamedTempFile;
-use rand::{Rng, thread_rng};
-use xmodem::{Xmodem,Checksum,BlockLength};
+use xmodem::{BlockLength, Checksum, Xmodem};
 
 struct ChildStdInOut {
     stdin: ChildStdin,
@@ -31,9 +31,9 @@ impl Write for ChildStdInOut {
 }
 
 #[cfg(test)]
-fn xmodem_recv(checksum_mode : Checksum, block_length : BlockLength, data_len : usize) {
+fn xmodem_recv(checksum_mode: Checksum, block_length: BlockLength, data_len: usize) {
     let mut data = vec![0; data_len];
-    thread_rng().fill_bytes(&mut data);
+    rng().fill_bytes(&mut data);
 
     let mut send_file = NamedTempFile::new().unwrap();
     send_file.write_all(&data).unwrap();
@@ -41,82 +41,89 @@ fn xmodem_recv(checksum_mode : Checksum, block_length : BlockLength, data_len :
     let mut send_builder = Command::new("sb");
     send_builder.arg("--xmodem");
     match block_length {
-        BlockLength::OneK => { send_builder.arg("--1k"); }
+        BlockLength::OneK => {
+            send_builder.arg("--1k");
+        }
         _ => {}
     }
-    let send = send_builder.arg(send_file.path())
+    let send = send_builder
+        .arg(send_file.path())
         .stdin(Stdio::piped())
         .stdout(Stdio::piped())
         .stderr(Stdio::null())
-        .spawn().unwrap();
-    
+        .spawn()
+        .unwrap();
+
     let tx_stream = send.stdin.unwrap();
     let rx_stream = send.stdout.unwrap();
-    let mut serial_dev = ChildStdInOut { stdin: tx_stream, stdout: rx_stream };
+    let mut serial_dev = ChildStdInOut {
+        stdin: tx_stream,
+        stdout: rx_stream,
+    };
 
     let mut xmodem = Xmodem::new();
     let mut recv_data = Vec::new();
-    xmodem.recv(&mut serial_dev,&mut recv_data, checksum_mode).unwrap();
+    let bytes = xmodem
+        .recv(&mut serial_dev, &mut recv_data, checksum_mode)
+        .unwrap();
+    assert_eq!(bytes, (data_len + 127) & !127);
 
     let mut sent_data = Vec::new();
     send_file.seek(std::io::SeekFrom::Start(0)).unwrap();
     send_file.read_to_end(&mut sent_data).unwrap();
     let mut padded_data = sent_data.clone();
     for _ in 0..(128 - sent_data.len() % 128) {
-         padded_data.push(0x1a);
+        padded_data.push(0x1a);
     }
     assert_eq!(padded_data, recv_data);
 }
 
 #[test]
 fn xmodem_recv_standard() {
-    xmodem_recv(Checksum::Standard,
-                BlockLength::Standard,
-                2000);
+    xmodem_recv(Checksum::Standard, BlockLength::Standard, 2000);
 }
 
 #[test]
 fn xmodem_recv_crc() {
-    xmodem_recv(Checksum::CRC16,
-                BlockLength::Standard,
-                2000);
+    xmodem_recv(Checksum::CRC16, BlockLength::Standard, 2000);
 }
 
 #[test]
 fn xmodem_recv_1k_crc() {
-    xmodem_recv(Checksum::CRC16,
-                BlockLength::OneK,
-                8500);
+    xmodem_recv(Checksum::CRC16, BlockLength::OneK, 8500);
 }
 
 #[test]
 fn xmodem_recv_long() {
-    xmodem_recv(Checksum::CRC16,
-                BlockLength::Standard,
-                50000);
+    xmodem_recv(Checksum::CRC16, BlockLength::Standard, 50000);
 }
 
 #[test]
 fn xmodem_send_standard() {
     let data_len = 2000;
     let mut data = vec![0; data_len];
-    thread_rng().fill_bytes(&mut data);
+    rng().fill_bytes(&mut data);
 
     let mut recv_file = NamedTempFile::new().unwrap();
     let recv = Command::new("rb")
-                       .arg("--xmodem")
-                       .arg(recv_file.path())
-                       .stdin(Stdio::piped())
-                       .stdout(Stdio::piped())
-                       .stderr(Stdio::null())
-                       .spawn().unwrap();
+        .arg("--xmodem")
+        .arg(recv_file.path())
+        .stdin(Stdio::piped())
+        .stdout(Stdio::piped())
+        .stderr(Stdio::null())
+        .spawn()
+        .unwrap();
 
     let tx_stream = recv.stdin.unwrap();
     let rx_stream = recv.stdout.unwrap();
-    let mut serial_dev = ChildStdInOut { stdin: tx_stream, stdout: rx_stream };
+    let mut serial_dev = ChildStdInOut {
+        stdin: tx_stream,
+        stdout: rx_stream,
+    };
 
     let mut xmodem = Xmodem::new();
-    xmodem.send(&mut serial_dev, &mut &data[..]).unwrap();
+    let bytes = xmodem.send(&mut serial_dev, &mut &data[..]).unwrap();
+    assert_eq!(bytes, data_len);
 
     let mut received_data = Vec::new();
     recv_file.read_to_end(&mut received_data).unwrap();
@@ -131,21 +138,25 @@ fn xmodem_send_standard() {
 fn xmodem_send_crc() {
     let data_len = 2000;
     let mut data = vec![0; data_len];
-    thread_rng().fill_bytes(&mut data);
+    rng().fill_bytes(&mut data);
 
     let mut recv_file = NamedTempFile::new().unwrap();
     let recv = Command::new("rb")
-                       .arg("--xmodem")
-                       .arg("--with-crc")
-                       .arg(recv_file.path())
-                       .stdin(Stdio::piped())
-                       .stdout(Stdio::piped())
-                       .stderr(Stdio::null())
-                       .spawn().unwrap();
+        .arg("--xmodem")
+        .arg("--with-crc")
+        .arg(recv_file.path())
+        .stdin(Stdio::piped())
+        .stdout(Stdio::piped())
+        .stderr(Stdio::null())
+        .spawn()
+        .unwrap();
 
     let tx_stream = recv.stdin.unwrap();
     let rx_stream = recv.stdout.unwrap();
-    let mut serial_dev = ChildStdInOut { stdin: tx_stream, stdout: rx_stream };
+    let mut serial_dev = ChildStdInOut {
+        stdin: tx_stream,
+        stdout: rx_stream,
+    };
 
     let mut xmodem = Xmodem::new();
     xmodem.send(&mut serial_dev, &mut &data[..]).unwrap();
@@ -153,7 +164,7 @@ fn xmodem_send_crc() {
     let mut received_data = Vec::new();
     recv_file.read_to_end(&mut received_data).unwrap();
     let mut padded_data = data.clone();
-    
+
     for _ in 0..(128 - data.len() % 128) {
         padded_data.push(0x1a);
     }
diff --git a/tests/loopback.rs b/tests/loopback.rs
index 1425f72..0cc66be 100644
--- a/tests/loopback.rs
+++ b/tests/loopback.rs
@@ -1,15 +1,15 @@
 //! Test against own implementation
-extern crate tempfile;
 extern crate rand;
+extern crate tempfile;
 extern crate xmodem;
 
-use std::io::{self, Read, Write, ErrorKind};
-use xmodem::{Xmodem,Checksum,BlockLength};
-use std::sync::mpsc::{channel,Sender,Receiver};
+use std::io::{self, ErrorKind, Read, Write};
+use std::sync::mpsc::{Receiver, Sender, channel};
+use xmodem::{BlockLength, Checksum, Xmodem};
 
 struct BidirectionalPipe {
-    pin : Receiver<u8>,
-    pout : Sender<u8>,
+    pin: Receiver<u8>,
+    pout: Sender<u8>,
 }
 
 impl Read for BidirectionalPipe {
@@ -17,7 +17,7 @@ impl Read for BidirectionalPipe {
         for idx in 0..buf.len() {
             buf[idx] = match self.pin.recv() {
                 Ok(v) => v,
-                Err(e) => return Err(std::io::Error::new(ErrorKind::BrokenPipe,e)),
+                Err(e) => return Err(std::io::Error::new(ErrorKind::BrokenPipe, e)),
             }
         }
         Ok(buf.len())
@@ -26,7 +26,9 @@ impl Read for BidirectionalPipe {
 
 impl Write for BidirectionalPipe {
     fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
-        for v in buf { self.pout.send(*v).unwrap(); }
+        for v in buf {
+            self.pout.send(*v).unwrap();
+        }
         Ok(buf.len())
     }
 
@@ -36,56 +38,66 @@ impl Write for BidirectionalPipe {
 }
 
 fn loopback() -> (BidirectionalPipe, BidirectionalPipe) {
-    let (s1,r1) = channel();
-    let (s2,r2) = channel();
-    (BidirectionalPipe{ pin : r1, pout : s2 }, BidirectionalPipe{ pin : r2, pout : s1 })
+    let (s1, r1) = channel();
+    let (s2, r2) = channel();
+    (
+        BidirectionalPipe { pin: r1, pout: s2 },
+        BidirectionalPipe { pin: r2, pout: s1 },
+    )
 }
 
 #[cfg(test)]
-fn xmodem_loopback(checksum_mode:Checksum,block_length:BlockLength, data_len : usize) {
+fn xmodem_loopback(checksum_mode: Checksum, block_length: BlockLength, data_len: usize) {
     let mut data_out = vec![0; data_len];
     // We don't really need the rng here
-    for idx in 0..data_len { data_out[idx] = ((idx+7) * 13) as u8; }
+    for idx in 0..data_len {
+        data_out[idx] = ((idx + 7) * 13) as u8;
+    }
     let (mut p1, mut p2) = loopback();
     let handle = std::thread::spawn(move || {
         let mut xmodem = Xmodem::new();
         xmodem.block_length = block_length;
-        xmodem.send(&mut p1, &mut &data_out[..]).unwrap();
-        data_out
+        let bytes_out = xmodem.send(&mut p1, &mut &data_out[..]).unwrap();
+        (data_out, bytes_out)
     });
     let handle2 = std::thread::spawn(move || {
         let mut xmodem = Xmodem::new();
-        let mut data_in= vec![0; 0];
-        xmodem.recv(&mut p2, &mut data_in, checksum_mode).unwrap();
-        data_in
+        let mut data_in = vec![0; 0];
+        let bytes_in = xmodem.recv(&mut p2, &mut data_in, checksum_mode).unwrap();
+        (data_in, bytes_in)
     });
-    
-    let mut dato = handle.join().unwrap();
+
+    let (mut dato, bytes_out) = handle.join().unwrap();
+    assert_eq!(bytes_out, dato.len());
+
     // Pad output data to multiple of block length for comparison
     let bl = block_length as usize;
-    for _ in 0..(bl - data_len % bl) { dato.push(0x1a); }
-    let dati = handle2.join().unwrap();
-    assert_eq!(dato.len(),dati.len());
-    assert_eq!(dato,dati);
+    for _ in 0..(bl - data_len % bl) {
+        dato.push(0x1a);
+    }
+    let (dati, bytes_in) = handle2.join().unwrap();
+    assert_eq!(dato.len(), dati.len());
+    assert_eq!(dato, dati);
+    assert_eq!(bytes_in, dati.len());
 }
 
 #[test]
 fn xmodem_loopback_standard() {
-    xmodem_loopback(Checksum::Standard,BlockLength::Standard,2000);
+    xmodem_loopback(Checksum::Standard, BlockLength::Standard, 2000);
 }
 
 #[test]
 fn xmodem_loopback_onek() {
-    xmodem_loopback(Checksum::Standard,BlockLength::OneK,2200);
+    xmodem_loopback(Checksum::Standard, BlockLength::OneK, 2200);
 }
 
 #[test]
 fn xmodem_loopback_crc() {
-    xmodem_loopback(Checksum::CRC16,BlockLength::Standard,2000);
+    xmodem_loopback(Checksum::CRC16, BlockLength::Standard, 2000);
 }
 
 #[test]
 fn xmodem_loopback_long_crc() {
     // make sure we wrap block counter
-    xmodem_loopback(Checksum::CRC16,BlockLength::Standard,50000);
+    xmodem_loopback(Checksum::CRC16, BlockLength::Standard, 50000);
 }


Reviews:
user: login: orangecms
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T22:56:31Z
user: login: dancrossnyc
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T11:50:43Z

Review comments:
diff_hunk: @@ -1,13 +1,22 @@
 # XMODEM.rs
 
-[![Build Status](https://travis-ci.org/awelkie/xmodem.rs.svg?branch=master)](https://travis-ci.org/awelkie/xmodem.rs)
-[![](https://img.shields.io/crates/v/xmodem.svg)](https://crates.io/crates/xmodem)
-[![](https://img.shields.io/crates/l/xmodem.svg)](https://crates.io/crates/xmodem)
-
path: README.md
user: login: orangecms
body: Should this remain if Allen still publishes a release?
When ownership is transfered, we'd just need to renew the CI setup.
created_at: 2025-04-15T22:56:25Z
author_association: NONE
diff_hunk: @@ -1,13 +1,22 @@
 # XMODEM.rs
 
-[![Build Status](https://travis-ci.org/awelkie/xmodem.rs.svg?branch=master)](https://travis-ci.org/awelkie/xmodem.rs)
-[![](https://img.shields.io/crates/v/xmodem.svg)](https://crates.io/crates/xmodem)
-[![](https://img.shields.io/crates/l/xmodem.svg)](https://crates.io/crates/xmodem)
-
path: README.md
user: login: dancrossnyc
body: It could do. We could probably switch over to github actions for CI.
created_at: 2025-04-16T11:50:43Z
author_association: COLLABORATOR

PR comments:
user: login: awelkie
created_at: 2025-04-15T19:15:07Z
body: Hey Dan, thanks for the changes! I can merge these in and push a new release to crates.io. I'm also happy to add ownership of the crate on crates.io to Oxide, so you can switch the crate over to your internal fork, seeing as I haven't touched this repo in quite some time. Just let me know if you'd prefer to take over the management of the crate.
author_association: OWNER
user: login: dancrossnyc
created_at: 2025-04-15T19:54:01Z
body: Thanks, Allen. I think if you could merge these now, that'd be great; I know there are a few other folks that are floating patches on top of xmodem.rs, and, if you're game to transfer maintenance, I'm happy to reach out to those folks and see if they would like to chart a path forward together. But it would be nice to push this now, if that's not too much hassle on your side.

Thanks again!
author_association: COLLABORATOR
user: login: orangecms
created_at: 2025-04-15T22:50:24Z
body: Dan just pinged me about this because I'd also forked this, but from nycresistor, and filed a PR a few years back - though never got a response:
https://github.com/nycresistor/xmodem.rs/pull/1

I'd be happy to contribute to putting this all together.
Would be great if Oxide took over publishing the crate. :)
author_association: NONE
user: login: dancrossnyc
created_at: 2025-04-16T11:50:08Z
body: > Dan just pinged me about this because I'd also forked this, but from nycresistor, and filed a PR a few years back - though never got a response: [nycresistor#1](https://github.com/nycresistor/xmodem.rs/pull/1)
> 
> I'd be happy to contribute to putting this all together. Would be great if Oxide took over publishing the crate. :)

Sure, I think Oxide can host. I do think we should reach out to the folks at NYC Resistor and see how we might further collaborate, though.

Allen, I want to be respectful of your time and position here, but I think if you're ok with it, we could set things up so that Oxide coordinates and we, along with Daniel and the folks from NYCR (if they are interested) contribute/review?
author_association: COLLABORATOR
user: login: awelkie
created_at: 2025-04-16T20:10:52Z
body: Sure, sounds good to me. Dan, I just added you as an owner to the crates.io crate and as a collaborator for this repo. I'm not super interested in maintaining this library myself, so I'll let you and Oxide take over, if you don't mind. Feel free to continue with this repo, or to fork it, as you prefer.

And Daniel (from NYCR), sorry for never getting back to you about your other pull request!
author_association: OWNER
user: login: dancrossnyc
created_at: 2025-04-19T00:59:14Z
body: Thanks, Allen; sorry for not writing back sooner, it's been a big week for us (we're doing bringup of our next-gen boards).

I think we'll probably reconsolidate on the Oxide copy of the repo. I'll try to carve out some time to have a look at getting it all set up next week. Thanks again!
author_association: COLLABORATOR