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::bootstrap::mode::ExecutionPrivileges;
13use crate::error::{BootstrapError, BootstrapErrorKind, BootstrapResult};
14use crate::fs::ambient_dir_and_path;
15
16#[cfg(unix)]
17use cap_std::fs::PermissionsExt;
18
19#[cfg(unix)]
20const WORKER_BINARY_NAME: &str = "pg_worker";
21#[cfg(windows)]
22const WORKER_BINARY_NAME: &str = "pg_worker.exe";
23
24pub(super) const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(15);
25const MAX_SHUTDOWN_TIMEOUT_SECS: u64 = 600;
26const SHUTDOWN_TIMEOUT_ENV: &str = "PG_SHUTDOWN_TIMEOUT_SECS";
27
28fn discover_worker_from_path() -> Option<Utf8PathBuf> {
29 let path_var = env::var_os("PATH")?;
30 for dir in env::split_paths(&path_var) {
31 let Some(worker_path) = Utf8PathBuf::from_path_buf(dir.join(WORKER_BINARY_NAME)).ok()
32 else {
33 continue;
34 };
35
36 #[cfg(unix)]
37 {
38 let dir_str = dir.as_os_str().to_string_lossy();
39 if dir_str == "." || dir_str.is_empty() {
40 continue;
41 }
42 }
43
44 if worker_path.is_file() && is_executable(&worker_path) {
45 return Some(worker_path);
46 }
47 }
48
49 None
50}
51
52#[cfg(unix)]
53fn is_executable(path: &Utf8Path) -> bool {
54 path.metadata()
55 .map(|m| {
56 #[cfg(unix)]
57 {
58 use std::os::unix::fs::PermissionsExt;
59 m.permissions().mode() & 0o111 != 0
60 }
61 #[cfg(not(unix))]
62 {
63 true
64 }
65 })
66 .unwrap_or(false)
67}
68
69#[cfg(not(unix))]
70fn is_executable(_path: &Utf8Path) -> bool {
71 true
72}
73
74#[cfg(unix)]
78pub const TZDIR_CANDIDATES: [&str; 4] = [
79 "/usr/share/zoneinfo",
80 "/usr/lib/zoneinfo",
81 "/etc/zoneinfo",
82 "/share/zoneinfo",
83];
84
85#[must_use]
101pub fn find_timezone_dir() -> Option<&'static Utf8Path> {
102 #[cfg(unix)]
103 {
104 TZDIR_CANDIDATES
105 .iter()
106 .copied()
107 .map(Utf8Path::new)
108 .find(|path| path.exists())
109 }
110
111 #[cfg(not(unix))]
112 {
113 None
114 }
115}
116
117pub(super) fn shutdown_timeout_from_env() -> BootstrapResult<Duration> {
118 match env::var(SHUTDOWN_TIMEOUT_ENV) {
119 Ok(raw) => {
120 let trimmed = raw.trim();
121 if trimmed.is_empty() {
122 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
123 "{SHUTDOWN_TIMEOUT_ENV} is present but empty"
124 )));
125 }
126
127 let seconds: u64 = trimmed.parse().map_err(|err| {
128 BootstrapError::from(color_eyre::eyre::eyre!(
129 "failed to parse {SHUTDOWN_TIMEOUT_ENV} from '{trimmed}': {err}"
130 ))
131 })?;
132
133 if seconds == 0 {
134 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
135 "{SHUTDOWN_TIMEOUT_ENV} must be at least 1 second (received {trimmed})"
136 )));
137 }
138
139 if seconds > MAX_SHUTDOWN_TIMEOUT_SECS {
140 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
141 "{SHUTDOWN_TIMEOUT_ENV} must be {MAX_SHUTDOWN_TIMEOUT_SECS} seconds or less (received {trimmed})"
142 )));
143 }
144
145 Ok(Duration::from_secs(seconds))
146 }
147 Err(VarError::NotPresent) => Ok(DEFAULT_SHUTDOWN_TIMEOUT),
148 Err(VarError::NotUnicode(value)) => Err(BootstrapError::from(color_eyre::eyre::eyre!(
149 "{SHUTDOWN_TIMEOUT_ENV} must contain a valid UTF-8 value (received {:?})",
150 value
151 ))),
152 }
153}
154
155pub(super) fn worker_binary_from_env(
156 privileges: ExecutionPrivileges,
157) -> BootstrapResult<Option<Utf8PathBuf>> {
158 if let Some(raw) = env::var_os("PG_EMBEDDED_WORKER") {
159 let path = Utf8PathBuf::from_path_buf(PathBuf::from(&raw)).map_err(|_| {
160 let invalid_value = raw.to_string_lossy().to_string();
161 BootstrapError::from(color_eyre::eyre::eyre!(
162 "PG_EMBEDDED_WORKER contains a non-UTF-8 value: {invalid_value:?}. \
163 Provide a UTF-8 encoded absolute path to the worker binary."
164 ))
165 })?;
166
167 validate_worker_path(&path)?;
168 return Ok(Some(path));
169 }
170
171 #[cfg(unix)]
172 {
173 use crate::bootstrap::mode::ExecutionPrivileges;
174 if privileges == ExecutionPrivileges::Root {
175 if let Some(worker) = discover_worker_from_path() {
176 validate_worker_path(&worker)?;
177 return Ok(Some(worker));
178 }
179 }
180 }
181
182 #[cfg(not(unix))]
183 {
184 let _ = privileges;
185 }
186
187 Ok(None)
188}
189
190fn validate_worker_path(path: &Utf8PathBuf) -> BootstrapResult<()> {
191 if path.as_str().is_empty() {
192 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
193 "Worker binary path must not be empty"
194 )));
195 }
196 if path.as_str() == "/" {
197 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
198 "Worker binary path must not point at filesystem root"
199 )));
200 }
201
202 validate_worker_binary(path)?;
203 Ok(())
204}
205
206fn validate_worker_binary(path: &Utf8PathBuf) -> BootstrapResult<()> {
207 let (dir, relative) =
208 ambient_dir_and_path(path).map_err(|err| worker_binary_error(path, err))?;
209 let metadata = dir
210 .metadata(relative.as_std_path())
211 .map_err(|err| worker_binary_error(path, Report::new(err)))?;
212
213 if !metadata.is_file() {
214 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
215 "PG_EMBEDDED_WORKER must reference a regular file: {path}"
216 )));
217 }
218
219 #[cfg(unix)]
220 {
221 if metadata.permissions().mode() & 0o111 == 0 {
222 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
223 "PG_EMBEDDED_WORKER must be executable: {path}"
224 )));
225 }
226 }
227
228 Ok(())
229}
230
231fn worker_binary_error(path: &Utf8Path, err: Report) -> BootstrapError {
232 let is_not_found = error_chain_has_not_found(&err);
233 let context = format!("failed to access PG_EMBEDDED_WORKER at {path}: {err}");
234 let report = err.wrap_err(context);
235
236 if is_not_found {
237 BootstrapError::new(BootstrapErrorKind::WorkerBinaryMissing, report)
238 } else {
239 BootstrapError::from(report)
240 }
241}
242
243fn error_chain_has_not_found(err: &Report) -> bool {
244 err.chain()
245 .filter_map(|source| source.downcast_ref::<std::io::Error>())
246 .any(|source| source.kind() == ErrorKind::NotFound)
247}
248
249#[derive(Debug, Clone)]
250pub(super) struct TimezoneEnv {
251 pub(super) dir: Option<Utf8PathBuf>,
252 pub(super) zone: String,
253}
254
255#[derive(Debug, Clone)]
273pub struct TestBootstrapEnvironment {
274 pub home: Utf8PathBuf,
276 pub xdg_cache_home: Utf8PathBuf,
278 pub xdg_runtime_dir: Utf8PathBuf,
280 pub pgpass_file: Utf8PathBuf,
282 pub tz_dir: Option<Utf8PathBuf>,
284 pub timezone: String,
286}
287
288impl TestBootstrapEnvironment {
289 pub(super) fn from_components(
290 xdg: XdgDirs,
291 pgpass_file: Utf8PathBuf,
292 timezone: TimezoneEnv,
293 ) -> Self {
294 Self {
295 home: xdg.home,
296 xdg_cache_home: xdg.cache,
297 xdg_runtime_dir: xdg.runtime,
298 pgpass_file,
299 tz_dir: timezone.dir,
300 timezone: timezone.zone,
301 }
302 }
303
304 #[must_use]
322 pub fn to_env(&self) -> Vec<(String, Option<String>)> {
323 let mut env = vec![
324 ("HOME".into(), Some(self.home.as_str().into())),
325 (
326 "XDG_CACHE_HOME".into(),
327 Some(self.xdg_cache_home.as_str().into()),
328 ),
329 (
330 "XDG_RUNTIME_DIR".into(),
331 Some(self.xdg_runtime_dir.as_str().into()),
332 ),
333 ("PGPASSFILE".into(), Some(self.pgpass_file.as_str().into())),
334 ];
335
336 env.push((
337 "TZDIR".into(),
338 self.tz_dir.as_ref().map(|dir| dir.as_str().into()),
339 ));
340
341 env.push(("TZ".into(), Some(self.timezone.clone())));
342
343 env
344 }
345}
346
347#[derive(Debug, Clone)]
348pub(super) struct XdgDirs {
349 pub(super) home: Utf8PathBuf,
350 pub(super) cache: Utf8PathBuf,
351 pub(super) runtime: Utf8PathBuf,
352}
353
354pub(super) fn prepare_timezone_env() -> BootstrapResult<TimezoneEnv> {
355 const DEFAULT_TIMEZONE: &str = "UTC";
356
357 let tz_dir = if let Some(dir) = env::var_os("TZDIR") {
358 let path = Utf8PathBuf::from_path_buf(PathBuf::from(dir)).map_err(
359 |_| -> crate::error::BootstrapError {
360 color_eyre::eyre::eyre!("TZDIR must be valid UTF-8").into()
361 },
362 )?;
363 if !path.exists() {
364 return Err(color_eyre::eyre::eyre!(
365 "time zone database not found at {}. Set TZDIR or install tzdata.",
366 path
367 )
368 .into());
369 }
370 Some(path)
371 } else {
372 discover_timezone_dir()?
373 };
374
375 let timezone = match env::var("TZ") {
376 Ok(value) if !value.trim().is_empty() => value,
377 Ok(_) | Err(std::env::VarError::NotPresent) => DEFAULT_TIMEZONE.to_owned(),
378 Err(std::env::VarError::NotUnicode(_)) => {
379 return Err(color_eyre::eyre::eyre!("TZ must be valid UTF-8").into());
380 }
381 };
382
383 Ok(TimezoneEnv {
384 dir: tz_dir,
385 zone: timezone,
386 })
387}
388
389fn discover_timezone_dir() -> BootstrapResult<Option<Utf8PathBuf>> {
390 #[cfg(unix)]
391 {
392 let candidate = find_timezone_dir().ok_or_else(|| -> crate::error::BootstrapError {
393 color_eyre::eyre::eyre!("time zone database not found. Set TZDIR or install tzdata.")
394 .into()
395 })?;
396
397 Ok(Some(candidate.to_owned()))
398 }
399
400 #[cfg(not(unix))]
401 {
402 Ok(None)
403 }
404}