lrzsz2 0.7.0

ZMODEM file transfer utilities
Documentation
// SPDX-License-Identifier: GPL-3-0-or-later
// Copyright (c) 2017-2020 Alexey Arbuzov
// Copyright (c) 2023-2025 Jarkko Sakkinen

extern crate zmodem2;

use anyhow::{bail, Context};
use argh::FromArgs;
use indicatif::{ProgressBar, ProgressStyle};
use lrzsz2::ReadWrite;
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs::{File, OpenOptions};
use std::io::{IsTerminal, Read, Seek, SeekFrom, Write};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::thread::sleep;
use std::time::Duration;
use zmodem2::{Action, Event, FileInfo, Position};

#[derive(FromArgs, Debug)]
/// Sends files using the ZMODEM protocol.
struct Arguments {
    /// device file [default: /dev/ttyS0]
    #[argh(option, short = 'p', default = "String::from(\"/dev/ttyS0\")")]
    port: String,
    /// disable progress bars
    #[argh(switch)]
    no_progress: bool,
    /// list of files
    #[argh(positional)]
    paths: Vec<String>,
}

#[cfg(unix)]
fn filename_u8_from_os(name: &OsStr) -> Cow<'_, [u8]> {
    Cow::Borrowed(name.as_bytes())
}

#[cfg(not(unix))]
fn filename_u8_from_os(name: &OsStr) -> Cow<'_, [u8]> {
    Cow::Owned(name.to_string_lossy().into_owned().into_bytes())
}

fn prepare_send_file(
    path: &str,
    show_progress: bool,
) -> anyhow::Result<(File, ProgressBar, String, Vec<u8>, u32)> {
    let file = File::open(path).with_context(|| format!("'{}'", path))?;
    let filename_path = Path::new(path);
    let filename = filename_path
        .file_name()
        .with_context(|| format!("'{}'", path))?;
    let filename_str = filename.to_string_lossy().into_owned();
    let filename_u8 = filename_u8_from_os(filename).into_owned();
    let size = file.metadata()?.len();
    if size > u64::from(u32::MAX) {
        bail!(
            "'{}' is too large: {} bytes exceeds {}",
            path,
            size,
            u32::MAX
        );
    }
    let size_u32 = size as u32;

    let pb = if show_progress {
        let pb = ProgressBar::new(size);
        pb.set_style(
            ProgressStyle::default_bar()
                .template(
                    "{spinner} [{elapsed_precise}] {msg} [{bar:40}] {bytes}/{total_bytes} ({eta})",
                )?
                .progress_chars("=>-"),
        );
        pb.set_message(format!("Sending {}", filename_str));
        pb
    } else {
        ProgressBar::hidden()
    };

    Ok((file, pb, filename_str, filename_u8, size_u32))
}

