Skip to main content

pg_client/
parameter.rs

1//! Validated PostgreSQL configuration parameter names and values.
2//!
3//! Recommended import style:
4//!
5//! ```
6//! use pg_client::parameter;
7//!
8//! let name = parameter::Name::from_static_or_panic("synchronous_commit");
9//! let value = parameter::Value::from_static_or_panic("off");
10//! ```
11//!
12//! These types model PG's GUC (Grand Unified Configuration) parameters in
13//! every context where they apply: `postgresql.conf`, `-c` command-line flags,
14//! `ALTER SYSTEM SET`, `ALTER DATABASE … SET`, `ALTER ROLE … SET`, `SET`,
15//! `SET LOCAL`, and `set_config(...)`. The `pg_settings.context` column on the
16//! PG side determines which of those targets a given parameter can be set
17//! through, but the name/value validity rules themselves are uniform.
18//!
19//! See [PG 18: Setting Parameters](https://www.postgresql.org/docs/current/config-setting.html)
20//! for upstream documentation. PG itself does not impose a documented byte
21//! length on string parameter values; the cap here is a sanity-check against
22//! programmer error, not a platform constraint.
23
24use std::borrow::Cow;
25
26/// Validated PostgreSQL configuration parameter name.
27///
28/// Parameter names follow the GUC identifier rules:
29/// - Non-empty.
30/// - First byte: ASCII letter or underscore.
31/// - Subsequent bytes: ASCII alphanumeric, underscore, or dot.
32///
33/// The dot enables namespaced extension parameters such as
34/// `pg_stat_statements.track`.
35#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, serde::Serialize)]
36#[serde(try_from = "String", into = "String")]
37pub struct Name(Cow<'static, str>);
38
39impl From<Name> for String {
40    fn from(name: Name) -> Self {
41        name.0.into()
42    }
43}
44
45impl Name {
46    /// Returns the parameter name as a string slice.
47    #[must_use]
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51
52    /// Validated parameter name for `'static` inputs.
53    ///
54    /// # Panics
55    ///
56    /// Panics at compile time when used in a `const` context, or at runtime
57    /// otherwise, if the name is empty, starts with an invalid character, or
58    /// contains any character outside the allowed set.
59    #[must_use]
60    pub const fn from_static_or_panic(name: &'static str) -> Self {
61        match validate_name(name) {
62            Ok(()) => {}
63            Err(NameError::Empty) => {
64                panic!("PostgreSQL parameter name cannot be empty");
65            }
66            Err(NameError::InvalidStartCharacter) => {
67                panic!("PostgreSQL parameter name must start with a letter or underscore");
68            }
69            Err(NameError::InvalidCharacter) => {
70                panic!("PostgreSQL parameter name contains an invalid character");
71            }
72        }
73        Self(Cow::Borrowed(name))
74    }
75}
76
77impl AsRef<str> for Name {
78    fn as_ref(&self) -> &str {
79        &self.0
80    }
81}
82
83impl std::fmt::Display for Name {
84    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        formatter.write_str(self.as_str())
86    }
87}
88
89#[derive(Debug, thiserror::Error)]
90pub enum NameError {
91    #[error("PostgreSQL parameter name cannot be empty")]
92    Empty,
93    #[error("PostgreSQL parameter name must start with a letter or underscore")]
94    InvalidStartCharacter,
95    #[error("PostgreSQL parameter name contains an invalid character")]
96    InvalidCharacter,
97}
98
99impl std::str::FromStr for Name {
100    type Err = NameError;
101
102    fn from_str(name: &str) -> Result<Self, Self::Err> {
103        validate_name(name).map(|()| Self(Cow::Owned(name.to_string())))
104    }
105}
106
107impl TryFrom<String> for Name {
108    type Error = NameError;
109
110    fn try_from(name: String) -> Result<Self, Self::Error> {
111        validate_name(&name).map(|()| Self(Cow::Owned(name)))
112    }
113}
114
115impl From<&'static str> for Name {
116    fn from(name: &'static str) -> Self {
117        Self::from_static_or_panic(name)
118    }
119}
120
121const fn validate_name(name: &str) -> Result<(), NameError> {
122    let bytes = name.as_bytes();
123    if bytes.is_empty() {
124        return Err(NameError::Empty);
125    }
126    let first = bytes[0];
127    if !(first.is_ascii_alphabetic() || first == b'_') {
128        return Err(NameError::InvalidStartCharacter);
129    }
130    let mut index = 1;
131    while index < bytes.len() {
132        let byte = bytes[index];
133        if !(byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'.') {
134            return Err(NameError::InvalidCharacter);
135        }
136        index += 1;
137    }
138    Ok(())
139}
140
141/// Maximum byte length of a validated PostgreSQL parameter value.
142///
143/// PG itself does not impose a documented length cap; this is a sanity-check
144/// safely above any realistic value (`shared_preload_libraries` with a long
145/// extension list is well under 1 KB) and well below kernel argv per-string
146/// limits.
147///
148/// Keep the panic message in [`Value::from_static_or_panic`] in sync if this
149/// changes.
150pub const VALUE_MAX_LEN: usize = 4096;
151
152/// Validated PostgreSQL configuration parameter value.
153///
154/// Ensures the value contains no NUL bytes and does not exceed
155/// [`VALUE_MAX_LEN`] bytes.
156#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, serde::Serialize)]
157#[serde(try_from = "String", into = "String")]
158pub struct Value(Cow<'static, str>);
159
160impl From<Value> for String {
161    fn from(value: Value) -> Self {
162        value.0.into()
163    }
164}
165
166impl Value {
167    /// Returns the parameter value as a string slice.
168    #[must_use]
169    pub fn as_str(&self) -> &str {
170        &self.0
171    }
172
173    /// Validated parameter value for `'static` inputs.
174    ///
175    /// # Panics
176    ///
177    /// Panics at compile time when used in a `const` context, or at runtime
178    /// otherwise, if the value contains a NUL byte or exceeds
179    /// [`VALUE_MAX_LEN`] bytes.
180    #[must_use]
181    pub const fn from_static_or_panic(value: &'static str) -> Self {
182        match validate_value(value) {
183            Ok(()) => {}
184            Err(ValueError::ContainsNul { .. }) => {
185                panic!("PostgreSQL parameter value cannot contain NUL byte");
186            }
187            Err(ValueError::TooLong { .. }) => {
188                panic!("PostgreSQL parameter value exceeds maximum of 4096 bytes");
189            }
190        }
191        Self(Cow::Borrowed(value))
192    }
193}
194
195impl AsRef<str> for Value {
196    fn as_ref(&self) -> &str {
197        &self.0
198    }
199}
200
201impl std::fmt::Display for Value {
202    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        formatter.write_str(self.as_str())
204    }
205}
206
207#[derive(Debug, thiserror::Error)]
208pub enum ValueError {
209    #[error("PostgreSQL parameter value contains NUL byte at index {index}")]
210    ContainsNul { index: usize },
211    #[error("PostgreSQL parameter value length {length} exceeds maximum {max}")]
212    TooLong { length: usize, max: usize },
213}
214
215impl std::str::FromStr for Value {
216    type Err = ValueError;
217
218    fn from_str(value: &str) -> Result<Self, Self::Err> {
219        validate_value(value).map(|()| Self(Cow::Owned(value.to_string())))
220    }
221}
222
223impl TryFrom<String> for Value {
224    type Error = ValueError;
225
226    fn try_from(value: String) -> Result<Self, Self::Error> {
227        validate_value(&value).map(|()| Self(Cow::Owned(value)))
228    }
229}
230
231impl From<&'static str> for Value {
232    fn from(value: &'static str) -> Self {
233        Self::from_static_or_panic(value)
234    }
235}
236
237const fn validate_value(value: &str) -> Result<(), ValueError> {
238    let bytes = value.as_bytes();
239    if bytes.len() > VALUE_MAX_LEN {
240        return Err(ValueError::TooLong {
241            length: bytes.len(),
242            max: VALUE_MAX_LEN,
243        });
244    }
245    let mut index = 0;
246    while index < bytes.len() {
247        if bytes[index] == 0 {
248            return Err(ValueError::ContainsNul { index });
249        }
250        index += 1;
251    }
252    Ok(())
253}
254
255/// Map of PostgreSQL configuration parameters.
256///
257/// `BTreeMap` for stable iteration order and dedup-on-key semantics — same
258/// shape as `ociman::container::EnvironmentVariables`.
259pub type Map = std::collections::BTreeMap<Name, Value>;