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
22pub(super) fn shutdown_timeout_from_env() -> BootstrapResult<Duration> {
23 match env::var(SHUTDOWN_TIMEOUT_ENV) {
24 Ok(raw) => {
25 let trimmed = raw.trim();
26 if trimmed.is_empty() {
27 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
28 "{SHUTDOWN_TIMEOUT_ENV} is present but empty"
29 )));
30 }
31
32 let seconds: u64 = trimmed.parse().map_err(|err| {
33 BootstrapError::from(color_eyre::eyre::eyre!(
34 "failed to parse {SHUTDOWN_TIMEOUT_ENV} from '{trimmed}': {err}"
35 ))
36 })?;
37
38 if seconds == 0 {
39 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
40 "{SHUTDOWN_TIMEOUT_ENV} must be at least 1 second (received {trimmed})"
41 )));
42 }
43
44 if seconds > MAX_SHUTDOWN_TIMEOUT_SECS {
45 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
46 "{SHUTDOWN_TIMEOUT_ENV} must be {MAX_SHUTDOWN_TIMEOUT_SECS} seconds or less (received {trimmed})"
47 )));
48 }
49
50 Ok(Duration::from_secs(seconds))
51 }
52 Err(VarError::NotPresent) => Ok(DEFAULT_SHUTDOWN_TIMEOUT),
53 Err(VarError::NotUnicode(value)) => Err(BootstrapError::from(color_eyre::eyre::eyre!(
54 "{SHUTDOWN_TIMEOUT_ENV} must contain a valid UTF-8 value (received {:?})",
55 value
56 ))),
57 }
58}
59
60pub(super) fn worker_binary_from_env() -> BootstrapResult<Option<Utf8PathBuf>> {
61 let Some(raw) = env::var_os("PG_EMBEDDED_WORKER") else {
62 return Ok(None);
63 };
64
65 let path = Utf8PathBuf::from_path_buf(PathBuf::from(&raw)).map_err(|_| {
66 let invalid_value = raw.to_string_lossy().to_string();
67 BootstrapError::from(color_eyre::eyre::eyre!(
68 "PG_EMBEDDED_WORKER contains a non-UTF-8 value: {invalid_value:?}. \
69 Provide a UTF-8 encoded absolute path to the worker binary."
70 ))
71 })?;
72
73 if path.as_str().is_empty() {
74 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
75 "PG_EMBEDDED_WORKER must not be empty"
76 )));
77 }
78 if path.as_str() == "/" {
79 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
80 "PG_EMBEDDED_WORKER must not point at the filesystem root"
81 )));
82 }
83
84 validate_worker_binary(&path)?;
85 Ok(Some(path))
86}
87
88fn validate_worker_binary(path: &Utf8PathBuf) -> BootstrapResult<()> {
89 let (dir, relative) =
90 ambient_dir_and_path(path).map_err(|err| worker_binary_error(path, err))?;
91 let metadata = dir
92 .metadata(relative.as_std_path())
93 .map_err(|err| worker_binary_error(path, Report::new(err)))?;
94
95 if !metadata.is_file() {
96 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
97 "PG_EMBEDDED_WORKER must reference a regular file: {path}"
98 )));
99 }
100
101 #[cfg(unix)]
102 {
103 if metadata.permissions().mode() & 0o111 == 0 {
104 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
105 "PG_EMBEDDED_WORKER must be executable: {path}"
106 )));
107 }
108 }
109
110 Ok(())
111}
112
113fn worker_binary_error(path: &Utf8Path, err: Report) -> BootstrapError {
114 let is_not_found = error_chain_has_not_found(&err);
115 let context = format!("failed to access PG_EMBEDDED_WORKER at {path}: {err}");
116 let report = err.wrap_err(context);
117
118 if is_not_found {
119 BootstrapError::new(BootstrapErrorKind::WorkerBinaryMissing, report)
120 } else {
121 BootstrapError::from(report)
122 }
123}
124
125fn error_chain_has_not_found(err: &Report) -> bool {
126 err.chain()
127 .filter_map(|source| source.downcast_ref::<std::io::Error>())
128 .any(|source| source.kind() == ErrorKind::NotFound)
129}
130
131#[derive(Debug, Clone)]
132pub(super) struct TimezoneEnv {
133 pub(super) dir: Option<Utf8PathBuf>,
134 pub(super) zone: String,
135}
136
137#[derive(Debug, Clone)]
155pub struct TestBootstrapEnvironment {
156 pub home: Utf8PathBuf,
158 pub xdg_cache_home: Utf8PathBuf,
160 pub xdg_runtime_dir: Utf8PathBuf,
162 pub pgpass_file: Utf8PathBuf,
164 pub tz_dir: Option<Utf8PathBuf>,
166 pub timezone: String,
168}
169
170impl TestBootstrapEnvironment {
171 pub(super) fn from_components(
172 xdg: XdgDirs,
173 pgpass_file: Utf8PathBuf,
174 timezone: TimezoneEnv,
175 ) -> Self {
176 Self {
177 home: xdg.home,
178 xdg_cache_home: xdg.cache,
179 xdg_runtime_dir: xdg.runtime,
180 pgpass_file,
181 tz_dir: timezone.dir,
182 timezone: timezone.zone,
183 }
184 }
185
186 #[must_use]
204 pub fn to_env(&self) -> Vec<(String, Option<String>)> {
205 let mut env = vec![
206 ("HOME".into(), Some(self.home.as_str().into())),
207 (
208 "XDG_CACHE_HOME".into(),
209 Some(self.xdg_cache_home.as_str().into()),
210 ),
211 (
212 "XDG_RUNTIME_DIR".into(),
213 Some(self.xdg_runtime_dir.as_str().into()),
214 ),
215 ("PGPASSFILE".into(), Some(self.pgpass_file.as_str().into())),
216 ];
217
218 env.push((
219 "TZDIR".into(),
220 self.tz_dir.as_ref().map(|dir| dir.as_str().into()),
221 ));
222
223 env.push(("TZ".into(), Some(self.timezone.clone())));
224
225 env
226 }
227}
228
229#[derive(Debug, Clone)]
230pub(super) struct XdgDirs {
231 pub(super) home: Utf8PathBuf,
232 pub(super) cache: Utf8PathBuf,
233 pub(super) runtime: Utf8PathBuf,
234}
235
236pub(super) fn prepare_timezone_env() -> BootstrapResult<TimezoneEnv> {
237 const DEFAULT_TIMEZONE: &str = "UTC";
238
239 let tz_dir = if let Some(dir) = env::var_os("TZDIR") {
240 let path = Utf8PathBuf::from_path_buf(PathBuf::from(dir)).map_err(
241 |_| -> crate::error::BootstrapError {
242 color_eyre::eyre::eyre!("TZDIR must be valid UTF-8").into()
243 },
244 )?;
245 if !path.exists() {
246 return Err(color_eyre::eyre::eyre!(
247 "time zone database not found at {}. Set TZDIR or install tzdata.",
248 path
249 )
250 .into());
251 }
252 Some(path)
253 } else {
254 discover_timezone_dir()?
255 };
256
257 let timezone = match env::var("TZ") {
258 Ok(value) if !value.trim().is_empty() => value,
259 Ok(_) | Err(std::env::VarError::NotPresent) => DEFAULT_TIMEZONE.to_owned(),
260 Err(std::env::VarError::NotUnicode(_)) => {
261 return Err(color_eyre::eyre::eyre!("TZ must be valid UTF-8").into());
262 }
263 };
264
265 Ok(TimezoneEnv {
266 dir: tz_dir,
267 zone: timezone,
268 })
269}
270
271fn discover_timezone_dir() -> BootstrapResult<Option<Utf8PathBuf>> {
272 #[cfg(unix)]
273 {
274 static CANDIDATES: [&str; 4] = [
275 "/usr/share/zoneinfo",
276 "/usr/lib/zoneinfo",
277 "/etc/zoneinfo",
278 "/share/zoneinfo",
279 ];
280
281 let candidate = CANDIDATES
282 .iter()
283 .map(Utf8Path::new)
284 .find(|path| path.exists())
285 .ok_or_else(|| -> crate::error::BootstrapError {
286 color_eyre::eyre::eyre!(
287 "time zone database not found. Set TZDIR or install tzdata."
288 )
289 .into()
290 })?;
291
292 Ok(Some(candidate.to_owned()))
293 }
294
295 #[cfg(not(unix))]
296 {
297 Ok(None)
298 }
299}