Skip to content

Commit

Permalink
Merge pull request #1 from fusion-engineering/hayley/rfc-2347
Browse files Browse the repository at this point in the history
add support for RFC-2347 (options)
  • Loading branch information
HayleyDeckers authored Jan 16, 2023
2 parents 53adb9c + 2b02aa2 commit 9661448
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 5 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ It aims to be easy to use / read.
### RFCs

- [1350 - The TFTP Protocol (Revision 2)](https://www.rfc-editor.org/rfc/rfc1350)
- [2347 - TFTP Option Extension](https://www.rfc-editor.org/rfc/inline-errata/rfc2347.html)
73 changes: 68 additions & 5 deletions src/packet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub enum OpCode {
Data = 3,
Acknowledgement = 4,
Error = 5,
OptionAck = 6,
}

impl TryFrom<u16> for OpCode {
Expand All @@ -19,6 +20,7 @@ impl TryFrom<u16> for OpCode {
3 => Ok(Self::Data),
4 => Ok(Self::Acknowledgement),
5 => Ok(Self::Error),
6 => Ok(Self::OptionAck),
e => Err(TftpError::InvalidOpcode(e)),
}
}
Expand Down Expand Up @@ -66,7 +68,7 @@ impl std::fmt::Display for ErrorCode {
pub struct Request<'a> {
is_read: bool,
pub filename: &'a str,
//mode is always octet
//only the octet mode is supported so it isn't stored here
}

//want to use a DST here but transmuting between bytes and DST-fat pointers is undefined.
Expand All @@ -89,12 +91,18 @@ pub struct Error<'a> {
pub message: &'a str,
}

#[derive(Debug)]
pub struct OptionAck<'a> {
pub unknown_options: Vec<(&'a str, &'a str)>,
}

#[derive(Debug)]
pub enum Packet<'a> {
Data(Data<'a>),
Request(Request<'a>),
Error(Error<'a>),
Ack(Ack),
OptionAck(OptionAck<'a>),
}

impl<'a> Packet<'a> {
Expand Down Expand Up @@ -131,6 +139,7 @@ impl<'a> Packet<'a> {
OpCode::Data => Self::Data(Data::from_bytes_skip_opcode_check(data)),
OpCode::Acknowledgement => Self::Ack(Ack::from_bytes_skip_opcode_check(data)),
OpCode::Error => Self::Error(Error::from_bytes_skip_opcode_check(data)),
OpCode::OptionAck => Self::OptionAck(OptionAck::from_bytes_skip_opcode_check(data)),
})
}

Expand All @@ -141,6 +150,7 @@ impl<'a> Packet<'a> {
Self::Error(_) => OpCode::Error,
Self::Request(Request { is_read: true, .. }) => OpCode::ReadRequest,
Self::Request(Request { is_read: false, .. }) => OpCode::WriteRequest,
Self::OptionAck(_) => OpCode::OptionAck,
}
}

Expand All @@ -150,6 +160,7 @@ impl<'a> Packet<'a> {
Self::Data(x) => x.to_bytes(data),
Self::Error(x) => x.to_bytes(data),
Self::Request(x) => x.to_bytes(data),
Self::OptionAck(x) => x.to_bytes(data),
}
}
}
Expand Down Expand Up @@ -264,17 +275,32 @@ impl<'a> Data<'a> {
// The data is supposed to be netascii, a really outdated format. no good reason to limit ourselves to it aside from "the standard says so"
// and this function will usually be called on data generated by a remote host, which may not be compliant itself
// and instead send utf-8 or 'normal' ascii.
fn printable_ascii_str_from_u8(data: &[u8]) -> &str {
fn printable_ascii_str_from_u8(data: &[u8]) -> (&str, &[u8]) {
let first_non_ascii = data.into_iter().position(|&n| n < 32 || n > 127);
if let Some(index) = first_non_ascii {
if data[index] == 0 {
return unsafe { std::str::from_utf8_unchecked(&data[..index]) };
return unsafe {
(
std::str::from_utf8_unchecked(&data[..index]),
&data[(index + 1).min(data.len())..],
)
};
}
}
//todo: bubble error instead of panic.
panic!("invalid data, does not contain a null-terminated ascii string");
}

fn get_option_pair(data: &[u8]) -> Option<((&str, &str), &[u8])> {
if data.len() == 0 {
None
} else {
let (name, data) = printable_ascii_str_from_u8(data);
let (value, data) = printable_ascii_str_from_u8(data);
Some(((name, value), data))
}
}

impl<'a> Request<'a> {
pub fn new_read_request(filename: &'a str) -> Self {
Self {
Expand Down Expand Up @@ -302,10 +328,13 @@ impl<'a> Request<'a> {
}

fn from_bytes_skip_opcode_check(data: &'a [u8], is_read: bool) -> Self {
let (filename, data) = printable_ascii_str_from_u8(&data[2..]);
let mode = printable_ascii_str_from_u8(data).0;
assert!(mode.eq_ignore_ascii_case("octet"));
Self {
//todo: handle modes too.
is_read,
filename: printable_ascii_str_from_u8(&data[2..]),
filename,
}
}

Expand Down Expand Up @@ -389,7 +418,7 @@ impl<'a> Error<'a> {
let error_code = ErrorCode::possibly_invalid(u16::from_be_bytes([data[2], data[3]]));
Self {
error_code,
message: printable_ascii_str_from_u8(&data[4..]),
message: printable_ascii_str_from_u8(&data[4..]).0,
}
}
pub fn to_bytes(&self, buf: &'a mut [u8]) -> Result<usize, TftpError> {
Expand All @@ -404,3 +433,37 @@ impl<'a> Error<'a> {
Ok(n_bytes)
}
}

impl<'a> OptionAck<'a> {
pub fn new() -> Self {
//can't _construct_ an option ack with unknown fields because the server wouldn't know how to handle them.
Self {
unknown_options: Vec::with_capacity(0),
}
}
pub fn from_bytes(data: &'a [u8]) -> Self {
let opcode = u16::from_be_bytes([data[0], data[1]]);
assert_eq!(opcode, OpCode::OptionAck as u16);
Self::from_bytes_skip_opcode_check(data)
}
fn from_bytes_skip_opcode_check(data: &'a [u8]) -> Self {
let mut data = &data[2..];
let mut vec = Vec::with_capacity(0);
while let Some((option, remainder)) = get_option_pair(data) {
vec.push(option);
data = remainder;
}
Self {
unknown_options: vec,
}
}

pub fn to_bytes(&self, buf: &'a mut [u8]) -> Result<usize, TftpError> {
let n_bytes = 2; //ignore unknown options here as we shouldn't send them if we don't know them.
if n_bytes > buf.len() {
return Err(TftpError::BufferTooSmall);
}
buf[0..2].copy_from_slice(&(OpCode::OptionAck as u16).to_be_bytes());
Ok(n_bytes)
}
}

0 comments on commit 9661448

Please sign in to comment.