LCOV - differential code coverage report
Current view: top level - control_plane/src - postgresql_conf.rs (source / functions) Coverage Total Hit UBC CBC
Current: f6946e90941b557c917ac98cd5a7e9506d180f3e.info Lines: 67.2 % 131 88 43 88
Current Date: 2023-10-19 02:04:12 Functions: 50.0 % 20 10 10 10
Baseline: c8637f37369098875162f194f92736355783b050.info
Baseline Date: 2023-10-18 20:25:20

           TLA  Line data    Source code
       1                 : ///
       2                 : /// Module for parsing postgresql.conf file.
       3                 : ///
       4                 : /// NOTE: This doesn't implement the full, correct postgresql.conf syntax. Just
       5                 : /// enough to extract a few settings we need in Neon, assuming you don't do
       6                 : /// funny stuff like include-directives or funny escaping.
       7                 : use anyhow::{bail, Context, Result};
       8                 : use once_cell::sync::Lazy;
       9                 : use regex::Regex;
      10                 : use std::collections::HashMap;
      11                 : use std::fmt;
      12                 : use std::io::BufRead;
      13                 : use std::str::FromStr;
      14                 : 
      15                 : /// In-memory representation of a postgresql.conf file
      16 CBC         568 : #[derive(Default, Debug)]
      17                 : pub struct PostgresConf {
      18                 :     lines: Vec<String>,
      19                 :     hash: HashMap<String, String>,
      20                 : }
      21                 : 
      22 UBC           0 : static CONF_LINE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^((?:\w|\.)+)\s*=\s*(\S+)$").unwrap());
      23                 : 
      24                 : impl PostgresConf {
      25 CBC         568 :     pub fn new() -> PostgresConf {
      26             568 :         PostgresConf::default()
      27             568 :     }
      28                 : 
      29                 :     /// Read file into memory
      30 UBC           0 :     pub fn read(read: impl std::io::Read) -> Result<PostgresConf> {
      31               0 :         let mut result = Self::new();
      32                 : 
      33               0 :         for line in std::io::BufReader::new(read).lines() {
      34               0 :             let line = line?;
      35                 : 
      36                 :             // Store each line in a vector, in original format
      37               0 :             result.lines.push(line.clone());
      38               0 : 
      39               0 :             // Also parse each line and insert key=value lines into a hash map.
      40               0 :             //
      41               0 :             // FIXME: This doesn't match exactly the flex/bison grammar in PostgreSQL.
      42               0 :             // But it's close enough for our usage.
      43               0 :             let line = line.trim();
      44               0 :             if line.starts_with('#') {
      45                 :                 // comment, ignore
      46               0 :                 continue;
      47               0 :             } else if let Some(caps) = CONF_LINE_RE.captures(line) {
      48               0 :                 let name = caps.get(1).unwrap().as_str();
      49               0 :                 let raw_val = caps.get(2).unwrap().as_str();
      50                 : 
      51               0 :                 if let Ok(val) = deescape_str(raw_val) {
      52               0 :                     // Note: if there's already an entry in the hash map for
      53               0 :                     // this key, this will replace it. That's the behavior what
      54               0 :                     // we want; when PostgreSQL reads the file, each line
      55               0 :                     // overrides any previous value for the same setting.
      56               0 :                     result.hash.insert(name.to_string(), val.to_string());
      57               0 :                 }
      58               0 :             }
      59                 :         }
      60               0 :         Ok(result)
      61               0 :     }
      62                 : 
      63                 :     /// Return the current value of 'option'
      64               0 :     pub fn get(&self, option: &str) -> Option<&str> {
      65               0 :         self.hash.get(option).map(|x| x.as_ref())
      66               0 :     }
      67                 : 
      68                 :     /// Return the current value of a field, parsed to the right datatype.
      69                 :     ///
      70                 :     /// This calls the FromStr::parse() function on the value of the field. If
      71                 :     /// the field does not exist, or parsing fails, returns an error.
      72                 :     ///
      73                 :     pub fn parse_field<T>(&self, field_name: &str, context: &str) -> Result<T>
      74                 :     where
      75                 :         T: FromStr,
      76                 :         <T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
      77                 :     {
      78               0 :         self.get(field_name)
      79               0 :             .with_context(|| format!("could not find '{}' option {}", field_name, context))?
      80               0 :             .parse::<T>()
      81               0 :             .with_context(|| format!("could not parse '{}' option {}", field_name, context))
      82               0 :     }
      83                 : 
      84                 :     pub fn parse_field_optional<T>(&self, field_name: &str, context: &str) -> Result<Option<T>>
      85                 :     where
      86                 :         T: FromStr,
      87                 :         <T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
      88                 :     {
      89               0 :         if let Some(val) = self.get(field_name) {
      90               0 :             let result = val
      91               0 :                 .parse::<T>()
      92               0 :                 .with_context(|| format!("could not parse '{}' option {}", field_name, context))?;
      93                 : 
      94               0 :             Ok(Some(result))
      95                 :         } else {
      96               0 :             Ok(None)
      97                 :         }
      98               0 :     }
      99                 : 
     100                 :     ///
     101                 :     /// Note: if you call this multiple times for the same option, the config
     102                 :     /// file will a line for each call. It would be nice to have a function
     103                 :     /// to change an existing line, but that's a TODO.
     104                 :     ///
     105 CBC        9959 :     pub fn append(&mut self, option: &str, value: &str) {
     106            9959 :         self.lines
     107            9959 :             .push(format!("{}={}\n", option, escape_str(value)));
     108            9959 :         self.hash.insert(option.to_string(), value.to_string());
     109            9959 :     }
     110                 : 
     111                 :     /// Append an arbitrary non-setting line to the config file
     112             568 :     pub fn append_line(&mut self, line: &str) {
     113             568 :         self.lines.push(line.to_string());
     114             568 :     }
     115                 : }
     116                 : 
     117                 : impl fmt::Display for PostgresConf {
     118                 :     /// Return the whole configuration file as a string
     119             568 :     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     120           10527 :         for line in self.lines.iter() {
     121           10527 :             f.write_str(line)?;
     122                 :         }
     123             568 :         Ok(())
     124             568 :     }
     125                 : }
     126                 : 
     127                 : /// Escape a value for putting in postgresql.conf.
     128            9975 : fn escape_str(s: &str) -> String {
     129            9975 :     // If the string doesn't contain anything that needs quoting or escaping, return it
     130            9975 :     // as it is.
     131            9975 :     //
     132            9975 :     // The first part of the regex, before the '|', matches the INTEGER rule in the
     133            9975 :     // PostgreSQL flex grammar (guc-file.l). It matches plain integers like "123" and
     134            9975 :     // "-123", and also accepts units like "10MB". The second part of the regex matches
     135            9975 :     // the UNQUOTED_STRING rule, and accepts strings that contain a single word, beginning
     136            9975 :     // with a letter. That covers words like "off" or "posix". Everything else is quoted.
     137            9975 :     //
     138            9975 :     // This regex is a bit more conservative than the rules in guc-file.l, so we quote some
     139            9975 :     // strings that PostgreSQL would accept without quoting, but that's OK.
     140            9975 : 
     141            9975 :     static UNQUOTED_RE: Lazy<Regex> =
     142            9975 :         Lazy::new(|| Regex::new(r"(^[-+]?[0-9]+[a-zA-Z]*$)|(^[a-zA-Z][a-zA-Z0-9]*$)").unwrap());
     143            9975 : 
     144            9975 :     if UNQUOTED_RE.is_match(s) {
     145            8831 :         s.to_string()
     146                 :     } else {
     147                 :         // Otherwise escape and quote it
     148            1144 :         let s = s
     149            1144 :             .replace('\\', "\\\\")
     150            1144 :             .replace('\n', "\\n")
     151            1144 :             .replace('\'', "''");
     152            1144 : 
     153            1144 :         "\'".to_owned() + &s + "\'"
     154                 :     }
     155            9975 : }
     156                 : 
     157                 : /// De-escape a possibly-quoted value.
     158                 : ///
     159                 : /// See `DeescapeQuotedString` function in PostgreSQL sources for how PostgreSQL
     160                 : /// does this.
     161               4 : fn deescape_str(s: &str) -> Result<String> {
     162               4 :     // If the string has a quote at the beginning and end, strip them out.
     163               4 :     if s.len() >= 2 && s.starts_with('\'') && s.ends_with('\'') {
     164               3 :         let mut result = String::new();
     165               3 : 
     166               3 :         let mut iter = s[1..(s.len() - 1)].chars().peekable();
     167              20 :         while let Some(c) = iter.next() {
     168              18 :             let newc = if c == '\\' {
     169               8 :                 match iter.next() {
     170               1 :                     Some('b') => '\x08',
     171               1 :                     Some('f') => '\x0c',
     172               2 :                     Some('n') => '\n',
     173               1 :                     Some('r') => '\r',
     174               1 :                     Some('t') => '\t',
     175               2 :                     Some('0'..='7') => {
     176                 :                         // TODO
     177               1 :                         bail!("octal escapes not supported");
     178                 :                     }
     179               1 :                     Some(n) => n,
     180 UBC           0 :                     None => break,
     181                 :                 }
     182 CBC          10 :             } else if c == '\'' && iter.peek() == Some(&'\'') {
     183                 :                 // doubled quote becomes just one quote
     184               1 :                 iter.next().unwrap()
     185                 :             } else {
     186               9 :                 c
     187                 :             };
     188                 : 
     189              17 :             result.push(newc);
     190                 :         }
     191               2 :         Ok(result)
     192                 :     } else {
     193               1 :         Ok(s.to_string())
     194                 :     }
     195               4 : }
     196                 : 
     197               1 : #[test]
     198               1 : fn test_postgresql_conf_escapes() -> Result<()> {
     199               1 :     assert_eq!(escape_str("foo bar"), "'foo bar'");
     200                 :     // these don't need to be quoted
     201               1 :     assert_eq!(escape_str("foo"), "foo");
     202               1 :     assert_eq!(escape_str("123"), "123");
     203               1 :     assert_eq!(escape_str("+123"), "+123");
     204               1 :     assert_eq!(escape_str("-10"), "-10");
     205               1 :     assert_eq!(escape_str("1foo"), "1foo");
     206               1 :     assert_eq!(escape_str("foo1"), "foo1");
     207               1 :     assert_eq!(escape_str("10MB"), "10MB");
     208               1 :     assert_eq!(escape_str("-10kB"), "-10kB");
     209                 : 
     210                 :     // these need quoting and/or escaping
     211               1 :     assert_eq!(escape_str("foo bar"), "'foo bar'");
     212               1 :     assert_eq!(escape_str("fo'o"), "'fo''o'");
     213               1 :     assert_eq!(escape_str("fo\no"), "'fo\\no'");
     214               1 :     assert_eq!(escape_str("fo\\o"), "'fo\\\\o'");
     215               1 :     assert_eq!(escape_str("10 cats"), "'10 cats'");
     216                 : 
     217                 :     // Test de-escaping
     218               1 :     assert_eq!(deescape_str(&escape_str("foo"))?, "foo");
     219               1 :     assert_eq!(deescape_str(&escape_str("fo'o\nba\\r"))?, "fo'o\nba\\r");
     220               1 :     assert_eq!(deescape_str("'\\b\\f\\n\\r\\t'")?, "\x08\x0c\n\r\t");
     221                 : 
     222                 :     // octal-escapes are currently not supported
     223               1 :     assert!(deescape_str("'foo\\7\\07\\007'").is_err());
     224                 : 
     225               1 :     Ok(())
     226               1 : }
        

Generated by: LCOV version 2.1-beta