sprintf/
parser.rs

1//! Parse printf format strings
2
3use crate::{PrintfError, Result};
4
5/// A part of a format string: either a string of characters to be included
6/// verbatim, or a format specifier that should be replaced based on an argument
7/// to the [vsprintf](crate::vsprintf) call.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum FormatElement<'a> {
10    /// Some characters that are copied to the output as-is
11    Verbatim(&'a str),
12    /// A format specifier
13    Format(ConversionSpecifier),
14}
15
16/// Parsed printf conversion specifier
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct ConversionSpecifier {
19    /// flag `#`: use `0x`, etc?
20    pub alt_form: bool,
21    /// flag `0`: left-pad with zeros?
22    pub zero_pad: bool,
23    /// flag `-`: left-adjust (pad with spaces on the right)
24    pub left_adj: bool,
25    /// flag `' '` (space): indicate sign with a space?
26    pub space_sign: bool,
27    /// flag `+`: Always show sign? (for signed numbers)
28    pub force_sign: bool,
29    /// field width
30    pub width: NumericParam,
31    /// floating point field precision
32    pub precision: NumericParam,
33    /// data type
34    pub conversion_type: ConversionType,
35}
36
37/// Width / precision parameter
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum NumericParam {
40    /// The literal width
41    Literal(i32),
42    /// Get the width from the previous argument
43    ///
44    /// This should never be passed to [Printf::format()][crate::Printf::format()].
45    FromArgument,
46}
47
48/// Printf data type
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ConversionType {
51    /// `d`, `i`, or `u`
52    DecInt,
53    /// `o`
54    OctInt,
55    /// `x` or `p`
56    HexIntLower,
57    /// `X`
58    HexIntUpper,
59    /// `e`
60    SciFloatLower,
61    /// `E`
62    SciFloatUpper,
63    /// `f`
64    DecFloatLower,
65    /// `F`
66    DecFloatUpper,
67    /// `g`
68    CompactFloatLower,
69    /// `G`
70    CompactFloatUpper,
71    /// `c`
72    Char,
73    /// `s`
74    String,
75    /// `%`
76    PercentSign,
77}
78
79/// Parses a string to a vector of [FormatElement]
80///
81/// Takes a printf-style format string `fmt`
82///
83///     use sprintf::parser::{
84///         parse_format_string, ConversionSpecifier, ConversionType, FormatElement, NumericParam,
85///     };
86///     let fmt = "Hello %#06x";
87///     let parsed = parse_format_string(fmt).unwrap();
88///     assert_eq!(parsed[0], FormatElement::Verbatim("Hello "));
89///     assert_eq!(
90///         parsed[1],
91///         FormatElement::Format(ConversionSpecifier {
92///             alt_form: true,
93///             zero_pad: true,
94///             left_adj: false,
95///             space_sign: false,
96///             force_sign: false,
97///             width: NumericParam::Literal(6),
98///             precision: NumericParam::Literal(6),
99///             conversion_type: ConversionType::HexIntLower,
100///         })
101///     );
102///
103pub fn parse_format_string(fmt: &str) -> Result<Vec<FormatElement>> {
104    // find the first %
105    let mut res = Vec::new();
106
107    let mut rem = fmt;
108
109    while !rem.is_empty() {
110        if let Some((verbatim_prefix, rest)) = rem.split_once('%') {
111            if !verbatim_prefix.is_empty() {
112                res.push(FormatElement::Verbatim(verbatim_prefix));
113            }
114            let (spec, rest) = take_conversion_specifier(rest)?;
115            res.push(FormatElement::Format(spec));
116            rem = rest;
117        } else {
118            res.push(FormatElement::Verbatim(rem));
119            break;
120        }
121    }
122
123    Ok(res)
124}
125
126fn take_conversion_specifier(s: &str) -> Result<(ConversionSpecifier, &str)> {
127    let mut spec = ConversionSpecifier {
128        alt_form: false,
129        zero_pad: false,
130        left_adj: false,
131        space_sign: false,
132        force_sign: false,
133        width: NumericParam::Literal(0),
134        precision: NumericParam::FromArgument, // Placeholder - must not be returned!
135        // ignore length modifier
136        conversion_type: ConversionType::DecInt,
137    };
138
139    let mut s = s;
140
141    // parse flags
142    loop {
143        match s.chars().next() {
144            Some('#') => {
145                spec.alt_form = true;
146            }
147            Some('0') => {
148                spec.zero_pad = true;
149            }
150            Some('-') => {
151                spec.left_adj = true;
152            }
153            Some(' ') => {
154                spec.space_sign = true;
155            }
156            Some('+') => {
157                spec.force_sign = true;
158            }
159            _ => {
160                break;
161            }
162        }
163        s = &s[1..];
164    }
165    // parse width
166    let (w, mut s) = take_numeric_param(s);
167    spec.width = w;
168    // parse precision
169    if matches!(s.chars().next(), Some('.')) {
170        s = &s[1..];
171        let (p, s2) = take_numeric_param(s);
172        spec.precision = p;
173        s = s2;
174    }
175    // check length specifier
176    for len_spec in ["hh", "h", "ll", "l", "q", "L", "j", "z", "Z", "t"] {
177        if s.starts_with(len_spec) {
178            s = s.strip_prefix(len_spec).ok_or(PrintfError::ParseError)?;
179            break; // only allow one length specifier
180        }
181    }
182    // parse conversion type
183    spec.conversion_type = match s.chars().next() {
184        Some('i') | Some('d') | Some('u') => ConversionType::DecInt,
185        Some('o') => ConversionType::OctInt,
186        Some('x') => ConversionType::HexIntLower,
187        Some('X') => ConversionType::HexIntUpper,
188        Some('e') => ConversionType::SciFloatLower,
189        Some('E') => ConversionType::SciFloatUpper,
190        Some('f') => ConversionType::DecFloatLower,
191        Some('F') => ConversionType::DecFloatUpper,
192        Some('g') => ConversionType::CompactFloatLower,
193        Some('G') => ConversionType::CompactFloatUpper,
194        Some('c') | Some('C') => ConversionType::Char,
195        Some('s') | Some('S') => ConversionType::String,
196        Some('p') => {
197            spec.alt_form = true;
198            ConversionType::HexIntLower
199        }
200        Some('%') => ConversionType::PercentSign,
201        _ => {
202            return Err(PrintfError::ParseError);
203        }
204    };
205
206    if spec.precision == NumericParam::FromArgument {
207        // If precision is not specified, set to default value
208        let p = if spec.conversion_type == ConversionType::String {
209            // Default to max limit (aka no limit) for strings
210            i32::MAX
211        } else {
212            // Default to 6 for all other types
213            6
214        };
215        spec.precision = NumericParam::Literal(p);
216    }
217
218    Ok((spec, &s[1..]))
219}
220
221fn take_numeric_param(s: &str) -> (NumericParam, &str) {
222    match s.chars().next() {
223        Some('*') => (NumericParam::FromArgument, &s[1..]),
224        Some(digit) if ('1'..='9').contains(&digit) => {
225            let mut s = s;
226            let mut w = 0;
227            loop {
228                match s.chars().next() {
229                    Some(digit) if ('0'..='9').contains(&digit) => {
230                        w = 10 * w + (digit as i32 - '0' as i32);
231                    }
232                    _ => {
233                        break;
234                    }
235                }
236                s = &s[1..];
237            }
238            (NumericParam::Literal(w), s)
239        }
240        _ => (NumericParam::Literal(0), s),
241    }
242}