1use serde::{Deserialize, Serialize};
8
9use crate::error::TemplateReloadError;
10
11#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
24#[serde(rename_all = "kebab-case", deny_unknown_fields)]
25pub struct ExternalTemplateLimitsConfig {
26 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub max_total_source_bytes: Option<usize>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub max_include_count: Option<u32>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub max_include_depth: Option<u32>,
39
40 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub max_template_size: Option<usize>,
44
45 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub reload_timeout_ms: Option<u64>,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct ResolvedExternalTemplateLimits {
55 pub max_total_source_bytes: usize,
56 pub max_include_count: u32,
57 pub max_include_depth: u32,
58 pub max_template_size: usize,
59 pub reload_timeout_ms: u64,
60}
61
62impl ExternalTemplateLimitsConfig {
63 pub fn resolve(&self) -> Result<ResolvedExternalTemplateLimits, TemplateReloadError> {
67 if let Some(0) = self.max_total_source_bytes {
68 return Err(TemplateReloadError::BoundExceeded(
69 "zero value not permitted",
70 ));
71 }
72 if let Some(0) = self.max_include_count {
73 return Err(TemplateReloadError::BoundExceeded(
74 "zero value not permitted",
75 ));
76 }
77 if let Some(0) = self.max_include_depth {
78 return Err(TemplateReloadError::BoundExceeded(
79 "zero value not permitted",
80 ));
81 }
82 if let Some(0) = self.max_template_size {
83 return Err(TemplateReloadError::BoundExceeded(
84 "zero value not permitted",
85 ));
86 }
87 if let Some(0) = self.reload_timeout_ms {
88 return Err(TemplateReloadError::BoundExceeded(
89 "zero value not permitted",
90 ));
91 }
92
93 Ok(ResolvedExternalTemplateLimits {
94 max_total_source_bytes: self.max_total_source_bytes.unwrap_or(16 * 1024 * 1024),
95 max_include_count: self.max_include_count.unwrap_or(64),
96 max_include_depth: self.max_include_depth.unwrap_or(16),
97 max_template_size: self.max_template_size.unwrap_or(1024 * 1024),
98 reload_timeout_ms: self.reload_timeout_ms.unwrap_or(5000),
99 })
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn limits_resolve_defaults() {
109 let cfg = ExternalTemplateLimitsConfig::default();
110 let resolved = cfg.resolve().expect("default config resolves");
111 assert_eq!(resolved.max_total_source_bytes, 16 * 1024 * 1024);
112 assert_eq!(resolved.max_include_count, 64);
113 assert_eq!(resolved.max_include_depth, 16);
114 assert_eq!(resolved.max_template_size, 1024 * 1024);
115 assert_eq!(resolved.reload_timeout_ms, 5000);
116 }
117
118 #[test]
119 fn limits_reject_zero() {
120 let cfg = ExternalTemplateLimitsConfig {
121 max_include_count: Some(0),
122 ..Default::default()
123 };
124 let err = cfg.resolve().expect_err("zero bound must be rejected");
125 assert!(matches!(err, TemplateReloadError::BoundExceeded(_)));
126 }
127
128 #[test]
129 fn deny_unknown_fields() {
130 let toml = "max-include-count = 3\nbogus = 1\n";
131 let result: Result<ExternalTemplateLimitsConfig, _> = toml::from_str(toml);
132 assert!(result.is_err(), "deny_unknown_fields must reject `bogus`");
133 }
134}