pg_embedded_setup_unpriv/bootstrap/
env.rs1use std::env::{self, VarError};
5use std::io::ErrorKind;
6use std::path::PathBuf;
7use std::time::Duration;
8
9use camino::{Utf8Path, Utf8PathBuf};
10use color_eyre::eyre::Report;
11
12use crate::error::{BootstrapError, BootstrapErrorKind, BootstrapResult};
13use crate::fs::ambient_dir_and_path;
14
15#[cfg(unix)]
16use cap_std::fs::PermissionsExt;
17
18pub(super) const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(15);
19const MAX_SHUTDOWN_TIMEOUT_SECS: u64 = 600;
20const SHUTDOWN_TIMEOUT_ENV: &str = "PG_SHUTDOWN_TIMEOUT_SECS";
21
22#[cfg(unix)]
26pub const TZDIR_CANDIDATES: [&str; 4] = [
27 "/usr/share/zoneinfo",
28 "/usr/lib/zoneinfo",
29 "/etc/zoneinfo",
30 "/share/zoneinfo",
31];
32
33#[must_use]
49pub fn find_timezone_dir() -> Option<&'static Utf8Path> {
50 #[cfg(unix)]
51 {
52 TZDIR_CANDIDATES
53 .iter()
54 .copied()
55 .map(Utf8Path::new)
56 .find(|path| path.exists())
57 }
58
59 #[cfg(not(unix))]
60 {
61 None
62 }
63}
64
65pub(super) fn shutdown_timeout_from_env() -> BootstrapResult<Duration> {
66 match env::var(SHUTDOWN_TIMEOUT_ENV) {
67 Ok(raw) => {
68 let trimmed = raw.trim();
69 if trimmed.is_empty() {
70 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
71 "{SHUTDOWN_TIMEOUT_ENV} is present but empty"
72 )));
73 }
74
75 let seconds: u64 = trimmed.parse().map_err(|err| {
76 BootstrapError::from(color_eyre::eyre::eyre!(
77 "failed to parse {SHUTDOWN_TIMEOUT_ENV} from '{trimmed}': {err}"
78 ))
79 })?;
80
81 if seconds == 0 {
82 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
83 "{SHUTDOWN_TIMEOUT_ENV} must be at least 1 second (received {trimmed})"
84 )));
85 }
86
87 if seconds > MAX_SHUTDOWN_TIMEOUT_SECS {
88 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
89 "{SHUTDOWN_TIMEOUT_ENV} must be {MAX_SHUTDOWN_TIMEOUT_SECS} seconds or less (received {trimmed})"
90 )));
91 }
92
93 Ok(Duration::from_secs(seconds))
94 }
95 Err(VarError::NotPresent) => Ok(DEFAULT_SHUTDOWN_TIMEOUT),
96 Err(VarError::NotUnicode(value)) => Err(BootstrapError::from(color_eyre::eyre::eyre!(
97 "{SHUTDOWN_TIMEOUT_ENV} must contain a valid UTF-8 value (received {:?})",
98 value
99 ))),
100 }
101}
102
103pub(super) fn worker_binary_from_env() -> BootstrapResult<Option<Utf8PathBuf>> {
104 let Some(raw) = env::var_os("PG_EMBEDDED_WORKER") else {
105 return Ok(None);
106 };
107
108 let path = Utf8PathBuf::from_path_buf(PathBuf::from(&raw)).map_err(|_| {
109 let invalid_value = raw.to_string_lossy().to_string();
110 BootstrapError::from(color_eyre::eyre::eyre!(
111 "PG_EMBEDDED_WORKER contains a non-UTF-8 value: {invalid_value:?}. \
112 Provide a UTF-8 encoded absolute path to the worker binary."
113 ))
114 })?;
115
116 if path.as_str().is_empty() {
117 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
118 "PG_EMBEDDED_WORKER must not be empty"
119 )));
120 }
121 if path.as_str() == "/" {
122 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
123 "PG_EMBEDDED_WORKER must not point at the filesystem root"
124 )));
125 }
126
127 validate_worker_binary(&path)?;
128 Ok(Some(path))
129}
130
131fn validate_worker_binary(path: &Utf8PathBuf) -> BootstrapResult<()> {
132 let (dir, relative) =
133 ambient_dir_and_path(path).map_err(|err| worker_binary_error(path, err))?;
134 let metadata = dir
135 .metadata(relative.as_std_path())
136 .map_err(|err| worker_binary_error(path, Report::new(err)))?;
137
138 if !metadata.is_file() {
139 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
140 "PG_EMBEDDED_WORKER must reference a regular file: {path}"
141 )));
142 }
143
144 #[cfg(unix)]
145 {
146 if metadata.permissions().mode() & 0o111 == 0 {
147 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
148 "PG_EMBEDDED_WORKER must be executable: {path}"
149 )));
150 }
151 }
152
153 Ok(())
154}
155
156fn worker_binary_error(path: &Utf8Path, err: Report) -> BootstrapError {
157 let is_not_found = error_chain_has_not_found(&err);
158 let context = format!("failed to access PG_EMBEDDED_WORKER at {path}: {err}");
159 let report = err.wrap_err(context);
160
161 if is_not_found {
162 BootstrapError::new(BootstrapErrorKind::WorkerBinaryMissing, report)
163 } else {
164 BootstrapError::from(report)
165 }
166}
167
168fn error_chain_has_not_found(err: &Report) -> bool {
169 err.chain()
170 .filter_map(|source| source.downcast_ref::<std::io::Error>())
171 .any(|source| source.kind() == ErrorKind::NotFound)
172}
173
174#[derive(Debug, Clone)]
175pub(super) struct TimezoneEnv {
176 pub(super) dir: Option<Utf8PathBuf>,
177 pub(super) zone: String,
178}
179
180#[derive(Debug, Clone)]
198pub struct TestBootstrapEnvironment {
199 pub home: Utf8PathBuf,
201 pub xdg_cache_home: Utf8PathBuf,
203 pub xdg_runtime_dir: Utf8PathBuf,
205 pub pgpass_file: Utf8PathBuf,
207 pub tz_dir: Option<Utf8PathBuf>,
209 pub timezone: String,
211}
212
213impl TestBootstrapEnvironment {
214 pub(super) fn from_components(
215 xdg: XdgDirs,
216 pgpass_file: Utf8PathBuf,
217 timezone: TimezoneEnv,
218 ) -> Self {
219 Self {
220 home: xdg.home,
221 xdg_cache_home: xdg.cache,
222 xdg_runtime_dir: xdg.runtime,
223 pgpass_file,
224 tz_dir: timezone.dir,
225 timezone: timezone.zone,
226 }
227 }
228
229 #[must_use]
247 pub fn to_env(&self) -> Vec<(String, Option<String>)> {
248 let mut env = vec![
249 ("HOME".into(), Some(self.home.as_str().into())),
250 (
251 "XDG_CACHE_HOME".into(),
252 Some(self.xdg_cache_home.as_str().into()),
253 ),
254 (
255 "XDG_RUNTIME_DIR".into(),
256 Some(self.xdg_runtime_dir.as_str().into()),
257 ),
258 ("PGPASSFILE".into(), Some(self.pgpass_file.as_str().into())),
259 ];
260
261 env.push((
262 "TZDIR".into(),
263 self.tz_dir.as_ref().map(|dir| dir.as_str().into()),
264 ));
265
266 env.push(("TZ".into(), Some(self.timezone.clone())));
267
268 env
269 }
270}
271
272#[derive(Debug, Clone)]
273pub(super) struct XdgDirs {
274 pub(super) home: Utf8PathBuf,
275 pub(super) cache: Utf8PathBuf,
276 pub(super) runtime: Utf8PathBuf,
277}
278
279pub(super) fn prepare_timezone_env() -> BootstrapResult<TimezoneEnv> {
280 const DEFAULT_TIMEZONE: &str = "UTC";
281
282 let tz_dir = if let Some(dir) = env::var_os("TZDIR") {
283 let path = Utf8PathBuf::from_path_buf(PathBuf::from(dir)).map_err(
284 |_| -> crate::error::BootstrapError {
285 color_eyre::eyre::eyre!("TZDIR must be valid UTF-8").into()
286 },
287 )?;
288 if !path.exists() {
289 return Err(color_eyre::eyre::eyre!(
290 "time zone database not found at {}. Set TZDIR or install tzdata.",
291 path
292 )
293 .into());
294 }
295 Some(path)
296 } else {
297 discover_timezone_dir()?
298 };
299
300 let timezone = match env::var("TZ") {
301 Ok(value) if !value.trim().is_empty() => value,
302 Ok(_) | Err(std::env::VarError::NotPresent) => DEFAULT_TIMEZONE.to_owned(),
303 Err(std::env::VarError::NotUnicode(_)) => {
304 return Err(color_eyre::eyre::eyre!("TZ must be valid UTF-8").into());
305 }
306 };
307
308 Ok(TimezoneEnv {
309 dir: tz_dir,
310 zone: timezone,
311 })
312}
313
314fn discover_timezone_dir() -> BootstrapResult<Option<Utf8PathBuf>> {
315 #[cfg(unix)]
316 {
317 let candidate = find_timezone_dir().ok_or_else(|| -> crate::error::BootstrapError {
318 color_eyre::eyre::eyre!("time zone database not found. Set TZDIR or install tzdata.")
319 .into()
320 })?;
321
322 Ok(Some(candidate.to_owned()))
323 }
324
325 #[cfg(not(unix))]
326 {
327 Ok(None)
328 }
329}