1use crate::error::{Error, Result};
16use crate::model::Database;
17
18pub const DEFAULT_SOURCE_BYTES_MAX: usize = 512 * 1024 * 1024;
20pub const DEFAULT_ZONE_COUNT_MAX: usize = 1_000_000;
22pub const DEFAULT_RULE_COUNT_MAX: usize = 1_000_000;
24pub const DEFAULT_LINK_COUNT_MAX: usize = 1_000_000;
26pub const DEFAULT_LEAP_COUNT_MAX: usize = 100_000;
28pub const DEFAULT_LINK_CHAIN_DEPTH_MAX: usize = 256;
31pub const DEFAULT_ZONE_ERA_COUNT_MAX: usize = 100_000;
33
34#[derive(Debug, Clone, Copy)]
38pub struct ResourceLimits {
39 pub source_bytes_max: usize,
41 pub zone_count_max: usize,
43 pub rule_count_max: usize,
45 pub link_count_max: usize,
47 pub leap_count_max: usize,
49 pub link_chain_depth_max: usize,
51 pub zone_era_count_max: usize,
53}
54
55impl Default for ResourceLimits {
56 fn default() -> Self {
57 ResourceLimits {
58 source_bytes_max: DEFAULT_SOURCE_BYTES_MAX,
59 zone_count_max: DEFAULT_ZONE_COUNT_MAX,
60 rule_count_max: DEFAULT_RULE_COUNT_MAX,
61 link_count_max: DEFAULT_LINK_COUNT_MAX,
62 leap_count_max: DEFAULT_LEAP_COUNT_MAX,
63 link_chain_depth_max: DEFAULT_LINK_CHAIN_DEPTH_MAX,
64 zone_era_count_max: DEFAULT_ZONE_ERA_COUNT_MAX,
65 }
66 }
67}
68
69impl ResourceLimits {
70 pub fn check_source_bytes(&self, len: usize, path: &std::path::Path) -> Result<()> {
73 if len > self.source_bytes_max {
74 return Err(Error::config(format!(
75 "source file {} is {len} bytes, exceeding the zic-rs resource limit of {} bytes",
76 path.display(),
77 self.source_bytes_max
78 )));
79 }
80 Ok(())
81 }
82
83 pub fn check_leap_count(&self, n: usize) -> Result<()> {
85 if n > self.leap_count_max {
86 return Err(Error::config(format!(
87 "leap-second table has {n} entries, exceeding the zic-rs resource limit of {}",
88 self.leap_count_max
89 )));
90 }
91 Ok(())
92 }
93
94 pub fn enforce(&self, db: &Database) -> Result<()> {
97 if db.zones.len() > self.zone_count_max {
98 return Err(Error::config(format!(
99 "zone count {} exceeds the zic-rs resource limit of {}",
100 db.zones.len(),
101 self.zone_count_max
102 )));
103 }
104 if db.links.len() > self.link_count_max {
105 return Err(Error::config(format!(
106 "link count {} exceeds the zic-rs resource limit of {}",
107 db.links.len(),
108 self.link_count_max
109 )));
110 }
111 for (name, set) in &db.rules {
112 if set.len() > self.rule_count_max {
113 return Err(Error::config(format!(
114 "rule set {name:?} has {} rows, exceeding the zic-rs resource limit of {}",
115 set.len(),
116 self.rule_count_max
117 )));
118 }
119 }
120 for z in &db.zones {
121 if z.eras.len() > self.zone_era_count_max {
122 return Err(Error::config(format!(
123 "zone {:?} has {} continuation eras, exceeding the zic-rs resource limit of {}",
124 z.name,
125 z.eras.len(),
126 self.zone_era_count_max
127 )));
128 }
129 }
130 Ok(())
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::model::time::Offset;
138 use crate::model::{Database, LinkRecord, Origin, ZoneEra, ZoneRecord, ZoneRules};
139
140 fn origin() -> Origin {
141 Origin::new(std::path::Path::new("test.zi"), 1)
142 }
143
144 fn tiny() -> ResourceLimits {
145 ResourceLimits {
146 source_bytes_max: 10,
147 zone_count_max: 1,
148 rule_count_max: 1,
149 link_count_max: 1,
150 leap_count_max: 1,
151 link_chain_depth_max: 4,
152 zone_era_count_max: 1,
153 }
154 }
155
156 fn zone(name: &str) -> ZoneRecord {
157 ZoneRecord {
158 name: name.to_string(),
159 eras: Vec::new(),
160 origin: origin(),
161 }
162 }
163
164 fn link(from: &str, to: &str) -> LinkRecord {
165 LinkRecord {
166 target: to.to_string(),
167 link_name: from.to_string(),
168 origin: origin(),
169 }
170 }
171
172 #[test]
173 fn source_bytes_cap_rejects_oversize() {
174 let l = tiny();
175 assert!(l
176 .check_source_bytes(11, std::path::Path::new("big.zi"))
177 .is_err());
178 assert!(l
179 .check_source_bytes(10, std::path::Path::new("ok.zi"))
180 .is_ok());
181 }
182
183 #[test]
184 fn leap_count_cap_rejects_overflow() {
185 let l = tiny();
186 assert!(l.check_leap_count(2).is_err());
187 assert!(l.check_leap_count(1).is_ok());
188 }
189
190 #[test]
191 fn zone_and_link_count_caps() {
192 let l = tiny();
193 let mut db = Database::default();
194 db.zones.push(zone("A"));
195 db.links.push(link("L", "A"));
196 assert!(l.enforce(&db).is_ok(), "one each is within the cap");
197 db.zones.push(zone("B"));
198 let err = l.enforce(&db).unwrap_err();
199 assert!(err.to_string().contains("zone count 2 exceeds"));
200 }
201
202 fn era() -> ZoneEra {
203 ZoneEra {
204 stdoff: Offset(0),
205 rules: ZoneRules::None,
206 format: String::new(),
207 until: None,
208 origin: origin(),
209 }
210 }
211
212 #[test]
213 fn era_count_cap() {
214 let l = tiny();
217 let mut db = Database::default();
218 let mut z = zone("A");
219 z.eras.push(era());
220 db.zones.push(z.clone());
221 assert!(l.enforce(&db).is_ok(), "one era is within the cap");
222 db.zones[0].eras.push(era());
223 let err = l.enforce(&db).unwrap_err();
224 assert!(
225 err.to_string().contains("continuation eras"),
226 "expected an era-count breach, got: {err}"
227 );
228 }
229
230 #[test]
231 fn link_chain_depth_cap_bounds_long_acyclic_chains() {
232 let mut db = Database::default();
235 for i in 0..400 {
236 db.links
237 .push(link(&format!("L{i}"), &format!("L{}", i + 1)));
238 }
239 let err = crate::resolve_link_target(&db, "L0").unwrap_err();
240 assert!(
241 err.to_string().contains("depth limit"),
242 "expected a link-chain depth-limit error, got: {err}"
243 );
244 }
245
246 #[test]
247 fn defaults_pass_a_realistic_database() {
248 let l = ResourceLimits::default();
250 let mut db = Database::default();
251 for i in 0..500 {
252 db.zones.push(zone(&format!("Zone/{i}")));
253 db.links
254 .push(link(&format!("Alias/{i}"), &format!("Zone/{i}")));
255 }
256 assert!(l.enforce(&db).is_ok());
257 }
258}