lrzsz2 0.3.4

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;

#[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,
    /// 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();
    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 = 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(&filename_u8, 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;

    while !session_done || !sender.drain_outgoing().is_empty() {
        let mut progressed = false;

        if !sender.drain_outgoing().is_empty() {
            match port.write(sender.drain_outgoing()) {
                Ok(0) => {}
                Ok(n) => {
                    sender.advance_outgoing(n);
                    progressed = true;
                }
                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
                Err(e) => bail!("wire write failed: {e}"),
            }
        }

        if sender.drain_outgoing().is_empty() {
            if let Some(request) = sender.poll_file() {
                let file = current_file
                    .as_mut()
                    .context("no active file for data request")?;
                file.seek(SeekFrom::Start(u64::from(request.offset)))?;
                if request.len > file_buf.len() {
                    file_buf.resize(request.len, 0);
                }
                let mut read_total = 0;
                while read_total < request.len {
                    let n = file.read(&mut file_buf[read_total..request.len])?;
                    if n == 0 {
                        break;
                    }
                    read_total += n;
                }
                if read_total == 0 && request.len > 0 {
                    bail!("unexpected EOF while reading '{}'", current_filename);
                }
                sender
                    .feed_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(request.offset) + read_total as u64;
                    if next_pos > pb.position() {
                        pb.set_position(next_pos);
                    }
                }
                progressed = true;
            }
        }

        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}"),
        }

        if sender.drain_outgoing().is_empty() && input_offset < input_buf.len() {
            let consumed = sender
                .feed_incoming(&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 let Some(event) = sender.poll_event() {
            match event {
                zmodem2::SenderEvent::FileComplete => {
                    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(&filename_u8, 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_session()
                            .map_err(|e| anyhow::anyhow!("ZMODEM error: {:#}", e))?;
                    }
                }
                zmodem2::SenderEvent::SessionComplete => {
                    session_done = true;
                }
            }
        }

        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(())
}