use crate::error::{Error, Result};
use crate::model::Database;
pub const DEFAULT_SOURCE_BYTES_MAX: usize = 512 * 1024 * 1024;
pub const DEFAULT_ZONE_COUNT_MAX: usize = 1_000_000;
pub const DEFAULT_RULE_COUNT_MAX: usize = 1_000_000;
pub const DEFAULT_LINK_COUNT_MAX: usize = 1_000_000;
pub const DEFAULT_LEAP_COUNT_MAX: usize = 100_000;
pub const DEFAULT_LINK_CHAIN_DEPTH_MAX: usize = 256;
pub const DEFAULT_ZONE_ERA_COUNT_MAX: usize = 100_000;
#[derive(Debug, Clone, Copy)]
pub struct ResourceLimits {
pub source_bytes_max: usize,
pub zone_count_max: usize,
pub rule_count_max: usize,
pub link_count_max: usize,
pub leap_count_max: usize,
pub link_chain_depth_max: usize,
pub zone_era_count_max: usize,
}
impl Default for ResourceLimits {
fn default() -> Self {
ResourceLimits {
source_bytes_max: DEFAULT_SOURCE_BYTES_MAX,
zone_count_max: DEFAULT_ZONE_COUNT_MAX,
rule_count_max: DEFAULT_RULE_COUNT_MAX,
link_count_max: DEFAULT_LINK_COUNT_MAX,
leap_count_max: DEFAULT_LEAP_COUNT_MAX,
link_chain_depth_max: DEFAULT_LINK_CHAIN_DEPTH_MAX,
zone_era_count_max: DEFAULT_ZONE_ERA_COUNT_MAX,
}
}
}
impl ResourceLimits {
pub fn check_source_bytes(&self, len: usize, path: &std::path::Path) -> Result<()> {
if len > self.source_bytes_max {
return Err(Error::config(format!(
"source file {} is {len} bytes, exceeding the zic-rs resource limit of {} bytes",
path.display(),
self.source_bytes_max
)));
}
Ok(())
}
pub fn check_leap_count(&self, n: usize) -> Result<()> {
if n > self.leap_count_max {
return Err(Error::config(format!(
"leap-second table has {n} entries, exceeding the zic-rs resource limit of {}",
self.leap_count_max
)));
}
Ok(())
}
pub fn enforce(&self, db: &Database) -> Result<()> {
if db.zones.len() > self.zone_count_max {
return Err(Error::config(format!(
"zone count {} exceeds the zic-rs resource limit of {}",
db.zones.len(),
self.zone_count_max
)));
}
if db.links.len() > self.link_count_max {
return Err(Error::config(format!(
"link count {} exceeds the zic-rs resource limit of {}",
db.links.len(),
self.link_count_max
)));
}
for (name, set) in &db.rules {
if set.len() > self.rule_count_max {
return Err(Error::config(format!(
"rule set {name:?} has {} rows, exceeding the zic-rs resource limit of {}",
set.len(),
self.rule_count_max
)));
}
}
for z in &db.zones {
if z.eras.len() > self.zone_era_count_max {
return Err(Error::config(format!(
"zone {:?} has {} continuation eras, exceeding the zic-rs resource limit of {}",
z.name,
z.eras.len(),
self.zone_era_count_max
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::time::Offset;
use crate::model::{Database, LinkRecord, Origin, ZoneEra, ZoneRecord, ZoneRules};
fn origin() -> Origin {
Origin::new(std::path::Path::new("test.zi"), 1)
}
fn tiny() -> ResourceLimits {
ResourceLimits {
source_bytes_max: 10,
zone_count_max: 1,
rule_count_max: 1,
link_count_max: 1,
leap_count_max: 1,
link_chain_depth_max: 4,
zone_era_count_max: 1,
}
}
fn zone(name: &str) -> ZoneRecord {
ZoneRecord {
name: name.to_string(),
eras: Vec::new(),
origin: origin(),
}
}
fn link(from: &str, to: &str) -> LinkRecord {
LinkRecord {
target: to.to_string(),
link_name: from.to_string(),
origin: origin(),
}
}
#[test]
fn source_bytes_cap_rejects_oversize() {
let l = tiny();
assert!(l
.check_source_bytes(11, std::path::Path::new("big.zi"))
.is_err());
assert!(l
.check_source_bytes(10, std::path::Path::new("ok.zi"))
.is_ok());
}
#[test]
fn leap_count_cap_rejects_overflow() {
let l = tiny();
assert!(l.check_leap_count(2).is_err());
assert!(l.check_leap_count(1).is_ok());
}
#[test]
fn zone_and_link_count_caps() {
let l = tiny();
let mut db = Database::default();
db.zones.push(zone("A"));
db.links.push(link("L", "A"));
assert!(l.enforce(&db).is_ok(), "one each is within the cap");
db.zones.push(zone("B"));
let err = l.enforce(&db).unwrap_err();
assert!(err.to_string().contains("zone count 2 exceeds"));
}
fn era() -> ZoneEra {
ZoneEra {
stdoff: Offset(0),
rules: ZoneRules::None,
format: String::new(),
until: None,
origin: origin(),
}
}
#[test]
fn era_count_cap() {
let l = tiny();
let mut db = Database::default();
let mut z = zone("A");
z.eras.push(era());
db.zones.push(z.clone());
assert!(l.enforce(&db).is_ok(), "one era is within the cap");
db.zones[0].eras.push(era());
let err = l.enforce(&db).unwrap_err();
assert!(
err.to_string().contains("continuation eras"),
"expected an era-count breach, got: {err}"
);
}
#[test]
fn link_chain_depth_cap_bounds_long_acyclic_chains() {
let mut db = Database::default();
for i in 0..400 {
db.links
.push(link(&format!("L{i}"), &format!("L{}", i + 1)));
}
let err = crate::resolve_link_target(&db, "L0").unwrap_err();
assert!(
err.to_string().contains("depth limit"),
"expected a link-chain depth-limit error, got: {err}"
);
}
#[test]
fn defaults_pass_a_realistic_database() {
let l = ResourceLimits::default();
let mut db = Database::default();
for i in 0..500 {
db.zones.push(zone(&format!("Zone/{i}")));
db.links
.push(link(&format!("Alias/{i}"), &format!("Zone/{i}")));
}
assert!(l.enforce(&db).is_ok());
}
}