taler-rust

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

commit 1afd5eec11621049a1abeb930e77b5cddb749222
parent f254d533d053b7f85c3caf1bc67346f2ccd105f2
Author: Antoine A <>
Date:   Sat, 23 May 2026 10:51:18 +0200

common: us mimalloc and clean code

Diffstat:
Mcommon/taler-api/src/api.rs | 4++--
Mcommon/taler-api/src/db.rs | 4++--
Mcommon/taler-api/src/error.rs | 18+++++++++++++++---
Mcommon/taler-api/src/lib.rs | 1+
Mcommon/taler-api/src/subject.rs | 15+++++++++------
Mcommon/taler-common/Cargo.toml | 1+
Mcommon/taler-common/src/cli.rs | 12+++++++-----
Mcommon/taler-common/src/config.rs | 3++-
Mcommon/taler-common/src/lib.rs | 10++++++----
Mcommon/taler-common/src/types/amount.rs | 15+++++++++++++--
Mcommon/taler-common/src/types/timestamp.rs | 28+++++++++++++++++++++++++++-
Mcommon/taler-macros/src/lib.rs | 20+++++++++++++++++++-
Mtaler-apns-relay/src/main.rs | 8++++----
Mtaler-cyclos/src/api.rs | 6+++---
Mtaler-cyclos/src/bin/cyclos-harness.rs | 4++--
Mtaler-cyclos/src/lib.rs | 2+-
Mtaler-cyclos/src/main.rs | 8++++----
Mtaler-magnet-bank/src/api.rs | 6+++---
Mtaler-magnet-bank/src/bin/magnet-bank-harness.rs | 4++--
Mtaler-magnet-bank/src/dev.rs | 10+++++-----
Mtaler-magnet-bank/src/lib.rs | 2+-
Mtaler-magnet-bank/src/main.rs | 8++++----
22 files changed, 133 insertions(+), 56 deletions(-)

