terminfo_lean/
expand.rs

1// Copyright 2019 The Rust Project Developers. See the COPYRIGHT
2// file at the top-level directory of this distribution and at
3// http://rust-lang.org/COPYRIGHT.
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! Expansion of capability strings with parameters
12
13use 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/// Parameter that can be used for capability expansion
41#[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/// Errors reported when expanding a string
72#[derive(thiserror::Error, Debug, PartialEq)]
73#[non_exhaustive]
74pub enum Error {
75    /// The operator does not have required data on stack
76    #[error("Not enough stack elements for operator {0}")]
77    StackUnderflow(char),
78    /// The data type on stack is not expected by the operator
79    #[error("Parameter type not expected by operator {0}")]
80    TypeMismatch(char),
81    /// Unknown or unsupported format option
82    #[error("Unrecognized format option: {0}")]
83    UnrecognizedFormatOption(char),
84    /// Variable name is invalid
85    #[error("Invalid variable name: {0}")]
86    InvalidVariableName(char),
87    /// Parameter index is invalid
88    #[error("Invalid parameter index: {0}")]
89    InvalidParameterIndex(char),
90    /// Character constant is invalid
91    #[error("Malformed character constant")]
92    MalformedCharacterConstant,
93    /// Integer constant is too large
94    #[error("Integer constant too large")]
95    IntegerConstantOverflow,
96    /// Integer constant is invalid
97    #[error("Integer constant malformed")]
98    MalformedIntegerConstant,
99    /// Format width is too large
100    #[error("Overflow in format width")]
101    FormatWidthOverflow,
102    // Format precision is too large
103    #[error("Overflow in format precision")]
104    FormatPrecisionOverflow,
105    /// Argument type is incompatible with the format
106    #[error("Unexpected type for format")]
107    FormatTypeMismatch,
108}
109
110/// Context for variable expansion
111///
112/// To be compatible with ncurses, the `ExpandContext` instance should be the same
113/// for the same terminal.
114pub struct ExpandContext {
115    /// Static variables A-Z
116    static_variables: [Parameter; 26],
117}
118
119impl ExpandContext {
120    /// Return a newly initialized `ExpandContext`
121    #[must_use]
122    pub fn new() -> Self {
123        Self {
124            static_variables: from_fn(|_| Parameter::from(0)),
125        }
126    }
127
128    /// Expand a parameterized capability
129    ///
130    /// # Arguments
131    /// * `cap`    - string to expand
132    /// * `params` - vector of params for %p1 etc
133    pub fn expand(&mut self, cap: &[u8], params: &[Parameter]) -> Result<Vec<u8>, Error> {
134        let mut state = States::Nothing;
135
136        // expanded cap will only rarely be larger than the cap itself
137        let mut output = Vec::with_capacity(cap.len());
138
139        let mut stack = Vec::new();
140
141        // Dynamic variables a-z
142        let mut dynamic_variables: [Parameter; 26] = from_fn(|_| Parameter::from(0));
143
144        // Copy parameters into a local vector for mutability
145        let mut mparams = params.to_vec();
146
147        // The increment should only be done once
148        let mut incremented = false;
149
150        // Make sure there are at least 9 parameters
151        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                                // if c is 0, use 0200 (128) for ncurses compatibility
183                                Some(Parameter::Number(0)) => output.push(128u8),
184                                // Don't check bounds. ncurses just casts and truncates.
185                                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                        // printf-style support for %doxXs
261                        '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                        // conditionals
288                        '?' | ';' => (),
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                    // params are 1-indexed
301                    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                                // will cause state to go to States::Nothing
361                                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                            // Leading octal zero counts against precision.
519                            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    /// Compare the result of `expand()` to the expected string
606    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}