1use std::{array::from_fn, iter::repeat_n};
14
15#[derive(Clone, Copy, PartialEq)]
16enum States {
17 Nothing,
18 Delay,
19 Percent,
20 SetVar,
21 GetVar,
22 PushParam,
23 CharConstant,
24 CharClose,
25 IntConstant(i32),
26 FormatPattern(Flags, FormatState),
27 SeekIfElse(usize),
28 SeekIfElsePercent(usize),
29 SeekIfEnd(usize),
30 SeekIfEndPercent(usize),
31}
32
33#[derive(Copy, PartialEq, Clone)]
34enum FormatState {
35 Flags,
36 Width,
37 Precision,
38}
39
40#[derive(Clone)]
42pub enum Parameter {
43 Number(i32),
44 String(Vec<u8>),
45}
46
47impl From<i32> for Parameter {
48 fn from(value: i32) -> Self {
49 Self::Number(value)
50 }
51}
52
53impl From<&[u8]> for Parameter {
54 fn from(value: &[u8]) -> Self {
55 Self::String(value.to_vec())
56 }
57}
58
59impl<const N: usize> From<&[u8; N]> for Parameter {
60 fn from(value: &[u8; N]) -> Self {
61 Self::String(value.to_vec())
62 }
63}
64
65impl From<&str> for Parameter {
66 fn from(value: &str) -> Self {
67 Self::String(value.as_bytes().to_vec())
68 }
69}
70
71#[derive(thiserror::Error, Debug, PartialEq)]
73#[non_exhaustive]
74pub enum Error {
75 #[error("Not enough stack elements for operator {0}")]
77 StackUnderflow(char),
78 #[error("Parameter type not expected by operator {0}")]
80 TypeMismatch(char),
81 #[error("Unrecognized format option: {0}")]
83 UnrecognizedFormatOption(char),
84 #[error("Invalid variable name: {0}")]
86 InvalidVariableName(char),
87 #[error("Invalid parameter index: {0}")]
89 InvalidParameterIndex(char),
90 #[error("Malformed character constant")]
92 MalformedCharacterConstant,
93 #[error("Integer constant too large")]
95 IntegerConstantOverflow,
96 #[error("Integer constant malformed")]
98 MalformedIntegerConstant,
99 #[error("Overflow in format width")]
101 FormatWidthOverflow,
102 #[error("Overflow in format precision")]
104 FormatPrecisionOverflow,
105 #[error("Unexpected type for format")]
107 FormatTypeMismatch,
108}
109
110pub struct ExpandContext {
115 static_variables: [Parameter; 26],
117}
118
119impl ExpandContext {
120 #[must_use]
122 pub fn new() -> Self {
123 Self {
124 static_variables: from_fn(|_| Parameter::from(0)),
125 }
126 }
127
128 pub fn expand(&mut self, cap: &[u8], params: &[Parameter]) -> Result<Vec<u8>, Error> {
134 let mut state = States::Nothing;
135
136 let mut output = Vec::with_capacity(cap.len());
138
139 let mut stack = Vec::new();
140
141 let mut dynamic_variables: [Parameter; 26] = from_fn(|_| Parameter::from(0));
143
144 let mut mparams = params.to_vec();
146
147 let mut incremented = false;
149
150 while mparams.len() < 9 {
152 mparams.push(Parameter::from(0));
153 }
154
155 for &c in cap {
156 let cur = c as char;
157 let mut old_state = state;
158 match state {
159 States::Nothing => {
160 if cur == '%' {
161 state = States::Percent;
162 } else if cur == '$' {
163 state = States::Delay;
164 } else {
165 output.push(c);
166 }
167 }
168 States::Delay => {
169 old_state = States::Nothing;
170 if cur == '>' {
171 state = States::Nothing;
172 }
173 }
174 States::Percent => {
175 match cur {
176 '%' => {
177 output.push(c);
178 state = States::Nothing;
179 }
180 'c' => {
181 match stack.pop() {
182 Some(Parameter::Number(0)) => output.push(128u8),
184 Some(Parameter::Number(c)) => output.push(c as u8),
186 Some(_) => return Err(Error::TypeMismatch(cur)),
187 None => return Err(Error::StackUnderflow(cur)),
188 }
189 }
190 'p' => state = States::PushParam,
191 'P' => state = States::SetVar,
192 'g' => state = States::GetVar,
193 '\'' => state = States::CharConstant,
194 '{' => state = States::IntConstant(0),
195 'l' => match stack.pop() {
196 Some(Parameter::String(s)) => {
197 stack.push(Parameter::from(s.len() as i32));
198 }
199 Some(_) => return Err(Error::TypeMismatch(cur)),
200 None => return Err(Error::StackUnderflow(cur)),
201 },
202 '+' | '-' | '*' | '/' | '|' | '&' | '^' | 'm' => {
203 match (stack.pop(), stack.pop()) {
204 (Some(Parameter::Number(y)), Some(Parameter::Number(x))) => {
205 let result = match cur {
206 '+' => x + y,
207 '-' => x - y,
208 '*' => x * y,
209 '/' => x / y,
210 '|' => x | y,
211 '&' => x & y,
212 '^' => x ^ y,
213 'm' => x % y,
214 _ => unreachable!("logic error"),
215 };
216 stack.push(Parameter::from(result));
217 }
218 (Some(_), Some(_)) => return Err(Error::TypeMismatch(cur)),
219 _ => return Err(Error::StackUnderflow(cur)),
220 }
221 }
222 '=' | '>' | '<' | 'A' | 'O' => match (stack.pop(), stack.pop()) {
223 (Some(Parameter::Number(y)), Some(Parameter::Number(x))) => {
224 let result = match cur {
225 '=' => x == y,
226 '<' => x < y,
227 '>' => x > y,
228 'A' => x > 0 && y > 0,
229 'O' => x > 0 || y > 0,
230 _ => unreachable!("logic error"),
231 };
232 stack.push(Parameter::from(i32::from(result)));
233 }
234 (Some(_), Some(_)) => return Err(Error::TypeMismatch(cur)),
235 _ => return Err(Error::StackUnderflow(cur)),
236 },
237 '!' | '~' => match stack.pop() {
238 Some(Parameter::Number(x)) => {
239 stack.push(Parameter::Number(match cur {
240 '!' if x > 0 => 0,
241 '!' => 1,
242 '~' => !x,
243 _ => unreachable!("logic error"),
244 }));
245 }
246 Some(_) => return Err(Error::TypeMismatch(cur)),
247 None => return Err(Error::StackUnderflow(cur)),
248 },
249 'i' => match (&mparams[0], &mparams[1]) {
250 (&Parameter::Number(x), &Parameter::Number(y)) => {
251 if !incremented {
252 mparams[0] = Parameter::from(x + 1);
253 mparams[1] = Parameter::from(y + 1);
254 incremented = true;
255 }
256 }
257 (_, _) => return Err(Error::TypeMismatch(cur)),
258 },
259
260 'd' | 'o' | 'x' | 'X' | 's' => {
262 if let Some(arg) = stack.pop() {
263 let flags = Flags::default();
264 let result = format(arg, cur, flags)?;
265 output.extend(result);
266 } else {
267 return Err(Error::StackUnderflow(cur));
268 }
269 }
270 ':' | '#' | ' ' | '.' | '0'..='9' => {
271 let mut flags = Flags::default();
272 let mut fstate = FormatState::Flags;
273 match cur {
274 ':' => (),
275 '#' => flags.alternate = true,
276 ' ' => flags.sign = SignFlags::Space,
277 '.' => fstate = FormatState::Precision,
278 '0'..='9' => {
279 flags.width = cur as u16 - '0' as u16;
280 fstate = FormatState::Width;
281 }
282 _ => unreachable!("logic error"),
283 }
284 state = States::FormatPattern(flags, fstate);
285 }
286
287 '?' | ';' => (),
289 't' => match stack.pop() {
290 Some(Parameter::Number(0)) => state = States::SeekIfElse(0),
291 Some(Parameter::Number(_)) => (),
292 Some(_) => return Err(Error::TypeMismatch(cur)),
293 None => return Err(Error::StackUnderflow(cur)),
294 },
295 'e' => state = States::SeekIfEnd(0),
296 c => return Err(Error::UnrecognizedFormatOption(c)),
297 }
298 }
299 States::PushParam => {
300 let index = match cur {
302 '1'..='9' => cur as usize - '1' as usize,
303 _ => return Err(Error::InvalidParameterIndex(cur)),
304 };
305 stack.push(mparams[index].clone());
306 }
307 States::SetVar => {
308 let Some(arg) = stack.pop() else {
309 return Err(Error::StackUnderflow('P'));
310 };
311 match cur {
312 'A'..='Z' => self.static_variables[usize::from((cur as u8) - b'A')] = arg,
313 'a'..='z' => dynamic_variables[usize::from((cur as u8) - b'a')] = arg,
314 _ => return Err(Error::InvalidVariableName(cur)),
315 }
316 }
317 States::GetVar => {
318 let value = match cur {
319 'A'..='Z' => &self.static_variables[usize::from((cur as u8) - b'A')],
320 'a'..='z' => &dynamic_variables[usize::from((cur as u8) - b'a')],
321 _ => return Err(Error::InvalidVariableName(cur)),
322 };
323 stack.push(value.clone());
324 }
325 States::CharConstant => {
326 stack.push(Parameter::from(i32::from(c)));
327 state = States::CharClose;
328 }
329 States::CharClose => {
330 if cur != '\'' {
331 return Err(Error::MalformedCharacterConstant);
332 }
333 }
334 States::IntConstant(i) => {
335 if cur == '}' {
336 stack.push(Parameter::from(i));
337 state = States::Nothing;
338 } else if let Some(digit) = cur.to_digit(10) {
339 match i
340 .checked_mul(10)
341 .and_then(|i_ten| i_ten.checked_add(digit as i32))
342 {
343 Some(i) => {
344 state = States::IntConstant(i);
345 old_state = States::Nothing;
346 }
347 None => return Err(Error::IntegerConstantOverflow),
348 }
349 } else {
350 return Err(Error::MalformedIntegerConstant);
351 }
352 }
353 States::FormatPattern(ref mut flags, ref mut fstate) => {
354 old_state = States::Nothing;
355 match (*fstate, cur) {
356 (_, 'd' | 'o' | 'x' | 'X' | 's') => {
357 if let Some(arg) = stack.pop() {
358 let res = format(arg, cur, *flags)?;
359 output.extend(res);
360 old_state = States::FormatPattern(*flags, *fstate);
362 } else {
363 return Err(Error::StackUnderflow(cur));
364 }
365 }
366 (FormatState::Flags, '#') => {
367 flags.alternate = true;
368 }
369 (FormatState::Flags, '-') => {
370 flags.left = true;
371 }
372 (FormatState::Flags, '+') => {
373 flags.sign = SignFlags::Plus;
374 }
375 (FormatState::Flags, ' ') => {
376 flags.sign = SignFlags::Space;
377 }
378 (FormatState::Flags, '0'..='9') => {
379 flags.width = cur as u16 - '0' as u16;
380 *fstate = FormatState::Width;
381 }
382 (FormatState::Width, '0'..='9') => {
383 flags.width = match flags
384 .width
385 .checked_mul(10)
386 .and_then(|w| w.checked_add(cur as u16 - '0' as u16))
387 {
388 Some(width) => width,
389 None => return Err(Error::FormatWidthOverflow),
390 }
391 }
392 (FormatState::Width | FormatState::Flags, '.') => {
393 *fstate = FormatState::Precision;
394 }
395 (FormatState::Precision, '0'..='9') => {
396 flags.precision = match flags
397 .precision
398 .unwrap_or(0)
399 .checked_mul(10)
400 .and_then(|w| w.checked_add(cur as u16 - '0' as u16))
401 {
402 Some(precision) => Some(precision),
403 None => return Err(Error::FormatPrecisionOverflow),
404 }
405 }
406 _ => return Err(Error::UnrecognizedFormatOption(cur)),
407 }
408 }
409 States::SeekIfElse(level) => {
410 if cur == '%' {
411 state = States::SeekIfElsePercent(level);
412 }
413 old_state = States::Nothing;
414 }
415 States::SeekIfElsePercent(level) => {
416 if cur == ';' {
417 if level == 0 {
418 state = States::Nothing;
419 } else {
420 state = States::SeekIfElse(level - 1);
421 }
422 } else if cur == 'e' && level == 0 {
423 state = States::Nothing;
424 } else if cur == '?' {
425 state = States::SeekIfElse(level + 1);
426 } else {
427 state = States::SeekIfElse(level);
428 }
429 }
430 States::SeekIfEnd(level) => {
431 if cur == '%' {
432 state = States::SeekIfEndPercent(level);
433 }
434 old_state = States::Nothing;
435 }
436 States::SeekIfEndPercent(level) => {
437 if cur == ';' {
438 if level == 0 {
439 state = States::Nothing;
440 } else {
441 state = States::SeekIfEnd(level - 1);
442 }
443 } else if cur == '?' {
444 state = States::SeekIfEnd(level + 1);
445 } else {
446 state = States::SeekIfEnd(level);
447 }
448 }
449 }
450 if state == old_state {
451 state = States::Nothing;
452 }
453 }
454 Ok(output)
455 }
456}
457
458#[derive(Copy, PartialEq, Clone, Default)]
459enum SignFlags {
460 #[default]
461 Empty,
462 Space,
463 Plus,
464}
465
466#[derive(Copy, PartialEq, Clone, Default)]
467struct Flags {
468 width: u16,
469 precision: Option<u16>,
470 alternate: bool,
471 left: bool,
472 sign: SignFlags,
473}
474
475fn format(val: Parameter, op: char, flags: Flags) -> Result<Vec<u8>, Error> {
476 let mut s = match val {
477 Parameter::Number(d) => {
478 match op {
479 'd' => match flags.precision {
480 Some(precision) => {
481 if d < 0 {
482 format!("{d:0prec$}", prec = usize::from(precision + 1))
483 } else {
484 match flags.sign {
485 SignFlags::Empty => {
486 format!("{d:0prec$}", prec = precision.into())
487 }
488 SignFlags::Space => {
489 format!(" {d:0prec$}", prec = precision.into())
490 }
491 SignFlags::Plus => {
492 format!("{d:+0prec$}", prec = usize::from(precision + 1))
493 }
494 }
495 }
496 }
497 None => {
498 if d < 0 {
499 format!("{d}")
500 } else {
501 match flags.sign {
502 SignFlags::Empty => {
503 format!("{d}")
504 }
505 SignFlags::Space => {
506 format!(" {d}")
507 }
508 SignFlags::Plus => {
509 format!("{d:+}")
510 }
511 }
512 }
513 }
514 },
515 'o' => match flags.precision {
516 Some(precision) => {
517 if flags.alternate {
518 format!("0{d:0prec$o}", prec = precision.saturating_sub(1).into())
520 } else {
521 format!("{d:0prec$o}", prec = precision.into())
522 }
523 }
524 None => {
525 if flags.alternate {
526 format!("0{d:o}")
527 } else {
528 format!("{d:o}")
529 }
530 }
531 },
532 'x' => match flags.precision {
533 Some(precision) => {
534 if flags.alternate && d != 0 {
535 format!("0x{d:0prec$x}", prec = precision.into())
536 } else {
537 format!("{d:0prec$x}", prec = precision.into())
538 }
539 }
540 None => {
541 if flags.alternate && d != 0 {
542 format!("0x{d:x}")
543 } else {
544 format!("{d:x}")
545 }
546 }
547 },
548 'X' => match flags.precision {
549 Some(precision) => {
550 if flags.alternate && d != 0 {
551 format!("0X{d:0prec$X}", prec = precision.into())
552 } else {
553 format!("{d:0prec$X}", prec = precision.into())
554 }
555 }
556 None => {
557 if flags.alternate && d != 0 {
558 format!("0X{d:X}")
559 } else {
560 format!("{d:X}")
561 }
562 }
563 },
564 _ => return Err(Error::FormatTypeMismatch),
565 }
566 .into_bytes()
567 }
568 Parameter::String(mut s) => match op {
569 's' => {
570 if let Some(precision) = flags.precision
571 && let precision = usize::from(precision)
572 && precision < s.len()
573 {
574 s.truncate(precision);
575 }
576 s
577 }
578 _ => return Err(Error::FormatTypeMismatch),
579 },
580 };
581 if usize::from(flags.width) > s.len() {
582 let n = usize::from(flags.width) - s.len();
583 if flags.left {
584 s.extend(repeat_n(b' ', n));
585 } else {
586 let mut s_ = Vec::with_capacity(usize::from(flags.width));
587 s_.extend(repeat_n(b' ', n));
588 s_.extend(s);
589 s = s_;
590 }
591 }
592 Ok(s)
593}
594
595impl Default for ExpandContext {
596 fn default() -> Self {
597 Self::new()
598 }
599}
600
601#[cfg(test)]
602mod test {
603 use super::{Error, ExpandContext, Parameter};
604
605 fn assert_str(actual: Result<Vec<u8>, Error>, expected: &str) {
607 assert_eq!(str::from_utf8(&actual.unwrap()).unwrap(), expected);
608 }
609
610 #[test]
611 fn multiple_parameters() {
612 let mut expand_context = ExpandContext::default();
613 assert_str(
614 expand_context.expand(
615 b"%p1%p2%p3%p4%p5%p6%p7%p8%p9%d%d%d%d%d%s%s%s%d",
616 &[
617 Parameter::from(1),
618 Parameter::from(b"Two"),
619 Parameter::from(b"Three".as_slice()),
620 Parameter::from("Four"),
621 Parameter::from(5),
622 Parameter::from(6),
623 Parameter::from(7),
624 Parameter::from(8),
625 Parameter::from(9),
626 ],
627 ),
628 "98765FourThreeTwo1",
629 );
630 }
631
632 #[test]
633 fn delay_ignored() {
634 let mut expand_context = ExpandContext::new();
635 assert_str(
636 expand_context.expand(b"%p1%d$<5*/>%p1%d", &[Parameter::from(42)]),
637 "4242",
638 );
639 }
640
641 #[test]
642 fn percent_escape() {
643 let mut expand_context = ExpandContext::new();
644 assert_str(
645 expand_context.expand(b"%p1%%%%%d", &[Parameter::from(42)]),
646 "%%42",
647 );
648 }
649
650 #[test]
651 fn char_output() {
652 let mut expand_context = ExpandContext::new();
653 assert_eq!(
654 expand_context.expand(
655 b"%p1%c%p2%c%p3%c",
656 &[
657 Parameter::from(42),
658 Parameter::from(0),
659 Parameter::from(257)
660 ],
661 ),
662 Ok(vec![42, 128, 1]),
663 );
664 }
665
666 #[test]
667 fn type_mismatch_expected_number() {
668 let mut expand_context = ExpandContext::new();
669 for op in "c!~+-*/|&^m=><AOit".chars() {
670 let cap = format!("%p1%p2%{op}");
671 assert_eq!(
672 expand_context.expand(
673 cap.as_bytes(),
674 &[Parameter::from(42), Parameter::from("word")]
675 ),
676 Err(Error::TypeMismatch(op)),
677 "Failed for %{op}"
678 );
679 }
680 }
681
682 #[test]
683 fn type_mismatch_expected_string() {
684 let mut expand_context = ExpandContext::new();
685 assert_eq!(
686 expand_context.expand(b"%p1%l", &[Parameter::from(42)]),
687 Err(Error::TypeMismatch('l'))
688 );
689 }
690
691 #[test]
692 fn stack_underflow_unary() {
693 let mut expand_context = ExpandContext::new();
694 for op in "cl!~doxXst".chars() {
695 let cap = format!("%{op}");
696 assert_eq!(
697 expand_context.expand(cap.as_bytes(), &[]),
698 Err(Error::StackUnderflow(op)),
699 "Failed for %{op}"
700 );
701 }
702 }
703
704 #[test]
705 fn stack_underflow_format() {
706 let mut expand_context = ExpandContext::new();
707 for op in "doxXs".chars() {
708 let cap = format!("%:{op}");
709 assert_eq!(
710 expand_context.expand(cap.as_bytes(), &[]),
711 Err(Error::StackUnderflow(op)),
712 "Failed for %{op}"
713 );
714 }
715 }
716
717 #[test]
718 fn stack_underflow_binary() {
719 let mut expand_context = ExpandContext::new();
720 for op in "+-*/|&^m=><AO".chars() {
721 let cap = format!("%p1%{op}");
722 assert_eq!(
723 expand_context.expand(cap.as_bytes(), &[Parameter::from(42)]),
724 Err(Error::StackUnderflow(op)),
725 "Failed for %{op}"
726 );
727 }
728 }
729
730 #[test]
731 fn stack_underflow_variable() {
732 let mut expand_context = ExpandContext::new();
733 assert_eq!(
734 expand_context.expand(b"%P1", &[]),
735 Err(Error::StackUnderflow('P'))
736 );
737 }
738
739 #[test]
740 fn variable_persistence() {
741 let mut expand_context = ExpandContext::new();
742 assert_str(
743 expand_context.expand(
744 b"%p1%PA%p2%PZ%p3%Pa%p4%Pz%gA%d%gZ%d%ga%d%gz%d",
745 &[
746 Parameter::from(1),
747 Parameter::from(2),
748 Parameter::from(3),
749 Parameter::from(4),
750 ],
751 ),
752 "1234",
753 );
754 assert_str(expand_context.expand(b"%gA%d%gZ%d%ga%d%gz%d", &[]), "1200");
755 }
756
757 #[test]
758 fn variable_bad_name() {
759 let mut expand_context = ExpandContext::new();
760 assert_eq!(
761 expand_context.expand(b"%p1%P7", &[Parameter::from(42)]),
762 Err(Error::InvalidVariableName('7'))
763 );
764 assert_eq!(
765 expand_context.expand(b"%g8", &[]),
766 Err(Error::InvalidVariableName('8'))
767 );
768 }
769
770 #[test]
771 fn constants() {
772 let mut expand_context = ExpandContext::new();
773 assert_str(expand_context.expand(b"%{456}%d %'A'%d", &[]), "456 65");
774 }
775
776 #[test]
777 fn bad_char_constant() {
778 let mut expand_context = ExpandContext::new();
779 assert_eq!(
780 expand_context.expand(b"%'ab'", &[]),
781 Err(Error::MalformedCharacterConstant)
782 );
783 }
784
785 #[test]
786 fn bad_integer_constant() {
787 let mut expand_context = ExpandContext::new();
788 assert_eq!(
789 expand_context.expand(b"%{2b}", &[]),
790 Err(Error::MalformedIntegerConstant)
791 );
792 }
793
794 #[test]
795 fn integer_constant_overflow() {
796 let mut expand_context = ExpandContext::new();
797 assert_eq!(
798 expand_context.expand(b"%{2147483648}", &[]),
799 Err(Error::IntegerConstantOverflow)
800 );
801 }
802
803 #[test]
804 fn string_length() {
805 let mut expand_context = ExpandContext::new();
806 assert_str(
807 expand_context.expand(b"%p1%l%d", &[Parameter::from("Hello, World!")]),
808 "13",
809 );
810 }
811
812 #[test]
813 fn numeric_binary_operations() {
814 let tests = [
815 (12, '+', 29, "41"),
816 (35, '-', 7, "28"),
817 (3, '*', 16, "48"),
818 (70, '/', 3, "23"),
819 (3, '|', 5, "7"),
820 (15, '&', 35, "3"),
821 (15, '^', 35, "44"),
822 (101, 'm', 7, "3"),
823 (5, '=', 7, "0"),
824 (15, '=', 15, "1"),
825 (17, '<', 8, "0"),
826 (17, '<', 50, "1"),
827 (17, '>', 8, "1"),
828 (17, '>', 50, "0"),
829 (0, 'A', 0, "0"),
830 (15, 'A', 0, "0"),
831 (0, 'A', 9, "0"),
832 (15, 'A', 32, "1"),
833 (0, 'O', 0, "0"),
834 (15, 'O', 0, "1"),
835 (0, 'O', 9, "1"),
836 (15, 'O', 32, "1"),
837 ];
838 let mut expand_context = ExpandContext::new();
839 for (operand1, operation, operand2, expect) in tests {
840 let cap = format!("%p1%p2%{operation}%d");
841 assert_str(
842 expand_context.expand(
843 cap.as_bytes(),
844 &[Parameter::from(operand1), Parameter::from(operand2)],
845 ),
846 expect,
847 );
848 }
849 }
850
851 #[test]
852 fn negation() {
853 let mut expand_context = ExpandContext::new();
854 assert_str(
855 expand_context.expand(
856 b"%p1%!%d %p2%!%d %p1%~%d %p2%~%d",
857 &[Parameter::from(0), Parameter::from(15)],
858 ),
859 "1 0 -1 -16",
860 );
861 }
862
863 #[test]
864 fn increment() {
865 let mut expand_context = ExpandContext::new();
866 assert_str(
867 expand_context.expand(
868 b"%i%p1%d_%p2%d_%p3%d_%i%p1%d_%p2%d_%p3%d",
869 &[
870 Parameter::from(10),
871 Parameter::from(15),
872 Parameter::from(20),
873 ],
874 ),
875 "11_16_20_11_16_20",
876 );
877 }
878
879 #[test]
880 fn conditional_if_then() {
881 let mut expand_context = ExpandContext::new();
882 let cap = b"%p1%p2%?%<%tless%;";
883 assert_str(
884 expand_context.expand(cap, &[Parameter::from(1), Parameter::from(2)]),
885 "less",
886 );
887 assert_str(
888 expand_context.expand(cap, &[Parameter::from(2), Parameter::from(1)]),
889 "",
890 );
891 }
892
893 #[test]
894 fn conditional_if_then_else() {
895 let mut expand_context = ExpandContext::new();
896 let cap = b"%p1%p2%?%<%tless%emore%;";
897 assert_str(
898 expand_context.expand(cap, &[Parameter::from(1), Parameter::from(2)]),
899 "less",
900 );
901 assert_str(
902 expand_context.expand(cap, &[Parameter::from(2), Parameter::from(1)]),
903 "more",
904 );
905 }
906
907 #[test]
908 fn conditional_nested() {
909 let mut expand_context = ExpandContext::new();
910 let cap = b"%?%p1%t+%?%p2%t+%e-%;%e-%?%p2%t+%e-%;%;";
911 assert_str(
912 expand_context.expand(cap, &[Parameter::from(0), Parameter::from(0)]),
913 "--",
914 );
915 assert_str(
916 expand_context.expand(cap, &[Parameter::from(0), Parameter::from(1)]),
917 "-+",
918 );
919 assert_str(
920 expand_context.expand(cap, &[Parameter::from(1), Parameter::from(0)]),
921 "+-",
922 );
923 assert_str(
924 expand_context.expand(cap, &[Parameter::from(1), Parameter::from(1)]),
925 "++",
926 );
927 }
928
929 #[test]
930 fn format_flags() {
931 let tests = [
932 (63, "%x", "3f"),
933 (63, "%#x", "0x3f"),
934 (63, "%6x", " 3f"),
935 (63, "%:-6x", "3f "),
936 (63, "%:+d", "+63"),
937 (63, "%: d", " 63"),
938 (63, "%p1%:-+ #10.5x", "0x0003f "),
939 ];
940 let mut expand_context = ExpandContext::new();
941 for (param1, format, expected) in tests {
942 let cap = format!("%p1{format}");
943 assert_str(
944 expand_context.expand(cap.as_bytes(), &[Parameter::from(param1)]),
945 expected,
946 );
947 }
948 }
949
950 #[test]
951 fn format_bad_flag() {
952 let mut expand_context = ExpandContext::new();
953 assert_eq!(
954 expand_context.expand(b"%p1%:^x", &[Parameter::from(63)]),
955 Err(Error::UnrecognizedFormatOption('^'))
956 );
957 }
958
959 #[test]
960 fn format_decimal() {
961 let tests = [
962 (42, "%d", "42"),
963 (-42, "%d", "-42"),
964 (42, "%:+d", "+42"),
965 (-42, "%:+d", "-42"),
966 (42, "% d", " 42"),
967 (-42, "% d", "-42"),
968 (42, "%.5d", "00042"),
969 (-42, "%.5d", "-00042"),
970 (42, "%:+.5d", "+00042"),
971 (-42, "%:+.5d", "-00042"),
972 (42, "% .5d", " 00042"),
973 (-42, "% .5d", "-00042"),
974 ];
975 let mut expand_context = ExpandContext::new();
976 for (param1, format, expected) in tests {
977 let cap = format!("%p1{format}");
978 assert_str(
979 expand_context.expand(cap.as_bytes(), &[Parameter::from(param1)]),
980 expected,
981 );
982 }
983 }
984
985 #[test]
986 fn format_octal() {
987 let tests = [
988 (42, "%o", "52"),
989 (42, "%#o", "052"),
990 (42, "%.5o", "00052"),
991 (42, "%#.5o", "00052"),
992 ];
993 let mut expand_context = ExpandContext::new();
994 for (param1, format, expected) in tests {
995 let cap = format!("%p1{format}");
996 assert_str(
997 expand_context.expand(cap.as_bytes(), &[Parameter::from(param1)]),
998 expected,
999 );
1000 }
1001 }
1002
1003 #[test]
1004 fn format_hexadecimal() {
1005 let tests = [
1006 (42, "%x", "2a"),
1007 (42, "%#x", "0x2a"),
1008 (0, "%#x", "0"),
1009 (42, "%.5x", "0002a"),
1010 (42, "%#.5x", "0x0002a"),
1011 (0, "%#.5x", "00000"),
1012 (42, "%X", "2A"),
1013 (42, "%#X", "0X2A"),
1014 (0, "%#X", "0"),
1015 (42, "%.5X", "0002A"),
1016 (42, "%#.5X", "0X0002A"),
1017 (0, "%#.5X", "00000"),
1018 ];
1019 let mut expand_context = ExpandContext::new();
1020 for (param1, format, expected) in tests {
1021 let cap = format!("%p1{format}");
1022 assert_str(
1023 expand_context.expand(cap.as_bytes(), &[Parameter::from(param1)]),
1024 expected,
1025 );
1026 }
1027 }
1028
1029 #[test]
1030 fn format_string() {
1031 let tests = [
1032 ("One", "%s", "One"),
1033 ("One", "%5s", " One"),
1034 ("One", "%5.2s", " On"),
1035 ("One", "%:-5.4s", "One "),
1036 ];
1037 let mut expand_context = ExpandContext::new();
1038 for (param1, format, expected) in tests {
1039 let cap = format!("%p1{format}");
1040 assert_str(
1041 expand_context.expand(cap.as_bytes(), &[Parameter::from(param1)]),
1042 expected,
1043 );
1044 }
1045 }
1046
1047 #[test]
1048 fn format_width_overflow() {
1049 let mut expand_context = ExpandContext::new();
1050 assert_eq!(
1051 expand_context.expand(b"%{1}%65536d", &[]),
1052 Err(Error::FormatWidthOverflow)
1053 );
1054 }
1055
1056 #[test]
1057 fn format_precision_overflow() {
1058 let mut expand_context = ExpandContext::new();
1059 assert_eq!(
1060 expand_context.expand(b"%{1}%.65536d", &[]),
1061 Err(Error::FormatPrecisionOverflow)
1062 );
1063 }
1064
1065 #[test]
1066 fn format_type_mismatch() {
1067 let mut expand_context = ExpandContext::new();
1068 assert_eq!(
1069 expand_context.expand(b"%p1%s", &[Parameter::from(63)]),
1070 Err(Error::FormatTypeMismatch)
1071 );
1072 assert_eq!(
1073 expand_context.expand(b"%p1%3d", &[Parameter::from("one")]),
1074 Err(Error::FormatTypeMismatch)
1075 );
1076 }
1077
1078 #[test]
1079 fn unrecornized_format_option() {
1080 let mut expand_context = ExpandContext::new();
1081 assert_eq!(
1082 expand_context.expand(b"%Y", &[]),
1083 Err(Error::UnrecognizedFormatOption('Y'))
1084 );
1085 }
1086
1087 #[test]
1088 fn bad_parameter_index() {
1089 let mut expand_context = ExpandContext::new();
1090 assert_eq!(
1091 expand_context.expand(b"%p0", &[]),
1092 Err(Error::InvalidParameterIndex('0'))
1093 );
1094 }
1095}