Merge pull request #6906 from sylvestre/base-wrap

base32/base64: handle two corner cases
This commit is contained in:
Daniel Hofstetter 2024-12-09 13:55:15 +01:00 committed by GitHub
commit 2a9e7c937a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 145 additions and 540 deletions

83
Cargo.lock generated
View file

@ -185,21 +185,6 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -1557,7 +1542,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -1821,32 +1805,6 @@ dependencies = [
"hex",
]
[[package]]
name = "proptest"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d"
dependencies = [
"bit-set",
"bit-vec",
"bitflags 2.6.0",
"lazy_static",
"num-traits",
"rand",
"rand_chacha",
"rand_xorshift",
"regex-syntax",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.1"
@ -1907,15 +1865,6 @@ dependencies = [
"rand_core",
]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core",
]
[[package]]
name = "rayon"
version = "1.10.0"
@ -2084,18 +2033,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error 1.2.3",
"tempfile",
"wait-timeout",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -2482,12 +2419,6 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicode-ident"
version = "1.0.13"
@ -2574,7 +2505,6 @@ name = "uu_base32"
version = "0.0.28"
dependencies = [
"clap",
"proptest",
"uucore",
]
@ -2686,7 +2616,7 @@ dependencies = [
"filetime",
"indicatif",
"libc",
"quick-error 2.0.1",
"quick-error",
"selinux",
"uucore",
"walkdir",
@ -3134,7 +3064,7 @@ dependencies = [
"chrono",
"clap",
"itertools",
"quick-error 2.0.1",
"quick-error",
"regex",
"uucore",
]
@ -3631,15 +3561,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "walkdir"
version = "2.5.0"

View file

@ -1,5 +1,3 @@
# spell-checker:ignore proptest
[package]
name = "uu_base32"
version = "0.0.28"
@ -22,9 +20,6 @@ path = "src/base32.rs"
clap = { workspace = true }
uucore = { workspace = true, features = ["encoding"] }
[dev-dependencies]
proptest = "1.5.0"
[[bin]]
name = "base32"
path = "src/main.rs"

View file

@ -5,6 +5,7 @@
pub mod base_common;
use base_common::ReadSeek;
use clap::Command;
use uucore::{encoding::Format, error::UResult, help_about, help_usage};
@ -17,7 +18,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let config = base_common::parse_base_cmd_args(args, ABOUT, USAGE)?;
let mut input = base_common::get_input(&config)?;
let mut input: Box<dyn ReadSeek> = base_common::get_input(&config)?;
base_common::handle_input(&mut input, format, config)
}

View file

