basenc: implement --base58 encoding option (#8751)

* basenc: implement --base58 encoding option

Add support for Base58 encoding to basenc as per GNU coreutils 9.8.
Base58 uses the alphabet '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
which excludes visually ambiguous characters (0, O, I, l).

Resolves issue #8744.

* basenc: fix clippy warnings and spelling issues

Fix explicit iteration clippy warnings. Add Base58 alphabet to spell-checker ignore list to resolve cspell errors.
This commit is contained in:
AnarchistHoneybun 2025-10-05 17:33:45 +05:30 committed by GitHub
parent c0cdd904b8
commit e16ce60a38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 177 additions and 15 deletions

View file

@ -42,6 +42,7 @@ basenc-help-base2msbf = bit string with most significant bit (msb) first
basenc-help-z85 = ascii85-like encoding;
when encoding, input length must be a multiple of 4;
when decoding, input length must be a multiple of 5
basenc-help-base58 = visually unambiguous base58 encoding
# Error messages
basenc-error-missing-encoding-type = missing encoding type

View file

@ -37,6 +37,7 @@ basenc-help-base2msbf = chaîne de bits avec le bit de poids fort (msb) en premi
basenc-help-z85 = encodage de type ascii85 ;
lors de l'encodage, la longueur d'entrée doit être un multiple de 4 ;
lors du décodage, la longueur d'entrée doit être un multiple de 5
basenc-help-base58 = encodage base58 visuellement non ambigu
# Messages d'erreur
basenc-error-missing-encoding-type = type d'encodage manquant

View file

@ -12,8 +12,8 @@ use std::io::{self, ErrorKind, Read, Seek};
use std::path::{Path, PathBuf};
use uucore::display::Quotable;
use uucore::encoding::{
BASE2LSBF, BASE2MSBF, Base64SimdWrapper, EncodingWrapper, Format, SupportsFastDecodeAndEncode,
Z85Wrapper,
BASE2LSBF, BASE2MSBF, Base58Wrapper, Base64SimdWrapper, EncodingWrapper, Format,
SupportsFastDecodeAndEncode, Z85Wrapper,
for_base_common::{BASE32, BASE32HEX, BASE64URL, HEXUPPER_PERMISSIVE},
};
use uucore::error::{FromIo, UResult, USimpleError, UUsageError};
@ -285,6 +285,7 @@ pub fn get_supports_fast_decode_and_encode(
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=_-",
)),
Format::Z85 => Box::from(Z85Wrapper {}),
Format::Base58 => Box::from(Base58Wrapper {}),
}
}

View file

@ -39,6 +39,7 @@ fn get_encodings() -> Vec<(&'static str, Format, String)> {
translate!("basenc-help-base2msbf"),
),
("z85", Format::Z85, translate!("basenc-help-z85")),
("base58", Format::Base58, translate!("basenc-help-base58")),
]
}

View file

