1use std::ffi::OsString;
13use std::path::PathBuf;
14
15use crate::error::FigletError;
16
17const ARG_TAKING_SHORTS: &[char] = &['f', 'd', 'w', 'm', 'C', 'I'];
19
20const EXCLUDED_SHORTS: &[char] = &['L', 'R', 'I', 'N', 'C'];
22
23const EXCLUDED_LONGS: &[&str] = &[
25 "--info-dump",
26 "--no-controlfile",
27 "--color",
28 "--rainbow",
29 "--left-to-right",
30 "--right-to-left",
31];
32
33#[derive(Debug, Default, Clone)]
35pub struct StrictArgs {
36 pub font: Option<String>,
38 pub font_dirs: Vec<PathBuf>,
40 pub width: Option<u32>,
42 pub use_terminal_width: bool,
44 pub justify: Option<JustifyKind>,
46 pub layout: Option<LayoutKind>,
48 pub paragraph: Option<bool>,
50 pub message: Vec<String>,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum JustifyKind {
58 Center,
60 Left,
62 Right,
64 FontDefault,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum LayoutKind {
71 Kerning,
73 FullWidth,
75 ForceSmush,
77 DefaultSmush,
79 OverlapOnly,
81 Explicit(i32),
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum StrictError {
89 InvalidShortOption {
92 ch: char,
94 message: String,
96 },
97 UnrecognizedLongOption {
100 flag: String,
102 message: String,
104 },
105 MissingArgument {
107 ch: char,
109 message: String,
111 },
112}
113
114impl StrictError {
115 pub fn message(&self) -> &str {
117 match self {
118 Self::InvalidShortOption { message, .. }
119 | Self::UnrecognizedLongOption { message, .. }
120 | Self::MissingArgument { message, .. } => message,
121 }
122 }
123}
124
125impl From<StrictError> for FigletError {
126 fn from(err: StrictError) -> Self {
127 FigletError::Internal(match err {
128 StrictError::InvalidShortOption { .. } => "strict: invalid short option",
129 StrictError::UnrecognizedLongOption { .. } => "strict: unrecognized long option",
130 StrictError::MissingArgument { .. } => "strict: missing argument",
131 })
132 }
133}
134
135pub fn format_unknown_flag(token: &str) -> String {
142 if let Some(long) = token.strip_prefix("--") {
143 format!("figlet: unrecognized option '--{long}'")
144 } else if let Some(rest) = token.strip_prefix('-') {
145 let ch = rest.chars().next().unwrap_or('?');
146 format!("figlet: invalid option -- '{ch}'")
147 } else {
148 format!("figlet: unrecognized option '{token}'")
149 }
150}
151
152pub fn parse_argv(argv: &[OsString]) -> Result<StrictArgs, StrictError> {
156 let mut args = StrictArgs::default();
157 let mut i = 0usize;
158 let mut positional_only = false;
160
161 while i < argv.len() {
162 let token = match argv[i].to_str() {
163 Some(s) => s.to_owned(),
164 None => {
165 args.message.push(argv[i].to_string_lossy().into_owned());
166 i += 1;
167 continue;
168 }
169 };
170
171 if positional_only {
172 args.message.push(token);
173 i += 1;
174 continue;
175 }
176
177 if token == "--" {
178 positional_only = true;
179 i += 1;
180 continue;
181 }
182
183 if let Some(long) = token.strip_prefix("--") {
184 if long == "strict" || long == "no-strict" {
190 i += 1;
191 continue;
192 }
193 let _ = EXCLUDED_LONGS;
196 return Err(StrictError::UnrecognizedLongOption {
197 flag: token.clone(),
198 message: format_unknown_flag(&token),
199 });
200 }
201
202 if let Some(short_body) = token.strip_prefix('-').filter(|s| !s.is_empty()) {
203 let chars: Vec<char> = short_body.chars().collect();
207 let mut idx = 0usize;
208 while idx < chars.len() {
209 let ch = chars[idx];
210
211 if EXCLUDED_SHORTS.contains(&ch) {
212 let token_str = format!("-{ch}");
213 return Err(StrictError::InvalidShortOption {
214 ch,
215 message: format_unknown_flag(&token_str),
216 });
217 }
218
219 if ARG_TAKING_SHORTS.contains(&ch) {
220 let value = if idx + 1 < chars.len() {
221 chars[idx + 1..].iter().collect::<String>()
222 } else {
223 i += 1;
224 match argv.get(i).and_then(|os| os.to_str()).map(str::to_owned) {
225 Some(v) => v,
226 None => {
227 let msg = format!("figlet: option requires an argument -- '{ch}'");
228 return Err(StrictError::MissingArgument { ch, message: msg });
229 }
230 }
231 };
232 apply_short_with_value(&mut args, ch, &value);
233 idx = chars.len();
234 continue;
235 }
236
237 match ch {
238 'c' => args.justify = Some(JustifyKind::Center),
239 'l' => args.justify = Some(JustifyKind::Left),
240 'r' => args.justify = Some(JustifyKind::Right),
241 'x' => args.justify = Some(JustifyKind::FontDefault),
242 'k' => args.layout = Some(LayoutKind::Kerning),
243 'W' => args.layout = Some(LayoutKind::FullWidth),
244 'S' => args.layout = Some(LayoutKind::ForceSmush),
245 's' => args.layout = Some(LayoutKind::DefaultSmush),
246 'o' => args.layout = Some(LayoutKind::OverlapOnly),
247 't' => args.use_terminal_width = true,
248 'p' => args.paragraph = Some(true),
249 'n' => args.paragraph = Some(false),
250 other => {
251 let token_str = format!("-{other}");
252 return Err(StrictError::InvalidShortOption {
253 ch: other,
254 message: format_unknown_flag(&token_str),
255 });
256 }
257 }
258 idx += 1;
259 }
260 i += 1;
261 continue;
262 }
263
264 args.message.push(token);
266 i += 1;
267 }
268
269 Ok(args)
270}
271
272fn apply_short_with_value(args: &mut StrictArgs, ch: char, value: &str) {
273 match ch {
274 'f' => args.font = Some(value.to_owned()),
275 'd' => args.font_dirs.push(PathBuf::from(value)),
276 'w' => {
277 if let Ok(n) = value.parse::<u32>() {
278 args.width = Some(n);
279 }
280 }
281 'm' => {
282 if let Ok(n) = value.parse::<i32>() {
283 args.layout = Some(LayoutKind::Explicit(n));
284 }
285 }
286 _ => {}
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 fn parse(s: &[&str]) -> Result<StrictArgs, StrictError> {
297 let argv: Vec<OsString> = s.iter().map(|&v| OsString::from(v)).collect();
298 parse_argv(&argv)
299 }
300
301 #[test]
302 fn empty_argv_ok() {
303 let got = parse(&[]).unwrap();
304 assert!(got.message.is_empty());
305 }
306
307 #[test]
308 fn single_positional_collected() {
309 let got = parse(&["hello"]).unwrap();
310 assert_eq!(got.message, vec!["hello".to_owned()]);
311 }
312
313 #[test]
314 fn dash_f_with_separate_value() {
315 let got = parse(&["-f", "slant", "X"]).unwrap();
316 assert_eq!(got.font.as_deref(), Some("slant"));
317 assert_eq!(got.message, vec!["X".to_owned()]);
318 }
319
320 #[test]
321 fn dash_f_with_attached_value() {
322 let got = parse(&["-fslant", "X"]).unwrap();
323 assert_eq!(got.font.as_deref(), Some("slant"));
324 }
325
326 #[test]
327 #[allow(non_snake_case)]
328 fn excluded_short_L_rejected() {
329 let err = parse(&["-L", "X"]).unwrap_err();
330 match err {
331 StrictError::InvalidShortOption { ch, message } => {
332 assert_eq!(ch, 'L');
333 assert_eq!(message, "figlet: invalid option -- 'L'");
334 }
335 other => panic!("expected InvalidShortOption, got {other:?}"),
336 }
337 }
338
339 #[test]
340 #[allow(non_snake_case)]
341 fn excluded_short_C_rejected() {
342 let err = parse(&["-C", "file", "X"]).unwrap_err();
343 match err {
344 StrictError::InvalidShortOption { ch, .. } => assert_eq!(ch, 'C'),
345 other => panic!("expected InvalidShortOption, got {other:?}"),
346 }
347 }
348
349 #[test]
350 fn excluded_long_info_dump_rejected() {
351 let err = parse(&["--info-dump", "X"]).unwrap_err();
352 match err {
353 StrictError::UnrecognizedLongOption { flag, message } => {
354 assert_eq!(flag, "--info-dump");
355 assert_eq!(message, "figlet: unrecognized option '--info-dump'");
356 }
357 other => panic!("expected UnrecognizedLongOption, got {other:?}"),
358 }
359 }
360
361 #[test]
362 fn excluded_long_color_rejected() {
363 let err = parse(&["--color=always", "X"]).unwrap_err();
364 match err {
365 StrictError::UnrecognizedLongOption { .. } => {}
366 other => panic!("expected UnrecognizedLongOption, got {other:?}"),
367 }
368 }
369
370 #[test]
371 fn last_wins_justify_flags() {
372 let got = parse(&["-c", "-l", "-r", "X"]).unwrap();
373 assert_eq!(got.justify, Some(JustifyKind::Right));
374 }
375
376 #[test]
377 fn last_wins_layout_flags() {
378 let got = parse(&["-k", "-W", "-S", "X"]).unwrap();
379 assert_eq!(got.layout, Some(LayoutKind::ForceSmush));
380 }
381
382 #[test]
383 fn double_dash_makes_rest_positional() {
384 let got = parse(&["--", "-S", "-f"]).unwrap();
385 assert_eq!(got.message, vec!["-S".to_owned(), "-f".to_owned()]);
386 }
387
388 #[test]
389 fn format_unknown_flag_shapes() {
390 assert_eq!(format_unknown_flag("-L"), "figlet: invalid option -- 'L'");
391 assert_eq!(
392 format_unknown_flag("--rainbow"),
393 "figlet: unrecognized option '--rainbow'"
394 );
395 }
396}