1use std::fmt;
5use std::path::Path;
6use std::sync::{PoisonError, RwLock};
7
8use serde::Deserialize;
9use serde_saphyr::Budget;
10use serde_saphyr::budget::BudgetBreach;
11
12use super::{Input, InputError};
13
14pub const REPO_FILE: &str = ".ctl/input.yaml";
16
17macro_rules! limits {
18 ($( $(#[$doc:meta])* $field:ident: $ty:ty $(=> $wrap:ident)?, )*) => {
19 #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
37 #[serde(deny_unknown_fields, default)]
38 pub struct Limits {
39 $( $(#[$doc])* pub $field: Option<$ty>, )*
40 }
41
42 impl Limits {
43 pub const NONE: Self = Self { $( $field: None, )* };
45
46 #[must_use]
48 pub fn or(self, lower: &Self) -> Self {
49 Self { $( $field: self.$field.or(lower.$field), )* }
50 }
51
52 #[must_use]
54 pub fn budget(&self) -> Budget {
55 let mut budget = Budget::default();
56 $( if let Some(value) = self.$field {
57 $( let value = $wrap(value); )?
58 budget.$field = value;
59 } )*
60 budget
61 }
62
63 fn loosened(&self) -> Vec<Loosened> {
65 let default = Budget::default();
66 let mut loosened = Vec::new();
67 $( if let Some(value) = self.$field {
68 let wrapped = value;
69 $( let wrapped = $wrap(wrapped); )?
70 if wrapped.looser_than(&default.$field) {
71 loosened.push(Loosened {
72 field: stringify!($field),
73 value: value.to_string(),
74 default: default.$field.shown(),
75 });
76 }
77 } )*
78 loosened
79 }
80 }
81 };
82}
83
84limits! {
85 max_reader_input_bytes: usize => Some,
87 max_buffered_comment_events: usize,
89 simple_key_max_lookahead: usize,
91 flow_nesting_limit: usize,
93 max_events: usize,
95 max_aliases: usize,
97 max_anchors: usize,
99 max_recorded_anchor_events: usize,
101 max_recorded_anchor_bytes: usize,
103 max_property_expansion_depth: usize,
105 max_total_property_interpolation_work: usize,
107 max_depth: usize,
109 max_inclusion_depth: u32,
111 max_documents: usize,
113 max_nodes: usize,
115 max_total_scalar_bytes: usize,
117 max_total_comment_bytes: usize,
119 max_merge_keys: usize,
121 alias_anchor_min_aliases: usize,
123 alias_anchor_ratio_multiplier: usize,
125}
126
127static PROCESS: RwLock<Limits> = RwLock::new(Limits::NONE);
128static REPOSITORY: RwLock<Limits> = RwLock::new(Limits::NONE);
129
130pub fn set_default_limits(limits: Limits) {
133 *PROCESS.write().unwrap_or_else(PoisonError::into_inner) = limits;
134}
135
136pub fn use_repo_limits(root: &Path) -> Result<Vec<Loosened>, InputError> {
151 let path = root.join(REPO_FILE);
152 let limits = match std::fs::read_to_string(&path) {
153 Ok(text) => {
154 let process = PROCESS
155 .read()
156 .unwrap_or_else(PoisonError::into_inner)
157 .budget();
158 Input::new(path.display().to_string(), text)
159 .parse_within::<RepoFile>(&process, Raise::InProcess)?
160 .limits
161 }
162 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Limits::NONE,
163 Err(error) => {
164 return Err(InputError::whole(
165 path.display().to_string(),
166 error.to_string(),
167 ));
168 }
169 };
170 let loosened = limits.loosened();
171 *REPOSITORY.write().unwrap_or_else(PoisonError::into_inner) = limits;
172 Ok(loosened)
173}
174
175#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct Loosened {
178 pub field: &'static str,
180 pub value: String,
182 pub default: String,
184}
185
186impl fmt::Display for Loosened {
187 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
188 write!(
189 formatter,
190 "{REPO_FILE} raises {} to {}, above the default {}",
191 self.field, self.value, self.default
192 )
193 }
194}
195
196trait Bound {
198 fn looser_than(&self, default: &Self) -> bool;
199 fn shown(&self) -> String;
200}
201
202impl Bound for usize {
203 fn looser_than(&self, default: &Self) -> bool {
204 self > default
205 }
206
207 fn shown(&self) -> String {
208 self.to_string()
209 }
210}
211
212impl Bound for u32 {
213 fn looser_than(&self, default: &Self) -> bool {
214 self > default
215 }
216
217 fn shown(&self) -> String {
218 self.to_string()
219 }
220}
221
222impl Bound for Option<usize> {
224 fn looser_than(&self, default: &Self) -> bool {
225 match (self, default) {
226 (None, Some(_)) => true,
227 (Some(value), Some(default)) => value > default,
228 (_, None) => false,
229 }
230 }
231
232 fn shown(&self) -> String {
233 self.map_or_else(|| "none".to_owned(), |value| value.to_string())
234 }
235}
236
237#[derive(Deserialize)]
239#[serde(deny_unknown_fields)]
240struct RepoFile {
241 #[serde(default)]
242 limits: Limits,
243}
244
245pub(super) fn effective(call: &Limits) -> Limits {
247 let repository = REPOSITORY.read().unwrap_or_else(PoisonError::into_inner);
248 let process = PROCESS.read().unwrap_or_else(PoisonError::into_inner);
249 call.clone().or(&repository).or(&process)
250}
251
252#[derive(Clone, Copy)]
254pub(super) enum Raise {
255 InRepository,
257 InProcess,
260}
261
262pub(super) struct Exceeded {
264 field: &'static str,
265 limit: String,
266 raise: Raise,
267}
268
269impl Exceeded {
270 pub(super) fn input_bytes(limit: usize, raise: Raise) -> Self {
271 Self {
272 field: "max_reader_input_bytes",
273 limit: limit.to_string(),
274 raise,
275 }
276 }
277
278 #[expect(
282 clippy::wildcard_enum_match_arm,
283 reason = "BudgetBreach is non_exhaustive: a variant added later keeps serde-saphyr's wording"
284 )]
285 pub(super) fn breach(breach: &BudgetBreach, budget: &Budget, raise: Raise) -> Option<Self> {
286 let (field, limit) = match breach {
287 BudgetBreach::Events { .. } => ("max_events", budget.max_events.to_string()),
288 BudgetBreach::Aliases { .. } => ("max_aliases", budget.max_aliases.to_string()),
289 BudgetBreach::Anchors { .. } => ("max_anchors", budget.max_anchors.to_string()),
290 BudgetBreach::Depth { .. } => ("max_depth", budget.max_depth.to_string()),
291 BudgetBreach::InclusionDepth { .. } => (
292 "max_inclusion_depth",
293 budget.max_inclusion_depth.to_string(),
294 ),
295 BudgetBreach::Documents { .. } => ("max_documents", budget.max_documents.to_string()),
296 BudgetBreach::Nodes { .. } => ("max_nodes", budget.max_nodes.to_string()),
297 BudgetBreach::ScalarBytes { .. } => (
298 "max_total_scalar_bytes",
299 budget.max_total_scalar_bytes.to_string(),
300 ),
301 BudgetBreach::CommentBytes { .. } => (
302 "max_total_comment_bytes",
303 budget.max_total_comment_bytes.to_string(),
304 ),
305 BudgetBreach::MergeKeys { .. } => ("max_merge_keys", budget.max_merge_keys.to_string()),
306 BudgetBreach::AliasAnchorRatio { .. } => (
307 "alias_anchor_ratio_multiplier",
308 budget.alias_anchor_ratio_multiplier.to_string(),
309 ),
310 BudgetBreach::InputBytes { .. } => (
311 "max_reader_input_bytes",
312 budget
313 .max_reader_input_bytes
314 .map_or_else(|| "none".to_owned(), |limit| limit.to_string()),
315 ),
316 BudgetBreach::RecordedAnchorEvents { .. } => (
317 "max_recorded_anchor_events",
318 budget.max_recorded_anchor_events.to_string(),
319 ),
320 BudgetBreach::RecordedAnchorBytes { .. } => (
321 "max_recorded_anchor_bytes",
322 budget.max_recorded_anchor_bytes.to_string(),
323 ),
324 BudgetBreach::PropertyExpansionDepth { .. } => (
325 "max_property_expansion_depth",
326 budget.max_property_expansion_depth.to_string(),
327 ),
328 BudgetBreach::PropertyInterpolationWork { .. } => (
329 "max_total_property_interpolation_work",
330 budget.max_total_property_interpolation_work.to_string(),
331 ),
332 _ => return None,
333 };
334 Some(Self {
335 field,
336 limit,
337 raise,
338 })
339 }
340}
341
342impl fmt::Display for Exceeded {
343 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
344 write!(formatter, "exceeds {} ({}); ", self.field, self.limit)?;
345 match self.raise {
346 Raise::InRepository => {
347 write!(formatter, "raise limits.{} in {REPO_FILE}", self.field)
348 }
349 Raise::InProcess => write!(
350 formatter,
351 "raise {} in the tool's process default, which reads {REPO_FILE}",
352 self.field
353 ),
354 }
355 }
356}