1use crate::template::{
9 LocalizedTemplateSpec, TemplateComponent, TemplateVariant, TemplateVariants,
10};
11use crate::version::{MAX_TEMPLATE_COMPONENTS, MAX_TEMPLATE_NESTING_DEPTH};
12use crate::{BibliographySpec, CitationSpec, ResolutionError};
13
14use super::Style;
15
16#[cfg(test)]
17use crate::template::TemplateGroup;
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum SchemaWarning {
22 UnknownTypeName {
28 name: String,
30 location: String,
32 },
33}
34
35impl std::fmt::Display for SchemaWarning {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 SchemaWarning::UnknownTypeName { name, location } => {
39 write!(
40 f,
41 "unknown reference type \"{name}\" in {location} \
42 (will silently match nothing; check for typos)"
43 )
44 }
45 }
46 }
47}
48
49impl Style {
50 pub fn validate_resource_limits(&self) -> Result<(), String> {
57 let mut budget = TemplateResourceBudget::default();
58
59 if let Some(templates) = &self.templates {
60 for (name, template) in templates {
61 budget.check_template(template, &format!("templates.{name}"), 0)?;
62 }
63 }
64 if let Some(citation) = &self.citation {
65 budget.check_citation_spec(citation, "citation", 0)?;
66 }
67 if let Some(bibliography) = &self.bibliography {
68 budget.check_bibliography_spec(bibliography, "bibliography", 0)?;
69 }
70
71 Ok(())
72 }
73
74 pub fn validate(&self) -> Vec<SchemaWarning> {
82 let mut warnings = Vec::new();
83 self.collect_type_selector_warnings(&mut warnings);
84 warnings
85 }
86
87 fn collect_type_selector_warnings(&self, warnings: &mut Vec<SchemaWarning>) {
89 if let Some(bib) = &self.bibliography
90 && let Some(type_variants) = &bib.type_variants
91 {
92 for selector in type_variants.keys() {
93 for name in selector.unknown_type_names() {
94 warnings.push(SchemaWarning::UnknownTypeName {
95 name: name.to_string(),
96 location: "bibliography.type-variants".to_string(),
97 });
98 }
99 }
100 }
101 if let Some(cit) = &self.citation {
102 collect_citation_spec_warnings(cit, "citation", warnings);
103 }
104 }
105
106 pub(crate) fn validate_profile_shape(&self) -> Result<(), ResolutionError> {
107 if self.templates.is_some() || yaml_path_present(self.raw_yaml.as_ref(), &["templates"]) {
108 return Err(ResolutionError::InvalidProfileOverride {
109 location: "templates".to_string(),
110 });
111 }
112
113 if let Some(location) = forbidden_profile_template_path(self.raw_yaml.as_ref()) {
114 return Err(ResolutionError::InvalidProfileOverride { location });
115 }
116
117 Ok(())
118 }
119}
120
121fn forbidden_profile_template_path(raw_yaml: Option<&serde_yaml::Value>) -> Option<String> {
122 let raw_yaml = raw_yaml?;
123 for (section, recursive) in [("citation", true), ("bibliography", false)] {
124 if let Some(section_value) = mapping_child(raw_yaml, section) {
125 if recursive {
126 if let Some(location) = forbidden_citation_template_path(section_value, section) {
127 return Some(location);
128 }
129 } else if let Some(location) = forbidden_section_template_path(section_value, section) {
130 return Some(location);
131 }
132 }
133 }
134 None
135}
136
137fn forbidden_section_template_path(section: &serde_yaml::Value, location: &str) -> Option<String> {
138 for key in ["template", "template-ref", "type-variants", "locales"] {
139 if mapping_child(section, key).is_some() {
140 return Some(format!("{location}.{key}"));
141 }
142 }
143 None
144}
145
146fn forbidden_citation_template_path(section: &serde_yaml::Value, location: &str) -> Option<String> {
147 if let Some(location) = forbidden_section_template_path(section, location) {
148 return Some(location);
149 }
150
151 for sub_section in ["integral", "non-integral", "subsequent", "ibid"] {
152 if let Some(child) = mapping_child(section, sub_section)
153 && let Some(location) =
154 forbidden_citation_template_path(child, &format!("{location}.{sub_section}"))
155 {
156 return Some(location);
157 }
158 }
159 None
160}
161
162fn mapping_child<'a>(value: &'a serde_yaml::Value, segment: &str) -> Option<&'a serde_yaml::Value> {
163 let serde_yaml::Value::Mapping(map) = value else {
164 return None;
165 };
166 let key = serde_yaml::Value::String(segment.to_string());
167 map.get(&key)
168}
169
170fn yaml_path_present(value: Option<&serde_yaml::Value>, path: &[&str]) -> bool {
171 let Some(mut current) = value else {
172 return false;
173 };
174 for segment in path {
175 let Some(next) = mapping_child(current, segment) else {
176 return false;
177 };
178 current = next;
179 }
180 true
181}
182
183fn collect_citation_spec_warnings(
185 spec: &CitationSpec,
186 location: &str,
187 warnings: &mut Vec<SchemaWarning>,
188) {
189 if let Some(type_variants) = &spec.type_variants {
190 for selector in type_variants.keys() {
191 for name in selector.unknown_type_names() {
192 warnings.push(SchemaWarning::UnknownTypeName {
193 name: name.to_string(),
194 location: format!("{location}.type-variants"),
195 });
196 }
197 }
198 }
199 for (sub_name, sub_spec) in [
201 ("integral", spec.integral.as_deref()),
202 ("non-integral", spec.non_integral.as_deref()),
203 ("subsequent", spec.subsequent.as_deref()),
204 ("ibid", spec.ibid.as_deref()),
205 ]
206 .into_iter()
207 .filter_map(|(n, s)| s.map(|s| (n, s)))
208 {
209 collect_citation_spec_warnings(sub_spec, &format!("{location}.{sub_name}"), warnings);
210 }
211}
212
213#[derive(Default)]
214struct TemplateResourceBudget {
215 component_count: usize,
216}
217
218impl TemplateResourceBudget {
219 fn check_template(
220 &mut self,
221 template: &[TemplateComponent],
222 location: &str,
223 depth: usize,
224 ) -> Result<(), String> {
225 if depth > MAX_TEMPLATE_NESTING_DEPTH {
226 return Err(format!(
227 "{location} exceeds maximum template nesting depth of {MAX_TEMPLATE_NESTING_DEPTH}"
228 ));
229 }
230 for component in template {
231 self.check_component(component, location, depth)?;
232 }
233 Ok(())
234 }
235
236 fn check_component(
237 &mut self,
238 component: &TemplateComponent,
239 location: &str,
240 depth: usize,
241 ) -> Result<(), String> {
242 self.component_count = self.component_count.saturating_add(1);
243 if self.component_count > MAX_TEMPLATE_COMPONENTS {
244 return Err(format!(
245 "style exceeds maximum template component count of {MAX_TEMPLATE_COMPONENTS}"
246 ));
247 }
248
249 match component {
250 TemplateComponent::Date(date) => {
251 if let Some(fallback) = &date.fallback {
252 self.check_template(fallback, &format!("{location}.date.fallback"), depth + 1)?;
253 }
254 }
255 TemplateComponent::Group(group) => {
256 if let Some(cond) = &group.render_when {
257 match (&cond.field_present, &cond.field_absent) {
258 (None, None) => {
259 return Err(format!(
260 "{location}.group.render-when: must set field-present or field-absent"
261 ));
262 }
263 (Some(present), Some(absent)) if present == absent => {
264 return Err(format!(
265 "{location}.group.render-when: field-present and field-absent must not be the same field ({present:?})"
266 ));
267 }
268 _ => {}
269 }
270 }
271 self.check_template(&group.group, &format!("{location}.group"), depth + 1)?;
272 }
273 TemplateComponent::Message(message) => {
274 for (name, source) in &message.args {
275 if let Some(component) = source.as_template_component() {
276 self.check_component(
277 &component,
278 &format!("{location}.message.args.{name}"),
279 depth + 1,
280 )?;
281 }
282 }
283 }
284 TemplateComponent::Contributor(_)
285 | TemplateComponent::Title(_)
286 | TemplateComponent::Number(_)
287 | TemplateComponent::Variable(_)
288 | TemplateComponent::Term(_)
289 | TemplateComponent::TypeLabel(_) => {}
290 }
291
292 Ok(())
293 }
294
295 fn check_variant(
296 &mut self,
297 variant: &TemplateVariant,
298 location: &str,
299 depth: usize,
300 ) -> Result<(), String> {
301 match variant {
302 TemplateVariant::Full(template) => self.check_template(template, location, depth),
303 TemplateVariant::Diff(diff) => {
304 for (index, add) in diff.add.iter().enumerate() {
305 self.check_component(
306 &add.component,
307 &format!("{location}.add[{index}].component"),
308 depth,
309 )?;
310 }
311 Ok(())
312 }
313 }
314 }
315
316 fn check_variants(
317 &mut self,
318 variants: &TemplateVariants,
319 location: &str,
320 depth: usize,
321 ) -> Result<(), String> {
322 for (selector, variant) in variants {
323 self.check_variant(variant, &format!("{location}.{selector:?}"), depth)?;
324 }
325 Ok(())
326 }
327
328 fn check_locales(
329 &mut self,
330 locales: &[LocalizedTemplateSpec],
331 location: &str,
332 depth: usize,
333 ) -> Result<(), String> {
334 for (index, locale) in locales.iter().enumerate() {
335 self.check_template(
336 &locale.template,
337 &format!("{location}[{index}].template"),
338 depth,
339 )?;
340 }
341 Ok(())
342 }
343
344 fn check_citation_spec(
345 &mut self,
346 spec: &CitationSpec,
347 location: &str,
348 depth: usize,
349 ) -> Result<(), String> {
350 if let Some(template) = &spec.template {
351 self.check_template(template, &format!("{location}.template"), depth)?;
352 }
353 if let Some(locales) = &spec.locales {
354 self.check_locales(locales, &format!("{location}.locales"), depth)?;
355 }
356 if let Some(variants) = &spec.type_variants {
357 self.check_variants(variants, &format!("{location}.type-variants"), depth)?;
358 }
359 for (sub_name, sub_spec) in [
360 ("integral", spec.integral.as_deref()),
361 ("non-integral", spec.non_integral.as_deref()),
362 ("subsequent", spec.subsequent.as_deref()),
363 ("ibid", spec.ibid.as_deref()),
364 ]
365 .into_iter()
366 .filter_map(|(n, s)| s.map(|s| (n, s)))
367 {
368 self.check_citation_spec(sub_spec, &format!("{location}.{sub_name}"), depth + 1)?;
369 }
370 Ok(())
371 }
372
373 fn check_bibliography_spec(
374 &mut self,
375 spec: &BibliographySpec,
376 location: &str,
377 depth: usize,
378 ) -> Result<(), String> {
379 if let Some(template) = &spec.template {
380 self.check_template(template, &format!("{location}.template"), depth)?;
381 }
382 if let Some(locales) = &spec.locales {
383 self.check_locales(locales, &format!("{location}.locales"), depth)?;
384 }
385 if let Some(variants) = &spec.type_variants {
386 self.check_variants(variants, &format!("{location}.type-variants"), depth)?;
387 }
388 Ok(())
389 }
390}
391
392#[cfg(test)]
393#[allow(
394 clippy::unwrap_used,
395 clippy::expect_used,
396 clippy::panic,
397 clippy::indexing_slicing,
398 clippy::todo,
399 clippy::unimplemented,
400 clippy::unreachable,
401 clippy::get_unwrap,
402 reason = "Panicking is acceptable and often desired in tests."
403)]
404mod security_resource_tests {
405 use super::*;
406
407 fn nested_group(depth: usize) -> TemplateComponent {
408 if depth == 0 {
409 TemplateComponent::default()
410 } else {
411 TemplateComponent::Group(TemplateGroup {
412 group: vec![nested_group(depth - 1)],
413 ..TemplateGroup::default()
414 })
415 }
416 }
417
418 #[test]
419 fn validate_resource_limits_rejects_deeply_nested_templates() {
420 let style = Style {
421 bibliography: Some(BibliographySpec {
422 template: Some(vec![nested_group(MAX_TEMPLATE_NESTING_DEPTH + 1)]),
423 ..BibliographySpec::default()
424 }),
425 ..Style::default()
426 };
427
428 let err = style
429 .validate_resource_limits()
430 .expect_err("deep template must be rejected");
431
432 assert!(err.contains("maximum template nesting depth"));
433 }
434
435 #[test]
436 fn validate_resource_limits_rejects_too_many_components() {
437 let style = Style {
438 bibliography: Some(BibliographySpec {
439 template: Some(vec![
440 TemplateComponent::default();
441 MAX_TEMPLATE_COMPONENTS + 1
442 ]),
443 ..BibliographySpec::default()
444 }),
445 ..Style::default()
446 };
447
448 let err = style
449 .validate_resource_limits()
450 .expect_err("oversized template must be rejected");
451
452 assert!(err.contains("maximum template component count"));
453 }
454}