taler-rust

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

config.rs (45524B)


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