feat: implement TFTP client and server binaries

Add command-line programs for TFTP operations:

tftpd - TFTP server daemon:
- Configurable root directory for serving files
- Optional write support with --writable flag
- Optional file overwriting with --overwrite flag
- Custom port binding (default: 69)
- Path traversal protection for security

tftp - TFTP client:
- get command for downloading files
- put command for uploading files
- Support for both octet and netascii modes
- Verbose mode for debugging

Also adds comprehensive integration tests that verify client-server
communication with real network I/O.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-21 13:19:21 +01:00
parent fd8dace0dc
commit f4aa70ca62
8 changed files with 824 additions and 34 deletions

View File

@@ -17,5 +17,5 @@ mod packet;
mod state;
pub use error::{Error, ErrorCode, Result};
pub use packet::{Mode, Opcode, Packet, MAX_DATA_SIZE, TFTP_PORT};
pub use packet::{MAX_DATA_SIZE, Mode, Opcode, Packet, TFTP_PORT};
pub use state::{ClientState, Event, ServerState, TransferDirection};

View File

@@ -244,12 +244,11 @@ impl Packet {
.position(|&b| b == 0)
.ok_or(Error::MissingNullTerminator { field: "filename" })?;
let filename = std::str::from_utf8(&payload[..filename_end]).map_err(|e| {
Error::InvalidUtf8 {
let filename =
std::str::from_utf8(&payload[..filename_end]).map_err(|e| Error::InvalidUtf8 {
field: "filename",
source: e,
}
})?;
})?;
// Find the mode string after the filename null terminator
let mode_start = filename_end + 1;
@@ -263,11 +262,12 @@ impl Packet {
.ok_or(Error::MissingNullTerminator { field: "mode" })?
+ mode_start;
let mode_str =
std::str::from_utf8(&payload[mode_start..mode_end]).map_err(|e| Error::InvalidUtf8 {
let mode_str = std::str::from_utf8(&payload[mode_start..mode_end]).map_err(|e| {
Error::InvalidUtf8 {
field: "mode",
source: e,
})?;
}
})?;
let mode = Mode::parse(mode_str)?;
@@ -594,10 +594,7 @@ mod tests {
#[test]
fn test_invalid_mode() {
assert!(matches!(
Mode::parse("binary"),
Err(Error::InvalidMode(_))
));
assert!(matches!(Mode::parse("binary"), Err(Error::InvalidMode(_))));
assert!(matches!(Mode::parse("mail"), Err(Error::InvalidMode(_))));
}

View File

@@ -25,7 +25,7 @@
//! ... (continues until final DATA with < 512 bytes)
//! ```
use crate::{Error, Mode, Packet, Result, MAX_DATA_SIZE};
use crate::{Error, MAX_DATA_SIZE, Mode, Packet, Result};
/// Direction of data transfer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]