commit 8942ad15d6b443de2b1a3fec92a5df7bf8e30435
parent d09d9e15bf11bc0b7250380a1ebfe6f9eef1c921
Author: Antoine A <>
Date: Wed, 19 Nov 2025 12:04:29 +0100
common: add config diagnostics
Diffstat:
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,