kych

OAuth 2.0 API for Swiyu to enable Taler integration of Swiyu for KYC (experimental)
Log | Files | Refs | README | LICENSE

config.rs (46220B)


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