coreutils/tests/by-util/test_printf.rs
Andrew Liebenow 961a27acff printf: accept non-UTF-8 input in FORMAT and ARGUMENT arguments
Other implementations of `printf` permit arbitrary data to be passed
to `printf`. The only restriction is that a null byte terminates
FORMAT and ARGUMENT argument strings (since they are C strings).

The current implementation only accepts FORMAT and ARGUMENT
arguments that are valid UTF-8 (this is being enforced by clap).

This commit removes the UTF-8 validation by switching to OsStr
and OsString.

This allows users to use `printf` to transmit or reformat null-safe
but not UTF-8-safe data, such as text encoded in an 8-bit text
encoding. See the `non_utf_8_input` test for an example (ISO-8859-1
text).

[drinkcat: also squashed in this commit to ease rebase]
Author: Justin Tracey <jdt.dev@unsuspicious.click>

uucore, printf: improve non-UTF-8 format arguments

This fixes handling of format arguments, in part by eliminating duplicate
implementations. Utilities with format arguments other than printf will no
longer accept things like "'a" as numbers, etc.

Co-authored-by: Justin Tracey <jdt.dev@unsuspicious.click>
2025-07-16 00:28:34 +02:00

1455 lines
32 KiB
Rust
Raw Blame History

// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore fffffffffffffffc
use uutests::new_ucmd;
#[test]
fn basic_literal() {
new_ucmd!()
.args(&["hello world"])
.succeeds()
.stdout_only("hello world");
}
#[test]
fn test_missing_escaped_hex_value() {
new_ucmd!()
.arg(r"\x")
.fails_with_code(1)
.stderr_only("printf: missing hexadecimal number in escape\n");
}
#[test]
fn escaped_octal_and_newline() {
new_ucmd!()
.args(&["\\101\\0377\\n"])
.succeeds()
.stdout_only("A\x1F7\n");
}
#[test]
fn variable_sized_octal() {
for x in ["|\\5|", "|\\05|", "|\\005|"] {
new_ucmd!()
.arg(x)
.succeeds()
.stdout_only_bytes([b'|', 5u8, b'|']);
}
new_ucmd!()
.arg("|\\0005|")
.succeeds()
.stdout_only_bytes([b'|', 0, b'5', b'|']);
}
#[test]
fn escaped_unicode_four_digit() {
new_ucmd!().args(&["\\u0125"]).succeeds().stdout_only("ĥ");
}
#[test]
fn escaped_unicode_eight_digit() {
new_ucmd!()
.args(&["\\U00000125"])
.succeeds()
.stdout_only("ĥ");
}
#[test]
fn escaped_unicode_null_byte() {
new_ucmd!()
.args(&["\\0001_"])
.succeeds()
.stdout_is_bytes([0u8, b'1', b'_']);
new_ucmd!()
.args(&["%b", "\\0001_"])
.succeeds()
.stdout_is_bytes([1u8, b'_']);
}
#[test]
fn escaped_unicode_incomplete() {
for arg in ["\\u", "\\U", "\\uabc", "\\Uabcd"] {
new_ucmd!()
.arg(arg)
.fails_with_code(1)
.stderr_only("printf: missing hexadecimal number in escape\n");
}
}
#[test]
fn escaped_unicode_invalid() {
for arg in ["\\ud9d0", "\\U0000D8F9"] {
new_ucmd!()
.arg(arg)
.fails_with_code(1)
.stderr_only(format!("printf: invalid universal character name {arg}\n"));
}
}
#[test]
fn escaped_percent_sign() {
new_ucmd!()
.args(&["hello%% world"])
.succeeds()
.stdout_only("hello% world");
}
#[test]
fn escaped_unrecognized() {
new_ucmd!().args(&["c\\d"]).succeeds().stdout_only("c\\d");
}
#[test]
fn sub_b_string_handle_escapes() {
new_ucmd!()
.args(&["hello %b", "\\tworld"])
.succeeds()
.stdout_only("hello \tworld");
}
#[test]
fn sub_b_string_variable_size_unicode() {
for x in ["\\5|", "\\05|", "\\005|", "\\0005|"] {
new_ucmd!()
.args(&["|%b", x])
.succeeds()
.stdout_only_bytes([b'|', 5u8, b'|']);
}
new_ucmd!()
.args(&["|%b", "\\00005|"])
.succeeds()
.stdout_only_bytes([b'|', 0, b'5', b'|']);
}
#[test]
fn sub_b_string_validate_field_params() {
new_ucmd!()
.args(&["hello %7b", "world"])
.fails()
.stdout_is("hello ")
.stderr_is("printf: %7b: invalid conversion specification\n");
}
#[test]
fn sub_b_string_ignore_subs() {
new_ucmd!()
.args(&["hello %b", "world %% %i"])
.succeeds()
.stdout_only("hello world %% %i");
}
#[test]
fn sub_q_string_non_printable() {
new_ucmd!()
.args(&["non-printable: %q", "\"$test\""])
.succeeds()
.stdout_only("non-printable: '\"$test\"'");
}
#[test]
fn sub_q_string_validate_field_params() {
new_ucmd!()
.args(&["hello %7q", "world"])
.fails()
.stdout_is("hello ")
.stderr_is("printf: %7q: invalid conversion specification\n");
}
#[test]
fn sub_q_string_special_non_printable() {
new_ucmd!()
.args(&["non-printable: %q", "test~"])
.succeeds()
.stdout_only("non-printable: test~");
}
#[test]
fn sub_q_string_empty() {
new_ucmd!().args(&["%q", ""]).succeeds().stdout_only("''");
}
#[test]
fn sub_char() {
new_ucmd!()
.args(&["the letter %c", "A"])
.succeeds()
.stdout_only("the letter A");
}
#[test]
fn sub_char_from_string() {
new_ucmd!()
.args(&["%c%c%c", "five", "%", "oval"])
.succeeds()
.stdout_only("f%o");
}
#[test]
fn sub_num_int() {
new_ucmd!()
.args(&["twenty is %i", "20"])
.succeeds()
.stdout_only("twenty is 20");
}
#[test]
fn sub_num_int_min_width() {
new_ucmd!()
.args(&["twenty is %1i", "20"])
.succeeds()
.stdout_only("twenty is 20");
}
#[test]
fn sub_num_int_neg() {
new_ucmd!()
.args(&["neg. twenty is %i", "-20"])
.succeeds()
.stdout_only("neg. twenty is -20");
}
#[test]
fn sub_num_int_oct_in() {
new_ucmd!()
.args(&["twenty is %i", "024"])
.succeeds()
.stdout_only("twenty is 20");
}
#[test]
fn sub_num_int_oct_in_neg() {
new_ucmd!()
.args(&["neg. twenty is %i", "-024"])
.succeeds()
.stdout_only("neg. twenty is -20");
}
#[test]
fn sub_num_int_hex_in() {
new_ucmd!()
.args(&["twenty is %i", "0x14"])
.succeeds()
.stdout_only("twenty is 20");
}
#[test]
fn sub_num_int_hex_in_neg() {
new_ucmd!()
.args(&["neg. twenty is %i", "-0x14"])
.succeeds()
.stdout_only("neg. twenty is -20");
}
#[test]
fn sub_num_int_char_const_in() {
new_ucmd!()
.args(&["ninety seven is %i", "'a"])
.succeeds()
.stdout_only("ninety seven is 97");
new_ucmd!()
.args(&["emoji is %i", "'🙃"])
.succeeds()
.stdout_only("emoji is 128579");
new_ucmd!()
.args(&["ninety seven is %i", "\"a"])
.succeeds()
.stdout_only("ninety seven is 97");
new_ucmd!()
.args(&["emoji is %i", "\"🙃"])
.succeeds()
.stdout_only("emoji is 128579");
}
#[test]
fn sub_num_thousands() {
// For "C" locale, the thousands separator is ignored but should
// not result in an error
new_ucmd!()
.args(&["%'i", "123456"])
.succeeds()
.stdout_only("123456");
}
#[test]
fn sub_num_uint() {
new_ucmd!()
.args(&["twenty is %u", "20"])
.succeeds()
.stdout_only("twenty is 20");
}
#[test]
fn sub_num_octal() {
new_ucmd!()
.args(&["twenty in octal is %o", "20"])
.succeeds()
.stdout_only("twenty in octal is 24");
}
#[test]
fn sub_num_hex_lower() {
new_ucmd!()
.args(&["thirty in hex is %x", "30"])
.succeeds()
.stdout_only("thirty in hex is 1e");
}
#[test]
fn sub_num_hex_upper() {
new_ucmd!()
.args(&["thirty in hex is %X", "30"])
.succeeds()
.stdout_only("thirty in hex is 1E");
}
#[test]
fn sub_num_hex_non_numerical() {
new_ucmd!()
.args(&["parameters need to be numbers %X", "%194"])
.fails_with_code(1);
}
#[test]
fn sub_num_float() {
new_ucmd!()
.args(&["twenty is %f", "20"])
.succeeds()
.stdout_only("twenty is 20.000000");
}
#[test]
fn sub_num_float_e_round() {
new_ucmd!()
.args(&["%e", "99999999"])
.succeeds()
.stdout_only("1.000000e+08");
}
#[test]
fn sub_num_float_e_no_round() {
new_ucmd!()
.args(&["%e", "99999994"])
.succeeds()
.stdout_only("9.999999e+07");
}
#[test]
fn sub_num_float_round_to_one() {
new_ucmd!()
.args(&["one is %f", "0.9999995"])
.succeeds()
.stdout_only("one is 1.000000");
}
#[test]
#[ignore = "Requires 'long double' precision floats to be used internally"]
fn sub_num_float_round_to_two() {
new_ucmd!()
.args(&["two is %f", "1.9999995"])
.succeeds()
.stdout_only("two is 2.000000");
}
#[test]
fn sub_num_float_round_nines_dec() {
new_ucmd!()
.args(&["%f", "0.99999999"])
.succeeds()
.stdout_only("1.000000");
}
#[test]
fn sub_num_sci_lower() {
new_ucmd!()
.args(&["twenty is %e", "20"])
.succeeds()
.stdout_only("twenty is 2.000000e+01");
}
#[test]
fn sub_num_sci_upper() {
new_ucmd!()
.args(&["twenty is %E", "20"])
.succeeds()
.stdout_only("twenty is 2.000000E+01");
}
#[test]
fn sub_num_sci_trunc() {
new_ucmd!()
.args(&["pi is ~ %e", "3.1415926535"])
.succeeds()
.stdout_only("pi is ~ 3.141593e+00");
}
#[test]
fn sub_num_dec_trunc() {
new_ucmd!()
.args(&["pi is ~ %g", "3.1415926535"])
.succeeds()
.stdout_only("pi is ~ 3.14159");
}
#[test]
fn sub_num_sci_negative() {
new_ucmd!()
.args(&["-1234 is %e", "-1234"])
.succeeds()
.stdout_only("-1234 is -1.234000e+03");
}
#[test]
fn sub_num_hex_float_lower() {
new_ucmd!()
.args(&["%a", ".875"])
.succeeds()
.stdout_only("0xep-4");
}
#[test]
fn sub_num_hex_float_upper() {
new_ucmd!()
.args(&["%A", ".875"])
.succeeds()
.stdout_only("0XEP-4");
}
#[test]
fn sub_min_width() {
new_ucmd!()
.args(&["hello %7s", "world"])
.succeeds()
.stdout_only("hello world");
}
#[test]
fn sub_min_width_negative() {
new_ucmd!()
.args(&["hello %-7s", "world"])
.succeeds()
.stdout_only("hello world ");
}
#[test]
fn sub_str_max_chars_input() {
new_ucmd!()
.args(&["hello %7.2s", "world"])
.succeeds()
.stdout_only("hello wo");
}
#[test]
fn sub_int_decimal() {
new_ucmd!()
.args(&["%0.i", "11"])
.succeeds()
.stdout_only("11");
}
#[test]
fn sub_int_leading_zeroes() {
new_ucmd!()
.args(&["%.4i", "11"])
.succeeds()
.stdout_only("0011");
}
#[test]
fn sub_int_leading_zeroes_padded() {
new_ucmd!()
.args(&["%5.4i", "11"])
.succeeds()
.stdout_only(" 0011");
}
#[test]
fn sub_float_dec_places() {
new_ucmd!()
.args(&["pi is ~ %.11f", "3.1415926535"])
.succeeds()
.stdout_only("pi is ~ 3.14159265350");
}
#[test]
fn sub_float_hex_in() {
new_ucmd!()
.args(&["%f", "0xF1.1F"])
.succeeds()
.stdout_only("241.121094");
}
#[test]
fn sub_float_no_octal_in() {
new_ucmd!()
.args(&["%f", "077"])
.succeeds()
.stdout_only("77.000000");
}
#[test]
fn sub_any_asterisk_first_param() {
new_ucmd!()
.args(&["%*i", "3", "11", "4", "12"])
.succeeds()
.stdout_only(" 11 12");
}
#[test]
fn sub_any_asterisk_second_param() {
new_ucmd!()
.args(&["%.*i", "3", "11", "4", "12"])
.succeeds()
.stdout_only("0110012");
}
#[test]
fn sub_any_asterisk_both_params() {
new_ucmd!()
.args(&["%*.*i", "4", "3", "11", "5", "4", "12"])
.succeeds()
.stdout_only(" 011 0012");
}
#[test]
fn sub_any_asterisk_octal_arg() {
new_ucmd!()
.args(&["%.*i", "011", "12345678"])
.succeeds()
.stdout_only("012345678");
}
#[test]
fn sub_any_asterisk_hex_arg() {
new_ucmd!()
.args(&["%.*i", "0xA", "123456789"])
.succeeds()
.stdout_only("0123456789");
}
#[test]
fn sub_any_asterisk_negative_first_param() {
new_ucmd!()
.args(&["a(%*s)b", "-5", "xyz"])
.succeeds()
.stdout_only("a(xyz )b"); // Would be 'a( xyz)b' if -5 was 5
// Negative octal
new_ucmd!()
.args(&["a(%*s)b", "-010", "xyz"])
.succeeds()
.stdout_only("a(xyz )b");
// Negative hexadecimal
new_ucmd!()
.args(&["a(%*s)b", "-0x10", "xyz"])
.succeeds()
.stdout_only("a(xyz )b");
// Should also work on %c
new_ucmd!()
.args(&["a(%*c)b", "-5", "x"])
.succeeds()
.stdout_only("a(x )b"); // Would be 'a( x)b' if -5 was 5
}
#[test]
fn sub_any_asterisk_first_param_with_integer() {
new_ucmd!()
.args(&["|%*d|", "3", "0"])
.succeeds()
.stdout_only("| 0|");
new_ucmd!()
.args(&["|%*d|", "1", "0"])
.succeeds()
.stdout_only("|0|");
new_ucmd!()
.args(&["|%*d|", "0", "0"])
.succeeds()
.stdout_only("|0|");
new_ucmd!()
.args(&["|%*d|", "-1", "0"])
.succeeds()
.stdout_only("|0|");
// Negative widths are left-aligned
new_ucmd!()
.args(&["|%*d|", "-3", "0"])
.succeeds()
.stdout_only("|0 |");
}
#[test]
fn sub_any_asterisk_second_param_with_integer() {
new_ucmd!()
.args(&["|%.*d|", "3", "10"])
.succeeds()
.stdout_only("|010|");
new_ucmd!()
.args(&["|%*.d|", "1", "10"])
.succeeds()
.stdout_only("|10|");
new_ucmd!()
.args(&["|%.*d|", "0", "10"])
.succeeds()
.stdout_only("|10|");
new_ucmd!()
.args(&["|%.*d|", "-1", "10"])
.succeeds()
.stdout_only("|10|");
new_ucmd!()
.args(&["|%.*d|", "-2", "10"])
.succeeds()
.stdout_only("|10|");
new_ucmd!()
.args(&["|%.*d|", &i64::MIN.to_string(), "10"])
.succeeds()
.stdout_only("|10|");
new_ucmd!()
.args(&["|%.*d|", &format!("-{}", u128::MAX), "10"])
.fails_with_code(1)
.stdout_is("|10|")
.stderr_is(
"printf: '-340282366920938463463374607431768211455': Numerical result out of range\n",
);
}
#[test]
fn sub_any_specifiers() {
// spell-checker:disable-next-line
for format in ["%ztlhLji", "%0ztlhLji", "%0.ztlhLji"] {
new_ucmd!().args(&[format, "3"]).succeeds().stdout_only("3");
}
}
#[test]
fn unspecified_left_justify_is_1_width() {
new_ucmd!().args(&["%-o"]).succeeds().stdout_only("0");
}
#[test]
fn sub_any_specifiers_after_second_param() {
new_ucmd!()
.args(&["%0.0ztlhLji", "3"]) //spell-checker:disable-line
.succeeds()
.stdout_only("3");
}
#[test]
fn stop_after_additional_escape() {
new_ucmd!()
.args(&["A%sC\\cD%sF", "B", "E"])
.succeeds()
.stdout_only("ABC");
}
#[test]
fn sub_float_leading_zeroes() {
new_ucmd!()
.args(&["%010f", "1"])
.succeeds()
.stdout_only("001.000000");
}
#[test]
fn sub_general_float() {
new_ucmd!()
.args(&["%g", "1.1"])
.succeeds()
.stdout_only("1.1");
}
#[test]
fn sub_general_truncate_to_integer() {
new_ucmd!().args(&["%g", "1.0"]).succeeds().stdout_only("1");
}
#[test]
fn sub_general_scientific_notation() {
new_ucmd!()
.args(&["%g", "1000010"])
.succeeds()
.stdout_only("1.00001e+06");
}
#[test]
fn sub_general_round_scientific_notation() {
new_ucmd!()
.args(&["%g", "123456789"])
.succeeds()
.stdout_only("1.23457e+08");
}
#[test]
fn sub_general_round_float() {
new_ucmd!()
.args(&["%g", "12345.6789"])
.succeeds()
.stdout_only("12345.7");
}
#[test]
fn sub_general_round_float_to_integer() {
new_ucmd!()
.args(&["%g", "123456.7"])
.succeeds()
.stdout_only("123457");
}
#[test]
fn sub_general_round_float_leading_zeroes() {
new_ucmd!()
.args(&["%g", "1.000009"])
.succeeds()
.stdout_only("1.00001");
}
#[test]
fn partial_float() {
new_ucmd!()
.args(&["%.2f is %s", "42.03x", "a lot"])
.fails_with_code(1)
.stdout_is("42.03 is a lot")
.stderr_is("printf: '42.03x': value not completely converted\n");
}
#[test]
fn partial_integer() {
new_ucmd!()
.args(&["%d is %s", "42x23", "a lot"])
.fails_with_code(1)
.stdout_is("42 is a lot")
.stderr_is("printf: '42x23': value not completely converted\n");
new_ucmd!()
.args(&["%d is not %s", "0xwa", "a lot"])
.fails_with_code(1)
.stdout_is("0 is not a lot")
.stderr_is("printf: '0xwa': value not completely converted\n");
}
#[test]
fn unsigned_hex_negative_wraparound() {
new_ucmd!()
.args(&["%x", "-0b100"])
.succeeds()
.stdout_only("fffffffffffffffc");
new_ucmd!()
.args(&["%x", "-0100"])
.succeeds()
.stdout_only("ffffffffffffffc0");
new_ucmd!()
.args(&["%x", "-100"])
.succeeds()
.stdout_only("ffffffffffffff9c");
new_ucmd!()
.args(&["%x", "-0x100"])
.succeeds()
.stdout_only("ffffffffffffff00");
new_ucmd!()
.args(&["%x", "-92233720368547758150"])
.fails_with_code(1)
.stdout_is("ffffffffffffffff")
.stderr_is("printf: '-92233720368547758150': Numerical result out of range\n");
new_ucmd!()
.args(&["%u", "-1002233720368547758150"])
.fails_with_code(1)
.stdout_is("18446744073709551615")
.stderr_is("printf: '-1002233720368547758150': Numerical result out of range\n");
}
#[test]
fn test_overflow() {
new_ucmd!()
.args(&["%d", "36893488147419103232"])
.fails_with_code(1)
.stdout_is("9223372036854775807")
.stderr_is("printf: '36893488147419103232': Numerical result out of range\n");
new_ucmd!()
.args(&["%d", "-36893488147419103232"])
.fails_with_code(1)
.stdout_is("-9223372036854775808")
.stderr_is("printf: '-36893488147419103232': Numerical result out of range\n");
new_ucmd!()
.args(&["%u", "36893488147419103232"])
.fails_with_code(1)
.stdout_is("18446744073709551615")
.stderr_is("printf: '36893488147419103232': Numerical result out of range\n");
}
#[test]
fn partial_char() {
new_ucmd!()
.args(&["%d", "'abc"])
.succeeds()
.stdout_is("97")
.stderr_is(
"printf: warning: bc: character(s) following character constant have been ignored\n",
);
}
#[test]
fn sub_alternative_lower_hex_0() {
new_ucmd!().args(&["%#x", "0"]).succeeds().stdout_only("0");
}
#[test]
fn sub_alternative_lower_hex() {
new_ucmd!()
.args(&["%#x", "42"])
.succeeds()
.stdout_only("0x2a");
}
#[test]
fn sub_alternative_upper_hex_0() {
new_ucmd!().args(&["%#X", "0"]).succeeds().stdout_only("0");
}
#[test]
fn sub_alternative_upper_hex() {
new_ucmd!()
.args(&["%#X", "42"])
.succeeds()
.stdout_only("0X2A");
}
#[test]
fn char_as_byte() {
new_ucmd!()
.args(&["%c", "🙃"])
.succeeds()
.no_stderr()
.stdout_is_bytes(b"\xf0");
}
#[test]
fn no_infinite_loop() {
new_ucmd!()
.args(&["a", "b"])
.succeeds()
.stdout_is("a")
.stderr_contains("warning: ignoring excess arguments, starting with 'b'");
}
#[test]
fn pad_octal_with_prefix() {
new_ucmd!()
.args(&[">%#15.6o<", "0"])
.succeeds()
.stdout_only("> 000000<");
new_ucmd!()
.args(&[">%#15.6o<", "01"])
.succeeds()
.stdout_only("> 000001<");
new_ucmd!()
.args(&[">%#15.6o<", "01234"])
.succeeds()
.stdout_only("> 001234<");
new_ucmd!()
.args(&[">%#15.6o<", "012345"])
.succeeds()
.stdout_only("> 012345<");
new_ucmd!()
.args(&[">%#15.6o<", "0123456"])
.succeeds()
.stdout_only("> 0123456<");
}
#[test]
fn pad_unsigned_zeroes() {
for format in ["%.3u", "%.3x", "%.3X", "%.3o"] {
new_ucmd!()
.args(&[format, "0"])
.succeeds()
.stdout_only("000");
}
}
#[test]
fn pad_unsigned_three() {
for (format, expected) in [
("%.3u", "003"),
("%.3x", "003"),
("%.3X", "003"),
("%.3o", "003"),
("%#.3x", "0x003"),
("%#.3X", "0X003"),
("%#.3o", "003"),
("%#05x", "0x003"),
("%#05X", "0X003"),
("%3x", " 3"),
("%3X", " 3"),
] {
new_ucmd!()
.args(&[format, "3"])
.succeeds()
.stdout_only(expected);
}
}
#[test]
fn pad_char() {
for (format, expected) in [("%3c", " X"), ("%1c", "X"), ("%-1c", "X"), ("%-3c", "X ")] {
new_ucmd!()
.args(&[format, "X"])
.succeeds()
.stdout_only(expected);
}
}
#[test]
fn pad_string() {
for (format, expected) in [
("%8s", " bottle"),
("%-8s", "bottle "),
("%6s", "bottle"),
("%-6s", "bottle"),
] {
new_ucmd!()
.args(&[format, "bottle"])
.succeeds()
.stdout_only(expected);
}
}
#[test]
fn format_spec_zero_fails() {
// It is invalid to have the format spec
for format in ["%0c", "%0s"] {
new_ucmd!().args(&[format, "3"]).fails_with_code(1);
}
}
#[test]
fn invalid_precision_tests() {
// It is invalid to have length of output string greater than i32::MAX
for format in ["%.*d", "%.*f"] {
let expected_error = "printf: invalid precision: '2147483648'\n";
new_ucmd!()
.args(&[format, "2147483648", "0"])
.fails()
.stderr_is(expected_error);
}
}
// The following padding-tests test for the cases in which flags in ['0', ' '] are given.
// For integer, only try to pad when no precision is given, while
// for float, always try to pad
#[test]
fn space_padding_with_space_test() {
// Check if printf gives an extra space in the beginning
new_ucmd!()
.args(&["% 3d", "1"])
.succeeds()
.stdout_only(" 1");
}
#[test]
fn zero_padding_with_space_test() {
new_ucmd!()
.args(&["% 03d", "1"])
.succeeds()
.stdout_only(" 01");
}
#[test]
fn zero_padding_with_plus_test() {
new_ucmd!()
.args(&["%+04d", "1"])
.succeeds()
.stdout_only("+001");
}
#[test]
fn negative_zero_padding_test() {
new_ucmd!()
.args(&["%03d", "-1"])
.succeeds()
.stdout_only("-01");
}
#[test]
fn negative_zero_padding_with_space_test() {
new_ucmd!()
.args(&["% 03d", "-1"])
.succeeds()
.stdout_only("-01");
}
#[test]
fn spaces_before_numbers_are_ignored() {
new_ucmd!()
.args(&["%*.*d", " 5", " 3", " 6"])
.succeeds()
.stdout_only(" 006");
}
#[test]
fn float_with_zero_precision_should_pad() {
new_ucmd!()
.args(&["%03.0f", "-1"])
.succeeds()
.stdout_only("-01");
}
#[test]
fn float_non_finite() {
new_ucmd!()
.args(&[
"%f %f %F %f %f %F",
"nan",
"-nan",
"nan",
"inf",
"-inf",
"inf",
])
.succeeds()
.stdout_only("nan -nan NAN inf -inf INF");
}
#[test]
fn float_zero_neg_zero() {
new_ucmd!()
.args(&["%f %f", "0.0", "-0.0"])
.succeeds()
.stdout_only("0.000000 -0.000000");
}
#[test]
fn precision_check() {
new_ucmd!()
.args(&["%.3d", "1"])
.succeeds()
.stdout_only("001");
}
#[test]
fn space_padding_with_precision() {
new_ucmd!()
.args(&["%4.3d", "1"])
.succeeds()
.stdout_only(" 001");
}
#[test]
fn float_zero_padding_with_precision() {
new_ucmd!()
.args(&["%04.1f", "1"])
.succeeds()
.stdout_only("01.0");
}
#[test]
fn float_space_padding_with_precision() {
new_ucmd!()
.args(&["%4.1f", "1"])
.succeeds()
.stdout_only(" 1.0");
}
#[test]
fn negative_float_zero_padding_with_precision() {
new_ucmd!()
.args(&["%05.1f", "-1"])
.succeeds()
.stdout_only("-01.0");
}
#[test]
fn float_default_precision_space_padding() {
new_ucmd!()
.args(&["%10f", "1"])
.succeeds()
.stdout_only(" 1.000000");
}
#[test]
fn float_default_precision_zero_padding() {
new_ucmd!()
.args(&["%010f", "1"])
.succeeds()
.stdout_only("001.000000");
}
#[test]
fn flag_position_space_padding() {
new_ucmd!()
.args(&["% +3.1d", "1"])
.succeeds()
.stdout_only(" +1");
}
#[test]
fn float_flag_position_space_padding() {
new_ucmd!()
.args(&["% +5.1f", "1"])
.succeeds()
.stdout_only(" +1.0");
}
#[test]
fn float_large_precision() {
// Note: This does not match GNU coreutils output (0.100000000000000000001355252716 on x86),
// as we parse and format using ExtendedBigDecimal, which provides arbitrary precision.
new_ucmd!()
.args(&["%.30f", "0.1"])
.succeeds()
.stdout_only("0.100000000000000000000000000000");
}
#[test]
fn float_non_finite_space_padding() {
new_ucmd!()
.args(&["% 5.2f|% 5.2f|% 5.2f|% 5.2f", "inf", "-inf", "nan", "-nan"])
.succeeds()
.stdout_only(" inf| -inf| nan| -nan");
}
#[test]
fn float_non_finite_zero_padding() {
// Zero-padding pads non-finite numbers with spaces.
new_ucmd!()
.args(&["%05.2f|%05.2f|%05.2f|%05.2f", "inf", "-inf", "nan", "-nan"])
.succeeds()
.stdout_only(" inf| -inf| nan| -nan");
}
#[test]
fn float_abs_value_less_than_one() {
new_ucmd!()
.args(&["%g", "0.1171875"])
.succeeds()
.stdout_only("0.117188");
// The original value from #7031 issue
new_ucmd!()
.args(&["%g", "-0.1171875"])
.succeeds()
.stdout_only("-0.117188");
new_ucmd!()
.args(&["%g", "0.01171875"])
.succeeds()
.stdout_only("0.0117188");
new_ucmd!()
.args(&["%g", "-0.01171875"])
.succeeds()
.stdout_only("-0.0117188");
new_ucmd!()
.args(&["%g", "0.001171875001"])
.succeeds()
.stdout_only("0.00117188");
new_ucmd!()
.args(&["%g", "-0.001171875001"])
.succeeds()
.stdout_only("-0.00117188");
}
#[test]
fn float_switch_switch_decimal_scientific() {
new_ucmd!()
.args(&["%g", "0.0001"])
.succeeds()
.stdout_only("0.0001");
new_ucmd!()
.args(&["%g", "0.00001"])
.succeeds()
.stdout_only("1e-05");
}
#[test]
fn float_arg_zero() {
new_ucmd!()
.args(&["%f", "0."])
.succeeds()
.stdout_only("0.000000");
new_ucmd!()
.args(&["%f", ".0"])
.succeeds()
.stdout_only("0.000000");
new_ucmd!()
.args(&["%f", ".0e100000"])
.succeeds()
.stdout_only("0.000000");
}
#[test]
fn float_arg_invalid() {
// Just a dot fails.
new_ucmd!()
.args(&["%f", "."])
.fails()
.stdout_is("0.000000")
.stderr_contains("expected a numeric value");
new_ucmd!()
.args(&["%f", "-."])
.fails()
.stdout_is("0.000000")
.stderr_contains("expected a numeric value");
// Just an exponent indicator fails.
new_ucmd!()
.args(&["%f", "e"])
.fails()
.stdout_is("0.000000")
.stderr_contains("expected a numeric value");
// No digit but only exponent fails
new_ucmd!()
.args(&["%f", ".e12"])
.fails()
.stdout_is("0.000000")
.stderr_contains("expected a numeric value");
// No exponent partially fails
new_ucmd!()
.args(&["%f", "123e"])
.fails()
.stdout_is("123.000000")
.stderr_contains("value not completely converted");
// Nothing past `0x` parses as zero
new_ucmd!()
.args(&["%f", "0x"])
.fails()
.stdout_is("0.000000")
.stderr_contains("value not completely converted");
new_ucmd!()
.args(&["%f", "0x."])
.fails()
.stdout_is("0.000000")
.stderr_contains("value not completely converted");
new_ucmd!()
.args(&["%f", "0xp12"])
.fails()
.stdout_is("0.000000")
.stderr_contains("value not completely converted");
}
#[test]
fn float_arg_with_whitespace() {
new_ucmd!()
.args(&["%f", " \u{0020}\u{000d}\t\n0.000001"])
.succeeds()
.stdout_only("0.000001");
new_ucmd!()
.args(&["%f", "0.1 "])
.fails()
.stderr_contains("value not completely converted");
// Unicode whitespace should not be allowed in a number
new_ucmd!()
.args(&["%f", "\u{2029}0.1"])
.fails()
.stderr_contains("expected a numeric value");
// An input string with a whitespace special character that has
// not already been expanded should fail.
new_ucmd!()
.args(&["%f", "\\t0.1"])
.fails()
.stderr_contains("expected a numeric value");
}
#[test]
fn mb_input() {
let cases = vec![
("%04x\n", "\"á", "00e1\n"),
("%04x\n", "", "00e1\n"),
("%04x\n", "'\u{e1}", "00e1\n"),
("%i\n", "\"á", "225\n"),
("%i\n", "", "225\n"),
("%i\n", "'\u{e1}", "225\n"),
("%f\n", "", "225.000000\n"),
];
for (format, arg, stdout) in cases {
new_ucmd!()
.args(&[format, arg])
.succeeds()
.stdout_only(stdout);
}
let cases = vec![
("%04x\n", "\"á=", "00e1\n", "="),
("%04x\n", "'á-", "00e1\n", "-"),
("%04x\n", "'á=-==", "00e1\n", "=-=="),
("%04x\n", "'á'", "00e1\n", "'"),
("%04x\n", "'\u{e1}++", "00e1\n", "++"),
("%04x\n", "''á'", "0027\n", "á'"),
("%i\n", "\"á=", "225\n", "="),
];
for (format, arg, stdout, stderr) in cases {
new_ucmd!()
.args(&[format, arg])
.succeeds()
.stdout_is(stdout)
.stderr_is(format!("printf: warning: {stderr}: character(s) following character constant have been ignored\n"));
}
for arg in ["\"", "'"] {
new_ucmd!()
.args(&["%04x\n", arg])
.fails()
.stderr_contains("expected a numeric value");
}
}
#[test]
#[cfg(target_family = "unix")]
fn mb_invalid_unicode() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let cases = vec![
("%04x\n", b"\"\xe1", "00e1\n"),
("%04x\n", b"'\xe1", "00e1\n"),
("%i\n", b"\"\xe1", "225\n"),
("%i\n", b"'\xe1", "225\n"),
("%f\n", b"'\xe1", "225.000000\n"),
];
for (format, arg, stdout) in cases {
new_ucmd!()
.arg(format)
.arg(OsStr::from_bytes(arg))
.succeeds()
.stdout_only(stdout);
}
let cases = vec![
(b"\"\xe1=".as_slice(), "="),
(b"'\xe1-".as_slice(), "-"),
(b"'\xe1=-==".as_slice(), "=-=="),
(b"'\xe1'".as_slice(), "'"),
// unclear if original or replacement character is better in stderr
//(b"''\xe1'".as_slice(), "'<27>'"),
];
for (arg, expected) in cases {
new_ucmd!()
.arg("%04x\n")
.arg(OsStr::from_bytes(arg))
.succeeds()
.stdout_is("00e1\n")
.stderr_is(format!("printf: warning: {expected}: character(s) following character constant have been ignored\n"));
}
}
#[test]
fn positional_format_specifiers() {
new_ucmd!()
.args(&["%1$d%d-", "5", "10", "6", "20"])
.succeeds()
.stdout_only("55-1010-66-2020-");
new_ucmd!()
.args(&["%2$d%d-", "5", "10", "6", "20"])
.succeeds()
.stdout_only("105-206-");
new_ucmd!()
.args(&["%3$d%d-", "5", "10", "6", "20"])
.succeeds()
.stdout_only("65-020-");
new_ucmd!()
.args(&["%4$d%d-", "5", "10", "6", "20"])
.succeeds()
.stdout_only("205-");
new_ucmd!()
.args(&["%5$d%d-", "5", "10", "6", "20"])
.succeeds()
.stdout_only("05-");
new_ucmd!()
.args(&["%0$d%d-", "5", "10", "6", "20"])
.fails_with_code(1)
.stderr_only("printf: %0$: invalid conversion specification\n");
new_ucmd!()
.args(&[
"Octal: %6$o, Int: %1$d, Float: %4$f, String: %2$s, Hex: %7$x, Scientific: %5$e, Char: %9$c, Unsigned: %3$u, Integer: %8$i",
"42", // 1$d - Int
"hello", // 2$s - String
"100", // 3$u - Unsigned
"3.14159", // 4$f - Float
"0.00001", // 5$e - Scientific
"77", // 6$o - Octal
"255", // 7$x - Hex
"123", // 8$i - Integer
"A", // 9$c - Char
])
.succeeds()
.stdout_only("Octal: 115, Int: 42, Float: 3.141590, String: hello, Hex: ff, Scientific: 1.000000e-05, Char: A, Unsigned: 100, Integer: 123");
}
#[test]
#[cfg(target_family = "unix")]
fn non_utf_8_input() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
// ISO-8859-1 encoded text
// spell-checker:disable
const INPUT_AND_OUTPUT: &[u8] =
b"Swer an rehte g\xFCete wendet s\xEEn gem\xFCete, dem volget s\xE6lde und \xEAre.";
// spell-checker:enable
let os_str = OsStr::from_bytes(INPUT_AND_OUTPUT);
new_ucmd!()
.arg("%s")
.arg(os_str)
.succeeds()
.stdout_only_bytes(INPUT_AND_OUTPUT);
new_ucmd!()
.arg(os_str)
.succeeds()
.stdout_only_bytes(INPUT_AND_OUTPUT);
new_ucmd!()
.arg("%d")
.arg(os_str)
.fails()
.stderr_contains("expected a numeric value");
}