Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions src/uucore/src/lib/features/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,36 +266,50 @@ impl SupportsFastDecodeAndEncode for Base58Wrapper {
return Ok(());
}

// Convert bytes to big integer
let mut num: Vec<u32> = Vec::new();
// Convert bytes to big integer (Vec<u32> in little-endian format)
let mut num = Vec::with_capacity(input_trimmed.len().div_ceil(4) + 1);
for &byte in input_trimmed {
let mut carry = byte as u32;
let mut carry = byte as u64;
for n in &mut num {
let tmp = (*n as u64) * 256 + carry as u64;
let tmp = (*n as u64) * 256 + carry;
*n = tmp as u32;
carry = (tmp >> 32) as u32;
carry = tmp >> 32;
}
if carry > 0 {
num.push(carry);
num.push(carry as u32);
}
}

// Convert to base58
let mut result = Vec::new();
let mut result = Vec::with_capacity((input_trimmed.len() * 138 / 100) + 1);
let alphabet = self.alphabet();

while !num.is_empty() && num.iter().any(|&n| n != 0) {
// Optimized check: stop when all elements are zero
while !num.is_empty() {
// Check if we're done (all zeros)
let mut all_zero = true;
let mut carry = 0u64;

for n in num.iter_mut().rev() {
let tmp = carry * (1u64 << 32) + *n as u64;
*n = (tmp / 58) as u32;
carry = tmp % 58;
if *n != 0 {
all_zero = false;
}
}

result.push(alphabet[carry as usize]);

// Remove leading zeros
while num.last() == Some(&0) && num.len() > 1 {
num.pop();
if all_zero {
break;
}

// Trim trailing zeros less frequently
if num.len() > 1 && result.len() % 8 == 0 {
while num.last() == Some(&0) && num.len() > 1 {
num.pop();
}
}
}

Expand All @@ -305,15 +319,18 @@ impl SupportsFastDecodeAndEncode for Base58Wrapper {
}

// Add result (reversed because we built it backwards)
for byte in result.into_iter().rev() {
for &byte in result.iter().rev() {
output.push_back(byte);
}

Ok(())
}

fn unpadded_multiple(&self) -> usize {
1 // Base58 doesn't use padding
// Base58 must encode the entire input as one big integer, not in chunks
// Use a very large value to effectively disable chunking, but avoid overflow
// when multiplied by ENCODE_IN_CHUNKS_OF_SIZE_MULTIPLE (1024) in base_common
usize::MAX / 2048
}

fn valid_decoding_multiple(&self) -> usize {
Expand Down
25 changes: 25 additions & 0 deletions tests/by-util/test_basenc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,31 @@ fn test_base58_decode() {
.stdout_only("Hello, World!");
}

#[test]
fn test_base58_large_file_no_chunking() {
// Regression test: base58 must process entire input as one big integer,
// not in 1024-byte chunks. This test ensures files >1024 bytes work correctly.
let (at, mut ucmd) = at_and_ucmd!();
let filename = "large_file.txt";

// spell-checker:disable
let input = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(50);
// spell-checker:enable
at.write(filename, &input);

let result = ucmd.arg("--base58").arg(filename).succeeds();
let encoded = result.stdout_str();

// Verify the output ends with the expected suffix (matches GNU basenc output)
// spell-checker:disable
assert!(
encoded
.trim_end()
.ends_with("ZNRRacEnhrY83ZEYkpwWVZNFK5DFRasr\nw693NsNGtiQ9fYAj")
);
// spell-checker:enable
}

#[test]
fn test_choose_last_encoding_base64() {
new_ucmd!()
Expand Down
Loading