ghostscope_compiler/script/
format_validator.rs1use crate::script::ast::Expr;
7use crate::script::parser::ParseError;
8
9pub struct FormatValidator;
10
11impl FormatValidator {
12 pub fn validate_format_arguments(format: &str, args: &[Expr]) -> Result<(), ParseError> {
14 let (placeholders, star_extras) = Self::count_required_args(format)?;
15 let required_args = placeholders + star_extras;
16
17 if required_args != args.len() {
18 let args_len = args.len();
19 return Err(ParseError::TypeError(format!(
20 "Format string '{format}' expects {required_args} argument(s) but received {args_len} argument(s)"
21 )));
22 }
23
24 Ok(())
28 }
29
30 fn count_required_args(format: &str) -> Result<(usize, usize), ParseError> {
36 let mut placeholders = 0usize;
37 let mut star_extras = 0usize;
38 let mut chars = format.chars().peekable();
39
40 while let Some(ch) = chars.next() {
41 match ch {
42 '{' => {
43 if chars.peek() == Some(&'{') {
44 chars.next(); } else {
46 let mut found_closing = false;
48 let mut placeholder_content = String::new();
49
50 for inner_ch in chars.by_ref() {
51 if inner_ch == '}' {
52 found_closing = true;
53 break;
54 }
55 placeholder_content.push(inner_ch);
56 }
57
58 if !found_closing {
59 return Err(ParseError::InvalidExpression);
60 }
61
62 if placeholder_content.is_empty() {
65 placeholders += 1;
66 } else {
67 if !placeholder_content.starts_with(':') {
69 return Err(ParseError::TypeError(format!(
70 "Invalid format specifier '{{{placeholder_content}}}': expected ':' prefix"
71 )));
72 }
73 let tail = &placeholder_content[1..];
75 let mut iter = tail.chars();
77 let conv = iter.next().ok_or_else(|| {
78 ParseError::TypeError("Empty format after ':'".to_string())
79 })?;
80 match conv {
81 'x' | 'X' | 'p' | 's' => {}
82 _ => {
83 return Err(ParseError::TypeError(format!(
84 "Unsupported format conversion '{{:{conv}}}'"
85 )));
86 }
87 }
88 let rest: String = iter.collect();
90 if rest.is_empty() {
91 } else if let Some(rem) = rest.strip_prefix('.') {
93 if rem == "*" {
94 star_extras += 1; } else if let Some(name) = rem.strip_suffix('$') {
96 let mut chars = name.chars();
98 let valid = if let Some(first) = chars.next() {
99 (first.is_ascii_alphabetic() || first == '_')
100 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
101 } else {
102 false
103 };
104 if !valid {
105 return Err(ParseError::TypeError(format!(
106 "Invalid capture variable in specifier '{{:{conv}.{rem}}}'"
107 )));
108 }
109 } else if rem.chars().all(|c| c.is_ascii_digit())
110 || (rem.starts_with("0x")
111 && rem.len() > 2
112 && rem[2..].chars().all(|c| c.is_ascii_hexdigit()))
113 || (rem.starts_with("0o")
114 && rem.len() > 2
115 && rem[2..].chars().all(|c| matches!(c, '0'..='7')))
116 || (rem.starts_with("0b")
117 && rem.len() > 2
118 && rem[2..].chars().all(|c| matches!(c, '0' | '1')))
119 {
120 } else {
122 return Err(ParseError::TypeError(format!(
123 "Invalid length in specifier '{{:{conv}{rest}}}'"
124 )));
125 }
126 } else {
127 return Err(ParseError::TypeError(format!(
128 "Invalid specifier syntax '{{:{conv}{rest}}}'"
129 )));
130 }
131 placeholders += 1;
132 }
133 }
134 }
135 '}' => {
136 if chars.peek() == Some(&'}') {
137 chars.next(); } else {
139 return Err(ParseError::InvalidExpression); }
141 }
142 _ => {}
143 }
144 }
145
146 Ok((placeholders, star_extras))
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::script::ast::Expr;
154
155 #[test]
156 fn test_count_placeholders() -> Result<(), ParseError> {
157 assert_eq!(FormatValidator::count_required_args("hello world")?, (0, 0));
159 assert_eq!(FormatValidator::count_required_args("hello {}")?, (1, 0));
160 assert_eq!(FormatValidator::count_required_args("{} {}")?, (2, 0));
161 assert_eq!(
162 FormatValidator::count_required_args("pid: {}, name: {}")?,
163 (2, 0)
164 );
165
166 assert_eq!(
168 FormatValidator::count_required_args("use {{}} for braces")?,
169 (0, 0)
170 );
171 assert_eq!(
172 FormatValidator::count_required_args("value: {}, braces: {{}}")?,
173 (1, 0)
174 );
175
176 assert!(FormatValidator::count_required_args("unclosed {").is_err());
178 assert!(FormatValidator::count_required_args("unmatched }").is_err());
179
180 assert_eq!(FormatValidator::count_required_args("{:x}")?, (1, 0));
182 assert_eq!(FormatValidator::count_required_args("{:X}")?, (1, 0));
183 assert_eq!(FormatValidator::count_required_args("{:p}")?, (1, 0));
184 assert_eq!(FormatValidator::count_required_args("{:s}")?, (1, 0));
185 assert_eq!(FormatValidator::count_required_args("{:x.16}")?, (1, 0));
186 assert_eq!(FormatValidator::count_required_args("{:s.*}")?, (1, 1));
187 assert_eq!(FormatValidator::count_required_args("{:x.len$}")?, (1, 0));
188 assert_eq!(FormatValidator::count_required_args("{:x.0x10}")?, (1, 0));
190 assert_eq!(FormatValidator::count_required_args("{:s.0o20}")?, (1, 0));
191 assert_eq!(FormatValidator::count_required_args("{:X.0b1000}")?, (1, 0));
192 assert!(FormatValidator::count_required_args("{:x.1a$}").is_err());
193 Ok(())
194 }
195
196 #[test]
197 fn test_validate_format_arguments() {
198 let args_empty: Vec<Expr> = vec![];
199 let args_one = vec![Expr::Variable("pid".to_string())];
200 let args_two = vec![
201 Expr::Variable("pid".to_string()),
202 Expr::String("test".to_string()),
203 ];
204
205 assert!(FormatValidator::validate_format_arguments("no placeholders", &args_empty).is_ok());
207 assert!(FormatValidator::validate_format_arguments("pid: {}", &args_one).is_ok());
208 assert!(FormatValidator::validate_format_arguments("pid: {}, name: {}", &args_two).is_ok());
209
210 assert!(FormatValidator::validate_format_arguments("need one: {}", &args_empty).is_err());
212 assert!(FormatValidator::validate_format_arguments("no placeholders", &args_one).is_err());
213 assert!(FormatValidator::validate_format_arguments("need two: {} {}", &args_one).is_err());
214 }
215}