@ -5,6 +5,7 @@
// spell-checker:ignore (encodings) lsbf msbf
// spell-checker:ignore unpadded
// spell-checker:ignore ABCDEFGHJKLMNPQRSTUVWXY Zabcdefghijkmnopqrstuvwxyz
use crate::error::{UResult, USimpleError};
use base64_simd;
@ -105,6 +106,7 @@ pub enum Format {
Base2Lsbf,
Base2Msbf,
Z85,
Base58,
}
pub const BASE2LSBF: Encoding = new_encoding! {
@ -119,6 +121,8 @@ pub const BASE2MSBF: Encoding = new_encoding! {
pub struct Z85Wrapper {}
pub struct Base58Wrapper {}
pub struct EncodingWrapper {
pub alphabet: &'static [u8],
pub encoding: Encoding,
@ -181,6 +185,142 @@ pub trait SupportsFastDecodeAndEncode {
fn valid_decoding_multiple(&self) -> usize;
}
impl SupportsFastDecodeAndEncode for Base58Wrapper {
fn alphabet(&self) -> &'static [u8] {
// Base58 alphabet
b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
}
fn decode_into_vec(&self, input: &[u8], output: &mut Vec<u8>) -> UResult<()> {
if input.is_empty() {
return Ok(());
}
// Count leading zeros (will become leading 1s in base58)
let leading_ones = input.iter().take_while(|&&b| b == b'1').count();
// Skip leading 1s for conversion
let input_trimmed = &input[leading_ones..];
if input_trimmed.is_empty() {
output.resize(output.len() + leading_ones, 0);
return Ok(());
}
// Convert base58 to big integer
let mut num: Vec<u32> = vec![0];
let alphabet = self.alphabet();
for &byte in input_trimmed {
// Find position in alphabet
let digit = alphabet
.iter()
.position(|&b| b == byte)
.ok_or_else(|| USimpleError::new(1, "error: invalid input".to_owned()))?;
// Multiply by 58 and add digit
let mut carry = digit as u32;
for n in &mut num {
let tmp = (*n as u64) * 58 + carry as u64;
*n = tmp as u32;
carry = (tmp >> 32) as u32;
}
if carry > 0 {
num.push(carry);
}
}
// Convert to bytes (little endian, then reverse)
let mut result = Vec::new();
for &n in &num {
result.extend_from_slice(&n.to_le_bytes());
}
// Remove trailing zeros and reverse to get big endian
while result.last() == Some(&0) && result.len() > 1 {
result.pop();
}
result.reverse();
// Add leading zeros for leading 1s in input
let mut final_result = vec![0; leading_ones];
final_result.extend_from_slice(&result);
output.extend_from_slice(&final_result);
Ok(())
}
fn encode_to_vec_deque(&self, input: &[u8], output: &mut VecDeque<u8>) -> UResult<()> {
if input.is_empty() {
return Ok(());
}
// Count leading zeros
let leading_zeros = input.iter().take_while(|&&b| b == 0).count();
// Skip leading zeros
let input_trimmed = &input[leading_zeros..];
if input_trimmed.is_empty() {
for _ in 0..leading_zeros {
output.push_back(b'1');
}
return Ok(());
}
// Convert bytes to big integer
let mut num: Vec<u32> = Vec::new();
for &byte in input_trimmed {
let mut carry = byte as u32;
for n in &mut num {
let tmp = (*n as u64) * 256 + carry as u64;
*n = tmp as u32;
carry = (tmp >> 32) as u32;
}
if carry > 0 {
num.push(carry);
}
}
// Convert to base58
let mut result = Vec::new();
let alphabet = self.alphabet();
while !num.is_empty() && num.iter().any(|&n| n != 0) {
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;
}
result.push(alphabet[carry as usize]);
// Remove leading zeros
while num.last() == Some(&0) && num.len() > 1 {
num.pop();
}
}
// Add leading 1s for leading zeros in input
for _ in 0..leading_zeros {
output.push_back(b'1');
}
// Add result (reversed because we built it backwards)
for byte in result.into_iter().rev() {
output.push_back(byte);
}
Ok(())
}
fn unpadded_multiple(&self) -> usize {
1 // Base58 doesn't use padding
}
fn valid_decoding_multiple(&self) -> usize {
1 // Any length is valid for Base58
}
}
impl SupportsFastDecodeAndEncode for Z85Wrapper {
fn alphabet(&self) -> &'static [u8] {
// Z85 alphabet

View file

@ -185,21 +185,30 @@ fn test_base2lsbf_decode() {
}
#[test]
fn test_choose_last_encoding_z85() {
fn test_z85_decode() {
new_ucmd!()
.args(&[
"--base2lsbf",
"--base2msbf",
"--base16",
"--base32hex",
"--base64url",
"--base32",
"--base64",
"--z85",
])
.pipe_in("Hello, World")
.args(&["--z85", "-d"])
.pipe_in("nm=QNz.92jz/PV8")
.succeeds()
.stdout_only("nm=QNz.92jz/PV8\n");
.stdout_only("Hello, World");
}
#[test]
fn test_base58() {
new_ucmd!()
.arg("--base58")
.pipe_in("Hello, World!")
.succeeds()
.stdout_only("72k1xXWG59fYdzSNoA\n");
}
#[test]
fn test_base58_decode() {
new_ucmd!()
.args(&["--base58", "-d"])
.pipe_in("72k1xXWG59fYdzSNoA")
.succeeds()
.stdout_only("Hello, World!");
}
#[test]
@ -238,6 +247,15 @@ fn test_choose_last_encoding_base2lsbf() {
.stdout_only("00110110110011100100011001100110\n");
}
#[test]
fn test_choose_last_encoding_base58() {
new_ucmd!()
.args(&["--base64", "--base32", "--base16", "--z85", "--base58"])
.pipe_in("Hello!")
.succeeds()
.stdout_only("d3yC1LKr\n");
}
#[test]
fn test_base32_decode_repeated() {
new_ucmd!()