commit bb173f9b41774547e2ea7b37b9b0c9f3dbb1aa34
parent b94ab22e7b48693d804a00c11166ab19a6860dd9
Author: Antoine A <>
Date: Wed, 25 Dec 2024 02:05:32 +0100
taler-common: refactor base32 code
Diffstat:
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()))?,
+ ))
+ }
+}