use crate::crypto::adaptive_crypto::AEAD_OVERHEAD;
use crate::transport::types::{PacketHeader, WireError};
use std::time::Duration;
pub const MAX_SHAPED_WIRE: usize = 1184;
pub const PAD_LEN_FIELD: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "bindings", derive(uniffi::Enum))]
pub enum PaddingPolicy {
#[default]
None,
Padme,
}
pub fn padme(l: usize) -> usize {
if l <= 2 {
return l;
}
let e = l.ilog2() as usize;
let s = if e == 0 { 1 } else { e.ilog2() as usize + 1 };
let mask_bits = e.saturating_sub(s);
let mask = (1usize << mask_bits) - 1;
(l + mask) & !mask
}
pub fn padding_trailer_len(plaintext_len: usize, policy: PaddingPolicy) -> usize {
match policy {
PaddingPolicy::None => 0,
PaddingPolicy::Padme => {
let wire = PacketHeader::SIZE + plaintext_len + AEAD_OVERHEAD;
let target = padme(wire + PAD_LEN_FIELD).min(MAX_SHAPED_WIRE);
if target >= wire + PAD_LEN_FIELD {
target - wire
} else {
0
}
}
}
}
pub fn append_padding(plaintext: &mut Vec<u8>, trailer: usize) {
if trailer < PAD_LEN_FIELD {
return;
}
let pad_n = (trailer - PAD_LEN_FIELD) as u16;
plaintext.resize(plaintext.len() + pad_n as usize, 0);
plaintext.extend_from_slice(&pad_n.to_be_bytes());
}
pub fn strip_padding(plaintext: &[u8]) -> Result<&[u8], WireError> {
if plaintext.len() < PAD_LEN_FIELD {
return Err(WireError::Truncated);
}
let split = plaintext.len() - PAD_LEN_FIELD;
let pad_n = u16::from_be_bytes([plaintext[split], plaintext[split + 1]]) as usize;
let inner_len = split.checked_sub(pad_n).ok_or(WireError::Truncated)?;
Ok(&plaintext[..inner_len])
}
pub fn random_jitter(max_ms: u32) -> Duration {
if max_ms == 0 {
return Duration::ZERO;
}
use rand::Rng;
let ms = rand::thread_rng().gen_range(0..=max_ms);
Duration::from_millis(ms as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn padme_rounds_to_expected_buckets() {
assert_eq!(padme(0), 0);
assert_eq!(padme(1), 1);
assert_eq!(padme(2), 2);
assert_eq!(padme(3), 3); assert_eq!(padme(63), 64); assert_eq!(padme(64), 64); assert_eq!(padme(100), 104); assert_eq!(padme(1000), 1024); }
#[test]
fn padme_is_monotone_idempotent_and_bounded() {
let mut prev = 0;
for l in 0..4096usize {
let p = padme(l);
assert!(p >= l, "padme never shrinks: padme({l})={p}");
assert!(p >= prev, "padme is monotone: padme({l})={p} < prev {prev}");
assert_eq!(padme(p), p, "padme is idempotent at the bucket {p}");
if l >= 8 {
assert!(
(p - l) * 100 <= l * 12,
"overhead bound: padme({l})={p} exceeds ~12%"
);
}
prev = p;
}
}
#[test]
fn append_then_strip_is_identity() {
for inner_len in [0usize, 1, 15, 16, 17, 100, 500, 1100] {
let inner: Vec<u8> = (0..inner_len).map(|i| (i % 251) as u8).collect();
let trailer = padding_trailer_len(inner_len, PaddingPolicy::Padme);
let mut padded = inner.clone();
append_padding(&mut padded, trailer);
let wire = PacketHeader::SIZE + padded.len() + AEAD_OVERHEAD;
if trailer > 0 {
assert!(wire <= MAX_SHAPED_WIRE);
assert_eq!(
wire,
padme(PacketHeader::SIZE + inner_len + AEAD_OVERHEAD + PAD_LEN_FIELD)
.min(MAX_SHAPED_WIRE)
);
}
let recovered = strip_padding(&padded).expect("strip");
assert_eq!(recovered, &inner[..], "strip(append(x)) == x");
}
}
#[test]
fn policy_none_and_mtu_cap() {
assert_eq!(padding_trailer_len(500, PaddingPolicy::None), 0);
let big = MAX_SHAPED_WIRE; assert_eq!(padding_trailer_len(big, PaddingPolicy::Padme), 0);
}
#[test]
fn random_jitter_is_bounded_and_varies() {
assert_eq!(random_jitter(0), Duration::ZERO, "0 disables jitter");
let max = 15u32;
let cap = Duration::from_millis(max as u64);
let mut saw_nonzero = false;
for _ in 0..1000 {
let d = random_jitter(max);
assert!(d <= cap, "jitter {d:?} exceeds the {max}ms ceiling");
if d > Duration::ZERO {
saw_nonzero = true;
}
}
assert!(
saw_nonzero,
"jitter must actually add delay (not always zero)"
);
}
#[test]
fn strip_rejects_malformed_trailer() {
let bad = vec![0u8, 0u8, 0xFF, 0xFF];
assert_eq!(strip_padding(&bad), Err(WireError::Truncated));
assert_eq!(strip_padding(&[0u8]), Err(WireError::Truncated));
}
}