Skip to main content

luau_printf/
printf_impl.rs

1/** Luau printf-compatible implementation, based on musl. */
2use super::arg::Arg;
3use super::fmt_fp::format_float;
4use super::locale::Locale;
5use bstr::{BStr, ByteSlice as _};
6use std::io::{self, Write as IoWrite};
7use std::mem;
8use std::result::Result;
9
10/// Possible errors from printf.
11#[derive(Debug, PartialEq, Eq)]
12pub enum Error {
13    /// Invalid format string.
14    BadFormatString,
15    /// Too few arguments.
16    MissingArg,
17    /// Argument type doesn't match format specifier.
18    BadArgType,
19    /// Precision is too large to represent.
20    Overflow,
21    /// Error emitted by the output stream.
22    Io(io::ErrorKind),
23}
24
25// Convenience conversion from io::Error.
26impl From<io::Error> for Error {
27    fn from(err: io::Error) -> Error {
28        Error::Io(err.kind())
29    }
30}
31
32#[derive(Debug, Copy, Clone, Default)]
33pub(super) struct ModifierFlags {
34    pub alt_form: bool, // #
35    pub zero_pad: bool, // 0
36    pub left_adj: bool, // negative field width
37    pub pad_pos: bool,  // space: blank before positive numbers
38    pub mark_pos: bool, // +: sign before positive numbers
39    pub grouped: bool,  // ': group indicator
40}
41
42impl ModifierFlags {
43    // If c is a modifier byte, set the flag and return true.
44    // Otherwise return false. Note we allow repeated modifier flags.
45    fn try_set(&mut self, c: u8) -> bool {
46        match c {
47            b'#' => self.alt_form = true,
48            b'0' => self.zero_pad = true,
49            b'-' => self.left_adj = true,
50            b' ' => self.pad_pos = true,
51            b'+' => self.mark_pos = true,
52            b'\'' => self.grouped = true,
53            _ => return false,
54        }
55        true
56    }
57}
58
59// The set of prefixes of conversion specifiers.
60// Note that we mostly ignore prefixes - we take sizes of values from the arguments themselves.
61#[derive(Debug, Copy, Clone, PartialEq, Eq)]
62#[allow(non_camel_case_types)]
63enum ConversionPrefix {
64    Empty,
65    hh,
66    h,
67    l,
68    ll,
69    j,
70    t,
71    z,
72    L,
73}
74
75#[derive(Debug, Copy, Clone, PartialEq, Eq)]
76#[allow(non_camel_case_types)]
77#[rustfmt::skip]
78pub(super) enum ConversionSpec {
79    // Integers, with prefixes "hh", "h", "l", "ll", "j", "t", "z"
80    // Note that we treat '%i' as '%d'.
81    d, o, u, x, X,
82
83    // USizeRef receiver, with same prefixes as ints
84    n,
85
86    // Float, with prefixes "l" and "L"
87    a, A, e, E, f, F, g, G,
88
89    // Pointer, no prefixes
90    p,
91
92    // Narrow byte or C string.
93    c, s,
94}
95
96impl ConversionSpec {
97    // Returns true if the given prefix is supported by this conversion specifier.
98    fn supports_prefix(self, prefix: ConversionPrefix) -> bool {
99        use ConversionPrefix::*;
100        use ConversionSpec::*;
101        if matches!(prefix, Empty) {
102            // No prefix is always supported.
103            return true;
104        }
105        match self {
106            d | o | u | x | X | n => matches!(prefix, hh | h | l | ll | j | t | z),
107            a | A | e | E | f | F | g | G => matches!(prefix, l | L),
108            p => false,
109            c | s => false,
110        }
111    }
112
113    // Returns true if the conversion specifier is lowercase,
114    // which affects certain rendering.
115    #[inline]
116    pub(super) fn is_lower(self) -> bool {
117        use ConversionSpec::*;
118        match self {
119            d | o | u | x | n | a | e | f | g | p | c | s => true,
120            X | A | E | F | G => false,
121        }
122    }
123
124    // Returns a ConversionSpec from a byte, or None if none.
125    fn from_byte(cc: u8) -> Option<Self> {
126        use ConversionSpec::*;
127        let res = match cc {
128            b'd' | b'i' => d,
129            b'o' => o,
130            b'u' => u,
131            b'x' => x,
132            b'X' => X,
133            b'n' => n,
134            b'a' => a,
135            b'A' => A,
136            b'e' => e,
137            b'E' => E,
138            b'f' => f,
139            b'F' => F,
140            b'g' => g,
141            b'G' => G,
142            b'p' => p,
143            b'c' => c,
144            b's' => s,
145            _ => return None,
146        };
147        Some(res)
148    }
149}
150
151trait FormatString<'a> {
152    fn is_empty(&self) -> bool;
153    fn at(&self, index: usize) -> Option<u8>;
154    fn advance_by(&mut self, n: usize);
155    fn take_literal(&mut self) -> &'a BStr;
156}
157
158impl<'a> FormatString<'a> for &'a BStr {
159    fn is_empty(&self) -> bool {
160        self.len() == 0
161    }
162
163    fn at(&self, index: usize) -> Option<u8> {
164        self.get(index).copied()
165    }
166
167    fn advance_by(&mut self, n: usize) {
168        debug_assert!(
169            n <= self.len(),
170            "FormatString::advance_by(): index out of bounds"
171        );
172        *self = self[n..].as_bstr();
173    }
174
175    fn take_literal(&mut self) -> &'a BStr {
176        let non_percents: usize = self.iter().take_while(|&&c| c != b'%').count();
177        // Take only an even number of percents. Note we know these have byte length 1.
178        let percent_pairs = self[non_percents..]
179            .iter()
180            .take_while(|&&c| c == b'%')
181            .count()
182            / 2;
183        let (prefix, rest) = self.split_at(non_percents + percent_pairs * 2);
184        *self = rest.as_bstr();
185        // Trim half of the trailing percent characters from the prefix.
186        prefix[..prefix.len() - percent_pairs].as_bstr()
187    }
188}
189
190// Read an int from a format string, stopping at the first non-digit.
191// Negative values are not supported.
192// If there are no digits, return 0.
193// Adjust the format string to point to the char after the int.
194fn get_int<'a>(fmt: &mut impl FormatString<'a>) -> Result<usize, Error> {
195    use Error::Overflow;
196    let mut i: usize = 0;
197    while let Some(digit) = fmt.at(0).and_then(|c| {
198        if c.is_ascii_digit() {
199            Some(c - b'0')
200        } else {
201            None
202        }
203    }) {
204        i = i.checked_mul(10).ok_or(Overflow)?;
205        i = i.checked_add(usize::from(digit)).ok_or(Overflow)?;
206        fmt.advance_by(1);
207    }
208    Ok(i)
209}
210
211// Read a conversion prefix from a format string, advancing it.
212fn get_prefix<'a>(fmt: &mut impl FormatString<'a>) -> ConversionPrefix {
213    use ConversionPrefix as CP;
214    let prefix = match fmt.at(0).unwrap_or(b'\0') {
215        b'h' if fmt.at(1) == Some(b'h') => CP::hh,
216        b'h' => CP::h,
217        b'l' if fmt.at(1) == Some(b'l') => CP::ll,
218        b'l' => CP::l,
219        b'j' => CP::j,
220        b't' => CP::t,
221        b'z' => CP::z,
222        b'L' => CP::L,
223        _ => CP::Empty,
224    };
225    fmt.advance_by(match prefix {
226        CP::Empty => 0,
227        CP::hh | CP::ll => 2,
228        _ => 1,
229    });
230    prefix
231}
232
233// Read an (optionally prefixed) format specifier, such as d, Lf, etc.
234// Adjust the cursor to point to the char after the specifier.
235fn get_specifier<'a>(fmt: &mut impl FormatString<'a>) -> Result<ConversionSpec, Error> {
236    let prefix = get_prefix(fmt);
237    let spec = fmt
238        .at(0)
239        .and_then(ConversionSpec::from_byte)
240        .ok_or(Error::BadFormatString)?;
241    if !spec.supports_prefix(prefix) {
242        return Err(Error::BadFormatString);
243    }
244    fmt.advance_by(1);
245    Ok(spec)
246}
247
248fn c_string_prefix(fmt: &BStr) -> &BStr {
249    let len = fmt.iter().position(|&c| c == b'\0').unwrap_or(fmt.len());
250    fmt[..len].as_bstr()
251}
252
253fn check_printf_count(count: usize) -> Result<usize, Error> {
254    if count > i32::MAX as usize {
255        return Err(Error::Overflow);
256    }
257    Ok(count)
258}
259
260fn add_printf_count(count: usize, add: usize) -> Result<usize, Error> {
261    check_printf_count(count.checked_add(add).ok_or(Error::Overflow)?)
262}
263
264pub(crate) trait FormatSink {
265    fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), Error>;
266
267    fn write_repeat(&mut self, byte: u8, count: usize) -> Result<(), Error> {
268        assert!(matches!(byte, b'0' | b' '));
269        const ZEROS: &[u8] = b"0000000000000000";
270        const SPACES: &[u8] = b"                ";
271        let bytes = if byte == b'0' { ZEROS } else { SPACES };
272        let mut remaining = count;
273        while remaining > 0 {
274            let size = remaining.min(bytes.len());
275            self.write_bytes(&bytes[..size])?;
276            remaining -= size;
277        }
278        Ok(())
279    }
280}
281
282struct IoSink<'a, W: IoWrite + ?Sized> {
283    output: &'a mut W,
284}
285
286impl<W: IoWrite + ?Sized> FormatSink for IoSink<'_, W> {
287    fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), Error> {
288        self.output.write_all(bytes)?;
289        Ok(())
290    }
291}
292
293struct SliceSink<'a> {
294    buffer: &'a mut [u8],
295    len: usize,
296}
297
298impl FormatSink for SliceSink<'_> {
299    fn write_bytes(&mut self, bytes: &[u8]) -> Result<(), Error> {
300        let remaining = self.buffer.len().saturating_sub(self.len);
301        let stored = remaining.min(bytes.len());
302        if stored != 0 {
303            self.buffer[self.len..self.len + stored].copy_from_slice(&bytes[..stored]);
304            self.len += stored;
305        }
306        Ok(())
307    }
308
309    fn write_repeat(&mut self, byte: u8, count: usize) -> Result<(), Error> {
310        assert!(matches!(byte, b'0' | b' '));
311        let remaining = self.buffer.len().saturating_sub(self.len);
312        let stored = remaining.min(count);
313        if stored != 0 {
314            self.buffer[self.len..self.len + stored].fill(byte);
315            self.len += stored;
316        }
317        Ok(())
318    }
319}
320
321pub fn printf_locale_to_slice(
322    buffer: &mut [u8],
323    fmt: &BStr,
324    locale: &Locale,
325    args: &mut [Arg],
326) -> Result<usize, Error> {
327    let mut sink = SliceSink { buffer, len: 0 };
328    format_locale(&mut sink, fmt, locale, args)
329}
330
331// Pad output by emitting `c` until `min_width` is reached.
332pub(super) fn pad(
333    f: &mut (impl FormatSink + ?Sized),
334    c: u8,
335    min_width: usize,
336    current_width: usize,
337) -> Result<(), Error> {
338    assert!(matches!(c, b'0' | b' '));
339    if current_width >= min_width {
340        return Ok(());
341    }
342    f.write_repeat(c, min_width - current_width)
343}
344
345fn format_unsigned_digits(
346    storage: &mut [u8; 64],
347    mut value: u64,
348    radix: u64,
349    uppercase: bool,
350) -> &[u8] {
351    debug_assert!(matches!(radix, 8 | 10 | 16));
352    debug_assert_ne!(value, 0);
353
354    let digits = if uppercase {
355        b"0123456789ABCDEF"
356    } else {
357        b"0123456789abcdef"
358    };
359    let mut index = storage.len();
360
361    while value != 0 {
362        index -= 1;
363        storage[index] = digits[(value % radix) as usize];
364        value /= radix;
365    }
366
367    &storage[index..]
368}
369
370/// Formats a byte string using the provided format specifiers, arguments, and locale.
371///
372/// # Parameters
373/// - `f`: The receiver of formatted output.
374/// - `fmt`: The format string being parsed.
375/// - `locale`: The locale to use for number formatting.
376/// - `args`: Iterator over the arguments to format.
377///
378/// # Returns
379/// A `Result` which is `Ok` containing the number of bytes written on success, or an `Error`.
380///
381/// # Example
382///
383/// ```
384/// use luau_printf::{locale, sprintf_locale, ToArg};
385///
386/// let mut output = Vec::new();
387/// let fmt = luau_printf::BStr::new("%'0.2f");
388/// let mut args = [1234567.89_f64.to_arg()];
389///
390/// let result = sprintf_locale(&mut output, fmt, &locale::EN_US_LOCALE, &mut args);
391///
392/// assert_eq!(result, Ok(12));
393/// assert_eq!(output.as_slice(), b"1,234,567.89");
394/// ```
395pub fn sprintf_locale<W: IoWrite + ?Sized>(
396    f: &mut W,
397    fmt: &BStr,
398    locale: &Locale,
399    args: &mut [Arg],
400) -> Result<usize, Error> {
401    let mut sink = IoSink { output: f };
402    format_locale(&mut sink, fmt, locale, args)
403}
404
405fn format_locale(
406    f: &mut (impl FormatSink + ?Sized),
407    fmt: &BStr,
408    locale: &Locale,
409    args: &mut [Arg],
410) -> Result<usize, Error> {
411    use ConversionSpec as CS;
412    let mut s = c_string_prefix(fmt);
413    let mut args = args.iter_mut();
414    let mut out_len: usize = 0;
415    let mut float_buf = None;
416    'main: while !s.is_empty() {
417        // Handle literal text and %% format specifiers.
418        let lit = s.take_literal();
419        if !lit.is_empty() {
420            f.write_bytes(lit.as_ref())?;
421            out_len = add_printf_count(out_len, lit.len())?;
422            continue 'main;
423        }
424
425        // Consume the % at the start of the format specifier.
426        debug_assert_eq!(s.at(0), Some(b'%'));
427        s.advance_by(1);
428
429        // Read modifier flags. '-' and '0' flags are mutually exclusive.
430        let mut flags = ModifierFlags::default();
431        while flags.try_set(s.at(0).unwrap_or(b'\0')) {
432            s.advance_by(1);
433        }
434        if flags.left_adj {
435            flags.zero_pad = false;
436        }
437
438        // Read field width. We do not support $.
439        let desired_width = if s.at(0) == Some(b'*') {
440            let arg_width = args.next().ok_or(Error::MissingArg)?.as_sint()?;
441            s.advance_by(1);
442            if arg_width < 0 {
443                flags.left_adj = true;
444            }
445            arg_width
446                .unsigned_abs()
447                .try_into()
448                .map_err(|_| Error::Overflow)?
449        } else {
450            get_int(&mut s)?
451        };
452        check_printf_count(desired_width)?;
453
454        // Optionally read precision. We do not support $.
455        let mut desired_precision: Option<usize> = if s.at(0) == Some(b'.') && s.at(1) == Some(b'*')
456        {
457            // "A negative precision is treated as though it were missing."
458            // Here we assume the precision is always signed.
459            s.advance_by(2);
460            let p = args.next().ok_or(Error::MissingArg)?.as_sint()?;
461            p.try_into().ok()
462        } else if s.at(0) == Some(b'.') {
463            s.advance_by(1);
464            Some(get_int(&mut s)?)
465        } else {
466            None
467        };
468        if let Some(precision) = desired_precision {
469            check_printf_count(precision)?;
470        }
471
472        // Read out the format specifier and arg.
473        let conv_spec = get_specifier(&mut s)?;
474        let arg = args.next().ok_or(Error::MissingArg)?;
475        let mut prefix = b"".as_slice();
476
477        // Thousands grouping only works for d,u,i,f,F.
478        // 'i' is mapped to 'd'.
479        if flags.grouped && !matches!(conv_spec, CS::d | CS::u | CS::f | CS::F) {
480            return Err(Error::BadFormatString);
481        }
482
483        // Disable zero-pad if we have an explicit precision.
484        // "If a precision is given with a numeric conversion (d, i, o, u, i, x, and X),
485        // the 0 flag is ignored." p is included here.
486        let spec_is_numeric = matches!(conv_spec, CS::d | CS::u | CS::o | CS::p | CS::x | CS::X);
487        if spec_is_numeric && desired_precision.is_some() {
488            flags.zero_pad = false;
489        }
490
491        // Apply the formatting. Some cases continue the main loop.
492        // Note that numeric conversions must leave 'body' empty if the value is 0.
493        let mut body_storage = [0u8; 64];
494        let body = match conv_spec {
495            CS::n => {
496                arg.set_count(out_len)?;
497                continue 'main;
498            }
499            CS::e | CS::f | CS::g | CS::a | CS::E | CS::F | CS::G | CS::A => {
500                // Floating point types handle output on their own.
501                let float = arg.as_float()?;
502                let buf = float_buf.get_or_insert_with(|| Vec::with_capacity(64));
503                buf.clear();
504                let len = format_float(
505                    f,
506                    float,
507                    desired_width,
508                    desired_precision,
509                    flags,
510                    locale,
511                    conv_spec,
512                    buf,
513                )?;
514                out_len = add_printf_count(out_len, len)?;
515                continue 'main;
516            }
517            CS::p => {
518                const PTR_HEX_DIGITS: usize = 2 * mem::size_of::<*const u8>();
519                desired_precision = desired_precision.map(|p| p.max(PTR_HEX_DIGITS));
520                let uint = arg.as_uint()?;
521                if uint == 0 {
522                    &[][..]
523                } else {
524                    prefix = b"0x";
525                    format_unsigned_digits(&mut body_storage, uint, 16, false)
526                }
527            }
528            CS::x | CS::X => {
529                // If someone passes us a negative value, format it with the width
530                // we were given.
531                let lower = conv_spec.is_lower();
532                let uint = arg.as_wrapping_sint()?;
533                if uint == 0 {
534                    &[][..]
535                } else {
536                    if flags.alt_form {
537                        prefix = if lower { b"0x" } else { b"0X" };
538                    }
539                    format_unsigned_digits(&mut body_storage, uint, 16, !lower)
540                }
541            }
542            CS::o => {
543                let uint = arg.as_uint()?;
544                let body = if uint == 0 {
545                    &[][..]
546                } else {
547                    format_unsigned_digits(&mut body_storage, uint, 8, false)
548                };
549                if flags.alt_form && desired_precision.unwrap_or(0) <= body.len() + 1 {
550                    desired_precision = Some(body.len() + 1);
551                }
552                body
553            }
554            CS::u => {
555                let uint = arg.as_uint()?;
556                if uint == 0 {
557                    &[][..]
558                } else {
559                    format_unsigned_digits(&mut body_storage, uint, 10, false)
560                }
561            }
562            CS::d => {
563                let arg_i = arg.as_sint()?;
564                if arg_i < 0 {
565                    prefix = b"-";
566                } else if flags.mark_pos {
567                    prefix = b"+";
568                } else if flags.pad_pos {
569                    prefix = b" ";
570                }
571                if arg_i == 0 {
572                    &[][..]
573                } else {
574                    format_unsigned_digits(&mut body_storage, arg_i.unsigned_abs(), 10, false)
575                }
576            }
577            CS::c => {
578                flags.zero_pad = false;
579                body_storage[0] = arg.as_uchar()?;
580                &body_storage[..1]
581            }
582            CS::s => {
583                let s = arg.as_bstr()?;
584                flags.zero_pad = false;
585                let scan_limit =
586                    desired_precision.map_or(s.len(), |precision| precision.min(s.len()));
587                let len = s[..scan_limit]
588                    .iter()
589                    .position(|&c| c == b'\0')
590                    .unwrap_or(scan_limit);
591                desired_precision = Some(len);
592                &s[..len]
593            }
594        };
595        // Numeric output should be empty iff the value is 0.
596        if spec_is_numeric && body.is_empty() {
597            debug_assert_eq!(arg.as_uint().unwrap(), 0);
598        }
599
600        // Decide if we want to apply thousands grouping to the body, and compute its size.
601        // Note we have already errored out if grouped is set and this is non-numeric.
602        let wants_grouping = flags.grouped && locale.thousands_sep.is_some();
603        let body_width = match wants_grouping {
604            // We assume that text representing numbers is ASCII, so len == width.
605            true => body.len() + locale.separator_count(body.len()),
606            false => body.len(),
607        };
608
609        // Resolve the precision.
610        // In the case of a non-numeric conversion, update the precision to at least the
611        // length of the string.
612        let desired_precision = if !spec_is_numeric {
613            desired_precision.unwrap_or(body_width)
614        } else {
615            desired_precision.unwrap_or(1).max(body_width)
616        };
617
618        let prefix_width = prefix.len();
619        let unpadded_width = prefix_width
620            .checked_add(desired_precision)
621            .ok_or(Error::Overflow)?;
622        let width = desired_width.max(unpadded_width);
623
624        // Pad on the left with spaces to the desired width?
625        if !flags.left_adj && !flags.zero_pad {
626            pad(f, b' ', width, unpadded_width)?;
627        }
628
629        // Output any prefix.
630        f.write_bytes(prefix)?;
631
632        // Pad after the prefix with zeros to the desired width?
633        if !flags.left_adj && flags.zero_pad {
634            pad(f, b'0', width, unpadded_width)?;
635        }
636
637        // Pad on the left to the given precision?
638        // TODO: why pad with 0 here?
639        pad(f, b'0', desired_precision, body_width)?;
640
641        // Output the actual value, perhaps with grouping.
642        if wants_grouping {
643            f.write_bytes(&locale.apply_grouping(body))?;
644        } else {
645            f.write_bytes(body)?;
646        }
647
648        // Pad on the right with spaces if we are left adjusted?
649        if flags.left_adj {
650            pad(f, b' ', width, unpadded_width)?;
651        }
652
653        out_len = add_printf_count(out_len, width)?;
654    }
655
656    Ok(out_len)
657}