@ -3,15 +3,15 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore hexupper lsbf msbf unpadded
// spell-checker:ignore hexupper lsbf msbf unpadded nopad aGVsbG8sIHdvcmxkIQ
use clap::{crate_version, Arg, ArgAction, Command};
use std::fs::File;
use std::io::{self, ErrorKind, Read};
use std::io::{self, ErrorKind, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use uucore::display::Quotable;
use uucore::encoding::{
for_base_common::{BASE32, BASE32HEX, BASE64, BASE64URL, HEXUPPER},
for_base_common::{BASE32, BASE32HEX, BASE64, BASE64URL, BASE64_NOPAD, HEXUPPER},
Format, Z85Wrapper, BASE2LSBF, BASE2MSBF,
};
use uucore::encoding::{EncodingWrapper, SupportsFastDecodeAndEncode};
@ -143,25 +143,50 @@ pub fn base_app(about: &'static str, usage: &str) -> Command {
)
}
pub fn get_input(config: &Config) -> UResult<Box<dyn Read>> {
/// A trait alias for types that implement both `Read` and `Seek`.
pub trait ReadSeek: Read + Seek {}
/// Automatically implement the `ReadSeek` trait for any type that implements both `Read` and `Seek`.
impl<T: Read + Seek> ReadSeek for T {}
pub fn get_input(config: &Config) -> UResult<Box<dyn ReadSeek>> {
match &config.to_read {
Some(path_buf) => {
// Do not buffer input, because buffering is handled by `fast_decode` and `fast_encode`
let file =
File::open(path_buf).map_err_context(|| path_buf.maybe_quote().to_string())?;
Ok(Box::new(file))
}
None => {
let stdin_lock = io::stdin().lock();
Ok(Box::new(stdin_lock))
let mut buffer = Vec::new();
io::stdin().read_to_end(&mut buffer)?;
Ok(Box::new(io::Cursor::new(buffer)))
}
}
}
pub fn handle_input(input: &mut dyn Read, format: Format, config: Config) -> UResult<()> {
let supports_fast_decode_and_encode = get_supports_fast_decode_and_encode(format);
/// Determines if the input buffer ends with padding ('=') after trimming trailing whitespace.
fn has_padding<R: Read + Seek>(input: &mut R) -> UResult<bool> {
let mut buf = Vec::new();
input
.read_to_end(&mut buf)
.map_err(|err| USimpleError::new(1, format_read_error(err.kind())))?;
// Reverse iterator and skip trailing whitespace without extra collections
let has_padding = buf
.iter()
.rfind(|&&byte| !byte.is_ascii_whitespace())
.is_some_and(|&byte| byte == b'=');
input.seek(SeekFrom::Start(0))?;
Ok(has_padding)
}
pub fn handle_input<R: Read + Seek>(input: &mut R, format: Format, config: Config) -> UResult<()> {
let has_padding = has_padding(input)?;
let supports_fast_decode_and_encode =
get_supports_fast_decode_and_encode(format, config.decode, has_padding);
let supports_fast_decode_and_encode_ref = supports_fast_decode_and_encode.as_ref();
@ -184,7 +209,11 @@ pub fn handle_input(input: &mut dyn Read, format: Format, config: Config) -> URe
}
}
pub fn get_supports_fast_decode_and_encode(format: Format) -> Box<dyn SupportsFastDecodeAndEncode> {
pub fn get_supports_fast_decode_and_encode(
format: Format,
decode: bool,
has_padding: bool,
) -> Box<dyn SupportsFastDecodeAndEncode> {
const BASE16_VALID_DECODING_MULTIPLE: usize = 2;
const BASE2_VALID_DECODING_MULTIPLE: usize = 8;
const BASE32_VALID_DECODING_MULTIPLE: usize = 8;
@ -231,13 +260,24 @@ pub fn get_supports_fast_decode_and_encode(format: Format) -> Box<dyn SupportsFa
// spell-checker:disable-next-line
b"0123456789ABCDEFGHIJKLMNOPQRSTUV=",
)),
Format::Base64 => Box::from(EncodingWrapper::new(
BASE64,
BASE64_VALID_DECODING_MULTIPLE,
BASE64_UNPADDED_MULTIPLE,
// spell-checker:disable-next-line
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=+/",
)),
Format::Base64 => {
let alphabet: &[u8] = if has_padding {
&b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/="[..]
} else {
&b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"[..]
};
let wrapper = if decode && !has_padding {
BASE64_NOPAD
} else {
BASE64
};
Box::from(EncodingWrapper::new(
wrapper,
BASE64_VALID_DECODING_MULTIPLE,
BASE64_UNPADDED_MULTIPLE,
alphabet,
))
}
Format::Base64Url => Box::from(EncodingWrapper::new(
BASE64URL,
BASE64_VALID_DECODING_MULTIPLE,
@ -316,6 +356,7 @@ pub mod fast_encode {
encoded_buffer: &mut VecDeque<u8>,
output: &mut dyn Write,
is_cleanup: bool,
empty_wrap: bool,
) -> io::Result<()> {
// TODO
// `encoded_buffer` only has to be a VecDeque if line wrapping is enabled
@ -324,7 +365,9 @@ pub mod fast_encode {
output.write_all(encoded_buffer.make_contiguous())?;
if is_cleanup {
output.write_all(b"\n")?;
if !empty_wrap {
output.write_all(b"\n")?;
}
} else {
encoded_buffer.clear();
}
@ -381,12 +424,13 @@ pub mod fast_encode {
encoded_buffer: &mut VecDeque<u8>,
output: &mut dyn Write,
is_cleanup: bool,
empty_wrap: bool,
) -> io::Result<()> {
// Write all data in `encoded_buffer` to `output`
if let &mut Some(ref mut li) = line_wrapping {
write_with_line_breaks(li, encoded_buffer, output, is_cleanup)?;
} else {
write_without_line_breaks(encoded_buffer, output, is_cleanup)?;
write_without_line_breaks(encoded_buffer, output, is_cleanup, empty_wrap)?;
}
Ok(())
@ -473,9 +517,14 @@ pub mod fast_encode {
)?;
assert!(leftover_buffer.len() < encode_in_chunks_of_size);
// Write all data in `encoded_buffer` to `output`
write_to_output(&mut line_wrapping, &mut encoded_buffer, output, false)?;
write_to_output(
&mut line_wrapping,
&mut encoded_buffer,
output,
false,
wrap == Some(0),
)?;
}
Err(er) => {
let kind = er.kind();
@ -499,7 +548,13 @@ pub mod fast_encode {
// Write all data in `encoded_buffer` to output
// `is_cleanup` triggers special cleanup-only logic
write_to_output(&mut line_wrapping, &mut encoded_buffer, output, true)?;
write_to_output(
&mut line_wrapping,
&mut encoded_buffer,
output,
true,
wrap == Some(0),
)?;
}
Ok(())
@ -759,3 +814,33 @@ fn format_read_error(kind: ErrorKind) -> String {
format!("read error: {kind_string_capitalized}")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_has_padding() {
let test_cases = vec![
("aGVsbG8sIHdvcmxkIQ==", true),
("aGVsbG8sIHdvcmxkIQ== ", true),
("aGVsbG8sIHdvcmxkIQ==\n", true),
("aGVsbG8sIHdvcmxkIQ== \n", true),
("aGVsbG8sIHdvcmxkIQ=", true),
("aGVsbG8sIHdvcmxkIQ= ", true),
("aGVsbG8sIHdvcmxkIQ \n", false),
("aGVsbG8sIHdvcmxkIQ", false),
];
for (input, expected) in test_cases {
let mut cursor = Cursor::new(input.as_bytes());
assert_eq!(
has_padding(&mut cursor).unwrap(),
expected,
"Failed for input: '{}'",
input
);
}
}
}

View file

@ -1,430 +0,0 @@
// spell-checker:ignore lsbf msbf proptest
use proptest::{prelude::TestCaseError, prop_assert, prop_assert_eq, test_runner::TestRunner};
use std::io::Cursor;
use uu_base32::base_common::{fast_decode, fast_encode, get_supports_fast_decode_and_encode};
use uucore::encoding::{Format, SupportsFastDecodeAndEncode};
const CASES: u32 = {
#[cfg(debug_assertions)]
{
32
}
#[cfg(not(debug_assertions))]
{
128
}
};
const NORMAL_INPUT_SIZE_LIMIT: usize = {
#[cfg(debug_assertions)]
{
// 256 kibibytes
256 * 1024
}
#[cfg(not(debug_assertions))]
{
// 4 mebibytes
4 * 1024 * 1024
}
};
const LARGE_INPUT_SIZE_LIMIT: usize = 4 * NORMAL_INPUT_SIZE_LIMIT;
// Note that `TestRunner`s cannot be reused
fn get_test_runner() -> TestRunner {
TestRunner::new(proptest::test_runner::Config {
cases: CASES,
failure_persistence: None,
..proptest::test_runner::Config::default()
})
}
fn generic_round_trip(format: Format) {
let supports_fast_decode_and_encode = get_supports_fast_decode_and_encode(format);
let supports_fast_decode_and_encode_ref = supports_fast_decode_and_encode.as_ref();
// Make sure empty inputs round trip
{
get_test_runner()
.run(
&(
proptest::bool::ANY,
proptest::bool::ANY,
proptest::option::of(0_usize..512_usize),
),
|(ignore_garbage, line_wrap_zero, line_wrap)| {
configurable_round_trip(
format,
supports_fast_decode_and_encode_ref,
ignore_garbage,
line_wrap_zero,
line_wrap,
// Do not add garbage
Vec::<(usize, u8)>::new(),
// Empty input
Vec::<u8>::new(),
)
},
)
.unwrap();
}
// Unusually large line wrapping settings
{
get_test_runner()
.run(
&(
proptest::bool::ANY,
proptest::bool::ANY,
proptest::option::of(512_usize..65_535_usize),
proptest::collection::vec(proptest::num::u8::ANY, 0..NORMAL_INPUT_SIZE_LIMIT),
),
|(ignore_garbage, line_wrap_zero, line_wrap, input)| {
configurable_round_trip(
format,
supports_fast_decode_and_encode_ref,
ignore_garbage,
line_wrap_zero,
line_wrap,
// Do not add garbage
Vec::<(usize, u8)>::new(),
input,
)
},
)
.unwrap();
}
// Spend more time on sane line wrapping settings
{
get_test_runner()
.run(
&(
proptest::bool::ANY,
proptest::bool::ANY,
proptest::option::of(0_usize..512_usize),
proptest::collection::vec(proptest::num::u8::ANY, 0..NORMAL_INPUT_SIZE_LIMIT),
),
|(ignore_garbage, line_wrap_zero, line_wrap, input)| {
configurable_round_trip(
format,
supports_fast_decode_and_encode_ref,
ignore_garbage,
line_wrap_zero,
line_wrap,
// Do not add garbage
Vec::<(usize, u8)>::new(),
input,
)
},
)
.unwrap();
}
// Test with garbage data
{
get_test_runner()
.run(
&(
proptest::bool::ANY,
proptest::bool::ANY,
proptest::option::of(0_usize..512_usize),
// Garbage data to insert
proptest::collection::vec(
(
// Random index
proptest::num::usize::ANY,
// In all of the encodings being tested, non-ASCII bytes are garbage
128_u8..=u8::MAX,
),
0..4_096,
),
proptest::collection::vec(proptest::num::u8::ANY, 0..NORMAL_INPUT_SIZE_LIMIT),
),
|(ignore_garbage, line_wrap_zero, line_wrap, garbage_data, input)| {
configurable_round_trip(
format,
supports_fast_decode_and_encode_ref,
ignore_garbage,
line_wrap_zero,
line_wrap,
garbage_data,
input,
)
},
)
.unwrap();
}
// Test small inputs
{
get_test_runner()
.run(
&(
proptest::bool::ANY,
proptest::bool::ANY,
proptest::option::of(0_usize..512_usize),
proptest::collection::vec(proptest::num::u8::ANY, 0..1_024),
),
|(ignore_garbage, line_wrap_zero, line_wrap, input)| {
configurable_round_trip(
format,
supports_fast_decode_and_encode_ref,
ignore_garbage,
line_wrap_zero,
line_wrap,
// Do not add garbage
Vec::<(usize, u8)>::new(),
input,
)
},
)
.unwrap();
}
// Test small inputs with garbage data
{
get_test_runner()
.run(
&(
proptest::bool::ANY,
proptest::bool::ANY,
proptest::option::of(0_usize..512_usize),
// Garbage data to insert
proptest::collection::vec(
(
// Random index
proptest::num::usize::ANY,
// In all of the encodings being tested, non-ASCII bytes are garbage
128_u8..=u8::MAX,
),
0..1_024,
),
proptest::collection::vec(proptest::num::u8::ANY, 0..1_024),
),
|(ignore_garbage, line_wrap_zero, line_wrap, garbage_data, input)| {
configurable_round_trip(
format,
supports_fast_decode_and_encode_ref,
ignore_garbage,
line_wrap_zero,
line_wrap,
garbage_data,
input,
)
},
)
.unwrap();
}
// Test large inputs
{
get_test_runner()
.run(
&(
proptest::bool::ANY,
proptest::bool::ANY,
proptest::option::of(0_usize..512_usize),
proptest::collection::vec(proptest::num::u8::ANY, 0..LARGE_INPUT_SIZE_LIMIT),
),
|(ignore_garbage, line_wrap_zero, line_wrap, input)| {
configurable_round_trip(
format,
supports_fast_decode_and_encode_ref,
ignore_garbage,
line_wrap_zero,
line_wrap,
// Do not add garbage
Vec::<(usize, u8)>::new(),
input,
)
},
)
.unwrap();
}
}
fn configurable_round_trip(
format: Format,
supports_fast_decode_and_encode: &dyn SupportsFastDecodeAndEncode,
ignore_garbage: bool,
line_wrap_zero: bool,
line_wrap: Option<usize>,
garbage_data: Vec<(usize, u8)>,
mut input: Vec<u8>,
) -> Result<(), TestCaseError> {
// Z85 only accepts inputs with lengths divisible by 4
if let Format::Z85 = format {
// Reduce length of "input" until it is divisible by 4
input.truncate((input.len() / 4) * 4);
assert!((input.len() % 4) == 0);
}
let line_wrap_to_use = if line_wrap_zero { Some(0) } else { line_wrap };
let input_len = input.len();
let garbage_data_len = garbage_data.len();
let garbage_data_is_empty = garbage_data_len == 0;
let (input, encoded) = {
let mut output = Vec::with_capacity(input_len * 8);
let mut cursor = Cursor::new(input);
fast_encode::fast_encode(
&mut cursor,
&mut output,
supports_fast_decode_and_encode,
line_wrap_to_use,
)
.unwrap();
(cursor.into_inner(), output)
};
let encoded_or_encoded_with_garbage = if garbage_data_is_empty {
encoded
} else {
let encoded_len = encoded.len();
let encoded_highest_index = match encoded_len.checked_sub(1) {
Some(0) | None => None,
Some(x) => Some(x),
};
let mut garbage_data_indexed = vec![Option::<u8>::None; encoded_len];
let mut encoded_with_garbage = Vec::<u8>::with_capacity(encoded_len + garbage_data_len);
for (index, garbage_byte) in garbage_data {
if let Some(x) = encoded_highest_index {
let index_to_use = index % x;
garbage_data_indexed[index_to_use] = Some(garbage_byte);
} else {
encoded_with_garbage.push(garbage_byte);
}
}
for (index, encoded_byte) in encoded.into_iter().enumerate() {
encoded_with_garbage.push(encoded_byte);
if let Some(garbage_byte) = garbage_data_indexed[index] {
encoded_with_garbage.push(garbage_byte);
}
}
encoded_with_garbage
};
match line_wrap_to_use {
Some(0) => {
let line_endings_count = encoded_or_encoded_with_garbage
.iter()
.filter(|byte| **byte == b'\n')
.count();
// If line wrapping is disabled, there should only be one '\n' character (at the very end of the output)
prop_assert_eq!(line_endings_count, 1);
}
_ => {
// TODO
// Validate other line wrapping settings
}
}
let decoded_or_error = {
let mut output = Vec::with_capacity(input_len);
let mut cursor = Cursor::new(encoded_or_encoded_with_garbage);
match fast_decode::fast_decode(
&mut cursor,
&mut output,
supports_fast_decode_and_encode,
ignore_garbage,
) {
Ok(()) => Ok(output),
Err(er) => Err(er),
}
};
let made_round_trip = match decoded_or_error {
Ok(ve) => input.as_slice() == ve.as_slice(),
Err(_) => false,
};
let result_was_correct = if garbage_data_is_empty || ignore_garbage {
// If there was no garbage data added, or if "ignore_garbage" was enabled, expect the round trip to succeed
made_round_trip
} else {
// If garbage data was added, and "ignore_garbage" was disabled, expect the round trip to fail
!made_round_trip
};
if !result_was_correct {
eprintln!(
"\
(configurable_round_trip) FAILURE
format: {format:?}
ignore_garbage: {ignore_garbage}
line_wrap_to_use: {line_wrap_to_use:?}
garbage_data_len: {garbage_data_len}
input_len: {input_len}
",
);
}
prop_assert!(result_was_correct);
Ok(())
}
#[test]
fn base16_round_trip() {
generic_round_trip(Format::Base16);
}
#[test]
fn base2lsbf_round_trip() {
generic_round_trip(Format::Base2Lsbf);
}
#[test]
fn base2msbf_round_trip() {
generic_round_trip(Format::Base2Msbf);
}
#[test]
fn base32_round_trip() {
generic_round_trip(Format::Base32);
}
#[test]
fn base32hex_round_trip() {
generic_round_trip(Format::Base32Hex);
}
#[test]
fn base64_round_trip() {
generic_round_trip(Format::Base64);
}
#[test]
fn base64url_round_trip() {
generic_round_trip(Format::Base64Url);
}
#[test]
fn z85_round_trip() {
generic_round_trip(Format::Z85);
}

View file

@ -40,6 +40,28 @@ fn test_encode_repeat_flags_later_wrap_15() {
.stdout_only("aGVsbG8sIHdvcmx\nkIQ==\n"); // spell-checker:disable-line
}
#[test]
fn test_decode_short() {
let input = "aQ";
new_ucmd!()
.args(&["--decode"])
.pipe_in(input)
.succeeds()
.stdout_only("i");
}
#[test]
fn test_multi_lines() {
let input = ["aQ\n\n\n", "a\nQ==\n\n\n"];
for i in input {
new_ucmd!()
.args(&["--decode"])
.pipe_in(i)
.succeeds()
.stdout_only("i");
}
}
#[test]
fn test_base64_encode_file() {
new_ucmd!()
@ -105,6 +127,17 @@ fn test_wrap() {
// spell-checker:disable-next-line
.stdout_only("VGhlIHF1aWNrIGJyb3du\nIGZveCBqdW1wcyBvdmVy\nIHRoZSBsYXp5IGRvZy4=\n");
}
let input = "hello, world";
new_ucmd!()
.args(&["--wrap", "0"])
.pipe_in(input)
.succeeds()
.stdout_only("aGVsbG8sIHdvcmxk"); // spell-checker:disable-line
new_ucmd!()
.args(&["--wrap", "30"])
.pipe_in(input)
.succeeds()
.stdout_only("aGVsbG8sIHdvcmxk\n"); // spell-checker:disable-line
}
#[test]