taler-rust

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

commit d66028551bc19183089ab4df0b7ecfaa68f9bf81
parent 95c2f76677ca08b5ce1ce603cd2229f00ff885d1
Author: Antoine A <>
Date:   Thu,  2 Jan 2025 18:36:28 +0100

taler-common: optimize base32 encoding

Diffstat:
MCargo.lock | 26++++++++++++++++++++++----
Mtaler-api/tests/api.rs | 4+---
Mtaler-common/benches/base32.rs | 13+++++++------
Mtaler-common/src/types/base32.rs | 65++++++++++++++++++++++++++++++++++++++++++++---------------------
4 files changed, 74 insertions(+), 34 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1236,10 +1236,13 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.1.16" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a46169c7a10358cdccfb179910e8a5a392fc291bdb409da9aeece5b19786d8" +checksum = "943611a469f78ab9afdac9022e473a80fca16a9deca6c5be3eb566d872231e76" dependencies = [ + "log", + "portable-atomic", + "portable-atomic-util", "serde", ] @@ -1633,6 +1636,21 @@ dependencies = [ ] [[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2361,9 +2379,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.93" +version = "2.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" +checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" dependencies = [ "proc-macro2", "quote", diff --git a/taler-api/tests/api.rs b/taler-api/tests/api.rs @@ -251,9 +251,7 @@ async fn incoming_history(pool: PgPool) { .map(|it| match it { IncomingBankTransaction::Reserve { row_id, .. } | IncomingBankTransaction::Wad { row_id, .. } - | IncomingBankTransaction::Kyc { row_id, .. } => { - *row_id as i64 - } + | IncomingBankTransaction::Kyc { row_id, .. } => *row_id as i64, }) .collect() }, diff --git a/taler-common/benches/base32.rs b/taler-common/benches/base32.rs @@ -15,10 +15,10 @@ */ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use taler_common::types::base32::{decode, encode, Base32}; +use taler_common::types::base32::{decode, encode_static, encoded_batch_len, Base32}; fn parser(c: &mut Criterion) { - let mut rng = fastrand::Rng::with_seed(42); + let mut rng = fastrand::Rng::new(); let encode_random: Vec<_> = (0..30) .map(|_| { let mut bytes = [0; 64]; @@ -31,21 +31,22 @@ fn parser(c: &mut Criterion) { .map(|_| (0..56).map(|_| rng.char(..)).collect()) .collect(); - c.bench_function("encode_random", |b| { + let mut buf = [0u8; encoded_batch_len(64)]; + c.bench_function("base32_encode_random", |b| { b.iter(|| { for case in &encode_random { - encode(black_box(case.as_ref())); + encode_static(black_box(case), &mut buf); } }) }); - c.bench_function("decode_valid", |b| { + c.bench_function("base32_decode_valid", |b| { b.iter(|| { for case in &decode_valid { decode::<32>(black_box(case.as_str())).ok(); } }) }); - c.bench_function("decode_randoms", |b| { + c.bench_function("base32_decode_random", |b| { b.iter(|| { for case in &decode_randoms { decode::<32>(black_box(case.as_str())).ok(); diff --git a/taler-common/src/types/base32.rs b/taler-common/src/types/base32.rs @@ -20,32 +20,54 @@ use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; pub const CROCKFORD_ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; -/** Encode bytes into a Crockford's base32 string */ -pub fn encode(bytes: &[u8]) -> String { - // Pre allocate for all chunks - let mut encoded = Vec::with_capacity((bytes.len() / 5 + 1) * 8); +/** Encoded bytes len of Crockford's base32 */ +#[inline] +pub const fn encoded_len(len: usize) -> usize { + return (len * 8 + 4) / 5; +} + +/** Buffer bytes len of Crockford's base32 using a batch of 8 chars */ +#[inline] +pub const fn encoded_batch_len(len: usize) -> usize { + return (len / 5 + 1) * 8; +} + +/** Encode bytes using Crockford's base32 */ +#[inline] +pub fn encode_static<'a, const N: usize>(bytes: &[u8; N], out: &'a mut [u8]) -> &'a str { + // Batch encoded + encode_batch(bytes, out); + + // Truncate incomplete ending chunk + let truncated = &out[..encoded_len(bytes.len())]; + + // SAFETY: only contains valid ASCII characters from CROCKFORD_ALPHABET + unsafe { std::str::from_utf8_unchecked(&truncated) } +} + +/** Batch encode bytes using Crockford's base32 */ +#[inline] +fn encode_batch(bytes: &[u8], out: &mut [u8]) { + // Check buffer len + assert!(out.len() >= encoded_batch_len(bytes.len())); + // Encode chunks of 5B for 8 chars - for chunk in bytes.chunks(5) { + for (i, chunk) in bytes.chunks(5).enumerate() { let mut buf = [0u8; 5]; for (i, &b) in chunk.iter().enumerate() { buf[i] = b; } - encoded.extend_from_slice(&[ - CROCKFORD_ALPHABET[((buf[0] & 0xF8) >> 3) as usize], - CROCKFORD_ALPHABET[(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6)) as usize], - CROCKFORD_ALPHABET[((buf[1] & 0x3E) >> 1) as usize], - CROCKFORD_ALPHABET[(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4)) as usize], - CROCKFORD_ALPHABET[(((buf[2] & 0x0F) << 1) | (buf[3] >> 7)) as usize], - CROCKFORD_ALPHABET[((buf[3] & 0x7C) >> 2) as usize], - CROCKFORD_ALPHABET[(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5)) as usize], - CROCKFORD_ALPHABET[(buf[4] & 0x1F) as usize], - ]); + // SAFETY: we only write at most encoded_batch_len bytes + let out = unsafe { out.get_unchecked_mut(i * 8..(i + 1) * 8) }; + out[0] = CROCKFORD_ALPHABET[((buf[0] & 0xF8) >> 3) as usize]; + out[1] = CROCKFORD_ALPHABET[(((buf[0] & 0x07) << 2) | ((buf[1] & 0xC0) >> 6)) as usize]; + out[2] = CROCKFORD_ALPHABET[((buf[1] & 0x3E) >> 1) as usize]; + out[3] = CROCKFORD_ALPHABET[(((buf[1] & 0x01) << 4) | ((buf[2] & 0xF0) >> 4)) as usize]; + out[4] = CROCKFORD_ALPHABET[(((buf[2] & 0x0F) << 1) | (buf[3] >> 7)) as usize]; + out[5] = CROCKFORD_ALPHABET[((buf[3] & 0x7C) >> 2) as usize]; + out[6] = CROCKFORD_ALPHABET[(((buf[3] & 0x03) << 3) | ((buf[4] & 0xE0) >> 5)) as usize]; + out[7] = CROCKFORD_ALPHABET[(buf[4] & 0x1F) as usize]; } - // Truncate incomplete ending chunk - encoded.truncate((bytes.len() * 8 + 4) / 5); - - // SAFETY: only contains valid ASCII characters from CROCKFORD_ALPHABET - unsafe { String::from_utf8_unchecked(encoded) } } #[derive(Debug, thiserror::Error)] @@ -144,7 +166,8 @@ impl<const L: usize> FromStr for Base32<L> { impl<const L: usize> Display for Base32<L> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&encode(&self.0)) + let mut buff = vec![0u8; encoded_batch_len(L)]; // TODO use a stack allocated buffer when supported + f.write_str(encode_static(&self.0, &mut buff)) } }