taler-rust

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

commit bb173f9b41774547e2ea7b37b9b0c9f3dbb1aa34
parent b94ab22e7b48693d804a00c11166ab19a6860dd9
Author: Antoine A <>
Date:   Wed, 25 Dec 2024 02:05:32 +0100

taler-common: refactor base32 code

Diffstat:
MCargo.lock | 4++--
Mtaler-api/src/db.rs | 3++-
Mtaler-api/tests/api.rs | 4++--
Mtaler-common/src/api_common.rs | 159++-----------------------------------------------------------------------------
Mtaler-common/src/types.rs | 1+
Mtaler-common/src/types/amount.rs | 2--
Ataler-common/src/types/base32.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 184 insertions(+), 162 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2729,9 +2729,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" diff --git a/taler-api/src/db.rs b/taler-api/src/db.rs @@ -22,10 +22,11 @@ use sqlx::{ }; use sqlx::{Postgres, Row}; use taler_common::{ - api_common::{Base32, SafeU64}, + api_common::SafeU64, api_params::{History, Page}, types::{ amount::{Amount, Decimal}, + base32::Base32, payto::Payto, timestamp::Timestamp, }, diff --git a/taler-api/tests/api.rs b/taler-api/tests/api.rs @@ -18,13 +18,13 @@ use common::sample_wire_gateway_api; use sqlx::PgPool; use taler_api::{auth::AuthMethod, db::IncomingType, standard_layer}; use taler_common::{ - api_common::{Base32, EddsaPublicKey, HashCode, ShortHashCode}, + api_common::{EddsaPublicKey, HashCode, ShortHashCode}, api_wire::{ IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList, TransferResponse, TransferState, TransferStatus, }, error_code::ErrorCode, - types::{amount::amount, url}, + types::{amount::amount, base32::Base32, url}, }; use test_utils::{ axum_test::TestServer, diff --git a/taler-common/src/api_common.rs b/taler-common/src/api_common.rs @@ -14,11 +14,13 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, ops::Deref, str::FromStr}; +use std::{fmt::Display, ops::Deref}; -use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{de::Error, Deserialize, Deserializer, Serialize}; use serde_json::value::RawValue; +use crate::types::base32::Base32; + /// <https://docs.taler.net/core/api-common.html#tsref-type-ErrorDetail> #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ErrorDetail { @@ -103,159 +105,6 @@ impl Display for SafeU64 { } } -#[derive(Debug, thiserror::Error)] -pub enum Base32Error<const L: usize> { - #[error("Invalid Crockford’s base32 format")] - Format, - #[error("Invalid length: expected {L} bytess got {0}")] - Length(usize), -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Base32<const L: usize>([u8; L]); - -impl<const L: usize> Base32<L> { - pub fn rand() -> Self { - let mut bytes = [0; L]; - fastrand::fill(&mut bytes); - Self(bytes) - } -} - -impl<const L: usize> From<[u8; L]> for Base32<L> { - fn from(array: [u8; L]) -> Self { - Self(array) - } -} - -impl<const L: usize> Deref for Base32<L> { - type Target = [u8; L]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[rustfmt::skip] -/* - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <, =, >, ?, @, A, B, C, - D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, - X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, - l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, -*/ -const CROCKFORD_INV: [i8; 75] = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, - 13, 14, 15, 16, 17, 1, 18, 19, 1, 20, 21, 0, 22, 23, 24, 25, 26, -1, 27, 28, - 29, 30, 31, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 1, 18, 19, - 1, 20, 21, 0, 22, 23, 24, 25, 26, -1, 27, 28, 29, 30, 31, -]; - -impl<const L: usize> FromStr for Base32<L> { - type Err = Base32Error<L>; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - let encoded = s.as_bytes(); - - // Check decode length - let output_length = encoded.len() * 5 / 8; - if output_length != L { - return Err(Base32Error::Length(output_length)); - } - - let mut decoded = [0u8; L]; - let mut bits = 0; - let mut buf = 0u16; - let mut cursor = 0usize; - - for b in encoded { - // Read input - match CROCKFORD_INV.get(b.wrapping_sub(b'0') as usize).copied() { - Some(-1) | None => return Err(Base32Error::Format), - Some(lookup) => { - // Add 5 bits - buf = (buf << 5) | (lookup as u16); - bits += 5; - - // Write byte if full - if bits >= 8 { - bits -= 8; - unsafe { - // SAFETY we know this algorithm never produce more than L bytes - *decoded.get_unchecked_mut(cursor) = (buf >> bits) as u8; - } - cursor += 1; - } - } - } - } - Ok(Self(decoded)) - } -} - -pub fn base32(bytes: &[u8]) -> String { - const CROCKFORD_ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; - let mut encoded = Vec::with_capacity((bytes.len() + 3) / 4 * 5); - let mut bits = 0; - let mut buf = 0u32; - - for &byte in bytes { - // Add a byte - buf = (buf << 8) | byte as u32; - bits += 8; - - // Write 5 bits - while bits >= 5 { - bits -= 5; - let index = (buf >> bits) & 0b11111; // Extract top 5 bits - encoded.push(CROCKFORD_ALPHABET[index as usize]); - } - } - - if bits > 0 { - // Handle remaining bits (padding) - let index = (buf << (5 - bits)) & 0b11111; // Shift to fill 5 bits - encoded.push(CROCKFORD_ALPHABET[index as usize]); - } - - unsafe { String::from_utf8_unchecked(encoded) } -} - -impl<const L: usize> Display for Base32<L> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&base32(&self.0)) - } -} - -impl<const L: usize> Serialize for Base32<L> { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de, const L: usize> Deserialize<'de> for Base32<L> { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - Base32::from_str(&String::deserialize(deserializer)?).map_err(D::Error::custom) - } -} - -impl<'a, const L: usize> TryFrom<&'a [u8]> for Base32<L> { - type Error = Base32Error<L>; - - fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { - Ok(Self( - value - .try_into() - .map_err(|_| Base32Error::Length(value.len()))?, - )) - } -} - /// EdDSA and ECDHE public keys always point on Curve25519 /// and represented using the standard 256 bits Ed25519 compact format, /// converted to Crockford Base32. diff --git a/taler-common/src/types.rs b/taler-common/src/types.rs @@ -17,6 +17,7 @@ pub mod amount; pub mod payto; pub mod timestamp; +pub mod base32; use url::Url; diff --git a/taler-common/src/types/amount.rs b/taler-common/src/types/amount.rs @@ -330,8 +330,6 @@ fn test_amount_parse() { for str in INVALID_AMOUNTS { let amount = Amount::from_str(str); assert!(amount.is_err(), "invalid {} got {:?}", str, &amount); - dbg!(&amount); - dbg!(amount.unwrap_err().to_string()); } let valid_amounts: Vec<(&str, Amount)> = vec![ diff --git a/taler-common/src/types/base32.rs b/taler-common/src/types/base32.rs @@ -0,0 +1,173 @@ +/* + This file is part of TALER + Copyright (C) 2024 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 + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use std::{fmt::Display, ops::Deref, str::FromStr}; + +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, thiserror::Error)] +pub enum Base32Error<const L: usize> { + #[error("Invalid Crockford’s base32 format")] + Format, + #[error("Invalid length: expected {L} bytess got {0}")] + Length(usize), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Base32<const L: usize>([u8; L]); + +impl<const L: usize> Base32<L> { + pub fn rand() -> Self { + let mut bytes = [0; L]; + fastrand::fill(&mut bytes); + Self(bytes) + } +} + +impl<const L: usize> From<[u8; L]> for Base32<L> { + fn from(array: [u8; L]) -> Self { + Self(array) + } +} + +impl<const L: usize> Deref for Base32<L> { + type Target = [u8; L]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[rustfmt::skip] +/* + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <, =, >, ?, @, A, B, C, + D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, + X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, + l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, +*/ +const CROCKFORD_INV: [i8; 75] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, + 13, 14, 15, 16, 17, 1, 18, 19, 1, 20, 21, 0, 22, 23, 24, 25, 26, -1, 27, 28, + 29, 30, 31, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 1, 18, 19, + 1, 20, 21, 0, 22, 23, 24, 25, 26, -1, 27, 28, 29, 30, 31, +]; + +pub const CROCKFORD_ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + +impl<const L: usize> FromStr for Base32<L> { + type Err = Base32Error<L>; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let encoded = s.as_bytes(); + + // Check decode length + let output_length = encoded.len() * 5 / 8; + if output_length != L { + return Err(Base32Error::Length(output_length)); + } + + let mut decoded = [0u8; L]; + let mut bits = 0; + let mut buf = 0u16; + let mut cursor = 0usize; + + for b in encoded { + // Read input + match CROCKFORD_INV.get(b.wrapping_sub(b'0') as usize).copied() { + Some(-1) | None => return Err(Base32Error::Format), + Some(lookup) => { + // Add 5 bits + buf = (buf << 5) | (lookup as u16); + bits += 5; + + // Write byte if full + if bits >= 8 { + bits -= 8; + unsafe { + // SAFETY we know this algorithm never produce more than L bytes + *decoded.get_unchecked_mut(cursor) = (buf >> bits) as u8; + } + cursor += 1; + } + } + } + } + Ok(Self(decoded)) + } +} + +pub fn base32(bytes: &[u8]) -> String { + let mut encoded = Vec::with_capacity((bytes.len() + 3) / 4 * 5); + let mut bits = 0; + let mut buf = 0u32; + + for &byte in bytes { + // Add a byte + buf = (buf << 8) | byte as u32; + bits += 8; + + // Write 5 bits + while bits >= 5 { + bits -= 5; + let index = (buf >> bits) & 0b11111; // Extract top 5 bits + encoded.push(CROCKFORD_ALPHABET[index as usize]); + } + } + + if bits > 0 { + // Handle remaining bits (padding) + let index = (buf << (5 - bits)) & 0b11111; // Shift to fill 5 bits + encoded.push(CROCKFORD_ALPHABET[index as usize]); + } + + unsafe { String::from_utf8_unchecked(encoded) } +} + +impl<const L: usize> Display for Base32<L> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&base32(&self.0)) + } +} + +impl<const L: usize> Serialize for Base32<L> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de, const L: usize> Deserialize<'de> for Base32<L> { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Base32::from_str(&String::deserialize(deserializer)?).map_err(D::Error::custom) + } +} + +impl<'a, const L: usize> TryFrom<&'a [u8]> for Base32<L> { + type Error = Base32Error<L>; + + fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> { + Ok(Self( + value + .try_into() + .map_err(|_| Base32Error::Length(value.len()))?, + )) + } +}