taler-rust

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

commit 9db73672548899f1626c9c8ab854f4b0b26a02bc
parent 82f62b5b336f494e432831af97aecea9d1b7383b
Author: Antoine A <>
Date:   Tue, 16 Jun 2026 12:29:29 +0200

common: improve config parsing and data encoding

Diffstat:
Mcommon/taler-common/src/config.rs | 43++++++++++++++++++++++++++++++++++++++-----
Mcommon/taler-common/src/db.rs | 20++++++++++++++++----
Mcommon/taler-common/src/encoding/base32.rs | 4++--
Mcommon/taler-common/src/encoding/base64.rs | 4++--
Mcommon/taler-common/src/encoding/hex.rs | 10+++++-----
Mcommon/taler-common/src/lib.rs | 2+-
Mcommon/taler-common/src/types/base32.rs | 6+++---
Mcommon/taler-test-utils/src/db.rs | 7+++++--
8 files changed, 72 insertions(+), 24 deletions(-)

diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -340,8 +340,9 @@ pub mod parser { Ok(()) } + /// Get a read-only shareable Config from the parser pub fn finish(self) -> Config { - // Convert to a readonly config struct without location info + // Convert to a read-only config struct without location info Config(Arc::new(Inner { sections: self.sections, files: self.files, @@ -450,7 +451,9 @@ pub mod parser { } impl Config { - pub fn from_file( + /// Load a config for a Taler component, optionally also load from a file. + /// This is the standard way to load a Taler component config + pub fn load( src: ConfigSource, path: Option<impl Into<PathBuf>>, ) -> Result<Config, ParserErr> { @@ -470,18 +473,45 @@ pub mod parser { Ok(parser.finish()) } + /// Load config from an in memory string for testing pub fn from_mem(str: &str) -> Result<Config, ParserErr> { let mut parser = Parser::empty(); parser.parse_str(str)?; Ok(parser.finish()) } + /// Load config from an in memory string with env from a Taler component for testing pub fn from_mem_with_env(src: ConfigSource, str: &str) -> Result<Config, ParserErr> { let mut parser = Parser::empty(); parser.load_env(src)?; parser.parse_str(str)?; Ok(parser.finish()) } + + /// Load a config for a Taler component, optionally also load from a file and an in memory string, for testing + pub fn from_file_override( + src: ConfigSource, + path: Option<impl Into<PathBuf>>, + str: &str, + ) -> Result<Config, ParserErr> { + let mut parser = Parser::empty(); + parser.load_env(src)?; + match path { + Some(path) => { + parser.parse_file(path.into(), 0)?; + } + None => { + if let Some(default) = src + .default_config_path() + .map_err(|(p, e)| io_err("find default config path", p, e))? + { + parser.parse_file(default, 0)?; + } + } + } + parser.parse_str(str)?; + Ok(parser.finish()) + } } } @@ -528,7 +558,7 @@ struct Line { } #[derive(Debug)] -pub struct Inner { +struct Inner { sections: IndexMap<String, IndexMap<String, Line>>, files: Vec<PathBuf>, install_path: PathBuf, @@ -544,6 +574,7 @@ impl Debug for Config { } impl Config { + /// Get a config section from its name pub fn section<'cfg, 'arg>(&'cfg self, section: &'arg str) -> Section<'cfg, 'arg> { Section { name: section, @@ -552,6 +583,7 @@ impl Config { } } + /// List all config sections pub fn sections<'cfg>(&'cfg self) -> impl Iterator<Item = Section<'cfg, 'cfg>> { self.0.sections.iter().map(|(section, values)| Section { name: section, @@ -660,6 +692,7 @@ impl Config { } } + /// Print config in a human format, optionally with diagnostics information pub fn print(&self, mut f: impl std::io::Write, diagnostics: bool) -> std::io::Result<()> { let Inner { sections, @@ -1029,8 +1062,8 @@ mod test { let config_path_fmt = config_path.to_string_lossy(); let second_path_fmt = second_path.to_string_lossy(); - let check_err = |err: String| check_err(err, Config::from_file(SOURCE, Some(&config_path))); - let check_ok = || Config::from_file(SOURCE, Some(&config_path)).unwrap(); + let check_err = |err: String| check_err(err, Config::load(SOURCE, Some(&config_path))); + let check_ok = || Config::load(SOURCE, Some(&config_path)).unwrap(); check_err(format!( "Could not read config at '{config_path_fmt}': entity not found" diff --git a/common/taler-common/src/db.rs b/common/taler-common/src/db.rs @@ -21,7 +21,7 @@ use std::{ }; use sqlx::{ - Connection, Executor, PgConnection, PgPool, Row, + Connection, Executor, PgConnection, PgPool, Postgres, Row, Transaction, postgres::{PgConnectOptions, PgPoolOptions, PgRow}, }; use taler_macros::EnumMeta; @@ -75,7 +75,17 @@ pub async fn dbinit( sql_dir: &Path, prefix: &str, reset: bool, -) -> Result<(), MigrationErr> { +) -> anyhow::Result<()> { + dbinit_setup(conn, sql_dir, prefix, reset, async |_| Ok(())).await +} + +pub async fn dbinit_setup( + conn: &mut PgConnection, + sql_dir: &Path, + prefix: &str, + reset: bool, + setup: impl AsyncFn(&mut Transaction<Postgres>) -> anyhow::Result<()>, +) -> anyhow::Result<()> { let mut tx = conn.begin().await?; let exec_sql_file = @@ -124,7 +134,7 @@ pub async fn dbinit( ); break; } - return Err(e); + return Err(e.into()); } } @@ -138,10 +148,12 @@ pub async fn dbinit( "no procedures.sql for the SQL collection: '{prefix}'" ); } else { - return Err(e); + return Err(e.into()); } } + setup(&mut tx).await?; + tx.commit().await?; Ok(()) diff --git a/common/taler-common/src/encoding/base32.rs b/common/taler-common/src/encoding/base32.rs @@ -75,7 +75,7 @@ pub fn fmt(bytes: impl AsRef<[u8]>) -> impl Display { /** Encode a chunk using Crockford's base32 */ #[inline(always)] -pub(crate) fn encode_chunk(chunk: &[u8], encoded: &mut [u8]) { +pub(crate) fn encode_chunk(chunk: &[u8], encoded: &mut [u8; 8]) { let mut buf = [0u8; 5]; for (i, &b) in chunk.iter().enumerate() { buf[i] = b; @@ -97,7 +97,7 @@ fn encode_batch(bytes: &[u8], encoded: &mut [u8]) { assert!(encoded.len() >= encoded_buf_len(bytes.len())); // Encode chunks of 5B for 8 chars - for (chunk, encoded) in bytes.chunks(5).zip(encoded.chunks_exact_mut(8)) { + for (chunk, encoded) in bytes.chunks(5).zip(encoded.as_chunks_mut().0) { encode_chunk(chunk, encoded); } } diff --git a/common/taler-common/src/encoding/base64.rs b/common/taler-common/src/encoding/base64.rs @@ -27,7 +27,7 @@ const fn encoded_len(len: usize) -> usize { /** Encode a chunk using base64 */ #[inline(always)] -fn encode_chunk(chunk: &[u8], encoded: &mut [u8]) { +fn encode_chunk(chunk: &[u8], encoded: &mut [u8; 4]) { let mut buf = [0u8; 3]; for (i, &b) in chunk.iter().enumerate() { buf[i] = b; @@ -47,7 +47,7 @@ pub fn encode(bytes: impl AsRef<[u8]>) -> String { let bytes = bytes.as_ref(); let mut buf = vec![b'='; encoded_len(bytes.len())]; - for (chunk, buf) in bytes.chunks(3).zip(buf.chunks_exact_mut(4)) { + for (chunk, buf) in bytes.chunks(3).zip(buf.as_chunks_mut().0) { encode_chunk(chunk, buf) } diff --git a/common/taler-common/src/encoding/hex.rs b/common/taler-common/src/encoding/hex.rs @@ -20,7 +20,7 @@ pub const HEX_ALPHABET: &[u8] = b"0123456789abcdef"; /** Encode a single byte to two hex characters */ #[inline(always)] -fn encode_byte(byte: u8, encoded: &mut [u8]) { +fn encode_byte(byte: u8, encoded: &mut [u8; 2]) { encoded[0] = HEX_ALPHABET[(byte >> 4) as usize]; encoded[1] = HEX_ALPHABET[(byte & 0x0F) as usize]; } @@ -30,7 +30,7 @@ pub fn encode(bytes: impl AsRef<[u8]>) -> String { let bytes = bytes.as_ref(); let mut buf = vec![0u8; bytes.len() * 2]; - for (chunk, buf) in bytes.iter().zip(buf.chunks_exact_mut(2)) { + for (chunk, buf) in bytes.iter().zip(buf.as_chunks_mut().0) { encode_byte(*chunk, buf); } @@ -85,9 +85,9 @@ pub fn decode(encoded: impl AsRef<[u8]>) -> Result<Vec<u8>, HexError> { let mut decoded = Vec::with_capacity(encoded.len() / 2); let mut invalid = false; - for chunk in encoded.chunks_exact(2) { - let hi = HEX_INV[chunk[0] as usize]; - let lo = HEX_INV[chunk[1] as usize]; + for [hi, lo] in encoded.as_chunks::<2>().0 { + let hi = HEX_INV[*hi as usize]; + let lo = HEX_INV[*lo as usize]; invalid |= hi == 255 || lo == 255; diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs @@ -59,7 +59,7 @@ pub fn taler_main( app: impl AsyncFnOnce(&Config) -> Result<(), anyhow::Error>, ) { taler_logger(args.log, args.verbose).init(); - let cfg = match Config::from_file(src, args.config) { + let cfg = match Config::load(src, args.config) { Ok(cfg) => cfg, Err(err) => { error!(target: "config", "{}", err); diff --git a/common/taler-common/src/types/base32.rs b/common/taler-common/src/types/base32.rs @@ -19,7 +19,7 @@ use std::{borrow::Cow, fmt::Display, ops::Deref, str::FromStr}; use rand::{TryRng, rngs::SysRng}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; -use crate::encoding::base32::{Base32Error, decode_static, encode_static, encoded_buf_len}; +use crate::encoding::base32::{self, Base32Error, decode_static}; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Base32<const L: usize>([u8; L]); @@ -60,8 +60,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 { - let mut buff = vec![0u8; encoded_buf_len(L)]; // TODO use a stack allocated buffer when supported - f.write_str(encode_static(&self.0, &mut buff)) + // TODO use a unique stack allocated buffer when supported + f.write_fmt(format_args!("{}", base32::fmt(&self.0))) } } diff --git a/common/taler-test-utils/src/db.rs b/common/taler-test-utils/src/db.rs @@ -28,14 +28,16 @@ use tracing::info; use crate::setup_tracing; +/// Create a reusable test database and run dbinit for a Taler component pub async fn db_test_setup(src: ConfigSource) -> (PoolConnection<Postgres>, PgPool) { - let cfg = Config::from_file(src, None::<&str>).unwrap(); + let cfg = Config::load(src, None::<&str>).unwrap(); let name = format!("{}db-postgres", src.component_name); let sect = cfg.section(&name); let db_cfg = DbCfg::parse(sect).unwrap(); db_test_setup_manual(db_cfg.sql_dir.as_ref(), src.component_name).await } +/// Create a reusable test database and run dbinit for a Taler component pub async fn db_test_setup_manual( sql_dir: &Path, component_name: &str, @@ -52,7 +54,8 @@ pub async fn db_test_setup_manual( (conn, pool) } -async fn test_db() -> PgConnectOptions { +/// Create a temporary test database that will be reuse by future tests +pub async fn test_db() -> PgConnectOptions { let mut conn = PgConnection::connect("postgres:///taler_rust_check") .await .unwrap();