ppt_rs/core/
validation.rs1use crate::exc::{messages, PptxError, Result};
4use std::collections::HashSet;
5
6pub type RequiredPart = (&'static str, &'static str);
8
9pub const REQUIRED_PARTS_MINIMAL: &[&'static str] = &[
11 "[Content_Types].xml",
12 "_rels/.rels",
13 "ppt/presentation.xml",
14 "docProps/core.xml",
15];
16
17pub const REQUIRED_PARTS_REPAIR: &[RequiredPart] = &[
19 ("[Content_Types].xml", "Content types definition"),
20 ("_rels/.rels", "Package relationships"),
21 ("ppt/presentation.xml", "Presentation document"),
22 (
23 "ppt/_rels/presentation.xml.rels",
24 "Presentation relationships",
25 ),
26];
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum ValidationIssue {
31 MissingPart {
32 path: String,
33 description: Option<String>,
34 },
35 EmptyXml {
36 path: String,
37 },
38 InvalidXml {
39 path: String,
40 error: String,
41 },
42}
43
44impl ValidationIssue {
45 pub fn message(&self) -> String {
46 match self {
47 ValidationIssue::MissingPart { path, description } => match description {
48 Some(desc) => format!("Missing required part '{path}' ({desc})"),
49 None => messages::missing_part(path),
50 },
51 ValidationIssue::EmptyXml { path } => messages::empty_xml(path),
52 ValidationIssue::InvalidXml { path, error } => messages::invalid_xml(path, error),
53 }
54 }
55}
56
57pub fn clamp_ratio(value: f64) -> f64 {
59 value.clamp(0.0, 1.0)
60}
61
62pub fn clamp_unit_interval(value: f64) -> f64 {
64 value.clamp(0.0, 1.0)
65}
66
67pub fn validate_non_empty_str(value: &str, field: &str) -> Result<()> {
69 if value.trim().is_empty() {
70 return Err(PptxError::InvalidValue(messages::must_not_be_empty(field)));
71 }
72 Ok(())
73}
74
75pub fn validate_non_empty<T>(items: &[T], field: &str) -> Result<()> {
77 if items.is_empty() {
78 return Err(PptxError::InvalidState(messages::must_not_be_empty(field)));
79 }
80 Ok(())
81}
82
83pub fn validate_index(index: usize, count: usize, field: &str) -> Result<()> {
85 if index >= count {
86 return Err(PptxError::NotFound(messages::index_out_of_range(
87 field, index, count,
88 )));
89 }
90 Ok(())
91}
92
93pub fn validate_positive(value: u32, field: &str) -> Result<()> {
95 if value == 0 {
96 return Err(PptxError::InvalidValue(messages::must_be_positive(field)));
97 }
98 Ok(())
99}
100
101pub fn validate_well_formed_xml(xml: &str) -> Result<()> {
103 let trimmed = xml.trim();
104 if trimmed.is_empty() {
105 return Err(PptxError::InvalidXml(messages::empty_xml_content()));
106 }
107
108 let mut in_tag = false;
109 let mut in_string = false;
110 let mut string_char = '"';
111
112 for ch in trimmed.chars() {
113 match ch {
114 '"' | '\'' if in_tag && !in_string => {
115 in_string = true;
116 string_char = ch;
117 }
118 c if in_string && c == string_char => {
119 in_string = false;
120 }
121 '<' if !in_string => {
122 in_tag = true;
123 }
124 '>' if !in_string => {
125 in_tag = false;
126 }
127 _ => {}
128 }
129 }
130
131 Ok(())
132}
133
134pub fn check_required_parts(
136 found: &HashSet<String>,
137 required: &[&str],
138) -> Vec<ValidationIssue> {
139 required
140 .iter()
141 .filter(|path| !found.contains(**path))
142 .map(|path| ValidationIssue::MissingPart {
143 path: (*path).to_string(),
144 description: None,
145 })
146 .collect()
147}
148
149pub fn check_required_parts_with_descriptions(
151 has_part: impl Fn(&str) -> bool,
152 required: &[RequiredPart],
153) -> Vec<ValidationIssue> {
154 required
155 .iter()
156 .filter(|(path, _)| !has_part(path))
157 .map(|(path, description)| ValidationIssue::MissingPart {
158 path: (*path).to_string(),
159 description: Some((*description).to_string()),
160 })
161 .collect()
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_clamp_ratio() {
170 assert_eq!(clamp_ratio(0.5), 0.5);
171 assert_eq!(clamp_ratio(-1.0), 0.0);
172 assert_eq!(clamp_ratio(2.0), 1.0);
173 }
174
175 #[test]
176 fn test_validate_non_empty_str() {
177 assert!(validate_non_empty_str("hello", "title").is_ok());
178 assert!(validate_non_empty_str(" ", "title").is_err());
179 }
180
181 #[test]
182 fn test_validate_index() {
183 assert!(validate_index(0, 3, "slide").is_ok());
184 assert!(validate_index(3, 3, "slide").is_err());
185 }
186
187 #[test]
188 fn test_validate_well_formed_xml() {
189 assert!(validate_well_formed_xml("<root/>").is_ok());
190 assert!(validate_well_formed_xml("").is_err());
191 assert!(validate_well_formed_xml(" ").is_err());
192 }
193
194 #[test]
195 fn test_check_required_parts() {
196 let found: HashSet<_> = ["a.xml", "b.xml"].into_iter().map(str::to_string).collect();
197 let issues = check_required_parts(&found, &["a.xml", "c.xml"]);
198 assert_eq!(issues.len(), 1);
199 assert_eq!(issues[0].message(), "Missing required part: c.xml");
200 }
201}