diff --git a/src/uu/base32/locales/en-US.ftl b/src/uu/base32/locales/en-US.ftl index c083d892804..925e5c70a25 100644 --- a/src/uu/base32/locales/en-US.ftl +++ b/src/uu/base32/locales/en-US.ftl @@ -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 diff --git a/src/uu/base32/locales/fr-FR.ftl b/src/uu/base32/locales/fr-FR.ftl index 98c554bfbb7..c5ca10b7109 100644 --- a/src/uu/base32/locales/fr-FR.ftl +++ b/src/uu/base32/locales/fr-FR.ftl @@ -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 diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 7e874f4c8ea..fe13e46cc0c 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -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 {}), } } diff --git a/src/uu/basenc/src/basenc.rs b/src/uu/basenc/src/basenc.rs index 6498832274c..42e4ef295bd 100644 --- a/src/uu/basenc/src/basenc.rs +++ b/src/uu/basenc/src/basenc.rs @@ -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")), ] } diff --git a/src/uucore/src/lib/features/encoding.rs b/src/uucore/src/lib/features/encoding.rs index 566dfe19f10..90a5e9ba8d8 100644 --- a/src/uucore/src/lib/features/encoding.rs +++ b/src/uucore/src/lib/features/encoding.rs @@ -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) -> 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 = 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) -> 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 = 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 diff --git a/tests/by-util/test_basenc.rs b/tests/by-util/test_basenc.rs index 52acd35d40b..0a6a6ddc1d6 100644 --- a/tests/by-util/test_basenc.rs +++ b/tests/by-util/test_basenc.rs @@ -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("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("nm=QNz.92jz/PV8\n"); + .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!()