use std::cell::Cell;
use std::path::Path;
use crate::error::{Par2Error, Result};
use crate::types::{CancellationToken, MAX_FILES_PER_SET};
use super::Packet;
pub const MAX_RECOVERY_EXPONENT: u32 = 65_535;
pub const RECOVERY_EXPONENT_DOMAIN: usize = MAX_RECOVERY_EXPONENT as usize + 1;
pub const DEFAULT_MAX_RETAINED_PACKETS: usize =
2 + 2 * MAX_FILES_PER_SET + RECOVERY_EXPONENT_DOMAIN;
pub const DEFAULT_INVENTORY_REDUNDANCY: u64 = 32;
pub const DEFAULT_MAX_EXAMINED_PACKETS: u64 =
DEFAULT_MAX_RETAINED_PACKETS as u64 * DEFAULT_INVENTORY_REDUNDANCY;
pub const DEFAULT_MAX_RETAINED_METADATA_BYTES: usize = 128 * 1024 * 1024;
const RETAINED_SLOT_OVERHEAD_BYTES: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PacketScanLimits {
pub max_retained_packets: usize,
pub max_examined_packets: u64,
pub max_retained_metadata_bytes: usize,
}
impl Default for PacketScanLimits {
fn default() -> Self {
Self {
max_retained_packets: DEFAULT_MAX_RETAINED_PACKETS,
max_examined_packets: DEFAULT_MAX_EXAMINED_PACKETS,
max_retained_metadata_bytes: DEFAULT_MAX_RETAINED_METADATA_BYTES,
}
}
}
impl PacketScanLimits {
pub fn with_max_retained_packets(mut self, packets: usize) -> Self {
self.max_retained_packets = packets;
self
}
pub fn with_max_examined_packets(mut self, packets: u64) -> Self {
self.max_examined_packets = packets;
self
}
pub fn with_max_retained_metadata_bytes(mut self, bytes: usize) -> Self {
self.max_retained_metadata_bytes = bytes;
self
}
}
pub struct PacketScanBudget {
limits: PacketScanLimits,
cancel: Option<CancellationToken>,
examined: Cell<u64>,
retained_packets: Cell<usize>,
retained_bytes: Cell<usize>,
}
impl std::fmt::Debug for PacketScanBudget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PacketScanBudget")
.field("limits", &self.limits)
.field("cancellable", &self.cancel.is_some())
.field("examined", &self.examined.get())
.field("retained_packets", &self.retained_packets.get())
.field("retained_bytes", &self.retained_bytes.get())
.finish()
}
}
impl PacketScanBudget {
pub fn new(limits: PacketScanLimits) -> Self {
Self {
limits,
cancel: None,
examined: Cell::new(0),
retained_packets: Cell::new(0),
retained_bytes: Cell::new(0),
}
}
pub fn with_cancellation(limits: PacketScanLimits, cancel: Option<CancellationToken>) -> Self {
Self {
cancel,
..Self::new(limits)
}
}
pub fn limits(&self) -> PacketScanLimits {
self.limits
}
pub fn examined(&self) -> u64 {
self.examined.get()
}
pub fn retained_packets(&self) -> usize {
self.retained_packets.get()
}
pub fn retained_bytes(&self) -> usize {
self.retained_bytes.get()
}
pub fn check_cancelled(&self) -> Result<()> {
if self
.cancel
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
{
return Err(Par2Error::Cancelled);
}
Ok(())
}
pub fn charge_examined(&self) -> Result<()> {
self.check_cancelled()?;
let examined = self
.examined
.get()
.checked_add(1)
.ok_or_else(|| self.limit_error("examined packet count overflows"))?;
if examined > self.limits.max_examined_packets {
return Err(self.limit_error(&format!(
"examined {examined} PAR2 packets across the inventory, limit is {}",
self.limits.max_examined_packets
)));
}
self.examined.set(examined);
Ok(())
}
pub fn charge_bytes(&self, bytes: usize) -> Result<()> {
self.check_cancelled()?;
let retained = self
.retained_bytes
.get()
.checked_add(bytes)
.ok_or_else(|| self.limit_error("retained metadata byte count overflows"))?;
if retained > self.limits.max_retained_metadata_bytes {
return Err(self.limit_error(&format!(
"retained {retained} bytes of PAR2 packet metadata, limit is {}",
self.limits.max_retained_metadata_bytes
)));
}
self.retained_bytes.set(retained);
Ok(())
}
pub fn charge_retained(&self, bytes: usize) -> Result<()> {
let packets = self
.retained_packets
.get()
.checked_add(1)
.ok_or_else(|| self.limit_error("retained packet count overflows"))?;
if packets > self.limits.max_retained_packets {
return Err(self.limit_error(&format!(
"retained {packets} PAR2 packets, limit is {}",
self.limits.max_retained_packets
)));
}
self.charge_bytes(bytes)?;
self.retained_packets.set(packets);
Ok(())
}
pub fn release_bytes(&self, bytes: usize) {
self.retained_bytes
.set(self.retained_bytes.get().saturating_sub(bytes));
}
pub fn release_retained(&self, bytes: usize) {
self.retained_packets
.set(self.retained_packets.get().saturating_sub(1));
self.release_bytes(bytes);
}
fn limit_error(&self, reason: &str) -> Par2Error {
Par2Error::ResourceLimitExceeded {
reason: reason.to_string(),
}
}
}
pub(crate) fn reserve_fallible<T>(vec: &mut Vec<T>, additional: usize) -> Result<()> {
vec.try_reserve(additional)
.map_err(|error| Par2Error::ResourceLimitExceeded {
reason: format!("could not allocate room for {additional} more PAR2 packets: {error}"),
})
}
pub(crate) fn interned_path_bytes(path: &Path) -> usize {
path.as_os_str().len() + 2 * size_of::<usize>()
}
pub(crate) fn packet_retained_bytes(packet: &Packet) -> usize {
let contents = match packet {
Packet::Main(main) => size_of::<super::MainPacket>().saturating_add(
main.recovery_file_ids
.len()
.saturating_add(main.non_recovery_file_ids.len())
.saturating_mul(size_of::<crate::types::FileId>()),
),
Packet::FileDescription(desc) => size_of::<super::FileDescriptionPacket>()
.saturating_add(desc.filename.len())
.saturating_add(desc.par2_name.len()),
Packet::InputFileSliceChecksum(ifsc) => size_of::<super::IfscPacket>().saturating_add(
ifsc.checksums
.len()
.saturating_mul(size_of::<crate::types::SliceChecksum>()),
),
Packet::RecoverySlice(_) => size_of::<super::RecoverySlicePacket>(),
Packet::Creator(creator) => {
size_of::<super::CreatorPacket>().saturating_add(creator.creator_id.len())
}
Packet::Unknown { body, .. } => size_of::<Packet>().saturating_add(body.len()),
};
contents.saturating_add(RETAINED_SLOT_OVERHEAD_BYTES)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_limits_match_their_documented_derivation() {
let limits = PacketScanLimits::default();
assert_eq!(limits.max_retained_packets, 131_074);
assert_eq!(limits.max_examined_packets, 4_194_368);
assert_eq!(limits.max_retained_metadata_bytes, 128 * 1024 * 1024);
}
#[test]
fn examined_meter_refuses_the_packet_past_the_limit() {
let budget =
PacketScanBudget::new(PacketScanLimits::default().with_max_examined_packets(2));
budget.charge_examined().unwrap();
budget.charge_examined().unwrap();
let error = budget.charge_examined().unwrap_err();
assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
assert_eq!(budget.examined(), 2);
}
#[test]
fn retained_charge_is_all_or_nothing_across_both_meters() {
let budget = PacketScanBudget::new(
PacketScanLimits::default()
.with_max_retained_packets(4)
.with_max_retained_metadata_bytes(100),
);
budget.charge_retained(60).unwrap();
let error = budget.charge_retained(60).unwrap_err();
assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
assert_eq!(budget.retained_packets(), 1);
assert_eq!(budget.retained_bytes(), 60);
}
#[test]
fn released_charges_return_to_both_meters() {
let budget = PacketScanBudget::new(PacketScanLimits::default());
budget.charge_retained(128).unwrap();
budget.release_retained(128);
assert_eq!(budget.retained_packets(), 0);
assert_eq!(budget.retained_bytes(), 0);
budget.release_retained(128);
assert_eq!(budget.retained_packets(), 0);
assert_eq!(budget.retained_bytes(), 0);
}
#[test]
fn byte_meter_rejects_overflow_rather_than_wrapping() {
let budget = PacketScanBudget::new(
PacketScanLimits::default().with_max_retained_metadata_bytes(usize::MAX),
);
budget.charge_bytes(usize::MAX).unwrap();
let error = budget.charge_bytes(1).unwrap_err();
assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
assert_eq!(budget.retained_bytes(), usize::MAX);
}
#[test]
fn examined_meter_rejects_overflow_rather_than_wrapping() {
let budget =
PacketScanBudget::new(PacketScanLimits::default().with_max_examined_packets(u64::MAX));
budget.examined.set(u64::MAX);
let error = budget.charge_examined().unwrap_err();
assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
}
#[test]
fn a_refused_allocation_is_an_error_not_an_abort() {
let mut packets = Vec::<Packet>::new();
let error = reserve_fallible(&mut packets, usize::MAX).unwrap_err();
assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
assert!(packets.is_empty());
let mut wide = Vec::<[u8; 4096]>::new();
let error = reserve_fallible(&mut wide, usize::MAX / 4096).unwrap_err();
assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
reserve_fallible(&mut packets, 4).unwrap();
assert!(packets.capacity() >= 4);
}
#[test]
fn every_charge_point_observes_cancellation() {
let cancel = CancellationToken::new();
let budget =
PacketScanBudget::with_cancellation(PacketScanLimits::default(), Some(cancel.clone()));
budget.charge_examined().unwrap();
cancel.cancel();
assert!(matches!(
budget.charge_examined(),
Err(Par2Error::Cancelled)
));
assert!(matches!(budget.charge_bytes(1), Err(Par2Error::Cancelled)));
assert!(matches!(
budget.charge_retained(1),
Err(Par2Error::Cancelled)
));
assert!(matches!(
budget.check_cancelled(),
Err(Par2Error::Cancelled)
));
}
}