pw_format/
core_fmt.rs

1// Copyright 2024 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15//! # Unsupported core::fmt features
16//! * Argument widths or precisions: `{:0$}` or `{:varname$}`
17
18use std::collections::HashSet;
19
20use nom::{
21    branch::alt,
22    bytes::complete::{tag, take_till1, take_while},
23    character::complete::{alpha1, alphanumeric1, anychar, digit1},
24    combinator::{map, map_res, opt, recognize, value},
25    multi::{many0, many0_count},
26    sequence::pair,
27    IResult,
28};
29
30use crate::{
31    fixed_width, precision, Alignment, Argument, ConversionSpec, Flag, FormatFragment,
32    FormatString, MinFieldWidth, Precision, Primitive, Style,
33};
34
35/// The `name` in a `{name}` format string.  Matches a Rust identifier.
36fn named_argument(input: &str) -> IResult<&str, Argument> {
37    let (input, ident) = recognize(pair(
38        alt((alpha1, tag("_"))),
39        many0_count(alt((alphanumeric1, tag("_")))),
40    ))(input)?;
41
42    Ok((input, Argument::Named(ident.to_string())))
43}
44
45/// The decimal value a `{0}` format string.  Matches a decimal value.
46fn positional_argument(input: &str) -> IResult<&str, Argument> {
47    let (input, index) = map_res(digit1, |val: &str| val.parse::<usize>())(input)?;
48
49    Ok((input, Argument::Positional(index)))
50}
51
52/// No argument value.
53///
54/// Fallback and does not consume any data.
55fn none_argument(input: &str) -> IResult<&str, Argument> {
56    Ok((input, Argument::None))
57}
58
59/// An optional named or positional argument.
60///
61/// ie. `{name:...}` or `{0:...}` of `{:...}
62fn argument(input: &str) -> IResult<&str, Argument> {
63    alt((named_argument, positional_argument, none_argument))(input)
64}
65
66/// An explicit formatting type
67///
68/// i.e. the `x?` in `{:x?}
69fn explicit_type(input: &str) -> IResult<&str, Style> {
70    alt((
71        value(Style::Debug, tag("?")),
72        value(Style::HexDebug, tag("x?")),
73        value(Style::UpperHexDebug, tag("X?")),
74        value(Style::Octal, tag("o")),
75        value(Style::Hex, tag("x")),
76        value(Style::UpperHex, tag("X")),
77        value(Style::Pointer, tag("p")),
78        value(Style::Binary, tag("b")),
79        value(Style::Exponential, tag("e")),
80        value(Style::UpperExponential, tag("E")),
81    ))(input)
82}
83
84/// An optional explicit formatting type
85///
86/// i.e. the `x?` in `{:x?} or no type as in `{:}`
87fn style(input: &str) -> IResult<&str, Style> {
88    let (input, spec) = explicit_type(input).unwrap_or((input, Style::None));
89
90    Ok((input, spec))
91}
92
93/// A formatting flag.  One of `-`, `+`, `#`, or `0`.
94fn map_flag(value: char) -> Result<Flag, String> {
95    match value {
96        '-' => Ok(Flag::LeftJustify),
97        '+' => Ok(Flag::ForceSign),
98        '#' => Ok(Flag::AlternateSyntax),
99        '0' => Ok(Flag::LeadingZeros),
100        _ => Err(format!("Unsupported flag '{}'", value)),
101    }
102}
103
104/// A collection of one or more formatting flags (`-`, `+`, `#`, or `0`).
105fn flags(input: &str) -> IResult<&str, HashSet<Flag>> {
106    let (input, flags) = many0(map_res(anychar, map_flag))(input)?;
107
108    Ok((input, flags.into_iter().collect()))
109}
110
111fn map_alignment(value: char) -> Result<Alignment, String> {
112    match value {
113        '<' => Ok(Alignment::Left),
114        '^' => Ok(Alignment::Center),
115        '>' => Ok(Alignment::Right),
116        _ => Err(format!("Unsupported alignment '{}'", value)),
117    }
118}
119
120/// An alignment flag (`<`, `^`, or `>`).
121fn bare_alignment(input: &str) -> IResult<&str, Alignment> {
122    map_res(anychar, map_alignment)(input)
123}
124
125/// A combined fill character and alignment flag (`<`, `^`, or `>`).
126fn fill_and_alignment(input: &str) -> IResult<&str, (char, Alignment)> {
127    let (input, fill) = anychar(input)?;
128    let (input, alignment) = bare_alignment(input)?;
129
130    Ok((input, (fill, alignment)))
131}
132
133/// An optional fill character plus and alignment flag, or none.
134fn alignment(input: &str) -> IResult<&str, (char, Alignment)> {
135    // First try to match alignment spec preceded with a fill character.  This
136    // is to match cases where the fill character is the same as one of the
137    // alignment spec characters.
138    if let Ok((input, (fill, alignment))) = fill_and_alignment(input) {
139        return Ok((input, (fill, alignment)));
140    }
141
142    // If the above fails, fall back on looking for the alignment spec without
143    // a fill character and default to ' ' as the fill character.
144    if let Ok((input, alignment)) = bare_alignment(input) {
145        return Ok((input, (' ', alignment)));
146    }
147
148    // Of all else false return none alignment with ' ' fill character.
149    Ok((input, (' ', Alignment::None)))
150}
151
152/// A complete format specifier (i.e. the part between the `{}`s).
153fn format_spec(input: &str) -> IResult<&str, ConversionSpec> {
154    let (input, _) = tag(":")(input)?;
155    let (input, (fill, alignment)) = alignment(input)?;
156    let (input, flags) = flags(input)?;
157    let (input, width) = opt(fixed_width)(input)?;
158    let (input, precision) = precision(input)?;
159    let (input, style) = style(input)?;
160
161    Ok((
162        input,
163        ConversionSpec {
164            argument: Argument::None, // This will get filled in by calling function.
165            fill,
166            alignment,
167            flags,
168            min_field_width: width.unwrap_or(MinFieldWidth::None),
169            precision,
170            length: None,
171            primitive: Primitive::Untyped, // All core::fmt primitives are untyped.
172            style,
173        },
174    ))
175}
176
177/// A complete conversion specifier (i.e. a `{}` expression).
178fn conversion(input: &str) -> IResult<&str, ConversionSpec> {
179    let (input, _) = tag("{")(input)?;
180    let (input, argument) = argument(input)?;
181    let (input, spec) = opt(format_spec)(input)?;
182    // Allow trailing whitespace.  Here we specifically match against Rust's
183    // idea of whitespace (specified in the Unicode Character Database) as it
184    // differs from nom's space0 combinator (just spaces and tabs).
185    let (input, _) = take_while(|c: char| c.is_whitespace())(input)?;
186    let (input, _) = tag("}")(input)?;
187
188    let mut spec = spec.unwrap_or_else(|| ConversionSpec {
189        argument: Argument::None,
190        fill: ' ',
191        alignment: Alignment::None,
192        flags: HashSet::new(),
193        min_field_width: MinFieldWidth::None,
194        precision: Precision::None,
195        length: None,
196        primitive: Primitive::Untyped,
197        style: Style::None,
198    });
199
200    spec.argument = argument;
201
202    Ok((input, spec))
203}
204
205/// A string literal (i.e. the non-`{}` part).
206fn literal_fragment(input: &str) -> IResult<&str, FormatFragment> {
207    map(take_till1(|c| c == '{' || c == '}'), |s: &str| {
208        FormatFragment::Literal(s.to_string())
209    })(input)
210}
211
212/// An escaped `{` or `}`.
213fn escape_fragment(input: &str) -> IResult<&str, FormatFragment> {
214    alt((
215        map(tag("{{"), |_| FormatFragment::Literal("{".to_string())),
216        map(tag("}}"), |_| FormatFragment::Literal("}".to_string())),
217    ))(input)
218}
219
220/// A complete conversion specifier (i.e. a `{}` expression).
221fn conversion_fragment(input: &str) -> IResult<&str, FormatFragment> {
222    map(conversion, FormatFragment::Conversion)(input)
223}
224
225/// An escape, literal, or conversion fragment.
226fn fragment(input: &str) -> IResult<&str, FormatFragment> {
227    alt((escape_fragment, conversion_fragment, literal_fragment))(input)
228}
229
230/// Parse a complete `core::fmt` style format string.
231pub(crate) fn format_string(input: &str) -> IResult<&str, FormatString> {
232    let (input, fragments) = many0(fragment)(input)?;
233
234    Ok((input, FormatString::from_fragments(&fragments)))
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn type_parses_correctly() {
243        assert_eq!(style(""), Ok(("", Style::None)));
244        assert_eq!(style("?"), Ok(("", Style::Debug)));
245        assert_eq!(style("x?"), Ok(("", Style::HexDebug)));
246        assert_eq!(style("X?"), Ok(("", Style::UpperHexDebug)));
247        assert_eq!(style("o"), Ok(("", Style::Octal)));
248    }
249
250    #[test]
251    fn flags_prase_correctly() {
252        assert_eq!(
253            flags("0"),
254            Ok(("", vec![Flag::LeadingZeros].into_iter().collect()))
255        );
256        assert_eq!(
257            flags("-"),
258            Ok(("", vec![Flag::LeftJustify].into_iter().collect()))
259        );
260        assert_eq!(
261            flags("+"),
262            Ok(("", vec![Flag::ForceSign].into_iter().collect()))
263        );
264        assert_eq!(
265            flags("#"),
266            Ok(("", vec![Flag::AlternateSyntax].into_iter().collect()))
267        );
268
269        assert_eq!(
270            flags("+#0"),
271            Ok((
272                "",
273                vec![Flag::ForceSign, Flag::AlternateSyntax, Flag::LeadingZeros]
274                    .into_iter()
275                    .collect()
276            ))
277        );
278
279        // Unlike printf ` ` is not a valid flag char.
280        assert_eq!(flags(" "), Ok((" ", HashSet::new())));
281    }
282
283    #[test]
284    fn alignment_parses_correctly() {
285        // Defaults to no alignment.
286        assert_eq!(alignment(""), Ok(("", (' ', Alignment::None))));
287
288        // Alignments w/o fill characters default to space fill.
289        assert_eq!(alignment("<"), Ok(("", (' ', Alignment::Left))));
290        assert_eq!(alignment("^"), Ok(("", (' ', Alignment::Center))));
291        assert_eq!(alignment(">"), Ok(("", (' ', Alignment::Right))));
292
293        // Alignments with fill characters.
294        assert_eq!(alignment("-<"), Ok(("", ('-', Alignment::Left))));
295        assert_eq!(alignment("-^"), Ok(("", ('-', Alignment::Center))));
296        assert_eq!(alignment("->"), Ok(("", ('-', Alignment::Right))));
297
298        // Alignments with alignment characters as fill characters.
299        assert_eq!(alignment("><"), Ok(("", ('>', Alignment::Left))));
300        assert_eq!(alignment("^^"), Ok(("", ('^', Alignment::Center))));
301        assert_eq!(alignment("<>"), Ok(("", ('<', Alignment::Right))));
302
303        // Non-alignment characters are not parsed and defaults to no alignment.
304        assert_eq!(alignment("1234"), Ok(("1234", (' ', Alignment::None))));
305    }
306
307    #[test]
308    fn empty_conversion_spec_has_sensible_defaults() {
309        assert_eq!(
310            conversion("{}"),
311            Ok((
312                "",
313                ConversionSpec {
314                    argument: Argument::None,
315                    fill: ' ',
316                    alignment: Alignment::None,
317                    flags: HashSet::new(),
318                    min_field_width: MinFieldWidth::None,
319                    precision: Precision::None,
320                    length: None,
321                    primitive: Primitive::Untyped,
322                    style: Style::None,
323                }
324            ))
325        );
326    }
327}