fn main() -> anyhow::Result<()> {
    let args: Arguments = argh::from_env();
    if args.paths.is_empty() {
        std::process::exit(2);
    }

    let port_file = OpenOptions::new()
        .read(true)
        .write(true)
        .open(&args.port)
        .with_context(|| format!("'{}'", args.port))?;

    lrzsz2::configure_raw_mode(&port_file).ok();
    lrzsz2::configure_nonblocking(&port_file).ok();

    let mut port: Box<dyn ReadWrite> = Box::new(port_file);

    let show_progress = !args.no_progress && std::io::stderr().is_terminal();
    let mut sender =
        zmodem2::Sender::new().map_err(|e| anyhow::anyhow!("ZMODEM error: {:#}", e))?;
    let mut file_iter = args.paths.iter();

    let first_path = file_iter.next().unwrap();
    let (file, pb, filename_str, filename_u8, size_u32) =
        prepare_send_file(first_path, show_progress)?;
    sender
        .start_file(FileInfo::new(&filename_u8, Some(Position::new(size_u32))))
        .map_err(|e| anyhow::anyhow!("ZMODEM error: {:#}", e))?;

    let mut current_file = Some(file);
    let mut current_pb = Some(pb);
    let mut current_filename = filename_str;
    let mut wire_buf = [0u8; 4096];
    let mut input_buf: Vec<u8> = Vec::new();
    let mut input_offset: usize = 0;
    let mut file_buf: Vec<u8> = vec![0u8; 1024];
    let mut session_done = false;

    loop {
        let mut progressed = false;
        let mut idle = false;

        match port.read(&mut wire_buf) {
            Ok(0) => {}
            Ok(n) => {
                input_buf.extend_from_slice(&wire_buf[..n]);
                progressed = true;
            }
            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
            Err(e) => bail!("wire read failed: {e}"),
        }

        match sender.poll() {
            Action::WriteWire(bytes) => match port.write(bytes) {
                Ok(0) => {}
                Ok(n) => {
                    sender.wire_written(n);
                    progressed = true;
                }
                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
                Err(e) => bail!("wire write failed: {e}"),
            },
            Action::ReadFile { offset, max_len } => {
                let file = current_file
                    .as_mut()
                    .context("no active file for data request")?;
                file.seek(SeekFrom::Start(u64::from(offset.get())))?;
                if max_len > file_buf.len() {
                    file_buf.resize(max_len, 0);
                }
                let mut read_total = 0;
                while read_total < max_len {
                    let n = file.read(&mut file_buf[read_total..max_len])?;
                    if n == 0 {
                        break;
                    }
                    read_total += n;
                }
                if read_total == 0 && max_len > 0 {
                    bail!("unexpected EOF while reading '{}'", current_filename);
                }
                sender
                    .submit_file(&file_buf[..read_total])
                    .map_err(|e| anyhow::anyhow!("ZMODEM error: {:#}", e))?;
                if let Some(pb) = current_pb.as_ref() {
                    let next_pos = u64::from(offset.get()) + read_total as u64;
                    if next_pos > pb.position() {
                        pb.set_position(next_pos);
                    }
                }
                progressed = true;
            }
            Action::Event(event) => {
                progressed = true;
                match event {
                    Event::FileCompleted => {
                        if let Some(pb) = current_pb.take() {
                            if show_progress {
                                pb.finish_and_clear();
                            } else {
                                pb.finish();
                                eprintln!("Sent {}", current_filename);
                            }
                        }
                        current_file = None;

                        if let Some(next_path) = file_iter.next() {
                            let (file, pb, filename_str, filename_u8, size_u32) =
                                prepare_send_file(next_path, show_progress)?;
                            sender
                                .start_file(FileInfo::new(
                                    &filename_u8,
                                    Some(Position::new(size_u32)),
                                ))
                                .map_err(|e| anyhow::anyhow!("ZMODEM error: {:#}", e))?;
                            current_file = Some(file);
                            current_pb = Some(pb);
                            current_filename = filename_str;
                        } else {
                            sender
                                .finish()
                                .map_err(|e| anyhow::anyhow!("ZMODEM error: {:#}", e))?;
                        }
                    }
                    Event::SessionCompleted => {
                        session_done = true;
                    }
                    Event::Aborted => bail!("ZMODEM session aborted"),
                    _ => {}
                }
            }
            Action::WriteFile(_) => unreachable!("sender never writes files"),
            Action::Idle => {
                idle = true;
                if input_offset < input_buf.len() {
                    let consumed = sender
                        .submit_wire(&input_buf[input_offset..])
                        .map_err(|e| anyhow::anyhow!("ZMODEM error: {:#}", e))?;
                    if consumed > 0 {
                        input_offset += consumed;
                        progressed = true;
                        if input_offset == input_buf.len() {
                            input_buf.clear();
                            input_offset = 0;
                        } else if input_offset > 4096 {
                            input_buf.drain(..input_offset);
                            input_offset = 0;
                        }
                    }
                }
            }
            _ => {}
        }

        if session_done && idle {
            break;
        }

        if !progressed {
            sleep(Duration::from_millis(1));
        }
    }

    if let Some(pb) = current_pb.take() {
        if !pb.is_finished() {
            if show_progress {
                pb.finish_and_clear();
            } else {
                pb.finish();
                eprintln!("Sent {}", current_filename);
            }
        }
    }

    Ok(())
}