oliphaunt_wasix/oliphaunt/
config.rs1use std::collections::BTreeMap;
2
3use anyhow::{Result, bail, ensure};
4
5use crate::oliphaunt::interface::DebugLevel;
6
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct PostgresConfig {
14 settings: BTreeMap<String, String>,
15}
16
17impl PostgresConfig {
18 pub fn new() -> Self {
20 Self::default()
21 }
22
23 pub fn set(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
25 self.settings.insert(name.into(), value.into());
26 self
27 }
28
29 pub(crate) fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) {
30 self.settings.insert(name.into(), value.into());
31 }
32
33 #[cfg(feature = "extensions")]
34 pub(crate) fn get(&self, name: &str) -> Option<&str> {
35 self.settings.get(name).map(String::as_str)
36 }
37
38 pub(crate) fn validate(&self) -> Result<()> {
39 for (name, value) in &self.settings {
40 validate_guc_name(name)?;
41 ensure!(
42 !value.contains('\0'),
43 "Postgres config value for '{name}' must not contain NUL bytes"
44 );
45 }
46 Ok(())
47 }
48
49 pub(crate) fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
50 self.settings
51 .iter()
52 .map(|(name, value)| (name.as_str(), value.as_str()))
53 }
54
55 #[cfg(feature = "extensions")]
56 pub(crate) fn stable_entries(&self) -> Vec<(String, String)> {
57 self.settings
58 .iter()
59 .map(|(name, value)| (name.clone(), value.clone()))
60 .collect()
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub(crate) struct StartupConfig {
66 pub(crate) username: String,
67 pub(crate) database: String,
68 pub(crate) debug_level: Option<DebugLevel>,
69 pub(crate) relaxed_durability: bool,
70 pub(crate) extra_args: Vec<String>,
71}
72
73impl Default for StartupConfig {
74 fn default() -> Self {
75 Self {
76 username: "postgres".to_owned(),
77 database: "template1".to_owned(),
78 debug_level: None,
79 relaxed_durability: false,
80 extra_args: Vec::new(),
81 }
82 }
83}
84
85impl StartupConfig {
86 pub(crate) fn validate(&self) -> Result<()> {
87 validate_startup_value("username", &self.username)?;
88 validate_startup_value("database", &self.database)?;
89 if let Some(level) = self.debug_level {
90 ensure!(
91 level <= 5,
92 "Postgres debug level must be between 0 and 5, got {level}"
93 );
94 }
95 for arg in &self.extra_args {
96 ensure!(
97 !arg.contains('\0'),
98 "Postgres startup argument must not contain NUL bytes"
99 );
100 }
101 Ok(())
102 }
103}
104
105fn validate_guc_name(name: &str) -> Result<()> {
106 ensure!(!name.is_empty(), "Postgres config name must not be empty");
107 ensure!(
108 !name.contains('\0') && !name.contains('='),
109 "Postgres config name '{name}' must not contain NUL bytes or '='"
110 );
111
112 for part in name.split('.') {
113 if part.is_empty() {
114 bail!("Postgres config name '{name}' contains an empty identifier part");
115 }
116 let mut chars = part.chars();
117 let first = chars.next().expect("part is non-empty");
118 if !(first == '_' || first.is_ascii_alphabetic()) {
119 bail!("Postgres config name '{name}' must start each identifier with a letter or '_'");
120 }
121 if chars.any(|ch| !(ch == '_' || ch.is_ascii_alphanumeric())) {
122 bail!("Postgres config name '{name}' may only contain letters, digits, '_', and '.'");
123 }
124 }
125
126 Ok(())
127}
128
129fn validate_startup_value(name: &str, value: &str) -> Result<()> {
130 ensure!(
131 !value.is_empty(),
132 "Postgres startup {name} must not be empty"
133 );
134 ensure!(
135 !value.contains('\0'),
136 "Postgres startup {name} must not contain NUL bytes"
137 );
138 Ok(())
139}
140
141#[cfg(test)]
142mod tests {
143 use super::PostgresConfig;
144
145 #[test]
146 fn validates_builtin_and_extension_guc_names() {
147 PostgresConfig::new()
148 .set("synchronous_commit", "off")
149 .set("pg_stat_statements.track", "all")
150 .validate()
151 .unwrap();
152 }
153
154 #[test]
155 fn rejects_invalid_guc_names_before_startup() {
156 let err = PostgresConfig::new()
157 .set("bad=name", "off")
158 .validate()
159 .expect_err("invalid GUC name should be rejected");
160 assert!(err.to_string().contains("must not contain"));
161 }
162}