lrzsz2 0.3.2

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::fs::{File, OpenOptions};
use std::path::{Component, Path, PathBuf};

#[derive(FromArgs, Debug)]
/// Receive files using the ZMODEM protocol.
struct Arguments {
    /// device file [default: /dev/ttyS0]
    #[argh(option, short = 'p', default = "String::from(\"/dev/ttyS0\")")]
    port: String,
    /// download directory
    #[argh(positional)]
    path: String,
}

fn main() -> anyhow::Result<()> {
    let args: Arguments = argh::from_env();

    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 mut state = zmodem2::Transmission::new();
    let dest_dir = PathBuf::from(&args.path);
    if !dest_dir.is_dir() {
        bail!("'{}' is not a directory", dest_dir.display());
    }

    let mut open_files: std::collections::HashMap<Vec<u8>, (File, ProgressBar)> =
        std::collections::HashMap::new();
    let mut current_filename = vec![];
    let mut sink = std::io::sink();

    while state.state() != zmodem2::State::SessionEnd {
        let current_state = state.state();

        if current_state == zmodem2::State::FileBegin
            && state.file_name() != current_filename.as_slice()
        {
            let filename_bytes = state.file_name();
            let received_filename = Path::new(
                std::str::from_utf8(filename_bytes)
                    .with_context(|| "invalid UTF-8 in filename".to_string())?,
            )
            .components()
            .next_back()
            .and_then(|c| match c {
                Component::Normal(name) => Some(name),
                _ => None,
            })
            .with_context(|| {
                format!(
                    "malformed filename: '{}'",
                    std::str::from_utf8(filename_bytes).unwrap_or("<invalid>")
                )
            })?;

            let path = dest_dir.join(received_filename);
            let file = File::create(&path)
                .with_context(|| format!("could not create '{}'", path.display()))?;

            let pb = ProgressBar::new(state.file_size() as u64);
            pb.set_style(
                ProgressStyle::default_bar()
                    .template(
                        "{msg}\n{spinner} [{elapsed_precise}] [{bar:40}] {bytes}/{total_bytes} ({eta})",
                    )?
                    .progress_chars("=>-"),
            );
            pb.set_message(format!("Receiving {}", path.display()));

            open_files.insert(filename_bytes.to_vec(), (file, pb));
            current_filename = filename_bytes.to_vec();
        }

        let has_file = open_files.contains_key(&current_filename);
        let result = if has_file {
            let (file, _pb) = open_files.get_mut(&current_filename).unwrap();
            state.receive(&mut port, file)
        } else {
            state.receive(&mut port, &mut sink)
        };

        match result {
            Ok(Some(())) => {
                let count = state.count();
                let current_state = state.state();

                if let Some((_file, pb)) = open_files.get(&current_filename) {
                    pb.set_position(count as u64);
                }
                if current_state == zmodem2::State::FileEnd {
                    if let Some((_file, pb)) = open_files.remove(&current_filename) {
                        pb.finish_with_message("Done");
                    }
                    current_filename.clear();
                }
            }
            Ok(None) => {
                std::thread::sleep(std::time::Duration::from_millis(10));
            }
            Err(e) => {
                for (_filename, (_file, pb)) in open_files.iter() {
                    pb.finish_with_message("Aborted");
                }
                bail!("ZMODEM error: {:#}", e);
            }
        }
    }

    for (_filename, (_file, pb)) in open_files.iter() {
        if !pb.is_finished() {
            pb.finish_with_message("Done");
        }
    }

    Ok(())
}