use std::{
fmt,
fs::File,
io::{self, BufWriter, Read, Write},
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
path::{Path, PathBuf},
sync::{Mutex, Weak},
time::{Duration, SystemTime},
};
use crate::path::secret::map::{cleaner::CLEANER_CYCLE, Entry, Epoch};
const HEADER: &str = "s2n-quic-dc path secret map";
const VERSION: &[u8] = b"v0";
const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024;
const MAX_SERIALIZED_SIZE: u64 = MAX_FILE_SIZE - 1024 * 1024;
struct CountingWriter<W> {
inner: W,
bytes_written: u64,
}
impl<W: Write> CountingWriter<W> {
fn new(inner: W) -> Self {
CountingWriter {
inner,
bytes_written: 0,
}
}
fn bytes_written(&self) -> u64 {
self.bytes_written
}
}
impl<W: Write> Write for CountingWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.inner.write(buf)?;
self.bytes_written += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
fn new_writer(tmp_path: &Path) -> io::Result<CountingWriter<BufWriter<File>>> {
let output = std::fs::File::options()
.create(true)
.truncate(true)
.write(true)
.open(tmp_path)?;
let mut output = CountingWriter::new(std::io::BufWriter::new(output));
output.write_all(HEADER.as_bytes())?;
Ok(output)
}
fn duration_to_epochs(duration: Duration) -> u64 {
if duration.is_zero() {
return 0;
}
let cycle = CLEANER_CYCLE.as_secs_f64();
((duration.as_secs_f64() / cycle).round() as u64).max(1)
}
#[derive(Clone, Debug)]
pub struct SerializerBuilder {
path: PathBuf,
period: Option<Duration>,
max_idle_epochs: Option<u64>,
}
impl SerializerBuilder {
pub fn with_period(mut self, period: Duration) -> Self {
self.period = Some(period);
self
}
pub fn with_max_idle(mut self, duration: Duration) -> Self {
self.max_idle_epochs = Some(duration_to_epochs(duration));
self
}
pub fn build(self) -> io::Result<Serializer> {
if let Some(parent) = self.path.parent().filter(|p| !p.as_os_str().is_empty()) {
if !parent.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"serializer destination directory {} does not exist",
parent.display()
),
));
}
}
Ok(Serializer {
path: self.path,
period: self.period,
max_idle_epochs: self.max_idle_epochs,
write_lock: Mutex::new(()),
})
}
}
pub struct Serializer {
path: PathBuf,
period: Option<Duration>,
max_idle_epochs: Option<u64>,
write_lock: Mutex<()>,
}
impl fmt::Debug for Serializer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Serializer")
.field("path", &self.path)
.field("period", &self.period)
.field("max_idle_epochs", &self.max_idle_epochs)
.finish()
}
}
impl Serializer {
pub fn builder(path: impl Into<PathBuf>) -> SerializerBuilder {
SerializerBuilder {
path: path.into(),
period: None,
max_idle_epochs: None,
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn period(&self) -> Option<Duration> {
self.period
}
fn min_epoch(&self, current_epoch: Epoch) -> Option<u64> {
self.max_idle_epochs
.map(|window| current_epoch.get().saturating_sub(window))
}
pub(crate) fn serialize(
&self,
entries: &[Weak<Entry>],
current_epoch: Epoch,
) -> io::Result<SerializeStats> {
self.serialize_with_max_size(entries, current_epoch, MAX_SERIALIZED_SIZE)
}
fn serialize_with_max_size(
&self,
entries: &[Weak<Entry>],
current_epoch: Epoch,
max_size: u64,
) -> io::Result<SerializeStats> {
let _guard = self.write_lock.lock().unwrap_or_else(|e| e.into_inner());
let min_epoch = self.min_epoch(current_epoch);
let tmp_path = self.path.with_extension("tmp");
let mut output = new_writer(&tmp_path)?;
output.write_all(VERSION)?;
let started_at = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
output.write_all(&started_at.to_le_bytes())?;
let mut written = 0;
for entry in entries.iter() {
if output.bytes_written() > max_size {
break;
}
let Some(entry) = entry.upgrade() else {
continue;
};
if min_epoch.is_some_and(|min| entry.accessed_at_epoch().get() < min) {
continue;
}
written += 1;
let peer = entry.peer();
match peer {
SocketAddr::V4(addr) => {
output.write_all(&[0])?;
output.write_all(&addr.ip().octets())?;
output.write_all(&addr.port().to_le_bytes())?;
}
SocketAddr::V6(addr) => {
let minimal = addr.flowinfo() == 0 && addr.scope_id() == 0;
if minimal {
output.write_all(&[1])?;
} else {
output.write_all(&[2])?;
}
output.write_all(&addr.ip().octets())?;
output.write_all(&addr.port().to_le_bytes())?;
if !minimal {
output.write_all(&addr.flowinfo().to_le_bytes())?;
output.write_all(&addr.scope_id().to_le_bytes())?;
}
}
}
}
output.flush()?;
let file_size = output.bytes_written();
std::fs::rename(&tmp_path, &self.path)?;
Ok(SerializeStats {
entries: written,
file_size,
})
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct SerializeStats {
pub(crate) entries: usize,
pub(crate) file_size: u64,
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct DiskEntry {
pub peer: SocketAddr,
}
pub fn deserialize(path: &Path) -> io::Result<Entries> {
let file = File::open(path)?;
let len = file.metadata()?.len();
if len > MAX_FILE_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("file is {len} bytes, larger than the maximum of {MAX_FILE_SIZE} bytes"),
));
}
let mut bytes = Vec::with_capacity(len as usize);
file.take(len).read_to_end(&mut bytes)?;
let mut reader = Reader { bytes: &bytes };
if reader.take(HEADER.len())? != HEADER.as_bytes() {
return Err(invalid_data("missing or invalid header"));
}
if reader.take(VERSION.len())? != VERSION {
return Err(invalid_data("missing or unsupported version"));
}
let started_at = {
let secs = u64::from_le_bytes(reader.take_array::<8>()?);
SystemTime::UNIX_EPOCH
.checked_add(Duration::from_secs(secs))
.ok_or_else(|| invalid_data("started_at timestamp out of range"))?
};
let pos = bytes.len() - reader.bytes.len();
Ok(Entries {
bytes,
pos,
started_at,
})
}
pub struct Entries {
bytes: Vec<u8>,
pos: usize,
pub started_at: SystemTime,
}
impl Iterator for Entries {
type Item = io::Result<DiskEntry>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.bytes.len() {
return None;
}
let mut reader = Reader {
bytes: &self.bytes[self.pos..],
};
let result = read_entry(&mut reader);
self.pos = if result.is_ok() {
self.bytes.len() - reader.bytes.len()
} else {
self.bytes.len()
};
Some(result)
}
}
fn read_entry(reader: &mut Reader) -> io::Result<DiskEntry> {
let tag = reader.take(1)?[0];
let peer = match tag {
0 => {
let ip = Ipv4Addr::from(reader.take_array::<4>()?);
let port = u16::from_le_bytes(reader.take_array::<2>()?);
SocketAddr::V4(SocketAddrV4::new(ip, port))
}
1 | 2 => {
let ip = Ipv6Addr::from(reader.take_array::<16>()?);
let port = u16::from_le_bytes(reader.take_array::<2>()?);
let (flowinfo, scope_id) = if tag == 2 {
(
u32::from_le_bytes(reader.take_array::<4>()?),
u32::from_le_bytes(reader.take_array::<4>()?),
)
} else {
(0, 0)
};
SocketAddr::V6(SocketAddrV6::new(ip, port, flowinfo, scope_id))
}
other => return Err(invalid_data(format!("unknown peer tag {other}"))),
};
Ok(DiskEntry { peer })
}
struct Reader<'a> {
bytes: &'a [u8],
}
impl<'a> Reader<'a> {
fn take(&mut self, n: usize) -> io::Result<&'a [u8]> {
self.bytes
.split_off(..n)
.ok_or_else(|| invalid_data("unexpected end of file"))
}
#[expect(
clippy::unwrap_in_result,
reason = "take(N) always yields exactly N bytes"
)]
fn take_array<const N: usize>(&mut self) -> io::Result<[u8; N]> {
Ok(self
.take(N)?
.try_into()
.expect("take(N) always returns N bytes"))
}
}
fn invalid_data(msg: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg.into())
}
#[cfg(test)]
mod test;