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};
use std::os::fd::AsFd;
#[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)]
struct Arguments {
#[argh(option, short = 'p')]
port: Option<String>,
#[argh(switch)]
no_progress: bool,
#[argh(switch)]
version: bool,
#[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.version {
println!(concat!(env!("CARGO_BIN_NAME"), " ", env!("CARGO_PKG_VERSION")));
return Ok(());
}
if args.paths.is_empty() {
std::process::exit(2);
}
let mut port: Box<dyn ReadWrite> = match &args.port {
Some(path) => {
let port_file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.with_context(|| format!("'{}'", path))?;
lrzsz2::configure_raw_mode(port_file.as_fd()).ok();
lrzsz2::configure_nonblocking(port_file.as_fd()).ok();
Box::new(port_file)
}
None => {
let stdin = std::io::stdin();
lrzsz2::configure_raw_mode(stdin.as_fd()).ok();
lrzsz2::configure_nonblocking(stdin.as_fd()).ok();
Box::new(lrzsz2::CombinedStdInOut::new())
}
};
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(())
}