#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
use super::Case;
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
use bech32::Hrp;
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
use bech32::primitives::iter::{ByteIterExt, Fe32IterExt};
#[cfg(feature = "encoding-bech32")]
use bech32::primitives::checksum::Checksum;
#[cfg(feature = "encoding-bech32")]
pub(crate) const GENERATOR_SH: [u32; 5] = [
0x3b6a_57b2,
0x2650_8e6d,
0x1ea1_19fa,
0x3d42_33dd,
0x2a14_62b3,
];
#[cfg(feature = "encoding-bech32")]
pub const BECH32_CODE_LENGTH: usize = 1023;
#[cfg(feature = "encoding-bech32")]
#[must_use]
pub const fn bech32_code_length(hrp_len: usize, payload_bytes: usize) -> usize {
let payload_chars = (payload_bytes / 5)
.saturating_mul(8)
.saturating_add(((payload_bytes % 5) * 8).div_ceil(5));
hrp_len
.saturating_add(1)
.saturating_add(payload_chars)
.saturating_add(6)
}
#[cfg(feature = "encoding-bech32")]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Bech32Sized<const CODE_LENGTH: usize> {}
#[cfg(feature = "encoding-bech32")]
impl<const N: usize> Checksum for Bech32Sized<N> {
type MidstateRepr = u32;
const CODE_LENGTH: usize = N;
const CHECKSUM_LENGTH: usize = 6;
const GENERATOR_SH: [u32; 5] = GENERATOR_SH;
const TARGET_RESIDUE: u32 = 1;
}
#[cfg(feature = "encoding-bech32")]
pub type Bech32Standard = Bech32Sized<BECH32_CODE_LENGTH>;
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
use crate::error::Bech32Error;
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
pub trait ToBech32 {
fn try_to_bech32(&self, hrp: &str, case: Case) -> Result<crate::EncodedSecret, Bech32Error>;
fn try_to_bech32_sized<const N: usize>(
&self,
hrp: &str,
case: Case,
) -> Result<crate::EncodedSecret, Bech32Error>;
}
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
impl<T: AsRef<[u8]> + super::EncodableBytes + ?Sized> ToBech32 for T {
#[inline(always)]
fn try_to_bech32(&self, hrp: &str, case: Case) -> Result<crate::EncodedSecret, Bech32Error> {
self.try_to_bech32_sized::<BECH32_CODE_LENGTH>(hrp, case)
}
#[inline(always)]
fn try_to_bech32_sized<const N: usize>(
&self,
hrp: &str,
case: Case,
) -> Result<crate::EncodedSecret, Bech32Error> {
let hrp_parsed = Hrp::parse(hrp).map_err(|_| Bech32Error::InvalidHrp)?;
let data = self.as_ref();
let len = bech32_code_length(hrp.len(), data.len());
if len > N {
return Err(Bech32Error::OperationFailed);
}
let mut out = alloc::string::String::with_capacity(len);
let chain = data
.iter()
.copied()
.bytes_to_fes()
.with_checksum::<Bech32Sized<N>>(&hrp_parsed)
.chars();
for c in chain {
out.push(c);
}
debug_assert_eq!(
out.len(),
len,
"bech32_code_length disagreed with the encoder"
);
if matches!(case, Case::Upper) {
out.make_ascii_uppercase();
}
Ok(crate::EncodedSecret::new(out))
}
}
#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
use bech32::primitives::iter::ByteIterExt;
use bech32::{Bech32, Fe32, Fe32IterExt, NoChecksum, decode, encode_lower};
#[test]
fn encoder_chain_carries_no_staging_buffer() {
let data = [0xABu8; 1568];
let hrp = Hrp::parse("age").unwrap();
let chain = data
.iter()
.copied()
.bytes_to_fes()
.with_checksum::<Bech32Sized<4096>>(&hrp)
.chars();
let size = core::mem::size_of_val(&chain);
assert!(
size <= 96,
"encoder chain is {size} bytes -- something payload-sized is being staged"
);
}
#[test]
fn direct_chain_matches_upstream_encode_lower() {
use bech32::encode_lower;
for (hrp, len) in [
("a", 0usize),
("age", 1),
("age", 4),
("age", 32),
("kem", 633),
("age", 634),
("kem", 1568),
("x", 4096),
] {
let data: Vec<u8> = (0..len).map(|i| (i * 131 + 7) as u8).collect();
let ours = data
.try_to_bech32_sized::<65535>(hrp, Case::Lower)
.expect("ours");
let theirs = encode_lower::<Bech32Sized<65535>>(Hrp::parse(hrp).unwrap(), &data)
.expect("upstream");
assert_eq!(&*ours, &*theirs, "hrp={hrp} len={len}");
}
}
#[test]
fn test_bech32_sized_with_checksum() {
let large_data = vec![0u8; 1000];
let hrp = Hrp::parse("test").unwrap();
let encoded = encode_lower::<Bech32Sized<2048>>(hrp, &large_data).unwrap();
let pos = encoded.rfind('1').unwrap();
let hrp_str = &encoded[..pos];
let data_str = &encoded[pos + 1..];
let decoded_hrp = Hrp::parse(hrp_str).unwrap();
let data_part = &data_str[..data_str.len() - 6];
let mut fe32s = Vec::new();
for c in data_part.chars() {
fe32s.push(Fe32::from_char(c).unwrap());
}
let decoded_data: Vec<u8> = fe32s.iter().copied().fes_to_bytes().collect();
assert_eq!(decoded_hrp, hrp);
assert_eq!(decoded_data, large_data);
let re_encoded = encode_lower::<Bech32Sized<2048>>(decoded_hrp, &decoded_data).unwrap();
assert_eq!(re_encoded, encoded);
}
#[test]
fn test_bit_conversion_large_uncapped() {
let large_data = vec![0u8; 4096];
let fes: Vec<Fe32> = large_data.iter().copied().bytes_to_fes().collect();
assert_eq!(fes.len(), (large_data.len() * 8).div_ceil(5));
let bytes_back: Vec<u8> = fes.iter().copied().fes_to_bytes().collect();
assert_eq!(bytes_back, large_data);
}
#[test]
fn test_full_encode_decode_uncapped() {
let large_data = vec![0u8; 1000];
let hrp = Hrp::parse("test").unwrap();
let encoded = encode_lower::<NoChecksum>(hrp, &large_data).unwrap();
assert!(encoded.len() > 1000 * 8 / 5);
let s = &encoded;
let pos = s.rfind('1').unwrap();
let hrp_str = &s[..pos];
let data_str = &s[pos + 1..];
let decoded_hrp = Hrp::parse(hrp_str).unwrap();
let mut fe32s = Vec::new();
for c in data_str.chars() {
fe32s.push(Fe32::from_char(c).unwrap());
}
let decoded_data: Vec<u8> = fe32s.iter().copied().fes_to_bytes().collect();
assert_eq!(decoded_hrp, hrp);
assert_eq!(decoded_data, large_data);
}
#[test]
fn test_bip173_roundtrip() {
let data = b"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"[4..].to_vec();
let hrp = Hrp::parse("bc").unwrap();
let encoded = encode_lower::<Bech32>(hrp, &data).unwrap();
let (decoded_hrp, decoded_data) = decode(&encoded).unwrap();
assert_eq!(decoded_hrp, hrp);
assert_eq!(decoded_data, data);
}
}