diff --git a/common/taler-api/src/api.rs b/common/taler-api/src/api.rs @@ -92,7 +92,7 @@ pub trait TalerRouter { fn finalize(self) -> Self; fn serve( self, - serve: Serve, + serve: &Serve, lifetime: Option<u32>, ) -> impl std::future::Future<Output = std::io::Result<()>> + Send; } @@ -116,7 +116,7 @@ impl TalerRouter for Router { .layer(middleware::from_fn(logger_middleware)) } - async fn serve(mut self, serve: Serve, lifetime: Option<u32>) -> std::io::Result<()> { + async fn serve(mut self, serve: &Serve, lifetime: Option<u32>) -> std::io::Result<()> { let listener = serve.resolve()?; let notify = Arc::new(tokio::sync::Notify::new()); diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -91,8 +91,8 @@ macro_rules! serialized { const MAX_RETRIES: u32 = 5; loop { - let res = $logic.await; - if let sqlx::Result::Err(e) = &res + let res: sqlx::Result<_, sqlx::Error> = $logic.await; + if let Err(e) = &res && e.is_retryable_err() && attempts < MAX_RETRIES { diff --git a/common/taler-api/src/error.rs b/common/taler-api/src/error.rs @@ -30,7 +30,7 @@ use taler_common::{ pub type ApiResult<T> = Result<T, ApiError>; -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub struct ApiError { code: ErrorCode, hint: Option<Box<str>>, @@ -40,6 +40,16 @@ pub struct ApiError { headers: Option<Box<HeaderMap>>, } +impl Display for ApiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(h) = &self.hint { + f.write_str(h) + } else { + f.write_str(self.code.description()) + } + } +} + impl ApiError { pub fn new(code: ErrorCode) -> Self { Self { @@ -93,10 +103,12 @@ impl From<sqlx::Error> for ApiError { sqlx::Error::Configuration(_) => { (ErrorCode::GENERIC_DB_SETUP_FAILED, StatusCode::BAD_GATEWAY) } - sqlx::Error::Database(_) | sqlx::Error::Io(_) | sqlx::Error::Tls(_) => { + sqlx::Error::Database(_) + | sqlx::Error::Io(_) + | sqlx::Error::Tls(_) + | sqlx::Error::PoolTimedOut => { (ErrorCode::GENERIC_DB_FETCH_FAILED, StatusCode::BAD_GATEWAY) } - sqlx::Error::PoolTimedOut => todo!(), _ => ( ErrorCode::BANK_UNMANAGED_EXCEPTION, StatusCode::INTERNAL_SERVER_ERROR, diff --git a/common/taler-api/src/lib.rs b/common/taler-api/src/lib.rs @@ -33,6 +33,7 @@ pub mod subject; #[cfg(test)] pub mod test; +#[derive(Debug, Clone)] pub enum Serve { Tcp(SocketAddr), Unix { diff --git a/common/taler-api/src/subject.rs b/common/taler-api/src/subject.rs @@ -472,12 +472,15 @@ mod test { assert_eq!(parse_incoming_unstructured(&case), result); } - // Check prefer prefixed over simple - for case in [format!("{mixed_l}-{mixed_r} {standard_l}-{standard_r}")] { + // Check prefer prefixed over simple ones + for case in [ + format!("{standard_l}-{standard_r} {mixed_l}-{mixed_r}"), + format!("{mixed_l}-{mixed_r} {standard_l}-{standard_r}"), + ] { let res = parse_incoming_unstructured(&case); - if ty == IncomingType::reserve { - assert_eq!(res, Err(IncomingSubjectErr::Ambiguous)); - } else { + if !(ty == IncomingType::reserve + && matches!(res, Err(IncomingSubjectErr::Ambiguous))) + { assert_eq!(res, result); } } @@ -486,7 +489,7 @@ mod test { for case in [ "does not contain any reserve", // Check fail if none &standard[1..], // Check fail if missing char - //"2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0", // Check fail if not a valid key + // "2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0", // Check fail if not a valid key TODO aws-lc does not check ] { assert_eq!( parse_incoming_unstructured(case), diff --git a/common/taler-common/Cargo.toml b/common/taler-common/Cargo.toml @@ -11,6 +11,7 @@ license-file.workspace = true doctest = false [dependencies] +mimalloc = "0.1" glob = "0.3" indexmap = "2.7" tempfile.workspace = true diff --git a/common/taler-common/src/cli.rs b/common/taler-common/src/cli.rs @@ -16,6 +16,8 @@ use std::io::Write as _; +use compact_str::CompactString; + use crate::config::Config; /// Inspect the configuration @@ -23,18 +25,18 @@ use crate::config::Config; pub enum ConfigCmd { /// Lookup config value Get { - section: String, - option: String, + section: CompactString, + option: CompactString, /// Interpret value as path with dollar-expansion - #[clap(short, long, default_value_t = false)] + #[arg(short, long)] filename: bool, }, /// Substitute variables in a path - Pathsub { path_expr: String }, + Pathsub { path_expr: CompactString }, /// Dump the configuration Dump { /// output extra diagnostics - #[clap(short, long, default_value_t = false)] + #[arg(short, long)] diagnostics: bool, }, } diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -1121,6 +1121,7 @@ mod test { const DEFAULT_CONF: &str = "[PATHS]\nDATADIR=mydir\nRECURSIVE=$RECURSIVE"; + #[allow(clippy::type_complexity)] fn routine<T: Debug + Eq>( ty: &str, mut lambda: impl for<'cfg, 'arg> FnMut(&Section<'cfg, 'arg>, &'arg str) -> Value<'arg, T>, @@ -1272,7 +1273,7 @@ mod test { format!("amount '{it}' invalid fraction (invalid digit found in string)") }), (&["KUDOS:999999999999999999"], |it| { - format!("amount '{it}' value overflow (must be <= 9007199254740992)") + format!("amount '{it}' value overflow (must be <= 4503599627370496)") }), (&["EUR:12"], |_| { "expected currency KUDOS got EUR".to_owned() diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs @@ -17,6 +17,7 @@ use std::{path::PathBuf, time::Duration}; use config::{Config, parser::ConfigSource}; +use mimalloc::MiMalloc; use tracing::error; use tracing_subscriber::util::SubscriberInitExt; @@ -34,16 +35,17 @@ pub mod json_file; pub mod log; pub mod types; +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + #[derive(clap::Parser, Debug, Clone)] pub struct CommonArgs { /// Specifies the configuration file - #[clap(long, short)] - #[arg(global = true)] + #[arg(short, long, global = true)] config: Option<PathBuf>, /// Configure logging to use LOGLEVEL - #[clap(long, short('L'))] - #[arg(global = true)] + #[arg(short('L'), long, global = true)] log: Option<tracing::Level>, } diff --git a/common/taler-common/src/types/amount.rs b/common/taler-common/src/types/amount.rs @@ -22,6 +22,8 @@ use std::{ str::FromStr, }; +use compact_str::format_compact; + use super::utils::InlineStr; /** Number of characters we use to represent currency names */ @@ -29,7 +31,7 @@ use super::utils::InlineStr; pub const CURRENCY_LEN: usize = 11; /** Maximum legal value for an amount, based on IEEE double */ -pub const MAX_VALUE: u64 = 2 << 52; +pub const MAX_VALUE: u64 = 2 << 51; /** The number of digits in a fraction part of an amount */ pub const FRAC_BASE_NB_DIGITS: u8 = 8; @@ -257,7 +259,7 @@ impl Display for Decimal { if self.frac == 0 { f.write_fmt(format_args!("{}", self.val)) } else { - let num = format!("{:08}", self.frac); + let num = format_compact!("{:08}", self.frac); f.write_fmt(format_args!("{}.{}", self.val, num.trim_end_matches('0'))) } } @@ -442,6 +444,15 @@ impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Amount { } #[test] +fn constants() { + assert_eq!(format!("{}", Amount::zero(&Currency::KUDOS)), "KUDOS:0"); + assert_eq!( + format!("{}", Amount::max(&Currency::KUDOS)), + "KUDOS:4503599627370496.99999999" + ); +} + +#[test] fn test_amount_parse() { const TALER_AMOUNT_FRAC_BASE: u32 = 100000000; // https://git.taler.net/exchange.git/tree/src/util/test_amount.c diff --git a/common/taler-common/src/types/timestamp.rs b/common/taler-common/src/types/timestamp.rs @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Display, ops::Add, time::Duration}; +use std::{fmt::Display, ops::Add, str::FromStr, time::Duration}; use jiff::{Timestamp, civil::Time, tz::TimeZone}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error, ser::SerializeStruct}; // codespell:ignore @@ -27,6 +27,19 @@ pub enum TalerTimestamp { Timestamp(Timestamp), } +impl FromStr for TalerTimestamp { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s == "never" { + return Ok(Self::Never); + } + let s: i64 = s.parse()?; + + Ok(Self::Timestamp(jiff::Timestamp::from_second(s)?)) + } +} + #[derive(Serialize, Deserialize)] struct TimestampImpl { t_s: Value, @@ -144,6 +157,19 @@ pub enum RelativeTime { Duration(Duration), } +impl FromStr for RelativeTime { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s == "forever" { + return Ok(Self::Forever); + } + let micros: u64 = s.parse()?; + + Ok(Self::Duration(Duration::from_micros(micros))) + } +} + #[derive(Serialize, Deserialize)] struct RelativeTimeImpl { d_us: Value, diff --git a/common/taler-macros/src/lib.rs b/common/taler-macros/src/lib.rs @@ -95,7 +95,25 @@ pub fn derive_domain_code(input: TokenStream) -> TokenStream { for variant in variants { let v_ident = &variant.ident; - let v_str = v_ident.to_string(); + let v_str = variant + .attrs + .iter() + .find_map(|attr| { + if attr.path().is_ident("enum_meta") { + let mut res = None; + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + let value: LitStr = meta.value()?.parse()?; + res = Some(value.value()); + } + Ok(()) + }); + res + } else { + None + } + }) + .unwrap_or_else(|| v_ident.to_string()); if repr_type.is_some() { if let Some((_, discriminant)) = &variant.discriminant { diff --git a/taler-apns-relay/src/main.rs b/taler-apns-relay/src/main.rs @@ -43,19 +43,19 @@ enum Command { /// Initialize taler-apns-relay database Dbinit { /// Reset database (DANGEROUS: All existing data is lost) - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, /// Check taler-apns-relay config Setup { /// Remove all registered devices - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, /// Run taler-apns-relay worker Worker { /// Execute once and return - #[clap(long, short)] + #[arg(short, long)] transient: bool, }, /// Run taler-apns-relay HTTP server @@ -77,7 +77,7 @@ async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> { let pool = pool(cfg).await?; let cfg = ServeCfg::parse(cfg)?; let api = Arc::new(RelayApi::new(pool)); - router(api).serve(cfg.serve, None).await?; + router(api).serve(&cfg.serve, None).await?; } Command::Worker { transient } => { let pool = pool(cfg).await?; diff --git a/taler-cyclos/src/api.rs b/taler-cyclos/src/api.rs @@ -480,7 +480,7 @@ mod test { let (server, db) = &setup().await; out_history_routine( - &server, + server, tasks!({ out_talerable(db).await }), tasks!( { out_bounce(db).await }, @@ -502,7 +502,7 @@ mod test { async fn in_history() { let (server, db) = &setup().await; in_history_routine( - &server, + server, &ACCOUNT, true, tasks!({ in_talerable(db).await }), @@ -520,7 +520,7 @@ mod test { async fn revenue() { let (server, db) = &setup().await; revenue_routine( - &server, + server, &ACCOUNT, true, tasks!({ in_malformed(db).await }, { in_talerable(db).await },), diff --git a/taler-cyclos/src/bin/cyclos-harness.rs b/taler-cyclos/src/bin/cyclos-harness.rs @@ -67,12 +67,12 @@ struct Args { enum Command { /// Run logic tests Logic { - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, /// Run online tests Online { - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, } diff --git a/taler-cyclos/src/lib.rs b/taler-cyclos/src/lib.rs @@ -43,6 +43,6 @@ pub async fn run_serve(cfg: &Config, pool: PgPool) -> anyhow::Result<()> { if let Some(cfg) = cfg.revenue { router = router.revenue(api, cfg.auth.method()); } - router.serve(cfg.serve, None).await?; + router.serve(&cfg.serve, None).await?; Ok(()) } diff --git a/taler-cyclos/src/main.rs b/taler-cyclos/src/main.rs @@ -43,26 +43,26 @@ enum Command { /// Initialize taler-cyclos database Dbinit { /// Reset database (DANGEROUS: All existing data is lost) - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, /// Check taler-cyclos config Setup { /// Reset connection info and overwrite keys file - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, /// Run taler-cyclos worker Worker { /// Execute once and return - #[clap(long, short)] + #[arg(short, long)] transient: bool, }, /// Run taler-cyclos HTTP server Serve { /// Check whether an API is in use (if it's useful to start the HTTP /// server). Exit with 0 if at least one API is enabled, otherwise 1 - #[clap(long)] + #[arg(long)] check: bool, }, #[command(subcommand)] diff --git a/taler-magnet-bank/src/api.rs b/taler-magnet-bank/src/api.rs @@ -454,7 +454,7 @@ mod test { let (server, db) = &setup().await; out_history_routine( - &server, + server, tasks!({ out_talerable(db).await }), tasks!( { out_bounce(db).await }, @@ -476,7 +476,7 @@ mod test { async fn in_history() { let (server, db) = &setup().await; in_history_routine( - &server, + server, &ACCOUNT, true, tasks!({ in_talerable(db).await }), @@ -494,7 +494,7 @@ mod test { async fn revenue() { let (server, db) = &setup().await; revenue_routine( - &server, + server, &ACCOUNT, true, tasks!({ in_malformed(db).await }, { in_talerable(db).await },), diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs @@ -66,12 +66,12 @@ struct Args { enum Command { /// Run logic tests Logic { - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, /// Run online tests Online { - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, } diff --git a/taler-magnet-bank/src/dev.rs b/taler-magnet-bank/src/dev.rs @@ -49,17 +49,17 @@ pub enum DevCmd { Accounts, Tx { account: HuIban, - #[clap(long, short, value_enum, default_value_t = DirArg::Both)] + #[arg(long, short, value_enum, default_value_t = DirArg::Both)] direction: DirArg, }, Transfer { - #[clap(long)] + #[arg(long)] debtor: HuPayto, - #[clap(long)] + #[arg(long)] creditor: TransferHuPayto, - #[clap(long)] + #[arg(long)] amount: Option<Amount>, - #[clap(long)] + #[arg(long)] subject: Option<String>, }, } diff --git a/taler-magnet-bank/src/lib.rs b/taler-magnet-bank/src/lib.rs @@ -47,7 +47,7 @@ pub async fn run_serve(cfg: &Config, pool: PgPool) -> anyhow::Result<()> { if let Some(cfg) = cfg.revenue { router = router.revenue(api, cfg.auth.method()); } - router.serve(cfg.serve, None).await?; + router.serve(&cfg.serve, None).await?; Ok(()) } diff --git a/taler-magnet-bank/src/main.rs b/taler-magnet-bank/src/main.rs @@ -43,26 +43,26 @@ enum Command { /// Initialize taler-magnet-bank database Dbinit { /// Reset database (DANGEROUS: All existing data is lost) - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, /// Setup taler-magnet-bank auth token and account settings for Wire Gateway use Setup { /// Reset connection info and overwrite keys file - #[clap(long, short)] + #[arg(short, long)] reset: bool, }, /// Run taler-magnet-bank worker Worker { /// Execute once and return - #[clap(long, short)] + #[arg(short, long)] transient: bool, }, /// Run taler-magnet-bank HTTP server Serve { /// Check whether an API is in use (if it's useful to start the HTTP /// server). Exit with 0 if at least one API is enabled, otherwise 1 - #[clap(long)] + #[arg(long)] check: bool, }, #[command(subcommand)]