use irtt_proto::{echo_packet_len, PacketLayout, Params, ServerFill};
const DEFAULT_FILL_PATTERN: &[u8] = b"irtt";
pub(crate) const DEFAULT_FILL_DESCRIPTOR: &str = "pattern:69727474";
const PATTERN_PREFIX: &str = "pattern:";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FillMode {
None,
Random,
Pattern(Vec<u8>),
}
impl FillMode {
pub(crate) fn default_fill() -> Self {
Self::Pattern(DEFAULT_FILL_PATTERN.to_vec())
}
fn parse(descriptor: &str) -> Option<Self> {
match descriptor {
"none" => Some(Self::None),
"rand" => Some(Self::Random),
_ => descriptor
.strip_prefix(PATTERN_PREFIX)
.and_then(parse_hex_pattern)
.map(Self::Pattern),
}
}
pub(crate) fn payload(&self, len: usize) -> Vec<u8> {
match self {
Self::None => Vec::new(),
Self::Random => random_payload(len, getrandom::fill),
Self::Pattern(pattern) => pattern.iter().copied().cycle().take(len).collect(),
}
}
}
fn parse_hex_pattern(body: &str) -> Option<Vec<u8>> {
let digits = body.as_bytes();
if digits.is_empty() || !digits.len().is_multiple_of(2) {
return None;
}
digits
.as_chunks::<2>()
.0
.iter()
.map(|pair| Some(hex_digit(pair[0])? << 4 | hex_digit(pair[1])?))
.collect()
}
fn hex_digit(digit: u8) -> Option<u8> {
match digit {
b'0'..=b'9' => Some(digit - b'0'),
b'a'..=b'f' => Some(digit - b'a' + 10),
b'A'..=b'F' => Some(digit - b'A' + 10),
_ => None,
}
}
fn random_payload<E>(len: usize, draw: impl FnOnce(&mut [u8]) -> Result<(), E>) -> Vec<u8> {
let mut payload = vec![0; len];
if len > 0 && draw(&mut payload).is_err() {
payload.fill(0);
}
payload
}
pub(crate) fn negotiate_server_fill(params: &mut Params) -> FillMode {
let Some(requested) = params
.server_fill
.as_ref()
.filter(|fill| !fill.value.is_empty())
else {
return FillMode::default_fill();
};
match FillMode::parse(&requested.value) {
Some(mode) => mode,
None => {
params.server_fill = Some(ServerFill {
value: DEFAULT_FILL_DESCRIPTOR.to_owned(),
});
FillMode::default_fill()
}
}
}
pub(crate) fn echo_payload_len(hmac: bool, params: &Params) -> usize {
let header_len = PacketLayout::echo(hmac, params).header_len();
echo_packet_len(hmac, params).map_or(0, |len| len.saturating_sub(header_len))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognized_descriptors_parse_to_their_mode() {
for (descriptor, expected) in [
("none", FillMode::None),
("rand", FillMode::Random),
("pattern:00", FillMode::Pattern(vec![0x00])),
("pattern:ff00", FillMode::Pattern(vec![0xff, 0x00])),
("pattern:AaBb", FillMode::Pattern(vec![0xaa, 0xbb])),
(
"pattern:69727474",
FillMode::Pattern(DEFAULT_FILL_PATTERN.to_vec()),
),
] {
assert_eq!(FillMode::parse(descriptor), Some(expected), "{descriptor}");
}
}
#[test]
fn unrecognized_and_malformed_descriptors_parse_to_nothing() {
for descriptor in [
"",
"bogus",
"RAND",
"None",
"Pattern:aabb",
"rand ",
"pattern:",
"pattern:f",
"pattern:abc",
"pattern:zz",
"pattern:0g",
"pattern:é",
] {
assert_eq!(FillMode::parse(descriptor), None, "{descriptor:?}");
}
}
#[test]
fn the_default_descriptor_and_the_default_pattern_agree() {
assert_eq!(
FillMode::parse(DEFAULT_FILL_DESCRIPTOR),
Some(FillMode::default_fill())
);
}
#[test]
fn a_pattern_repeats_from_its_first_byte_and_stops_at_the_region() {
let mode = FillMode::Pattern(vec![0xaa, 0xbb]);
assert_eq!(mode.payload(0), Vec::<u8>::new());
assert_eq!(mode.payload(1), vec![0xaa]);
assert_eq!(
mode.payload(7),
vec![0xaa, 0xbb, 0xaa, 0xbb, 0xaa, 0xbb, 0xaa]
);
}
#[test]
fn no_fill_leaves_the_region_to_the_encoder() {
assert!(FillMode::None.payload(16).is_empty());
}
#[test]
fn a_failed_random_draw_yields_zeroes() {
let scribble = |buffer: &mut [u8]| {
buffer.fill(0xab);
Err(())
};
assert_eq!(random_payload(6, scribble), vec![0; 6]);
}
#[test]
fn a_successful_random_draw_is_propagated_whole() {
let drawn = [1, 2, 3, 4, 5];
let source = |buffer: &mut [u8]| -> Result<(), ()> {
assert_eq!(buffer.len(), drawn.len(), "the whole region is offered");
buffer.copy_from_slice(&drawn);
Ok(())
};
assert_eq!(random_payload(drawn.len(), source), drawn);
}
#[test]
fn an_empty_region_never_reaches_the_random_source() {
let source = |_: &mut [u8]| -> Result<(), ()> { panic!("no bytes were needed") };
assert!(random_payload(0, source).is_empty());
}
}