taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit d555c5d5185aa88beb2c3be5460d030ef474028b
parent 71b444262a04ea7a362bb0da60edf5c68f90110e
Author: Antoine A <>
Date:   Sat,  7 Mar 2026 19:47:13 +0100

common: improve base32 utils

Diffstat:
Mcommon/taler-common/benches/base32.rs | 8++++----
Mcommon/taler-common/src/log.rs | 1+
Mcommon/taler-common/src/types/base32.rs | 42++++++++++++++++++++++++++++++++++++------
Mtaler-magnet-bank/Cargo.toml | 2+-
4 files changed, 42 insertions(+), 11 deletions(-)

diff --git a/common/taler-common/benches/base32.rs b/common/taler-common/benches/base32.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024-2025 Taler Systems SA + Copyright (C) 2024, 2025, 2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -15,7 +15,7 @@ */ use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; -use taler_common::types::base32::{Base32, decode, encode_static}; +use taler_common::types::base32::{Base32, decode_static, encode_static}; fn parser(c: &mut Criterion) { let mut buf = [0u8; 255]; @@ -35,14 +35,14 @@ fn parser(c: &mut Criterion) { c.bench_function("base32_decode_valid", |b| { b.iter_batched( || Base32::<64>::rand().to_string(), - |case| decode::<64>(case.as_bytes()).unwrap(), + |case| decode_static::<64>(case.as_bytes()).unwrap(), BatchSize::SmallInput, ) }); c.bench_function("base32_decode_random", |b| { b.iter_batched( || (0..56).map(|_| fastrand::char(..)).collect::<String>(), - |case| decode::<64>(case.as_bytes()).ok(), + |case| decode_static::<64>(case.as_bytes()).ok(), BatchSize::SmallInput, ) }); diff --git a/common/taler-common/src/log.rs b/common/taler-common/src/log.rs @@ -72,6 +72,7 @@ pub fn taler_logger(max_level: Option<Level>) -> impl SubscriberInitExt { *metadata.level() <= max_level && !(target.starts_with("sqlx") || target.contains("hyper_util") + || target.starts_with("h2") || target.starts_with("reqwest") || target.starts_with("rustls") || target.starts_with("hyper_rustls")) diff --git a/common/taler-common/src/types/base32.rs b/common/taler-common/src/types/base32.rs @@ -44,6 +44,19 @@ pub fn encode_static<'a, const N: usize>(bytes: &[u8; N], out: &'a mut [u8]) -> unsafe { std::str::from_utf8_unchecked(truncated) } } +/** Encode bytes using Crockford's base32 */ +pub fn encode(bytes: &[u8]) -> String { + let mut buf = vec![0u8; encoded_buf_len(bytes.len())]; + // Batch encoded + encode_batch(bytes, &mut buf); + + // Truncate incomplete ending chunk + buf.truncate(encoded_len(bytes.len())); + + // SAFETY: only contains valid ASCII characters from CROCKFORD_ALPHABET + unsafe { std::string::String::from_utf8_unchecked(buf) } +} + /** Batch encode bytes using Crockford's base32 */ #[inline] fn encode_batch(bytes: &[u8], encoded: &mut [u8]) { @@ -117,7 +130,7 @@ const fn decoded_buf_len(len: usize) -> usize { } /** Decode N bytes from a Crockford's base32 string */ -pub fn decode<const N: usize>(encoded: &[u8]) -> Result<[u8; N], Base32Error<N>> { +pub fn decode_static<const N: usize>(encoded: &[u8]) -> Result<[u8; N], Base32Error<N>> { // Check decode length let output_length = decoded_len(encoded.len()); if output_length != N { @@ -125,6 +138,26 @@ pub fn decode<const N: usize>(encoded: &[u8]) -> Result<[u8; N], Base32Error<N>> } let mut decoded = vec![0u8; decoded_buf_len(encoded.len())]; // TODO use a stack allocated buffer when supported + + if !decode_batch(encoded, &mut decoded) { + return Err(Base32Error::Format); + } + Ok(decoded[..N].try_into().unwrap()) +} + +/** Decode bytes from a Crockford's base32 string */ +pub fn decode(encoded: &[u8]) -> Result<Vec<u8>, Base32Error<0>> { + let mut decoded = vec![0u8; decoded_buf_len(encoded.len())]; + + if !decode_batch(encoded, &mut decoded) { + return Err(Base32Error::Format); + } + Ok(decoded) +} + +/** Batch decode bytes using Crockford's base32 */ +#[inline] +fn decode_batch(encoded: &[u8], decoded: &mut [u8]) -> bool { let mut invalid = false; // Encode chunks of 8 chars for 5B @@ -147,10 +180,7 @@ pub fn decode<const N: usize>(encoded: &[u8]) -> Result<[u8; N], Base32Error<N>> decoded[4] = (buf[6] << 5) | buf[7]; } - if invalid { - return Err(Base32Error::Format); - } - Ok(decoded[..N].try_into().unwrap()) + !invalid } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -182,7 +212,7 @@ impl<const L: usize> FromStr for Base32<L> { type Err = Base32Error<L>; fn from_str(s: &str) -> Result<Self, Self::Err> { - Ok(Self(decode(s.as_bytes())?)) + Ok(Self(decode_static(s.as_bytes())?)) } } diff --git a/taler-magnet-bank/Cargo.toml b/taler-magnet-bank/Cargo.toml @@ -12,7 +12,7 @@ license-file.workspace = true form_urlencoded = "1.2" percent-encoding = "2.3" rpassword = "7.4" -getrandom = "0.3.4" +getrandom = "0.4.1" sqlx.workspace = true serde_json = { workspace = true, features = ["raw_value"] } jiff = { workspace = true, features = ["serde"] }