commit 9db73672548899f1626c9c8ab854f4b0b26a02bc
parent 82f62b5b336f494e432831af97aecea9d1b7383b
Author: Antoine A <>
Date: Tue, 16 Jun 2026 12:29:29 +0200
common: improve config parsing and data encoding
Diffstat:
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();