lrzsz2 0.3.3

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};
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::Path;

#[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 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();

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

    let first_path = &args.paths[0];
    let mut file = File::open(first_path).with_context(|| format!("'{first_path}'"))?;
    let filename_path = Path::new(first_path);
    let filename = filename_path
        .file_name()
        .with_context(|| format!("'{}'", first_path))?;
    let filename_str = filename.to_string_lossy();
    let filename_u8 = filename_u8_from_os(filename);
    let size = file.metadata()?.len();
    let mut state = zmodem2::Transmission::set_first_file_u8(filename_u8.as_ref(), size as u32)?;

    let pb = ProgressBar::new(size);
    pb.set_style(
        ProgressStyle::default_bar()
            .template(
                "{msg}\n{spinner} [{elapsed_precise}] [{bar:40}] {bytes}/{total_bytes} ({eta})",
            )?
            .progress_chars("=>-"),
    );
    pb.set_message(format!("Sending {}", filename_str));
    let mut file_started = false;
    loop {
        match state.send(&mut port, &mut file) {
            Ok(Some(())) => {}
            Ok(None) => {
                std::thread::sleep(std::time::Duration::from_millis(10));
            }
            Err(e) => {
                pb.finish_with_message("Aborted");
                bail!("ZMODEM error: {:#}", e);
            }
        }
        if state.state() != zmodem2::State::FileEnd {
            file_started = true;
        }
        pb.set_position(state.count() as u64);
        if file_started && state.state() == zmodem2::State::FileEnd {
            break;
        }
    }
    pb.finish_with_message(format!("Sent {}", filename_str));

    for path in &args.paths[1..] {
        let mut 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();
        let filename_u8 = filename_u8_from_os(filename);
        let size = file.metadata()?.len();
        state.set_next_file_u8(filename_u8.as_ref(), size as u32)?;
        let pb = ProgressBar::new(size);
        pb.set_style(
            ProgressStyle::default_bar()
                .template(
                    "{msg}\n{spinner} [{elapsed_precise}] [{bar:40}] {bytes}/{total_bytes} ({eta})",
                )?
                .progress_chars("=>-"),
        );
        pb.set_message(format!("Sending {}", filename_str));
        let mut file_started = false;
        loop {
            match state.send(&mut port, &mut file) {
                Ok(Some(())) => {}
                Ok(None) => {
                    std::thread::sleep(std::time::Duration::from_millis(10));
                }
                Err(e) => {
                    pb.finish_with_message("Aborted");
                    bail!("ZMODEM error: {:#}", e);
                }
            }
            if state.state() != zmodem2::State::FileEnd {
                file_started = true;
            }
            pb.set_position(state.count() as u64);
            if file_started && state.state() == zmodem2::State::FileEnd {
                break;
            }
        }
        pb.finish_with_message(format!("Sent {}", filename_str));
    }
    while state.state() != zmodem2::State::SessionEnd {
        match state.finish(&mut port) {
            Ok(Some(())) => {}
            Ok(None) => {
                std::thread::sleep(std::time::Duration::from_millis(10));
            }
            Err(e) => {
                bail!("ZMODEM error during finish: {:#}", e);
            }
        }
    }
    Ok(())
}