taler-rust

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

commit 8942ad15d6b443de2b1a3fec92a5df7bf8e30435
parent d09d9e15bf11bc0b7250380a1ebfe6f9eef1c921
Author: Antoine A <>
Date:   Wed, 19 Nov 2025 12:04:29 +0100

common: add config diagnostics

Diffstat:
MMakefile | 2+-
Mcommon/taler-api/src/auth.rs | 4++--
Mcommon/taler-common/src/cli.rs | 28++++++++++++++++++++--------
Mcommon/taler-common/src/config.rs | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
4 files changed, 155 insertions(+), 75 deletions(-)

diff --git a/Makefile b/Makefile @@ -16,7 +16,7 @@ all: build .PHONY: build build: - cargo build --release + cargo build --release --bin taler-magnet-bank .PHONY: install-nobuild-files install-nobuild-files: diff --git a/common/taler-api/src/auth.rs b/common/taler-api/src/auth.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 Taler Systems SA + Copyright (C) 2024-2025 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 @@ -45,7 +45,7 @@ impl AuthMiddlewareState { let challenge = match method { AuthMethod::Basic(_) => format!("Basic realm=\"{realm}\" charset=\"UTF-8\""), AuthMethod::Bearer(_) => format!("Bearer realm=\"{realm}\""), - AuthMethod::None => format!(""), + AuthMethod::None => String::new(), }; Self { challenge: HeaderValue::from_str(&challenge).unwrap(), diff --git a/common/taler-common/src/cli.rs b/common/taler-common/src/cli.rs @@ -14,6 +14,8 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +use std::io::Write as _; + use crate::config::Config; /// Taler component version format, you should set GIT_HASH in your building script @@ -42,28 +44,38 @@ pub enum ConfigCmd { /// Substitute variables in a path Pathsub { path_expr: String }, /// Dump the configuration - Dump, + Dump { + /// output extra diagnostics + #[clap(short, long, default_value_t = false)] + diagnostics: bool, + }, } impl ConfigCmd { pub fn run(&self, cfg: &Config) -> anyhow::Result<()> { - let out = match self { + let mut out = std::io::stdout().lock(); + match self { ConfigCmd::Get { section, option, filename, } => { let sect = cfg.section(section); - if *filename { + let value = if *filename { sect.path(option).require()? } else { sect.str(option).require()? - } + }; + writeln!(&mut out, "{value}")?; + } + ConfigCmd::Pathsub { path_expr } => { + let path = cfg.pathsub(path_expr, 0)?; + writeln!(&mut out, "{path}")?; + } + ConfigCmd::Dump { diagnostics } => { + cfg.print(&mut out, *diagnostics)?; } - ConfigCmd::Pathsub { path_expr } => cfg.pathsub(path_expr, 0)?, - ConfigCmd::Dump => cfg.to_string(), - }; - println!("{out}"); + } Ok(()) } } diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -18,6 +18,7 @@ use std::{ fmt::{Debug, Display}, fs::Permissions, os::unix::fs::PermissionsExt, + path::PathBuf, str::FromStr, }; @@ -34,12 +35,15 @@ pub mod parser { borrow::Cow, fmt::Display, io::{BufRead, BufReader}, - path::{Path, PathBuf}, + path::PathBuf, + str::FromStr, }; use indexmap::IndexMap; use tracing::{trace, warn}; + use crate::config::{Line, Location}; + use super::{Config, ValueErr}; #[derive(Debug, thiserror::Error)] @@ -66,11 +70,11 @@ pub mod parser { path.to_string_lossy(), err.kind() ), - ParserErr::Line(msg, path, num, cause) => { + ParserErr::Line(msg, path, line, cause) => { if let Some(cause) = cause { - write!(f, "{msg} at '{}:{num}': {cause}", path.to_string_lossy()) + write!(f, "{msg} at '{}:{line}': {cause}", path.to_string_lossy()) } else { - write!(f, "{msg} at '{}:{num}'", path.to_string_lossy()) + write!(f, "{msg} at '{}:{line}'", path.to_string_lossy()) } } } @@ -101,22 +105,22 @@ pub mod parser { fn line_err( msg: impl Into<Cow<'static, str>>, path: impl Into<PathBuf>, - num: usize, + line: usize, ) -> ParserErr { - ParserErr::Line(msg.into(), path.into(), num, None) + ParserErr::Line(msg.into(), path.into(), line, None) } fn line_cause_err( msg: impl Into<Cow<'static, str>>, cause: impl Display, path: impl Into<PathBuf>, - num: usize, + line: usize, ) -> ParserErr { - ParserErr::Line(msg.into(), path.into(), num, Some(cause.to_string())) + ParserErr::Line(msg.into(), path.into(), line, Some(cause.to_string())) } - struct Parser { - sections: IndexMap<String, IndexMap<String, String>>, - install_path: String, + sections: IndexMap<String, IndexMap<String, Line>>, + files: Vec<PathBuf>, + install_path: PathBuf, buf: String, } @@ -124,7 +128,8 @@ pub mod parser { fn empty() -> Self { Self { sections: IndexMap::new(), - install_path: String::new(), + files: Vec::new(), + install_path: PathBuf::new(), buf: String::new(), } } @@ -136,7 +141,7 @@ pub mod parser { let dir = src .install_path() .map_err(|(p, e)| io_err("find installation path", p, e))?; - self.install_path = dir.to_string_lossy().into_owned(); + self.install_path = dir.clone(); let paths = IndexMap::from_iter( [ @@ -149,7 +154,15 @@ pub mod parser { ("LIBDIR", dir.join("lib").join(project_name)), ("DATADIR", dir.join("share").join(project_name)), ] - .map(|(a, b)| (a.to_owned(), b.to_string_lossy().into_owned())), + .map(|(a, b)| { + ( + a.to_owned(), + Line { + content: b.to_string_lossy().into_owned(), + loc: None, + }, + ) + }), ); self.sections.insert("PATHS".to_owned(), paths); @@ -159,7 +172,7 @@ pub mod parser { Ok(entries) => { for entry in entries { match entry { - Ok(entry) => self.parse_file(&entry.path(), 0)?, + Ok(entry) => self.parse_file(entry.path(), 0)?, Err(err) => { warn!(target: "config", "{}", io_err("read base config directory", &cfg_dir, err)); } @@ -174,9 +187,9 @@ pub mod parser { Ok(()) } - fn parse_file(&mut self, src: &Path, depth: u8) -> Result<(), ParserErr> { + fn parse_file(&mut self, src: PathBuf, depth: u8) -> Result<(), ParserErr> { trace!(target: "config", "load file at '{}'", src.to_string_lossy()); - match std::fs::File::open(src) { + match std::fs::File::open(&src) { Ok(file) => self.parse(BufReader::new(file), src, depth + 1), Err(e) => Err(io_err("read config", src, e)), } @@ -185,14 +198,19 @@ pub mod parser { fn parse<B: BufRead>( &mut self, mut reader: B, - src: &Path, + src: PathBuf, depth: u8, ) -> Result<(), ParserErr> { - let mut current_section: Option<&mut IndexMap<String, String>> = None; - let mut num = 0; + let file = self.files.len(); + self.files.push(src.clone()); + let src = &src; + + let mut current_section: Option<&mut IndexMap<String, Line>> = None; + let mut line = 0; + loop { // Read a new line - num += 1; + line += 1; self.buf.clear(); match reader.read_line(&mut self.buf) { Ok(0) => break, @@ -200,41 +218,39 @@ pub mod parser { Err(e) => return Err(io_err("read config", src, e)), } // Trim whitespace - let line = self.buf.trim_ascii(); + let l = self.buf.trim_ascii(); - if line.is_empty() || line.starts_with(['#', '%']) { + if l.is_empty() || l.starts_with(['#', '%']) { // Skip empty lines and comments continue; - } else if let Some(directive) = line.strip_prefix("@") { + } else if let Some(directive) = l.strip_prefix("@") { // Parse directive let Some((name, arg)) = directive.split_once('@') else { - return Err(line_err( - format!("Invalid directive line '{line}'"), - src, - num, - )); + return Err(line_err(format!("Invalid directive line '{l}'"), src, line)); }; let arg = arg.trim_ascii_start(); // Exit current section current_section = None; // Check current file has a parent let Some(parent) = src.parent() else { - return Err(line_err("no parent", src, num)); + return Err(line_err("no parent", src, line)); }; // Check recursion depth if depth > 128 { - return Err(line_err("Recursion limit in config inlining", src, num)); + return Err(line_err("Recursion limit in config inlining", src, line)); } match name.to_lowercase().as_str() { - "inline" => self.parse_file(&parent.join(arg), depth)?, + "inline" => self.parse_file(parent.join(arg), depth)?, "inline-matching" => { - let paths = glob::glob(&parent.join(arg).to_string_lossy()) - .map_err(|e| line_cause_err("Malformed glob regex", e, src, num))?; + let paths = + glob::glob(&parent.join(arg).to_string_lossy()).map_err(|e| { + line_cause_err("Malformed glob regex", e, src, line) + })?; for path in paths { let path = - path.map_err(|e| line_cause_err("Glob error", e, src, num))?; - self.parse_file(&path, depth)?; + path.map_err(|e| line_cause_err("Glob error", e, src, line))?; + self.parse_file(path, depth)?; } } "inline-secret" => { @@ -242,17 +258,16 @@ pub mod parser { line_err( "Invalid configuration, @inline-secret@ directive requires exactly two arguments", src, - num + line ) )?; let section_up = section.to_uppercase(); let mut secret_cfg = Parser::empty(); - if let Err(e) = secret_cfg.parse_file(&parent.join(secret_file), depth) - { + if let Err(e) = secret_cfg.parse_file(parent.join(secret_file), depth) { if let ParserErr::IO(_, path, err) = e { - warn!(target: "config", "{}", io_err(format!("read secret section [{section}]"), path, err)) + warn!(target: "config", "{}", io_err(format!("read secret section [{section}]"), &path, err)) } else { return Err(e); } @@ -264,23 +279,22 @@ pub mod parser { .or_default() .extend(secret_section); } else { - warn!(target: "config", "{}", line_err(format!("Configuration file at '{secret_file}' loaded with @inline-secret@ does not contain section '{section_up}'"), src, num)); + warn!(target: "config", "{}", line_err(format!("Configuration file at '{secret_file}' loaded with @inline-secret@ does not contain section '{section_up}'"), src, line)); } } unknown => { return Err(line_err( format!("Invalid directive '{unknown}'"), src, - num, + line, )); } } - } else if let Some(section) = - line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) + } else if let Some(section) = l.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { current_section = Some(self.sections.entry(section.to_uppercase()).or_default()); - } else if let Some((name, value)) = line.split_once('=') { + } else if let Some((name, value)) = l.split_once('=') { if let Some(current_section) = &mut current_section { // Trim whitespace let name = name.trim_ascii_end().to_uppercase(); @@ -292,15 +306,21 @@ pub mod parser { } else { value }; - current_section.insert(name, value.to_owned()); + current_section.insert( + name, + Line { + content: value.to_owned(), + loc: Some(Location { file, line }), + }, + ); } else { - return Err(line_err("Expected section header or directive", src, num)); + return Err(line_err("Expected section header or directive", src, line)); } } else { return Err(line_err( "Expected section header, option assignment or directive", src, - num, + line, )); } } @@ -308,8 +328,10 @@ pub mod parser { } pub fn finish(self) -> Config { + // Convert to a readonly config struct without location info Config { sections: self.sections, + files: self.files, install_path: self.install_path, } } @@ -417,18 +439,18 @@ pub mod parser { impl Config { pub fn from_file( src: ConfigSource, - path: Option<impl AsRef<Path>>, + path: Option<impl Into<PathBuf>>, ) -> Result<Config, ParserErr> { let mut parser = Parser::empty(); parser.load_env(src)?; match path { - Some(path) => parser.parse_file(path.as_ref(), 0)?, + 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_file(default, 0)?; } } } @@ -437,7 +459,11 @@ pub mod parser { pub fn from_mem(str: &str) -> Result<Config, ParserErr> { let mut parser = Parser::empty(); - parser.parse(std::io::Cursor::new(str), "mem".as_ref(), 0)?; + parser.parse( + std::io::Cursor::new(str), + PathBuf::from_str("mem").unwrap(), + 0, + )?; Ok(parser.finish()) } } @@ -474,9 +500,22 @@ pub enum PathsubErr { } #[derive(Debug, Clone)] +struct Location { + file: usize, + line: usize, +} + +#[derive(Debug, Clone)] +struct Line { + content: String, + loc: Option<Location>, +} + +#[derive(Debug, Clone)] pub struct Config { - sections: IndexMap<String, IndexMap<String, String>>, - install_path: String, + sections: IndexMap<String, IndexMap<String, Line>>, + files: Vec<PathBuf>, + install_path: PathBuf, } impl Config { @@ -509,7 +548,7 @@ impl Config { .get("PATHS") .and_then(|section| section.get(name)) { - return Some(cfg.pathsub(path_res, depth + 1)); + return Some(cfg.pathsub(&path_res.content, depth + 1)); } if let Ok(val) = std::env::var(name) { @@ -586,15 +625,44 @@ impl Config { return Err(PathsubErr::Unbound(name.to_owned())); } } -} -impl Display for Config { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "# install path: {}", self.install_path)?; + pub fn print(&self, mut f: impl std::io::Write, diagnostics: bool) -> std::io::Result<()> { + if diagnostics { + writeln!(f, "#")?; + writeln!(f, "# Configuration file diagnostics")?; + writeln!(f, "#")?; + writeln!(f, "# File Loaded:")?; + for path in &self.files { + writeln!(f, "# {}", path.to_string_lossy())?; + } + writeln!(f, "#")?; + writeln!( + f, + "# Installation path: {}", + self.install_path.to_string_lossy() + )?; + writeln!(f, "#")?; + writeln!(f)?; + } for (sect, values) in &self.sections { writeln!(f, "[{sect}]")?; - for (key, value) in values { - writeln!(f, "{key} = {value}")?; + if diagnostics { + writeln!(f)?; + } + for (key, Line { content, loc }) in values { + if diagnostics { + match loc { + Some(Location { file, line }) => { + let path = &self.files[*file]; + writeln!(f, "# {}:{line}", path.to_string_lossy())?; + } + None => writeln!(f, "# default")?, + } + } + writeln!(f, "{key} = {content}")?; + if diagnostics { + writeln!(f)?; + } } writeln!(f)?; } @@ -606,7 +674,7 @@ impl Display for Config { pub struct Section<'cfg, 'arg> { section: &'arg str, config: &'cfg Config, - values: Option<&'cfg IndexMap<String, String>>, + values: Option<&'cfg IndexMap<String, Line>>, } #[macro_export] @@ -647,8 +715,8 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { let value = self .values .and_then(|m| m.get(&option.to_uppercase())) - .filter(|it| !it.is_empty()) - .map(|raw| transform(raw)) + .filter(|it| !it.content.is_empty()) + .map(|raw| transform(&raw.content)) .transpose(); Value { value,