From e92d5a8aeed68702024741c06c8a24a426dd1ea8 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sun, 21 Dec 2025 11:38:20 +0100 Subject: [PATCH] feat: add TFTP server and client binaries --- src/bin/pfs-tftp-client.rs | 145 +++++++++++++++++++++++++++++++++++++ src/main.rs | 109 +++++++++++++++++++++++++++- 2 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/bin/pfs-tftp-client.rs diff --git a/src/bin/pfs-tftp-client.rs b/src/bin/pfs-tftp-client.rs new file mode 100644 index 0000000..03cb3ed --- /dev/null +++ b/src/bin/pfs-tftp-client.rs @@ -0,0 +1,145 @@ +use std::{net::SocketAddr, path::PathBuf, time::Duration}; + +use pfs_tftp_sync::{Client, ClientConfig, Mode}; + +fn main() { + match run() { + Ok(()) => {} + Err(e) => { + eprintln!("{e}"); + std::process::exit(2); + } + } +} + +fn run() -> Result<(), String> { + let mut args = std::env::args().skip(1); + + let Some(cmd) = args.next() else { + print_usage(); + return Err("missing command".to_string()); + }; + + if cmd == "--help" || cmd == "-h" { + print_usage(); + return Ok(()); + } + + let server_str = next_value(&mut args, "")?; + let server = parse_socket_addr(&server_str)?; + + let mut cfg = ClientConfig::default(); + let mut mode = Mode::Octet; + + let (remote, local, is_get) = match cmd.as_str() { + "get" => { + let remote = next_value(&mut args, "")?; + let local = next_value(&mut args, "")?; + (remote, local, true) + } + "put" => { + let local = next_value(&mut args, "")?; + let remote = next_value(&mut args, "")?; + (remote, local, false) + } + _ => { + print_usage(); + return Err(format!("unknown command: {cmd}")); + } + }; + + while let Some(arg) = args.next() { + match arg.as_str() { + "--help" | "-h" => { + print_usage(); + return Ok(()); + } + "--mode" => { + let v = next_value(&mut args, "--mode")?; + mode = parse_mode(&v)?; + } + "--timeout-ms" => { + let v = next_value(&mut args, "--timeout-ms")?; + cfg.timeout = Duration::from_millis(parse_u64("--timeout-ms", &v)?); + } + "--retries" => { + let v = next_value(&mut args, "--retries")?; + cfg.retries = parse_u32("--retries", &v)?; + } + "--dally-timeout-ms" => { + let v = next_value(&mut args, "--dally-timeout-ms")?; + cfg.dally_timeout = Duration::from_millis(parse_u64("--dally-timeout-ms", &v)?); + } + "--dally-retries" => { + let v = next_value(&mut args, "--dally-retries")?; + cfg.dally_retries = parse_u32("--dally-retries", &v)?; + } + other => return Err(format!("unknown argument: {other}")), + } + } + + let client = Client::new(server, cfg); + if is_get { + client + .get(&remote, &PathBuf::from(local), mode) + .map_err(|e| format!("{e}"))?; + } else { + client + .put(&PathBuf::from(local), &remote, mode) + .map_err(|e| format!("{e}"))?; + } + Ok(()) +} + +fn parse_socket_addr(s: &str) -> Result { + s.parse::().map_err(|e| format!("{e}")) +} + +fn parse_mode(s: &str) -> Result { + let Some(mode) = Mode::parse_case_insensitive(s) else { + return Err(format!("unknown mode: {s} (expected netascii or octet)")); + }; + if mode == Mode::Mail { + return Err("mail mode is obsolete (RFC 1350, Section 1)".to_string()); + } + Ok(mode) +} + +fn next_value( + args: &mut impl Iterator, + what: &'static str, +) -> Result { + args.next().ok_or_else(|| format!("missing {what}")) +} + +fn parse_u64(flag: &'static str, value: &str) -> Result { + value + .parse::() + .map_err(|e| format!("{flag}: invalid value: {e}")) +} + +fn parse_u32(flag: &'static str, value: &str) -> Result { + value + .parse::() + .map_err(|e| format!("{flag}: invalid value: {e}")) +} + +fn print_usage() { + eprintln!( + "\ +pfs-tftp-client + +USAGE: + pfs-tftp-client get [OPTIONS] + pfs-tftp-client put [OPTIONS] + +OPTIONS: + --mode Transfer mode (default: octet) + --timeout-ms Packet timeout (default: 5000) + --retries Retransmit attempts (default: 5) + --dally-timeout-ms Wait time for duplicate final DATA (default: timeout) + --dally-retries Dally attempts (default: 2) + --help, -h Show this help +" + ); +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..917cb7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,110 @@ +use std::{net::SocketAddr, path::PathBuf, time::Duration}; + +use pfs_tftp_sync::{Server, ServerConfig}; + fn main() { - println!("Hello, world!"); + match run() { + Ok(()) => {} + Err(e) => { + eprintln!("{e}"); + std::process::exit(2); + } + } +} + +fn run() -> Result<(), String> { + let mut cfg = ServerConfig::default(); + + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--help" | "-h" => { + print_usage(); + return Ok(()); + } + "--bind" => { + let value = next_value(&mut args, "--bind")?; + cfg.bind = value.parse::().map_err(|e| format!("{e}"))?; + } + "--root" => { + let value = next_value(&mut args, "--root")?; + cfg.root = PathBuf::from(value); + } + "--allow-write" => cfg.allow_write = true, + "--overwrite" => cfg.overwrite = true, + "--timeout-ms" => { + let value = next_value(&mut args, "--timeout-ms")?; + cfg.timeout = Duration::from_millis(parse_u64("--timeout-ms", &value)?); + } + "--retries" => { + let value = next_value(&mut args, "--retries")?; + cfg.retries = parse_u32("--retries", &value)?; + } + "--dally-timeout-ms" => { + let value = next_value(&mut args, "--dally-timeout-ms")?; + cfg.dally_timeout = Duration::from_millis(parse_u64("--dally-timeout-ms", &value)?); + } + "--dally-retries" => { + let value = next_value(&mut args, "--dally-retries")?; + cfg.dally_retries = parse_u32("--dally-retries", &value)?; + } + other => return Err(format!("unknown argument: {other}")), + } + } + + eprintln!( + "Serving TFTP on {} (root: {}, allow_write: {}, overwrite: {})", + cfg.bind, + cfg.root.display(), + cfg.allow_write, + cfg.overwrite + ); + + Server::new(cfg) + .serve() + .map_err(|e| format!("server error: {e}"))?; + + Ok(()) +} + +fn next_value( + args: &mut impl Iterator, + flag: &'static str, +) -> Result { + args.next() + .ok_or_else(|| format!("{flag} requires a value")) +} + +fn parse_u64(flag: &'static str, value: &str) -> Result { + value + .parse::() + .map_err(|e| format!("{flag}: invalid value: {e}")) +} + +fn parse_u32(flag: &'static str, value: &str) -> Result { + value + .parse::() + .map_err(|e| format!("{flag}: invalid value: {e}")) +} + +fn print_usage() { + eprintln!( + "\ +pfs-tftp (server) + +USAGE: + pfs-tftp [OPTIONS] + +OPTIONS: + --bind Bind address (default: 0.0.0.0:6969) + --root Root directory (default: .) + --allow-write Enable WRQ (default: disabled) + --overwrite Allow overwriting existing files (default: disabled) + --timeout-ms Packet timeout (default: 5000) + --retries Retransmit attempts (default: 5) + --dally-timeout-ms Wait time for duplicate final DATA (default: timeout) + --dally-retries Dally attempts (default: 2) + --help, -h Show this help +" + ); }