liarsping 0.1.0

A ping server which attempts to manipulate the ping times seen by the client
Documentation
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use crate::config::SignedDuration;

/// Linux ping payload layout:
///   bytes 0..8   : little-endian u64 seconds
///   bytes 8..12  : little-endian u32 microseconds
///
/// Returns a copy of `payload` with the embedded timestamp shifted by `by`.
/// Positive `by` advances the timestamp forward (sender's `now - embedded`
/// shrinks). Negative `by` retreats it (sender's RTT grows).
/// If `payload.len() < 12`, returns the payload unchanged.
/// Carry from microseconds into seconds is handled; the positive path
/// saturates seconds at `u64::MAX`, the negative path saturates at zero.
pub fn shave_signed(payload: &[u8], by: SignedDuration) -> Vec<u8> {
    if payload.len() < 12 {
        return payload.to_vec();
    }
    let mut out = payload.to_vec();
    let secs = u64::from_le_bytes(out[0..8].try_into().unwrap());
    let micros = u32::from_le_bytes(out[8..12].try_into().unwrap()) as u64;

    let total_micros = secs.saturating_mul(1_000_000).saturating_add(micros);
    let by_micros = by.magnitude.as_secs().saturating_mul(1_000_000)
        .saturating_add(by.magnitude.subsec_micros() as u64);

    let new_total = if by.negative {
        total_micros.saturating_sub(by_micros)
    } else {
        total_micros.saturating_add(by_micros)
    };

    let new_secs = new_total / 1_000_000;
    let new_micros = (new_total % 1_000_000) as u32;

    out[0..8].copy_from_slice(&new_secs.to_le_bytes());
    out[8..12].copy_from_slice(&new_micros.to_le_bytes());
    out
}

/// Parse the Linux-ping timestamp prefix (bytes 0..12) back into a `SystemTime`.
/// Inverse of the write path in `shave_signed`: layout is `u64` little-endian
/// seconds at bytes 0..8, `u32` little-endian microseconds at bytes 8..12.
///
/// Returns `None` if the payload is shorter than 12 bytes, or (unreachably in
/// practice) if the encoded timestamp overflows `SystemTime`'s platform range.
pub fn read_timestamp(payload: &[u8]) -> Option<SystemTime> {
    if payload.len() < 12 {
        return None;
    }
    let secs = u64::from_le_bytes(payload[0..8].try_into().unwrap());
    let micros = u32::from_le_bytes(payload[8..12].try_into().unwrap());
    let dur = Duration::from_secs(secs).checked_add(Duration::from_micros(micros as u64))?;
    UNIX_EPOCH.checked_add(dur)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::SignedDuration;

    fn pack(secs: u64, micros: u32, tail: &[u8]) -> Vec<u8> {
        let mut v = Vec::with_capacity(12 + tail.len());
        v.extend_from_slice(&secs.to_le_bytes());
        v.extend_from_slice(&micros.to_le_bytes());
        v.extend_from_slice(tail);
        v
    }

    fn unpack(payload: &[u8]) -> (u64, u32) {
        let mut s = [0u8; 8];
        s.copy_from_slice(&payload[0..8]);
        let mut m = [0u8; 4];
        m.copy_from_slice(&payload[8..12]);
        (u64::from_le_bytes(s), u32::from_le_bytes(m))
    }

    fn pos(d: Duration) -> SignedDuration { SignedDuration::from_duration(d) }
    fn neg(d: Duration) -> SignedDuration { SignedDuration { negative: true, magnitude: d } }

    #[test]
    fn advances_micros_without_carry() {
        let p = pack(1_000, 500_000, b"rest");
        let out = shave_signed(&p, pos(Duration::from_micros(20)));
        assert_eq!(unpack(&out), (1_000, 500_020));
        assert_eq!(&out[12..], b"rest", "tail must be preserved");
    }

    #[test]
    fn advances_micros_with_carry_into_seconds() {
        let p = pack(1_000, 999_900, b"");
        let out = shave_signed(&p, pos(Duration::from_micros(200)));
        assert_eq!(unpack(&out), (1_001, 100));
    }

    #[test]
    fn advances_whole_seconds() {
        let p = pack(1_000, 250, b"");
        let out = shave_signed(&p, pos(Duration::from_secs(3)));
        assert_eq!(unpack(&out), (1_003, 250));
    }

    #[test]
    fn short_payload_unchanged() {
        let p = b"short".to_vec();
        let out = shave_signed(&p, pos(Duration::from_millis(10)));
        assert_eq!(out, p);
    }

    #[test]
    fn short_payload_unchanged_negative() {
        let p = b"short".to_vec();
        let out = shave_signed(&p, neg(Duration::from_millis(10)));
        assert_eq!(out, p);
    }

    #[test]
    fn retreats_micros_without_underflow() {
        let p = pack(1_000, 500_000, b"rest");
        let out = shave_signed(&p, neg(Duration::from_micros(20)));
        assert_eq!(unpack(&out), (1_000, 499_980));
        assert_eq!(&out[12..], b"rest");
    }

    #[test]
    fn retreats_across_seconds_boundary() {
        let p = pack(1_000, 100, b"");
        let out = shave_signed(&p, neg(Duration::from_micros(200)));
        // 1_000 s 100 us - 200 us = 999 s 999_900 us
        assert_eq!(unpack(&out), (999, 999_900));
    }

    #[test]
    fn retreats_saturate_to_zero() {
        let p = pack(0, 100, b"");
        let out = shave_signed(&p, neg(Duration::from_secs(10)));
        assert_eq!(unpack(&out), (0, 0));
    }

    #[test]
    fn read_timestamp_happy_path() {
        let p = pack(1_800_000_000, 250_000, b"rest");
        let t = read_timestamp(&p).unwrap();
        let d = t.duration_since(UNIX_EPOCH).unwrap();
        assert_eq!(d.as_secs(), 1_800_000_000);
        assert_eq!(d.subsec_micros(), 250_000);
    }

    #[test]
    fn read_timestamp_short_payload_is_none() {
        assert!(read_timestamp(b"short").is_none());
    }
}