Skip to main content

coreutils_rs/printf/
core.rs

1/// GNU coreutils-compatible printf implementation.
2///
3/// Processes a printf format string with the given arguments, returning the
4/// raw output bytes. The format string is reused if there are more arguments
5/// than a single pass consumes.
6
7/// Sentinel returned inside `process_format_string` when `\c` is encountered.
8const STOP_OUTPUT: u8 = 0xFF;
9
10use std::cell::Cell;
11
12thread_local! {
13    /// Set to true when a numeric conversion warning occurs (invalid argument).
14    static CONV_ERROR: Cell<bool> = const { Cell::new(false) };
15}
16
17/// Reset conversion error flag. Call before processing a format string.
18pub fn reset_conv_error() {
19    CONV_ERROR.with(|c| c.set(false));
20}
21
22/// Returns true if a conversion warning occurred since last reset.
23pub fn had_conv_error() -> bool {
24    CONV_ERROR.with(|c| c.get())
25}
26
27fn mark_conv_error(s: &str) {
28    eprintln!("printf: '{}': expected a numeric value", s);
29    CONV_ERROR.with(|c| c.set(true));
30}
31
32fn mark_range_error(s: &str) {
33    eprintln!("printf: '{}': Numerical result out of range", s);
34    CONV_ERROR.with(|c| c.set(true));
35}
36
37/// Process a printf format string with the given arguments, returning raw bytes.
38///
39/// The format string repeats if there are more arguments than one pass consumes.
40/// Processing stops immediately when `\c` is encountered (in the format string
41/// itself or inside a `%b` argument).
42pub fn process_format_string(format: &str, args: &[&str]) -> Vec<u8> {
43    let mut output = Vec::with_capacity(256);
44    let fmt_bytes = format.as_bytes();
45
46    if args.is_empty() {
47        // Single pass with no arguments
48        let stop = format_one_pass(fmt_bytes, args, &mut 0, &mut output);
49        if stop {
50            // remove trailing STOP_OUTPUT sentinel if present
51            if output.last() == Some(&STOP_OUTPUT) {
52                output.pop();
53            }
54        }
55        return output;
56    }
57
58    let mut arg_idx: usize = 0;
59    loop {
60        let start_idx = arg_idx;
61        let stop = format_one_pass(fmt_bytes, args, &mut arg_idx, &mut output);
62        if stop {
63            if output.last() == Some(&STOP_OUTPUT) {
64                output.pop();
65            }
66            break;
67        }
68        // If no arguments were consumed, or we've used them all, stop
69        if arg_idx == start_idx || arg_idx >= args.len() {
70            break;
71        }
72    }
73
74    output
75}
76
77/// Run one pass of the format string. Returns `true` if output should stop (`\c`).
78/// `arg_idx` is advanced as arguments are consumed.
79fn format_one_pass(fmt: &[u8], args: &[&str], arg_idx: &mut usize, output: &mut Vec<u8>) -> bool {
80    let mut i = 0;
81    while i < fmt.len() {
82        match fmt[i] {
83            b'%' => {
84                i += 1;
85                if i >= fmt.len() {
86                    output.push(b'%');
87                    break;
88                }
89                if fmt[i] == b'%' {
90                    output.push(b'%');
91                    i += 1;
92                    continue;
93                }
94                let stop = process_conversion(fmt, &mut i, args, arg_idx, output);
95                if stop {
96                    return true;
97                }
98            }
99            b'\\' => {
100                i += 1;
101                let stop = process_format_escape(fmt, &mut i, output);
102                if stop {
103                    return true;
104                }
105            }
106            ch => {
107                output.push(ch);
108                i += 1;
109            }
110        }
111    }
112    false
113}
114
115/// Process a conversion specifier (the part after `%`).
116/// `i` points to the first character after `%`. Returns true if `\c` stop was hit.
117fn process_conversion(
118    fmt: &[u8],
119    i: &mut usize,
120    args: &[&str],
121    arg_idx: &mut usize,
122    output: &mut Vec<u8>,
123) -> bool {
124    // Parse flags
125    let mut flags = FormatFlags::default();
126    while *i < fmt.len() {
127        match fmt[*i] {
128            b'-' => flags.left_align = true,
129            b'+' => flags.plus_sign = true,
130            b' ' => flags.space_sign = true,
131            b'0' => flags.zero_pad = true,
132            b'#' => flags.alternate = true,
133            _ => break,
134        }
135        *i += 1;
136    }
137
138    // Parse width (may be '*' for dynamic width from args)
139    let (width, dyn_left_align) = if *i < fmt.len() && fmt[*i] == b'*' {
140        *i += 1;
141        let width_arg = consume_arg(args, arg_idx);
142        let w: i64 = width_arg.parse().unwrap_or(0);
143        if w < 0 {
144            ((-w) as usize, true) // negative width → left-align
145        } else {
146            (w as usize, false)
147        }
148    } else {
149        (parse_decimal(fmt, i), false)
150    };
151    if dyn_left_align {
152        flags.left_align = true;
153    }
154
155    // Parse precision (may be '*' for dynamic precision from args)
156    let precision = if *i < fmt.len() && fmt[*i] == b'.' {
157        *i += 1;
158        if *i < fmt.len() && fmt[*i] == b'*' {
159            *i += 1;
160            let prec_arg = consume_arg(args, arg_idx);
161            let p: i64 = prec_arg.parse().unwrap_or(0);
162            Some(if p < 0 { 0 } else { p as usize })
163        } else {
164            Some(parse_decimal(fmt, i))
165        }
166    } else {
167        None
168    };
169
170    // Parse conversion character
171    if *i >= fmt.len() {
172        return false;
173    }
174    let conv = fmt[*i];
175    *i += 1;
176
177    let arg = consume_arg(args, arg_idx);
178
179    match conv {
180        b's' => {
181            let s = arg;
182            let formatted = apply_string_format(s, &flags, width, precision);
183            output.extend_from_slice(&formatted);
184        }
185        b'b' => {
186            let (bytes, stop) = process_b_argument(arg);
187            let formatted = apply_string_format_bytes(&bytes, &flags, width, precision);
188            output.extend_from_slice(&formatted);
189            if stop {
190                return true;
191            }
192        }
193        b'c' => {
194            if let Some(ch) = arg.chars().next() {
195                let mut buf = [0u8; 4];
196                let encoded = ch.encode_utf8(&mut buf);
197                let formatted = apply_string_format(encoded, &flags, width, precision);
198                output.extend_from_slice(&formatted);
199            } else {
200                // empty arg: output a NUL byte (GNU compat)
201                let formatted = apply_string_format_bytes(&[0], &flags, width, precision);
202                output.extend_from_slice(&formatted);
203            }
204        }
205        b'd' | b'i' => {
206            let val = parse_integer(arg);
207            let mut buf = itoa::Buffer::new();
208            let s = buf.format(val);
209            let formatted = apply_numeric_format(s, val < 0, &flags, width, precision);
210            output.extend_from_slice(formatted.as_bytes());
211        }
212        b'u' => {
213            let val = parse_unsigned(arg);
214            let mut buf = itoa::Buffer::new();
215            let s = buf.format(val);
216            let formatted = apply_numeric_format(s, false, &flags, width, precision);
217            output.extend_from_slice(formatted.as_bytes());
218        }
219        b'o' => {
220            let val = parse_unsigned(arg);
221            let s = format!("{:o}", val);
222            let prefix = if flags.alternate && !s.starts_with('0') {
223                "0"
224            } else {
225                ""
226            };
227            let full = format!("{}{}", prefix, s);
228            let formatted = apply_numeric_format(&full, false, &flags, width, precision);
229            output.extend_from_slice(formatted.as_bytes());
230        }
231        b'x' => {
232            let val = parse_unsigned(arg);
233            let s = format!("{:x}", val);
234            let prefix = if flags.alternate && val != 0 {
235                "0x"
236            } else {
237                ""
238            };
239            let full = format!("{}{}", prefix, s);
240            let formatted = apply_numeric_format(&full, false, &flags, width, precision);
241            output.extend_from_slice(formatted.as_bytes());
242        }
243        b'X' => {
244            let val = parse_unsigned(arg);
245            let s = format!("{:X}", val);
246            let prefix = if flags.alternate && val != 0 {
247                "0X"
248            } else {
249                ""
250            };
251            let full = format!("{}{}", prefix, s);
252            let formatted = apply_numeric_format(&full, false, &flags, width, precision);
253            output.extend_from_slice(formatted.as_bytes());
254        }
255        b'f' => {
256            let val = parse_float(arg);
257            let prec = precision.unwrap_or(6);
258            let s = format!("{:.prec$}", val, prec = prec);
259            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
260            output.extend_from_slice(formatted.as_bytes());
261        }
262        b'e' => {
263            let val = parse_float(arg);
264            let prec = precision.unwrap_or(6);
265            let s = format_scientific(val, prec, 'e');
266            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
267            output.extend_from_slice(formatted.as_bytes());
268        }
269        b'E' => {
270            let val = parse_float(arg);
271            let prec = precision.unwrap_or(6);
272            let s = format_scientific(val, prec, 'E');
273            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
274            output.extend_from_slice(formatted.as_bytes());
275        }
276        b'g' => {
277            let val = parse_float(arg);
278            let prec = precision.unwrap_or(6);
279            let s = format_g(val, prec, false);
280            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
281            output.extend_from_slice(formatted.as_bytes());
282        }
283        b'G' => {
284            let val = parse_float(arg);
285            let prec = precision.unwrap_or(6);
286            let s = format_g(val, prec, true);
287            let formatted = apply_float_format(&s, val < 0.0, &flags, width);
288            output.extend_from_slice(formatted.as_bytes());
289        }
290        b'q' => {
291            let s = arg;
292            let quoted = shell_quote(s);
293            let formatted = apply_string_format(&quoted, &flags, width, precision);
294            output.extend_from_slice(&formatted);
295        }
296        _ => {
297            // Unknown conversion: output literally
298            output.push(b'%');
299            output.push(conv);
300        }
301    }
302    false
303}
304
305/// Consume the next argument, returning "" if exhausted.
306fn consume_arg<'a>(args: &[&'a str], arg_idx: &mut usize) -> &'a str {
307    if *arg_idx < args.len() {
308        let val = args[*arg_idx];
309        *arg_idx += 1;
310        val
311    } else {
312        ""
313    }
314}
315
316/// Process an escape sequence in the format string.
317/// `i` points to the character after `\`. Returns true if `\c` was encountered.
318fn process_format_escape(fmt: &[u8], i: &mut usize, output: &mut Vec<u8>) -> bool {
319    if *i >= fmt.len() {
320        output.push(b'\\');
321        return false;
322    }
323    match fmt[*i] {
324        b'\\' => {
325            output.push(b'\\');
326            *i += 1;
327        }
328        b'"' => {
329            output.push(b'"');
330            *i += 1;
331        }
332        b'a' => {
333            output.push(0x07);
334            *i += 1;
335        }
336        b'b' => {
337            output.push(0x08);
338            *i += 1;
339        }
340        b'c' => {
341            return true;
342        }
343        b'e' | b'E' => {
344            output.push(0x1B);
345            *i += 1;
346        }
347        b'f' => {
348            output.push(0x0C);
349            *i += 1;
350        }
351        b'n' => {
352            output.push(b'\n');
353            *i += 1;
354        }
355        b'r' => {
356            output.push(b'\r');
357            *i += 1;
358        }
359        b't' => {
360            output.push(b'\t');
361            *i += 1;
362        }
363        b'v' => {
364            output.push(0x0B);
365            *i += 1;
366        }
367        b'0' => {
368            // Octal: \0NNN (up to 3 octal digits after the leading 0)
369            *i += 1;
370            let val = parse_octal_digits(fmt, i, 3);
371            output.push(val);
372        }
373        b'1'..=b'7' => {
374            // Octal: \NNN (up to 3 octal digits)
375            let val = parse_octal_digits(fmt, i, 3);
376            output.push(val);
377        }
378        b'x' => {
379            *i += 1;
380            let val = parse_hex_digits(fmt, i, 2);
381            output.push(val as u8);
382        }
383        b'u' => {
384            *i += 1;
385            let val = parse_hex_digits(fmt, i, 4);
386            if let Some(ch) = char::from_u32(val) {
387                let mut buf = [0u8; 4];
388                let encoded = ch.encode_utf8(&mut buf);
389                output.extend_from_slice(encoded.as_bytes());
390            }
391        }
392        b'U' => {
393            *i += 1;
394            let val = parse_hex_digits(fmt, i, 8);
395            if let Some(ch) = char::from_u32(val) {
396                let mut buf = [0u8; 4];
397                let encoded = ch.encode_utf8(&mut buf);
398                output.extend_from_slice(encoded.as_bytes());
399            }
400        }
401        _ => {
402            // Unknown escape: output backslash and the character
403            output.push(b'\\');
404            output.push(fmt[*i]);
405            *i += 1;
406        }
407    }
408    false
409}
410
411/// Process backslash escapes in a %b argument string.
412/// Returns (bytes, stop) where stop is true if \c was found.
413fn process_b_argument(arg: &str) -> (Vec<u8>, bool) {
414    let bytes = arg.as_bytes();
415    let mut output = Vec::with_capacity(bytes.len());
416    let mut i = 0;
417    while i < bytes.len() {
418        if bytes[i] == b'\\' {
419            i += 1;
420            if i >= bytes.len() {
421                output.push(b'\\');
422                break;
423            }
424            match bytes[i] {
425                b'\\' => {
426                    output.push(b'\\');
427                    i += 1;
428                }
429                b'a' => {
430                    output.push(0x07);
431                    i += 1;
432                }
433                b'b' => {
434                    output.push(0x08);
435                    i += 1;
436                }
437                b'c' => {
438                    return (output, true);
439                }
440                b'e' | b'E' => {
441                    output.push(0x1B);
442                    i += 1;
443                }
444                b'f' => {
445                    output.push(0x0C);
446                    i += 1;
447                }
448                b'n' => {
449                    output.push(b'\n');
450                    i += 1;
451                }
452                b'r' => {
453                    output.push(b'\r');
454                    i += 1;
455                }
456                b't' => {
457                    output.push(b'\t');
458                    i += 1;
459                }
460                b'v' => {
461                    output.push(0x0B);
462                    i += 1;
463                }
464                b'0' => {
465                    i += 1;
466                    let val = parse_octal_digits(bytes, &mut i, 3);
467                    output.push(val);
468                }
469                b'1'..=b'7' => {
470                    let val = parse_octal_digits(bytes, &mut i, 3);
471                    output.push(val);
472                }
473                b'x' => {
474                    i += 1;
475                    let val = parse_hex_digits(bytes, &mut i, 2);
476                    output.push(val as u8);
477                }
478                _ => {
479                    // In %b, unknown escapes pass through literally
480                    output.push(b'\\');
481                    output.push(bytes[i]);
482                    i += 1;
483                }
484            }
485        } else {
486            output.push(bytes[i]);
487            i += 1;
488        }
489    }
490    (output, false)
491}
492
493/// Parse up to `max_digits` octal digits from `data` starting at `*i`.
494fn parse_octal_digits(data: &[u8], i: &mut usize, max_digits: usize) -> u8 {
495    let mut val: u32 = 0;
496    let mut count = 0;
497    while *i < data.len() && count < max_digits {
498        let ch = data[*i];
499        if ch >= b'0' && ch <= b'7' {
500            val = val * 8 + (ch - b'0') as u32;
501            *i += 1;
502            count += 1;
503        } else {
504            break;
505        }
506    }
507    (val & 0xFF) as u8
508}
509
510/// Parse up to `max_digits` hex digits from `data` starting at `*i`.
511fn parse_hex_digits(data: &[u8], i: &mut usize, max_digits: usize) -> u32 {
512    let mut val: u32 = 0;
513    let mut count = 0;
514    while *i < data.len() && count < max_digits {
515        let ch = data[*i];
516        if ch.is_ascii_hexdigit() {
517            val = val * 16 + hex_digit_value(ch) as u32;
518            *i += 1;
519            count += 1;
520        } else {
521            break;
522        }
523    }
524    val
525}
526
527fn hex_digit_value(ch: u8) -> u8 {
528    match ch {
529        b'0'..=b'9' => ch - b'0',
530        b'a'..=b'f' => ch - b'a' + 10,
531        b'A'..=b'F' => ch - b'A' + 10,
532        _ => 0,
533    }
534}
535
536/// Parse a decimal integer from `data` at position `*i`.
537fn parse_decimal(data: &[u8], i: &mut usize) -> usize {
538    let mut val: usize = 0;
539    while *i < data.len() && data[*i].is_ascii_digit() {
540        val = val
541            .saturating_mul(10)
542            .saturating_add((data[*i] - b'0') as usize);
543        *i += 1;
544    }
545    val
546}
547
548/// Parse an integer argument. Supports decimal, octal (0-prefix), hex (0x-prefix),
549/// and single-character constants ('c' or "c").
550fn parse_integer(s: &str) -> i64 {
551    let s = s.trim();
552    if s.is_empty() {
553        return 0;
554    }
555
556    // Character constants: 'X or "X
557    if (s.starts_with('\'') || s.starts_with('"')) && s.len() >= 2 {
558        return s[1..].chars().next().map_or(0, |c| c as i64);
559    }
560
561    // Try to detect sign
562    let (negative, digits) = if let Some(rest) = s.strip_prefix('-') {
563        (true, rest)
564    } else if let Some(rest) = s.strip_prefix('+') {
565        (false, rest)
566    } else {
567        (false, s)
568    };
569
570    // digits must be non-empty and parseable
571    if digits.is_empty() {
572        mark_conv_error(s);
573        return 0;
574    }
575
576    let magnitude = if let Some(hex) = digits
577        .strip_prefix("0x")
578        .or_else(|| digits.strip_prefix("0X"))
579    {
580        match u64::from_str_radix(hex, 16) {
581            Ok(v) => v,
582            Err(e) if e.kind() == &std::num::IntErrorKind::PosOverflow => {
583                mark_range_error(s);
584                if negative {
585                    return i64::MIN;
586                }
587                return i64::MAX;
588            }
589            Err(_) => {
590                mark_conv_error(s);
591                0
592            }
593        }
594    } else if let Some(oct) = digits.strip_prefix('0') {
595        if oct.is_empty() {
596            0
597        } else {
598            match u64::from_str_radix(oct, 8) {
599                Ok(v) => v,
600                Err(e) if e.kind() == &std::num::IntErrorKind::PosOverflow => {
601                    mark_range_error(s);
602                    if negative {
603                        return i64::MIN;
604                    }
605                    return i64::MAX;
606                }
607                Err(_) => {
608                    mark_conv_error(s);
609                    0
610                }
611            }
612        }
613    } else {
614        match digits.parse::<u64>() {
615            Ok(v) => v,
616            Err(e) if e.kind() == &std::num::IntErrorKind::PosOverflow => {
617                mark_range_error(s);
618                if negative {
619                    return i64::MIN;
620                }
621                return i64::MAX;
622            }
623            Err(_) => {
624                mark_conv_error(s);
625                0
626            }
627        }
628    };
629
630    if negative {
631        -(magnitude as i64)
632    } else {
633        magnitude as i64
634    }
635}
636
637/// Parse an unsigned integer argument.
638fn parse_unsigned(s: &str) -> u64 {
639    let s = s.trim();
640    if s.is_empty() {
641        return 0;
642    }
643
644    // Character constants
645    if (s.starts_with('\'') || s.starts_with('"')) && s.len() >= 2 {
646        return s[1..].chars().next().map_or(0, |c| c as u64);
647    }
648
649    // Negative values wrap around like C unsigned
650    let (negative, digits) = if let Some(rest) = s.strip_prefix('-') {
651        (true, rest)
652    } else if let Some(rest) = s.strip_prefix('+') {
653        (false, rest)
654    } else {
655        (false, s)
656    };
657
658    // digits must be non-empty and parseable
659    if digits.is_empty() {
660        mark_conv_error(s);
661        return 0;
662    }
663
664    let magnitude = if let Some(hex) = digits
665        .strip_prefix("0x")
666        .or_else(|| digits.strip_prefix("0X"))
667    {
668        match u64::from_str_radix(hex, 16) {
669            Ok(v) => v,
670            Err(e) if e.kind() == &std::num::IntErrorKind::PosOverflow => {
671                mark_range_error(s);
672                u64::MAX
673            }
674            Err(_) => {
675                mark_conv_error(s);
676                0
677            }
678        }
679    } else if let Some(oct) = digits.strip_prefix('0') {
680        if oct.is_empty() {
681            0
682        } else {
683            match u64::from_str_radix(oct, 8) {
684                Ok(v) => v,
685                Err(e) if e.kind() == &std::num::IntErrorKind::PosOverflow => {
686                    mark_range_error(s);
687                    u64::MAX
688                }
689                Err(_) => {
690                    mark_conv_error(s);
691                    0
692                }
693            }
694        }
695    } else {
696        match digits.parse::<u64>() {
697            Ok(v) => v,
698            Err(e) if e.kind() == &std::num::IntErrorKind::PosOverflow => {
699                mark_range_error(s);
700                u64::MAX
701            }
702            Err(_) => {
703                mark_conv_error(s);
704                0
705            }
706        }
707    };
708
709    if negative {
710        magnitude.wrapping_neg()
711    } else {
712        magnitude
713    }
714}
715
716/// Parse a floating-point argument.
717fn parse_float(s: &str) -> f64 {
718    let s = s.trim();
719    if s.is_empty() {
720        return 0.0;
721    }
722
723    // Character constants
724    if (s.starts_with('\'') || s.starts_with('"')) && s.len() >= 2 {
725        return s[1..].chars().next().map_or(0.0, |c| c as u32 as f64);
726    }
727
728    // Handle hex float prefix for parsing
729    if s.starts_with("0x") || s.starts_with("0X") || s.starts_with("-0x") || s.starts_with("-0X") {
730        // Rust doesn't parse hex floats natively; parse as integer
731        return parse_integer(s) as f64;
732    }
733
734    s.parse::<f64>().unwrap_or(0.0)
735}
736
737#[derive(Default)]
738struct FormatFlags {
739    left_align: bool,
740    plus_sign: bool,
741    space_sign: bool,
742    zero_pad: bool,
743    alternate: bool,
744}
745
746/// Apply string formatting with width and precision (for %s, %b, %c).
747fn apply_string_format(
748    s: &str,
749    flags: &FormatFlags,
750    width: usize,
751    precision: Option<usize>,
752) -> Vec<u8> {
753    let truncated: &str;
754    let owned: String;
755    if let Some(prec) = precision {
756        if s.len() > prec {
757            // Truncate to prec bytes, but respect UTF-8 boundaries
758            owned = s.chars().take(prec).collect();
759            truncated = &owned;
760        } else {
761            truncated = s;
762        }
763    } else {
764        truncated = s;
765    }
766
767    apply_padding(truncated.as_bytes(), flags, width)
768}
769
770/// Apply string formatting for raw bytes.
771fn apply_string_format_bytes(
772    s: &[u8],
773    flags: &FormatFlags,
774    width: usize,
775    precision: Option<usize>,
776) -> Vec<u8> {
777    let data = if let Some(prec) = precision {
778        if s.len() > prec { &s[..prec] } else { s }
779    } else {
780        s
781    };
782
783    apply_padding(data, flags, width)
784}
785
786/// Apply padding (left or right) to reach the desired width.
787fn apply_padding(data: &[u8], flags: &FormatFlags, width: usize) -> Vec<u8> {
788    if width == 0 || data.len() >= width {
789        return data.to_vec();
790    }
791    let pad_len = width - data.len();
792    let mut result = Vec::with_capacity(width);
793    if flags.left_align {
794        result.extend_from_slice(data);
795        result.resize(result.len() + pad_len, b' ');
796    } else {
797        result.resize(pad_len, b' ');
798        result.extend_from_slice(data);
799    }
800    result
801}
802
803/// Apply numeric formatting with width, flags, and optional precision for integers.
804fn apply_numeric_format(
805    num_str: &str,
806    is_negative: bool,
807    flags: &FormatFlags,
808    width: usize,
809    precision: Option<usize>,
810) -> String {
811    // For integers, precision specifies minimum number of digits
812    let raw_digits = if is_negative {
813        &num_str[1..] // strip the minus
814    } else {
815        num_str
816    };
817
818    let sign: &str = if is_negative {
819        "-"
820    } else if flags.plus_sign {
821        "+"
822    } else if flags.space_sign {
823        " "
824    } else {
825        ""
826    };
827
828    // Build digits with precision zero-padding
829    let mut digits_buf = String::new();
830    let digits: &str = if let Some(prec) = precision {
831        if prec > 0 && raw_digits.len() < prec {
832            let pad = prec - raw_digits.len();
833            digits_buf.reserve(prec);
834            for _ in 0..pad {
835                digits_buf.push('0');
836            }
837            digits_buf.push_str(raw_digits);
838            &digits_buf
839        } else if prec == 0 && raw_digits == "0" {
840            ""
841        } else {
842            raw_digits
843        }
844    } else {
845        raw_digits
846    };
847
848    let content_len = sign.len() + digits.len();
849
850    if width > 0 && content_len < width {
851        let pad_len = width - content_len;
852        let mut result = String::with_capacity(width);
853        if flags.left_align {
854            result.push_str(sign);
855            result.push_str(digits);
856            for _ in 0..pad_len {
857                result.push(' ');
858            }
859        } else if flags.zero_pad && precision.is_none() {
860            result.push_str(sign);
861            for _ in 0..pad_len {
862                result.push('0');
863            }
864            result.push_str(digits);
865        } else {
866            for _ in 0..pad_len {
867                result.push(' ');
868            }
869            result.push_str(sign);
870            result.push_str(digits);
871        }
872        result
873    } else {
874        let mut result = String::with_capacity(content_len);
875        result.push_str(sign);
876        result.push_str(digits);
877        result
878    }
879}
880
881/// Apply float formatting with width and flags.
882fn apply_float_format(
883    num_str: &str,
884    _is_negative: bool,
885    flags: &FormatFlags,
886    width: usize,
887) -> String {
888    let (sign_prefix, abs_str) = if num_str.starts_with('-') {
889        ("-", &num_str[1..])
890    } else if flags.plus_sign {
891        ("+", num_str)
892    } else if flags.space_sign {
893        (" ", num_str)
894    } else {
895        ("", num_str)
896    };
897
898    let content_len = sign_prefix.len() + abs_str.len();
899
900    if width > 0 && content_len < width {
901        let pad_len = width - content_len;
902        let mut result = String::with_capacity(width);
903        if flags.left_align {
904            result.push_str(sign_prefix);
905            result.push_str(abs_str);
906            for _ in 0..pad_len {
907                result.push(' ');
908            }
909        } else if flags.zero_pad {
910            result.push_str(sign_prefix);
911            for _ in 0..pad_len {
912                result.push('0');
913            }
914            result.push_str(abs_str);
915        } else {
916            for _ in 0..pad_len {
917                result.push(' ');
918            }
919            result.push_str(sign_prefix);
920            result.push_str(abs_str);
921        }
922        result
923    } else {
924        let mut result = String::with_capacity(content_len);
925        result.push_str(sign_prefix);
926        result.push_str(abs_str);
927        result
928    }
929}
930
931/// Format in scientific notation matching C's %e.
932fn format_scientific(value: f64, prec: usize, e_char: char) -> String {
933    if value == 0.0 {
934        let sign = if value.is_sign_negative() { "-" } else { "" };
935        if prec == 0 {
936            return format!("{sign}0{e_char}+00");
937        }
938        return format!("{sign}0.{:0>prec$}{e_char}+00", "", prec = prec);
939    }
940
941    let abs = value.abs();
942    let sign = if value < 0.0 { "-" } else { "" };
943    let exp = abs.log10().floor() as i32;
944    let mantissa = abs / 10f64.powi(exp);
945
946    let factor = 10f64.powi(prec as i32);
947    let mantissa = (mantissa * factor).round() / factor;
948
949    let (mantissa, exp) = if mantissa >= 10.0 {
950        (mantissa / 10.0, exp + 1)
951    } else {
952        (mantissa, exp)
953    };
954
955    let exp_sign = if exp >= 0 { '+' } else { '-' };
956    let exp_abs = exp.unsigned_abs();
957
958    if prec == 0 {
959        format!("{sign}{mantissa:.0}{e_char}{exp_sign}{exp_abs:02}")
960    } else {
961        format!(
962            "{sign}{mantissa:.prec$}{e_char}{exp_sign}{exp_abs:02}",
963            prec = prec
964        )
965    }
966}
967
968/// Format using %g - shortest representation.
969fn format_g(value: f64, prec: usize, upper: bool) -> String {
970    let prec = if prec == 0 { 1 } else { prec };
971
972    if value == 0.0 {
973        let sign = if value.is_sign_negative() { "-" } else { "" };
974        return format!("{sign}0");
975    }
976
977    let abs = value.abs();
978    let exp = abs.log10().floor() as i32;
979    let e_char = if upper { 'E' } else { 'e' };
980
981    if exp < -4 || exp >= prec as i32 {
982        let sig_prec = prec.saturating_sub(1);
983        let s = format_scientific(value, sig_prec, e_char);
984        trim_g_trailing_zeros(&s)
985    } else {
986        let decimal_prec = if prec as i32 > exp + 1 {
987            (prec as i32 - exp - 1) as usize
988        } else {
989            0
990        };
991        let s = format!("{value:.decimal_prec$}");
992        trim_g_trailing_zeros(&s)
993    }
994}
995
996/// Shell-quote a string for %q format specifier (GNU printf compat).
997/// Matches GNU coreutils quoting style (quotearg shell_escape_always_quoting_style):
998/// - Empty string -> ''
999/// - Safe chars only -> no quoting
1000/// - Has control/high bytes -> segment-based: 'safe'$'\t''safe'
1001/// - Has single quotes but no control -> double-quote: "it's"
1002/// - Otherwise -> single-quote: 'hello world'
1003fn shell_quote(s: &str) -> String {
1004    if s.is_empty() {
1005        return "''".to_string();
1006    }
1007
1008    // Check if the string needs quoting at all.
1009    let needs_quoting = s.starts_with('~')
1010        || s.bytes().any(|b| {
1011            !b.is_ascii_alphanumeric()
1012                && b != b'_'
1013                && b != b'/'
1014                && b != b'.'
1015                && b != b'-'
1016                && b != b':'
1017                && b != b','
1018                && b != b'+'
1019                && b != b'@'
1020                && b != b'%'
1021                && b != b'='
1022                && b != b'^'
1023                && b != b'~'
1024        });
1025
1026    if !needs_quoting {
1027        return s.to_string();
1028    }
1029
1030    let has_control = s.bytes().any(|b| b < 0x20 || b == 0x7f || b >= 0x80);
1031    let has_single_quote = s.contains('\'');
1032
1033    if has_control {
1034        // GNU uses segment-based quoting: 'safe'$'\t''safe'
1035        // GNU always starts in single-quote mode, so strings beginning with
1036        // a control char get an empty '' prefix: ''$'\t''hello'
1037        let mut result = String::new();
1038        let bytes = s.as_bytes();
1039        let mut i = 0;
1040        // Track whether we're at the start - if first char is control, emit ''
1041        let mut need_sq_start = true;
1042
1043        while i < bytes.len() {
1044            if is_control_byte(bytes[i]) {
1045                if need_sq_start {
1046                    // Emit empty single-quote segment before first $'...'
1047                    result.push_str("''");
1048                    need_sq_start = false;
1049                }
1050                // Emit $'...' segment for consecutive control/high bytes
1051                result.push_str("$'");
1052                while i < bytes.len() && is_control_byte(bytes[i]) {
1053                    emit_escape(bytes[i], &mut result);
1054                    i += 1;
1055                }
1056                result.push('\'');
1057            } else {
1058                need_sq_start = false;
1059                // Emit '...' segment for consecutive safe bytes
1060                result.push('\'');
1061                while i < bytes.len() && !is_control_byte(bytes[i]) {
1062                    if bytes[i] == b'\'' {
1063                        // Close single quote, emit escaped quote, reopen
1064                        result.push_str("'\\'");
1065                    } else {
1066                        result.push(bytes[i] as char);
1067                    }
1068                    i += 1;
1069                }
1070                result.push('\'');
1071            }
1072        }
1073        result
1074    } else if !has_single_quote {
1075        format!("'{}'", s)
1076    } else {
1077        // Has single quotes but no control chars.
1078        let unsafe_for_dquote = s
1079            .bytes()
1080            .any(|b| b == b'$' || b == b'`' || b == b'\\' || b == b'!' || b == b'"');
1081        if !unsafe_for_dquote {
1082            format!("\"{}\"", s)
1083        } else {
1084            // Use '\'' escaping for single quotes within single-quoted segments
1085            let mut result = String::from("'");
1086            for byte in s.bytes() {
1087                if byte == b'\'' {
1088                    result.push_str("'\\''");
1089                } else {
1090                    result.push(byte as char);
1091                }
1092            }
1093            result.push('\'');
1094            result
1095        }
1096    }
1097}
1098
1099fn is_control_byte(b: u8) -> bool {
1100    b < 0x20 || b == 0x7f || b >= 0x80
1101}
1102
1103fn emit_escape(byte: u8, result: &mut String) {
1104    match byte {
1105        b'\n' => result.push_str("\\n"),
1106        b'\t' => result.push_str("\\t"),
1107        b'\r' => result.push_str("\\r"),
1108        0x07 => result.push_str("\\a"),
1109        0x08 => result.push_str("\\b"),
1110        0x0c => result.push_str("\\f"),
1111        0x0b => result.push_str("\\v"),
1112        0x1b => result.push_str("\\E"),
1113        b => {
1114            result.push_str(&format!("\\{:03o}", b));
1115        }
1116    }
1117}
1118
1119/// Trim trailing zeros from %g formatted output.
1120/// Only trims after a decimal point to avoid turning "100000" into "1".
1121fn trim_g_trailing_zeros(s: &str) -> String {
1122    if let Some(e_pos) = s.find(['e', 'E']) {
1123        let (mantissa, exponent) = s.split_at(e_pos);
1124        if mantissa.contains('.') {
1125            let trimmed = mantissa.trim_end_matches('0').trim_end_matches('.');
1126            format!("{trimmed}{exponent}")
1127        } else {
1128            s.to_string()
1129        }
1130    } else if s.contains('.') {
1131        s.trim_end_matches('0').trim_end_matches('.').to_string()
1132    } else {
1133        s.to_string()
1134    }
1135}