taler-rust

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

config.rs (45970B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2025-2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{
     18     borrow::Cow,
     19     fmt::{Debug, Display},
     20     fs::Permissions,
     21     os::unix::fs::PermissionsExt,
     22     path::PathBuf,
     23     str::FromStr,
     24     sync::Arc,
     25     time::Duration,
     26 };
     27 
     28 use compact_str::CompactString;
     29 use indexmap::IndexMap;
     30 use jiff::{SignedDuration, Span};
     31 use url::Url;
     32 
     33 use crate::types::{
     34     amount::{Amount, Currency},
     35     payto::PaytoURI,
     36     validate_base_url,
     37 };
     38 
     39 pub mod parser {
     40     use std::{
     41         borrow::Cow,
     42         fmt::Display,
     43         io::{BufRead, BufReader},
     44         path::PathBuf,
     45         str::FromStr,
     46         sync::Arc,
     47     };
     48 
     49     use indexmap::IndexMap;
     50     use tracing::{trace, warn};
     51 
     52     use super::{Config, ValueErr};
     53     use crate::config::{Inner, Line, Location, make_lowercase};
     54 
     55     #[derive(Debug, thiserror::Error)]
     56     pub enum ConfigErr {
     57         #[error("config error, {0}")]
     58         Parser(#[from] ParserErr),
     59         #[error("invalid config, {0}")]
     60         Value(#[from] ValueErr),
     61     }
     62 
     63     #[derive(Debug)]
     64 
     65     pub enum ParserErr {
     66         IO(Cow<'static, str>, PathBuf, std::io::Error),
     67         Line(Cow<'static, str>, PathBuf, usize, Option<String>),
     68     }
     69 
     70     impl Display for ParserErr {
     71         fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     72             match self {
     73                 ParserErr::IO(action, path, err) => write!(
     74                     f,
     75                     "Could not {action} at '{}': {}",
     76                     path.to_string_lossy(),
     77                     err.kind()
     78                 ),
     79                 ParserErr::Line(msg, path, line, cause) => {
     80                     if let Some(cause) = cause {
     81                         write!(f, "{msg} at '{}:{line}': {cause}", path.to_string_lossy())
     82                     } else {
     83                         write!(f, "{msg} at '{}:{line}'", path.to_string_lossy())
     84                     }
     85                 }
     86             }
     87         }
     88     }
     89 
     90     impl std::error::Error for ParserErr {
     91         fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
     92             None
     93         }
     94 
     95         fn description(&self) -> &str {
     96             "description() is deprecated; use Display"
     97         }
     98 
     99         fn cause(&self) -> Option<&dyn std::error::Error> {
    100             self.source()
    101         }
    102     }
    103 
    104     fn io_err(
    105         action: impl Into<Cow<'static, str>>,
    106         path: impl Into<PathBuf>,
    107         err: std::io::Error,
    108     ) -> ParserErr {
    109         ParserErr::IO(action.into(), path.into(), err)
    110     }
    111     fn line_err(
    112         msg: impl Into<Cow<'static, str>>,
    113         path: impl Into<PathBuf>,
    114         line: usize,
    115     ) -> ParserErr {
    116         ParserErr::Line(msg.into(), path.into(), line, None)
    117     }
    118     fn line_cause_err(
    119         msg: impl Into<Cow<'static, str>>,
    120         cause: impl Display,
    121         path: impl Into<PathBuf>,
    122         line: usize,
    123     ) -> ParserErr {
    124         ParserErr::Line(msg.into(), path.into(), line, Some(cause.to_string()))
    125     }
    126     pub struct Parser {
    127         sections: IndexMap<String, IndexMap<String, Line>>,
    128         files: Vec<PathBuf>,
    129         install_path: PathBuf,
    130         buf: String,
    131     }
    132 
    133     impl Parser {
    134         pub fn empty() -> Self {
    135             Self {
    136                 sections: IndexMap::new(),
    137                 files: Vec::new(),
    138                 install_path: PathBuf::new(),
    139                 buf: String::new(),
    140             }
    141         }
    142 
    143         pub fn load_env(&mut self, src: ConfigSource) -> Result<(), ParserErr> {
    144             let ConfigSource { project_name, .. } = src;
    145 
    146             // Load default path
    147             let dir = src
    148                 .install_path()
    149                 .map_err(|(p, e)| io_err("find installation path", p, e))?;
    150             self.install_path = dir.clone();
    151 
    152             let paths = IndexMap::from_iter(
    153                 [
    154                     ("PREFIX", dir.join("")),
    155                     ("BINDIR", dir.join("bin")),
    156                     ("LIBEXECDIR", dir.join(project_name).join("libexec")),
    157                     ("DOCDIR", dir.join("share").join("doc").join(project_name)),
    158                     ("ICONDIR", dir.join("bin").join("share").join("icons")),
    159                     ("LOCALEDIR", dir.join("share").join("locale")),
    160                     ("LIBDIR", dir.join("lib").join(project_name)),
    161                     ("DATADIR", dir.join("share").join(project_name)),
    162                 ]
    163                 .map(|(a, b)| {
    164                     (
    165                         a.to_owned(),
    166                         Line {
    167                             content: b.to_string_lossy().into_owned(),
    168                             loc: None,
    169                         },
    170                     )
    171                 }),
    172             );
    173             self.sections.insert("paths".to_owned(), paths);
    174 
    175             // Load default configs
    176             let cfg_dir = dir.join("share").join(project_name).join("config.d");
    177             match std::fs::read_dir(&cfg_dir) {
    178                 Ok(entries) => {
    179                     for entry in entries {
    180                         match entry {
    181                             Ok(entry) => self.parse_file(entry.path(), 0)?,
    182                             Err(err) => {
    183                                 warn!(target: "config", "{}", io_err("read base config directory", &cfg_dir, err));
    184                             }
    185                         }
    186                     }
    187                 }
    188                 Err(err) => {
    189                     warn!(target: "config", "{}", io_err("read base config directory", &cfg_dir, err))
    190                 }
    191             }
    192 
    193             Ok(())
    194         }
    195 
    196         pub fn parse_str(&mut self, str: &str) -> Result<(), ParserErr> {
    197             self.parse(
    198                 std::io::Cursor::new(str),
    199                 PathBuf::from_str("mem").unwrap(),
    200                 0,
    201             )
    202         }
    203 
    204         pub fn parse_file(&mut self, src: PathBuf, depth: u8) -> Result<(), ParserErr> {
    205             trace!(target: "config", "load file at '{}'", src.to_string_lossy());
    206             match std::fs::File::open(&src) {
    207                 Ok(file) => self.parse(BufReader::new(file), src, depth + 1),
    208                 Err(e) => Err(io_err("read config", src, e)),
    209             }
    210         }
    211 
    212         fn parse<B: BufRead>(
    213             &mut self,
    214             mut reader: B,
    215             src: PathBuf,
    216             depth: u8,
    217         ) -> Result<(), ParserErr> {
    218             let file = self.files.len();
    219             self.files.push(src.clone());
    220             let src = &src;
    221 
    222             let mut current_section: Option<&mut IndexMap<String, Line>> = None;
    223             let mut line = 0;
    224 
    225             loop {
    226                 // Read a new line
    227                 line += 1;
    228                 self.buf.clear();
    229                 match reader.read_line(&mut self.buf) {
    230                     Ok(0) => break,
    231                     Ok(_) => {}
    232                     Err(e) => return Err(io_err("read config", src, e)),
    233                 }
    234                 // Trim whitespace
    235                 let l = self.buf.trim_ascii();
    236 
    237                 if l.is_empty() || l.starts_with(['#', '%']) {
    238                     // Skip empty lines and comments
    239                     continue;
    240                 } else if let Some(directive) = l.strip_prefix("@") {
    241                     // Parse directive
    242                     let Some((name, arg)) = directive.split_once('@') else {
    243                         return Err(line_err(format!("Invalid directive line '{l}'"), src, line));
    244                     };
    245                     let arg = arg.trim_ascii_start();
    246                     // Exit current section
    247                     current_section = None;
    248                     // Check current file has a parent
    249                     let Some(parent) = src.parent() else {
    250                         return Err(line_err("no parent", src, line));
    251                     };
    252                     // Check recursion depth
    253                     if depth > 128 {
    254                         return Err(line_err("Recursion limit in config inlining", src, line));
    255                     }
    256 
    257                     match make_lowercase(name).as_ref() {
    258                         "inline" => self.parse_file(parent.join(arg), depth)?,
    259                         "inline-matching" => {
    260                             let paths =
    261                                 glob::glob(&parent.join(arg).to_string_lossy()).map_err(|e| {
    262                                     line_cause_err("Malformed glob regex", e, src, line)
    263                                 })?;
    264                             for path in paths {
    265                                 let path =
    266                                     path.map_err(|e| line_cause_err("Glob error", e, src, line))?;
    267                                 self.parse_file(path, depth)?;
    268                             }
    269                         }
    270                         "inline-secret" => {
    271                             let (section, secret_file) = arg.split_once(" ").ok_or_else(||
    272                                 line_err(
    273                                     "Invalid configuration, @inline-secret@ directive requires exactly two arguments",
    274                                     src,
    275                                     line
    276                                 )
    277                             )?;
    278 
    279                             let section = section.to_lowercase();
    280                             let mut secret_cfg = Parser::empty();
    281 
    282                             if let Err(e) = secret_cfg.parse_file(parent.join(secret_file), depth) {
    283                                 if let ParserErr::IO(_, path, err) = e {
    284                                     warn!(target: "config", "{}", io_err(format!("read secret section [{section}]"), &path, err))
    285                                 } else {
    286                                     return Err(e);
    287                                 }
    288                             } else if let Some(secret_section) =
    289                                 secret_cfg.sections.swap_remove(&section)
    290                             {
    291                                 self.sections
    292                                     .entry(section)
    293                                     .or_default()
    294                                     .extend(secret_section);
    295                             } else {
    296                                 warn!(target: "config", "{}", line_err(format!("Configuration file at '{secret_file}' loaded with @inline-secret@ does not contain section [{section}]"), src, line));
    297                             }
    298                         }
    299                         unknown => {
    300                             return Err(line_err(
    301                                 format!("Invalid directive '{unknown}'"),
    302                                 src,
    303                                 line,
    304                             ));
    305                         }
    306                     }
    307                 } else if let Some(section) = l.strip_prefix('[').and_then(|l| l.strip_suffix(']'))
    308                 {
    309                     current_section =
    310                         Some(self.sections.entry(section.to_lowercase()).or_default());
    311                 } else if let Some((name, value)) = l.split_once('=') {
    312                     if let Some(current_section) = &mut current_section {
    313                         // Trim whitespace
    314                         let name = name.trim_ascii_end().to_uppercase();
    315                         let value = value.trim_ascii_start();
    316                         // Escape value
    317                         let value =
    318                             if value.len() > 1 && value.starts_with('"') && value.ends_with('"') {
    319                                 &value[1..value.len() - 1]
    320                             } else {
    321                                 value
    322                             };
    323                         current_section.insert(
    324                             name,
    325                             Line {
    326                                 content: value.to_owned(),
    327                                 loc: Some(Location { file, line }),
    328                             },
    329                         );
    330                     } else {
    331                         return Err(line_err("Expected section header or directive", src, line));
    332                     }
    333                 } else {
    334                     return Err(line_err(
    335                         "Expected section header, option assignment or directive",
    336                         src,
    337                         line,
    338                     ));
    339                 }
    340             }
    341             Ok(())
    342         }
    343 
    344         /// Get a read-only shareable Config from the parser
    345         pub fn finish(self) -> Config {
    346             // Convert to a read-only config struct without location info
    347             Config(Arc::new(Inner {
    348                 sections: self.sections,
    349                 files: self.files,
    350                 install_path: self.install_path,
    351             }))
    352         }
    353     }
    354 
    355     /** Information about how the configuration is loaded */
    356     #[derive(Debug, Clone, Copy)]
    357     pub struct ConfigSource {
    358         /** Name of the high-level project */
    359         pub project_name: &'static str,
    360         /** Name of the component within the package */
    361         pub component_name: &'static str,
    362         /**
    363          * Executable name that will be located on $PATH to
    364          * find the installation path of the package
    365          */
    366         pub exec_name: &'static str,
    367     }
    368 
    369     impl ConfigSource {
    370         /// Create a new config source
    371         pub const fn new(
    372             project_name: &'static str,
    373             component_name: &'static str,
    374             exec_name: &'static str,
    375         ) -> Self {
    376             Self {
    377                 project_name,
    378                 component_name,
    379                 exec_name,
    380             }
    381         }
    382 
    383         /// Create a config source where the project, component and exec names are the same
    384         pub const fn simple(name: &'static str) -> Self {
    385             Self::new(name, name, name)
    386         }
    387 
    388         /**
    389          * Search the default configuration file path
    390          *
    391          * I will be the first existing file from this list:
    392          * - $XDG_CONFIG_HOME/$componentName.conf
    393          * - $HOME/.config/$componentName.conf
    394          * - /etc/$componentName.conf
    395          * - /etc/$projectName/$componentName.conf
    396          * */
    397         fn default_config_path(&self) -> Result<Option<PathBuf>, (PathBuf, std::io::Error)> {
    398             // TODO use a generator
    399             let conf_name = format!("{}.conf", self.component_name);
    400 
    401             if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
    402                 let path = PathBuf::from(xdg).join(&conf_name);
    403                 match path.try_exists() {
    404                     Ok(false) => {}
    405                     Ok(true) => return Ok(Some(path)),
    406                     Err(e) => return Err((path, e)),
    407                 }
    408             }
    409 
    410             if let Some(home) = std::env::var_os("HOME") {
    411                 let path = PathBuf::from(home).join(".config").join(&conf_name);
    412                 match path.try_exists() {
    413                     Ok(false) => {}
    414                     Ok(true) => return Ok(Some(path)),
    415                     Err(e) => return Err((path, e)),
    416                 }
    417             }
    418 
    419             let path = PathBuf::from("/etc").join(&conf_name);
    420             match path.try_exists() {
    421                 Ok(false) => {}
    422                 Ok(true) => return Ok(Some(path)),
    423                 Err(e) => return Err((path, e)),
    424             }
    425 
    426             let path = PathBuf::from("/etc")
    427                 .join(self.project_name)
    428                 .join(&conf_name);
    429             match path.try_exists() {
    430                 Ok(false) => {}
    431                 Ok(true) => return Ok(Some(path)),
    432                 Err(e) => return Err((path, e)),
    433             }
    434 
    435             Ok(None)
    436         }
    437 
    438         /** Search for the binary installation path in PATH */
    439         fn install_path(&self) -> Result<PathBuf, (PathBuf, std::io::Error)> {
    440             let path_env = std::env::var("PATH").unwrap_or_default();
    441             for path_dir in path_env.split(':') {
    442                 let path_dir = PathBuf::from(path_dir);
    443                 let bin_path = path_dir.join(self.exec_name);
    444                 if bin_path.exists()
    445                     && let Some(parent) = path_dir.parent()
    446                 {
    447                     return parent.canonicalize().map_err(|e| (parent.to_path_buf(), e));
    448                 }
    449             }
    450             Ok(PathBuf::from("/usr"))
    451         }
    452     }
    453 
    454     impl Config {
    455         /// Load a config for a Taler component, optionally also load from a file.
    456         /// This is the standard way to load a Taler component config
    457         pub fn load(
    458             src: ConfigSource,
    459             path: Option<impl Into<PathBuf>>,
    460         ) -> Result<Config, ParserErr> {
    461             let mut parser = Parser::empty();
    462             parser.load_env(src)?;
    463             match path {
    464                 Some(path) => parser.parse_file(path.into(), 0)?,
    465                 None => {
    466                     if let Some(default) = src
    467                         .default_config_path()
    468                         .map_err(|(p, e)| io_err("find default config path", p, e))?
    469                     {
    470                         parser.parse_file(default, 0)?;
    471                     }
    472                 }
    473             }
    474             Ok(parser.finish())
    475         }
    476 
    477         /// Load config from an in memory string for testing
    478         pub fn from_mem(str: &str) -> Result<Config, ParserErr> {
    479             let mut parser = Parser::empty();
    480             parser.parse_str(str)?;
    481             Ok(parser.finish())
    482         }
    483 
    484         /// Load config from an in memory string with env from a Taler component for testing
    485         pub fn from_mem_with_env(src: ConfigSource, str: &str) -> Result<Config, ParserErr> {
    486             let mut parser = Parser::empty();
    487             parser.load_env(src)?;
    488             parser.parse_str(str)?;
    489             Ok(parser.finish())
    490         }
    491 
    492         /// Load a config for a Taler component, optionally also load from a file and an in memory string, for testing
    493         pub fn from_file_override(
    494             src: ConfigSource,
    495             path: Option<impl Into<PathBuf>>,
    496             str: &str,
    497         ) -> Result<Config, ParserErr> {
    498             let mut parser = Parser::empty();
    499             parser.load_env(src)?;
    500             match path {
    501                 Some(path) => {
    502                     parser.parse_file(path.into(), 0)?;
    503                 }
    504                 None => {
    505                     if let Some(default) = src
    506                         .default_config_path()
    507                         .map_err(|(p, e)| io_err("find default config path", p, e))?
    508                     {
    509                         parser.parse_file(default, 0)?;
    510                     }
    511                 }
    512             }
    513             parser.parse_str(str)?;
    514             Ok(parser.finish())
    515         }
    516     }
    517 }
    518 
    519 #[derive(Debug, thiserror::Error)]
    520 pub enum ValueErr {
    521     #[error("Missing {ty} option {option} in section [{section}]")]
    522     Missing {
    523         ty: String,
    524         section: String,
    525         option: String,
    526     },
    527     #[error("Invalid {ty} option {option} in section [{section}]: {err}")]
    528     Invalid {
    529         ty: String,
    530         section: String,
    531         option: String,
    532         err: String,
    533     },
    534 }
    535 
    536 #[derive(Debug, thiserror::Error)]
    537 
    538 pub enum PathsubErr {
    539     #[error("recursion limit in path substitution exceeded for '{0}'")]
    540     Recursion(String),
    541     #[error("unbalanced variable expression '{0}'")]
    542     Unbalanced(String),
    543     #[error("bad substitution '{0}'")]
    544     Substitution(String),
    545     #[error("unbound variable '{0}'")]
    546     Unbound(String),
    547 }
    548 
    549 #[derive(Debug, Clone)]
    550 struct Location {
    551     file: usize,
    552     line: usize,
    553 }
    554 
    555 #[derive(Debug, Clone)]
    556 struct Line {
    557     content: String,
    558     loc: Option<Location>,
    559 }
    560 
    561 #[derive(Debug)]
    562 struct Inner {
    563     sections: IndexMap<String, IndexMap<String, Line>>,
    564     files: Vec<PathBuf>,
    565     install_path: PathBuf,
    566 }
    567 
    568 #[derive(Clone)]
    569 pub struct Config(Arc<Inner>);
    570 
    571 impl Debug for Config {
    572     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    573         self.0.fmt(f)
    574     }
    575 }
    576 
    577 fn make_lowercase<'a>(s: &'a str) -> Cow<'a, str> {
    578     if s.chars().all(|c| c.is_ascii_lowercase()) {
    579         Cow::Borrowed(s)
    580     } else {
    581         Cow::Owned(s.to_ascii_lowercase())
    582     }
    583 }
    584 
    585 fn make_uppercase<'a>(s: &'a str) -> Cow<'a, str> {
    586     if s.chars().all(|c| c.is_ascii_uppercase()) {
    587         Cow::Borrowed(s)
    588     } else {
    589         Cow::Owned(s.to_ascii_uppercase())
    590     }
    591 }
    592 
    593 impl Config {
    594     /// Get a config section from its name
    595     pub fn section<'cfg, 'arg>(&'cfg self, name: &'arg str) -> Section<'cfg, 'arg> {
    596         Section {
    597             name,
    598             config: self,
    599             values: self.0.sections.get(make_lowercase(name).as_ref()),
    600         }
    601     }
    602 
    603     /// List all config sections
    604     pub fn sections<'cfg>(&'cfg self) -> impl Iterator<Item = Section<'cfg, 'cfg>> {
    605         self.0.sections.iter().map(|(name, values)| Section {
    606             name,
    607             config: self,
    608             values: Some(values),
    609         })
    610     }
    611 
    612     /**
    613      * Substitute ${...} and $... placeholders in a string
    614      * with values from the PATHS section in the
    615      * configuration and environment variables
    616      *
    617      * This substitution is typically only done for paths.
    618      */
    619     pub fn pathsub(&self, str: &str, depth: u8) -> Result<String, PathsubErr> {
    620         if depth > 128 {
    621             return Err(PathsubErr::Recursion(str.to_owned()));
    622         } else if !str.contains('$') {
    623             return Ok(str.to_owned());
    624         }
    625 
    626         /** Lookup for variable value from PATHS section in the configuration and environment variables */
    627         fn lookup(cfg: &Config, name: &str, depth: u8) -> Option<Result<String, PathsubErr>> {
    628             if let Some(path_res) = cfg
    629                 .0
    630                 .sections
    631                 .get("paths")
    632                 .and_then(|section| section.get(make_uppercase(name).as_ref()))
    633             {
    634                 return Some(cfg.pathsub(&path_res.content, depth + 1));
    635             }
    636 
    637             if let Ok(val) = std::env::var(name) {
    638                 return Some(Ok(val));
    639             }
    640             None
    641         }
    642 
    643         let mut result = String::new();
    644         let mut remaining = str;
    645         loop {
    646             // Look for the next variable
    647             let Some((normal, value)) = remaining.split_once('$') else {
    648                 result.push_str(remaining);
    649                 return Ok(result);
    650             };
    651 
    652             // Append normal character
    653             result.push_str(normal);
    654             remaining = value;
    655 
    656             // Check if variable is enclosed
    657             let is_enclosed = if let Some(enclosed) = remaining.strip_prefix('{') {
    658                 // ${var
    659                 remaining = enclosed;
    660                 true
    661             } else {
    662                 false // $var
    663             };
    664 
    665             // Extract variable name
    666             let name_end = remaining
    667                 .find(|c: char| !c.is_alphanumeric() && c != '_')
    668                 .unwrap_or(remaining.len());
    669             let (name, after_name) = remaining.split_at(name_end);
    670 
    671             // Extract variable default if enclosed
    672             let default = if !is_enclosed {
    673                 remaining = after_name;
    674                 None
    675             } else if let Some(after_enclosed) = after_name.strip_prefix('}') {
    676                 // ${var}
    677                 remaining = after_enclosed;
    678                 None
    679             } else if let Some(default) = after_name.strip_prefix(":-") {
    680                 // ${var:-default}
    681                 let mut depth = 1;
    682                 let Some((default, after_default)) = default.split_once(|c| {
    683                     if c == '{' {
    684                         depth += 1;
    685                         false
    686                     } else if c == '}' {
    687                         depth -= 1;
    688                         depth == 0
    689                     } else {
    690                         false
    691                     }
    692                 }) else {
    693                     return Err(PathsubErr::Unbalanced(default.to_owned()));
    694                 };
    695                 remaining = after_default;
    696                 Some(default)
    697             } else {
    698                 return Err(PathsubErr::Substitution(after_name.to_owned()));
    699             };
    700             if let Some(resolved) = lookup(self, name, depth + 1) {
    701                 result.push_str(&resolved?);
    702                 continue;
    703             } else if let Some(default) = default {
    704                 let resolved = self.pathsub(default, depth + 1)?;
    705                 result.push_str(&resolved);
    706                 continue;
    707             }
    708             return Err(PathsubErr::Unbound(name.to_owned()));
    709         }
    710     }
    711 
    712     /// Print config in a human format, optionally with diagnostics information
    713     pub fn print(&self, mut f: impl std::io::Write, diagnostics: bool) -> std::io::Result<()> {
    714         let Inner {
    715             sections,
    716             files,
    717             install_path,
    718         } = self.0.as_ref();
    719         if diagnostics {
    720             writeln!(f, "#")?;
    721             writeln!(f, "# Configuration file diagnostics")?;
    722             writeln!(f, "#")?;
    723             writeln!(f, "# File Loaded:")?;
    724             for path in files {
    725                 writeln!(f, "# {}", path.to_string_lossy())?;
    726             }
    727             writeln!(f, "#")?;
    728             writeln!(f, "# Installation path: {}", install_path.to_string_lossy())?;
    729             writeln!(f, "#")?;
    730             writeln!(f)?;
    731         }
    732         for (sect, values) in sections {
    733             writeln!(f, "[{sect}]")?;
    734             if diagnostics {
    735                 writeln!(f)?;
    736             }
    737             for (key, Line { content, loc }) in values {
    738                 if diagnostics {
    739                     match loc {
    740                         Some(Location { file, line }) => {
    741                             let path = &files[*file];
    742                             writeln!(f, "# {}:{line}", path.to_string_lossy())?;
    743                         }
    744                         None => writeln!(f, "# default")?,
    745                     }
    746                 }
    747                 writeln!(f, "{key} = {content}")?;
    748                 if diagnostics {
    749                     writeln!(f)?;
    750                 }
    751             }
    752             writeln!(f)?;
    753         }
    754         Ok(())
    755     }
    756 }
    757 
    758 /** Accessor/Converter for Taler-like configuration sections */
    759 pub struct Section<'cfg, 'arg> {
    760     pub name: &'arg str,
    761     config: &'cfg Config,
    762     values: Option<&'cfg IndexMap<String, Line>>,
    763 }
    764 
    765 #[macro_export]
    766 macro_rules! map_config {
    767     ($self:expr, $ty:expr, $option:expr, $($key:expr => $parse:block),*$(,)?) => {
    768         {
    769             let keys = &[$($key,)*];
    770             $self.map($ty, $option, |value| {
    771                 match value {
    772                     $($key => {
    773                         (||Ok($parse))().map_err(|e| ::taler_common::config::MapErr::Err(e))
    774                     })*,
    775                     _ => Err(::taler_common::config::MapErr::Invalid(keys))
    776                 }
    777             })
    778         }
    779     }
    780 }
    781 
    782 pub use map_config;
    783 
    784 #[doc(hidden)]
    785 pub enum MapErr {
    786     Invalid(&'static [&'static str]),
    787     Err(ValueErr),
    788 }
    789 
    790 impl<'cfg, 'arg> Section<'cfg, 'arg> {
    791     #[doc(hidden)]
    792     fn inner<T>(
    793         &self,
    794         ty: &'arg str,
    795         option: &'arg str,
    796         transform: impl FnOnce(&'cfg str) -> Result<T, ValueErr>,
    797     ) -> Value<'arg, T> {
    798         let value = self
    799             .values
    800             .and_then(|m| m.get(make_uppercase(option).as_ref()))
    801             .filter(|it| !it.content.is_empty())
    802             .map(|raw| transform(&raw.content))
    803             .transpose();
    804         Value {
    805             value,
    806             option,
    807             ty,
    808             section: self.name,
    809         }
    810     }
    811 
    812     #[doc(hidden)]
    813     pub fn map<T>(
    814         &self,
    815         ty: &'arg str,
    816         option: &'arg str,
    817         transform: impl FnOnce(&'cfg str) -> Result<T, MapErr>,
    818     ) -> Value<'arg, T> {
    819         self.value(ty, option, |v| {
    820             transform(v).map_err(|e| match e {
    821                 MapErr::Invalid(keys) => {
    822                     let mut buf = "expected '".to_owned();
    823                     match keys {
    824                         [] => unreachable!("you must provide at least one mapping"),
    825                         [unique] => buf.push_str(unique),
    826                         [first, other @ .., last] => {
    827                             buf.push_str(first);
    828                             for k in other {
    829                                 buf.push_str("', '");
    830                                 buf.push_str(k);
    831                             }
    832                             buf.push_str("' or '");
    833                             buf.push_str(last);
    834                         }
    835                     }
    836                     buf.push_str("' got '");
    837                     buf.push_str(v);
    838                     buf.push('\'');
    839                     ValueErr::Invalid {
    840                         ty: ty.to_owned(),
    841                         section: self.name.to_lowercase(),
    842                         option: option.to_uppercase(),
    843                         err: buf,
    844                     }
    845                 }
    846                 MapErr::Err(e) => e,
    847             })
    848         })
    849     }
    850 
    851     /** Setup an accessor/converted for a [type] at [option] using [transform] */
    852     pub fn value<T, E: Display>(
    853         &self,
    854         ty: &'arg str,
    855         option: &'arg str,
    856         transform: impl FnOnce(&'cfg str) -> Result<T, E>,
    857     ) -> Value<'arg, T> {
    858         self.inner(ty, option, |v| {
    859             transform(v).map_err(|e| ValueErr::Invalid {
    860                 ty: ty.to_owned(),
    861                 section: self.name.to_lowercase(),
    862                 option: option.to_uppercase(),
    863                 err: e.to_string(),
    864             })
    865         })
    866     }
    867 
    868     /** Access [option] as a parsable type */
    869     pub fn parse<E: std::fmt::Display, T: FromStr<Err = E>>(
    870         &self,
    871         ty: &'arg str,
    872         option: &'arg str,
    873     ) -> Value<'arg, T> {
    874         self.value(ty, option, |it| it.parse::<T>().map_err(|e| e.to_string()))
    875     }
    876 
    877     /** Access [option] as str */
    878     pub fn str(&self, option: &'arg str) -> Value<'arg, String> {
    879         self.value("string", option, |it| Ok::<_, &str>(it.to_owned()))
    880     }
    881 
    882     /** Access [option] as compact str */
    883     pub fn cstr(&self, option: &'arg str) -> Value<'arg, CompactString> {
    884         self.value("string", option, |it| Ok::<_, CompactString>(it.into()))
    885     }
    886 
    887     /** Access [option] as hex encode bytes */
    888     pub fn hex(&self, option: &'arg str) -> Value<'arg, Vec<u8>> {
    889         self.value("hex", option, |it| {
    890             crate::encoding::hex::decode(it.as_bytes())
    891         })
    892     }
    893 
    894     /** Access [option] as base32 encode bytes */
    895     pub fn b32(&self, option: &'arg str) -> Value<'arg, Vec<u8>> {
    896         self.value("b32", option, |it| {
    897             crate::encoding::base32::decode(it.as_bytes())
    898         })
    899     }
    900 
    901     /** Access [option] as base64 encode bytes */
    902     pub fn b64(&self, option: &'arg str) -> Value<'arg, Vec<u8>> {
    903         self.value("b64", option, |it| {
    904             crate::encoding::base64::decode(it.as_bytes())
    905         })
    906     }
    907 
    908     /** Access [option] as path */
    909     pub fn path(&self, option: &'arg str) -> Value<'arg, String> {
    910         self.value("path", option, |it| self.config.pathsub(it, 0))
    911     }
    912 
    913     /** Access [option] as UNIX permissions */
    914     pub fn unix_mode(&self, option: &'arg str) -> Value<'arg, Permissions> {
    915         self.value("unix mode", option, |it| {
    916             u32::from_str_radix(it, 8)
    917                 .map(Permissions::from_mode)
    918                 .map_err(|_| format!("'{it}' not a valid number"))
    919         })
    920     }
    921 
    922     /** Access [option] as a number */
    923     pub fn number<T: FromStr>(&self, option: &'arg str) -> Value<'arg, T> {
    924         self.value("number", option, |it| {
    925             it.parse::<T>()
    926                 .map_err(|_| format!("'{it}' not a valid number"))
    927         })
    928     }
    929 
    930     /** Access [option] as Boolean */
    931     pub fn boolean(&self, option: &'arg str) -> Value<'arg, bool> {
    932         self.value("boolean", option, |it| match it.to_uppercase().as_str() {
    933             "YES" => Ok(true),
    934             "NO" => Ok(false),
    935             _ => Err(format!("expected 'YES' or 'NO' got '{it}'")),
    936         })
    937     }
    938 
    939     /** Access [option] as a Currency */
    940     pub fn currency(&self, option: &'arg str) -> Value<'arg, Currency> {
    941         self.parse("currency", option)
    942     }
    943 
    944     /** Access [option] as an Amount */
    945     pub fn amount(&self, option: &'arg str, currency: &Currency) -> Value<'arg, Amount> {
    946         self.value("amount", option, |it| {
    947             let amount = it.parse::<Amount>().map_err(|e| e.to_string())?;
    948             if amount.currency != *currency {
    949                 return Err(format!(
    950                     "expected currency {currency} got {}",
    951                     amount.currency
    952                 ));
    953             }
    954             Ok(amount)
    955         })
    956     }
    957 
    958     /** Access [option] as url */
    959     pub fn url(&self, option: &'arg str) -> Value<'arg, Url> {
    960         self.parse("url", option)
    961     }
    962 
    963     /** Access [option] as base url */
    964     pub fn base_url(&self, option: &'arg str) -> Value<'arg, Url> {
    965         self.value("url", option, |s| {
    966             let url = Url::from_str(s).map_err(|e| e.to_string())?;
    967             validate_base_url(&url)?;
    968             Ok::<_, String>(url)
    969         })
    970     }
    971 
    972     /** Access [option] as payto */
    973     pub fn payto(&self, option: &'arg str) -> Value<'arg, PaytoURI> {
    974         self.parse("payto", option)
    975     }
    976 
    977     /** Access [option] as Postgres URI */
    978     pub fn postgres(&self, option: &'arg str) -> Value<'arg, sqlx::postgres::PgConnectOptions> {
    979         self.parse("Postgres URI", option)
    980     }
    981 
    982     /** Access [option] as a timestamp */
    983     pub fn timestamp(&self, option: &'arg str) -> Value<'arg, jiff::Timestamp> {
    984         self.parse("Timestamp", option)
    985     }
    986 
    987     /** Access [option] as a time */
    988     pub fn time(&self, option: &'arg str) -> Value<'arg, jiff::civil::Time> {
    989         self.parse("Time", option)
    990     }
    991 
    992     /** Access [option] as a date */
    993     pub fn date(&self, option: &'arg str) -> Value<'arg, jiff::civil::Date> {
    994         self.parse("Date", option)
    995     }
    996 
    997     /** Access [option] as a duration */
    998     pub fn duration(&self, option: &'arg str) -> Value<'arg, Duration> {
    999         self.value("temporal", option, |it| {
   1000             let tmp = SignedDuration::from_str(it).map_err(|e| e.to_string())?;
   1001             Ok::<_, String>(Duration::from_millis(tmp.as_millis() as u64))
   1002         })
   1003     }
   1004 
   1005     /** Access [option] as a duration */
   1006     pub fn span(&self, option: &'arg str) -> Value<'arg, Span> {
   1007         self.parse("temporal", option)
   1008     }
   1009 
   1010     /** Access [option] as a regex */
   1011     pub fn regex(&self, option: &'arg str) -> Value<'arg, regex::Regex> {
   1012         self.parse("Pattern", option)
   1013     }
   1014 
   1015     /** Access option as json object */
   1016     pub fn json<'de, T: serde::Deserialize<'de>>(&'de self, option: &'arg str) -> Value<'arg, T> {
   1017         self.value("json", option, |it| serde_json::from_str(it))
   1018     }
   1019 }
   1020 
   1021 pub struct Value<'arg, T> {
   1022     value: Result<Option<T>, ValueErr>,
   1023     option: &'arg str,
   1024     ty: &'arg str,
   1025     section: &'arg str,
   1026 }
   1027 
   1028 impl<T> Value<'_, T> {
   1029     pub fn opt(self) -> Result<Option<T>, ValueErr> {
   1030         self.value
   1031     }
   1032 
   1033     /** Converted value of default if missing */
   1034     pub fn default(self, default: T) -> Result<T, ValueErr> {
   1035         Ok(self.value?.unwrap_or(default))
   1036     }
   1037 
   1038     /** Converted value or throw if missing */
   1039     pub fn require(self) -> Result<T, ValueErr> {
   1040         self.value?.ok_or_else(|| ValueErr::Missing {
   1041             ty: self.ty.to_owned(),
   1042             section: self.section.to_lowercase(),
   1043             option: self.option.to_uppercase(),
   1044         })
   1045     }
   1046 }
   1047 
   1048 #[cfg(test)]
   1049 mod test {
   1050     use std::{
   1051         fmt::{Debug, Display},
   1052         fs::{File, Permissions},
   1053         os::unix::fs::PermissionsExt,
   1054     };
   1055 
   1056     use tracing::error;
   1057 
   1058     use super::{Config, Section, Value};
   1059     use crate::{
   1060         config::parser::ConfigSource,
   1061         types::amount::{self, Currency},
   1062     };
   1063 
   1064     const SOURCE: ConfigSource = ConfigSource::new("test", "test", "test");
   1065 
   1066     #[track_caller]
   1067     fn check_err<T: Debug, E: Display>(err: impl AsRef<str>, lambda: Result<T, E>) {
   1068         let failure = lambda.unwrap_err();
   1069         let fmt = failure.to_string();
   1070         assert_eq!(err.as_ref(), fmt);
   1071     }
   1072 
   1073     #[test]
   1074     fn fs() {
   1075         let dir = tempfile::tempdir().unwrap();
   1076         let config_path = dir.path().join("test-conf.conf");
   1077         let second_path = dir.path().join("test-second-conf.conf");
   1078 
   1079         let config_path_fmt = config_path.to_string_lossy();
   1080         let second_path_fmt = second_path.to_string_lossy();
   1081 
   1082         let check_err = |err: String| check_err(err, Config::load(SOURCE, Some(&config_path)));
   1083         let check_ok = || Config::load(SOURCE, Some(&config_path)).unwrap();
   1084 
   1085         check_err(format!(
   1086             "Could not read config at '{config_path_fmt}': entity not found"
   1087         ));
   1088 
   1089         let config_file = std::fs::File::create_new(&config_path).unwrap();
   1090         config_file
   1091             .set_permissions(Permissions::from_mode(0o222))
   1092             .unwrap();
   1093         if File::open(&config_path).is_ok() {
   1094             error!("Cannot finish this test if root");
   1095             return;
   1096         }
   1097         check_err(format!(
   1098             "Could not read config at '{config_path_fmt}': permission denied"
   1099         ));
   1100 
   1101         config_file
   1102             .set_permissions(Permissions::from_mode(0o666))
   1103             .unwrap();
   1104         check_ok();
   1105         std::fs::write(&config_path, "@inline@ test-second-conf.conf").unwrap();
   1106         check_err(format!(
   1107             "Could not read config at '{second_path_fmt}': entity not found"
   1108         ));
   1109 
   1110         let second_file = std::fs::File::create_new(&second_path).unwrap();
   1111         second_file
   1112             .set_permissions(Permissions::from_mode(0o222))
   1113             .unwrap();
   1114         check_err(format!(
   1115             "Could not read config at '{second_path_fmt}': permission denied"
   1116         ));
   1117 
   1118         std::fs::write(&config_path, "@inline-matching@[*").unwrap();
   1119         check_err(format!(
   1120             "Malformed glob regex at '{config_path_fmt}:1': Pattern syntax error near position 16: invalid range pattern"
   1121         ));
   1122 
   1123         std::fs::write(&config_path, "@inline-matching@*second-conf.conf").unwrap();
   1124         check_err(format!(
   1125             "Could not read config at '{second_path_fmt}': permission denied"
   1126         ));
   1127 
   1128         std::fs::write(&config_path, "\n@inline-matching@*.conf").unwrap();
   1129         check_err(format!(
   1130             "Recursion limit in config inlining at '{config_path_fmt}:2'"
   1131         ));
   1132         std::fs::write(&config_path, "\n\n@inline-matching@ *.conf").unwrap();
   1133         check_err(format!(
   1134             "Recursion limit in config inlining at '{config_path_fmt}:3'"
   1135         ));
   1136 
   1137         std::fs::write(&config_path, "@inline-secret@ secret test-second-conf.conf").unwrap();
   1138         check_ok();
   1139     }
   1140 
   1141     #[test]
   1142     fn parsing() {
   1143         let check = |err: &str, content: &str| check_err(err, Config::from_mem(content));
   1144 
   1145         check(
   1146             "Expected section header, option assignment or directive at 'mem:1'",
   1147             "syntax error",
   1148         );
   1149         check(
   1150             "Expected section header or directive at 'mem:1'",
   1151             "key=value",
   1152         );
   1153         check(
   1154             "Expected section header, option assignment or directive at 'mem:2'",
   1155             "[section]\nbad-line",
   1156         );
   1157 
   1158         let cfg = Config::from_mem(
   1159             r#"
   1160 
   1161         [section-a]
   1162 
   1163         bar = baz
   1164 
   1165         [section-b]
   1166 
   1167         first_value = 1
   1168         second_value = "test"
   1169 
   1170         "#,
   1171         )
   1172         .unwrap();
   1173 
   1174         // Missing section
   1175         check_err(
   1176             "Missing string option VALUE in section [unknown]",
   1177             cfg.section("unknown").str("value").require(),
   1178         );
   1179 
   1180         // Missing value
   1181         check_err(
   1182             "Missing string option VALUE in section [section-a]",
   1183             cfg.section("section-a").str("value").require(),
   1184         );
   1185     }
   1186 
   1187     const DEFAULT_CONF: &str = "[PATHS]\nDATADIR=mydir\nRECURSIVE=$RECURSIVE";
   1188 
   1189     #[allow(clippy::type_complexity)]
   1190     fn routine<T: Debug + Eq>(
   1191         ty: &str,
   1192         mut lambda: impl for<'cfg, 'arg> FnMut(&Section<'cfg, 'arg>, &'arg str) -> Value<'arg, T>,
   1193         wellformed: &[(&[&str], T)],
   1194         malformed: &[(&[&str], fn(&str) -> String)],
   1195     ) {
   1196         let conf = |content: &str| Config::from_mem(&format!("{DEFAULT_CONF}\n{content}")).unwrap();
   1197 
   1198         // Check missing msg
   1199         let cfg = conf("");
   1200         check_err(
   1201             format!("Missing {ty} option VALUE in section [section]"),
   1202             lambda(&cfg.section("section"), "value").require(),
   1203         );
   1204 
   1205         // Check wellformed options are properly parsed
   1206         for (raws, expected) in wellformed {
   1207             for raw in *raws {
   1208                 let cfg = conf(&format!("[section]\nvalue={raw}"));
   1209                 dbg!(&cfg);
   1210                 assert_eq!(
   1211                     *expected,
   1212                     lambda(&cfg.section("section"), "value").require().unwrap()
   1213                 );
   1214             }
   1215         }
   1216 
   1217         // Check malformed options have proper error message
   1218         for (raws, error_fmt) in malformed {
   1219             for raw in *raws {
   1220                 let cfg = conf(&format!("[section]\nvalue={raw}"));
   1221                 check_err(
   1222                     format!(
   1223                         "Invalid {ty} option VALUE in section [section]: {}",
   1224                         error_fmt(raw)
   1225                     ),
   1226                     lambda(&cfg.section("section"), "value").require(),
   1227                 )
   1228             }
   1229         }
   1230     }
   1231 
   1232     #[test]
   1233     fn string() {
   1234         routine(
   1235             "string",
   1236             |sect, value| sect.str(value),
   1237             &[
   1238                 (&["1", "\"1\""], "1".to_owned()),
   1239                 (&["test", "\"test\""], "test".to_owned()),
   1240                 (&["\""], "\"".to_owned()),
   1241             ],
   1242             &[],
   1243         );
   1244     }
   1245 
   1246     #[test]
   1247     fn path() {
   1248         routine(
   1249             "path",
   1250             |sect, value| sect.path(value),
   1251             &[
   1252                 (&["path"], "path".to_owned()),
   1253                 (
   1254                     &["foo/$DATADIR/bar", "foo/${DATADIR}/bar"],
   1255                     "foo/mydir/bar".to_owned(),
   1256                 ),
   1257                 (
   1258                     &["foo/$DATADIR$DATADIR/bar"],
   1259                     "foo/mydirmydir/bar".to_owned(),
   1260                 ),
   1261                 (
   1262                     &["foo/pre_$DATADIR/bar", "foo/pre_${DATADIR}/bar"],
   1263                     "foo/pre_mydir/bar".to_owned(),
   1264                 ),
   1265                 (
   1266                     &[
   1267                         "foo/${DATADIR}_next/bar",
   1268                         "foo/${UNKNOWN:-$DATADIR}_next/bar",
   1269                     ],
   1270                     "foo/mydir_next/bar".to_owned(),
   1271                 ),
   1272                 (
   1273                     &[
   1274                         "foo/${UNKNOWN:-default}_next/bar",
   1275                         "foo/${UNKNOWN:-${UNKNOWN:-default}}_next/bar",
   1276                     ],
   1277                     "foo/default_next/bar".to_owned(),
   1278                 ),
   1279                 (
   1280                     &["foo/${UNKNOWN:-pre_${UNKNOWN:-default}_next}_next/bar"],
   1281                     "foo/pre_default_next_next/bar".to_owned(),
   1282                 ),
   1283             ],
   1284             &[
   1285                 (&["foo/${A/bar"], |_| "bad substitution '/bar'".to_owned()),
   1286                 (&["foo/${A:-pre_${B}/bar"], |_| {
   1287                     "unbalanced variable expression 'pre_${B}/bar'".to_owned()
   1288                 }),
   1289                 (&["foo/${A:-${B${C}/bar"], |_| {
   1290                     "unbalanced variable expression '${B${C}/bar'".to_owned()
   1291                 }),
   1292                 (&["foo/$UNKNOWN/bar", "foo/${UNKNOWN}/bar"], |_| {
   1293                     "unbound variable 'UNKNOWN'".to_owned()
   1294                 }),
   1295                 (&["foo/$RECURSIVE/bar"], |_| {
   1296                     "recursion limit in path substitution exceeded for '$RECURSIVE'".to_owned()
   1297                 }),
   1298             ],
   1299         )
   1300     }
   1301 
   1302     #[test]
   1303     fn number() {
   1304         routine(
   1305             "number",
   1306             |sect, value| sect.number(value),
   1307             &[(&["1"], 1), (&["42"], 42)],
   1308             &[(&["true", "YES"], |it| format!("'{it}' not a valid number"))],
   1309         );
   1310     }
   1311 
   1312     #[test]
   1313     fn boolean() {
   1314         routine(
   1315             "boolean",
   1316             |sect, value| sect.boolean(value),
   1317             &[(&["yes", "YES", "Yes"], true), (&["no", "NO", "No"], false)],
   1318             &[(&["true", "1"], |it| {
   1319                 format!("expected 'YES' or 'NO' got '{it}'")
   1320             })],
   1321         );
   1322     }
   1323 
   1324     #[test]
   1325     fn amount() {
   1326         routine(
   1327             "amount",
   1328             |sect, value| sect.amount(value, &Currency::KUDOS),
   1329             &[(
   1330                 &["KUDOS:12", "KUDOS:12.0", "KUDOS:012.0"],
   1331                 amount::amount("KUDOS:12"),
   1332             )],
   1333             &[
   1334                 (&["test", "42"], |it| {
   1335                     format!("amount '{it}' invalid format")
   1336                 }),
   1337                 (&["KUDOS:0.3ABC"], |it| {
   1338                     format!("amount '{it}' invalid fraction (invalid digit found in string)")
   1339                 }),
   1340                 (&["KUDOS:999999999999999999"], |it| {
   1341                     format!("amount '{it}' value overflow (must be <= 4503599627370496)")
   1342                 }),
   1343                 (&["EUR:12"], |_| {
   1344                     "expected currency KUDOS got EUR".to_owned()
   1345                 }),
   1346             ],
   1347         )
   1348     }
   1349 }