pg_embedded_setup_unpriv/bootstrap/
env.rs1use std::env::{self, VarError};
5use std::fs;
6use std::path::PathBuf;
7use std::time::Duration;
8
9use camino::{Utf8Path, Utf8PathBuf};
10
11use crate::error::{BootstrapError, BootstrapErrorKind, BootstrapResult};
12
13#[cfg(unix)]
14use std::os::unix::fs::PermissionsExt;
15
16pub(super) const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(15);
17const MAX_SHUTDOWN_TIMEOUT_SECS: u64 = 600;
18const SHUTDOWN_TIMEOUT_ENV: &str = "PG_SHUTDOWN_TIMEOUT_SECS";
19
20pub(super) fn shutdown_timeout_from_env() -> BootstrapResult<Duration> {
21 match env::var(SHUTDOWN_TIMEOUT_ENV) {
22 Ok(raw) => {
23 let trimmed = raw.trim();
24 if trimmed.is_empty() {
25 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
26 "{SHUTDOWN_TIMEOUT_ENV} is present but empty"
27 )));
28 }
29
30 let seconds: u64 = trimmed.parse().map_err(|err| {
31 BootstrapError::from(color_eyre::eyre::eyre!(
32 "failed to parse {SHUTDOWN_TIMEOUT_ENV} from '{trimmed}': {err}"
33 ))
34 })?;
35
36 if seconds == 0 {
37 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
38 "{SHUTDOWN_TIMEOUT_ENV} must be at least 1 second (received {trimmed})"
39 )));
40 }
41
42 if seconds > MAX_SHUTDOWN_TIMEOUT_SECS {
43 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
44 "{SHUTDOWN_TIMEOUT_ENV} must be {MAX_SHUTDOWN_TIMEOUT_SECS} seconds or less (received {trimmed})"
45 )));
46 }
47
48 Ok(Duration::from_secs(seconds))
49 }
50 Err(VarError::NotPresent) => Ok(DEFAULT_SHUTDOWN_TIMEOUT),
51 Err(VarError::NotUnicode(value)) => Err(BootstrapError::from(color_eyre::eyre::eyre!(
52 "{SHUTDOWN_TIMEOUT_ENV} must contain a valid UTF-8 value (received {:?})",
53 value
54 ))),
55 }
56}
57
58pub(super) fn worker_binary_from_env() -> BootstrapResult<Option<Utf8PathBuf>> {
59 let Some(raw) = env::var_os("PG_EMBEDDED_WORKER") else {
60 return Ok(None);
61 };
62
63 let path = Utf8PathBuf::from_path_buf(PathBuf::from(&raw)).map_err(|_| {
64 let invalid_value = raw.to_string_lossy().to_string();
65 BootstrapError::from(color_eyre::eyre::eyre!(
66 "PG_EMBEDDED_WORKER contains a non-UTF-8 value: {invalid_value:?}. \
67 Provide a UTF-8 encoded absolute path to the worker binary."
68 ))
69 })?;
70
71 validate_worker_binary(&path)?;
72 Ok(Some(path))
73}
74
75fn validate_worker_binary(path: &Utf8PathBuf) -> BootstrapResult<()> {
76 let metadata = fs::metadata(path.as_std_path()).map_err(|err| {
77 if err.kind() == std::io::ErrorKind::NotFound {
78 return BootstrapError::new(
79 BootstrapErrorKind::WorkerBinaryMissing,
80 color_eyre::eyre::eyre!("failed to access PG_EMBEDDED_WORKER at {path}: {err}"),
81 );
82 }
83
84 BootstrapError::from(color_eyre::eyre::eyre!(
85 "failed to access PG_EMBEDDED_WORKER at {path}: {err}"
86 ))
87 })?;
88
89 if !metadata.is_file() {
90 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
91 "PG_EMBEDDED_WORKER must reference a regular file: {path}"
92 )));
93 }
94
95 #[cfg(unix)]
96 {
97 if metadata.permissions().mode() & 0o111 == 0 {
98 return Err(BootstrapError::from(color_eyre::eyre::eyre!(
99 "PG_EMBEDDED_WORKER must be executable: {path}"
100 )));
101 }
102 }
103
104 Ok(())
105}
106
107#[derive(Debug, Clone)]
108pub(super) struct TimezoneEnv {
109 pub(super) dir: Option<Utf8PathBuf>,
110 pub(super) zone: String,
111}
112
113#[derive(Debug, Clone)]
114pub struct TestBootstrapEnvironment {
115 pub home: Utf8PathBuf,
117 pub xdg_cache_home: Utf8PathBuf,
119 pub xdg_runtime_dir: Utf8PathBuf,
121 pub pgpass_file: Utf8PathBuf,
123 pub tz_dir: Option<Utf8PathBuf>,
125 pub timezone: String,
127}
128
129impl TestBootstrapEnvironment {
130 pub(super) fn from_components(
131 xdg: XdgDirs,
132 pgpass_file: Utf8PathBuf,
133 timezone: TimezoneEnv,
134 ) -> Self {
135 Self {
136 home: xdg.home,
137 xdg_cache_home: xdg.cache,
138 xdg_runtime_dir: xdg.runtime,
139 pgpass_file,
140 tz_dir: timezone.dir,
141 timezone: timezone.zone,
142 }
143 }
144
145 #[must_use]
163 pub fn to_env(&self) -> Vec<(String, Option<String>)> {
164 let mut env = vec![
165 ("HOME".into(), Some(self.home.as_str().into())),
166 (
167 "XDG_CACHE_HOME".into(),
168 Some(self.xdg_cache_home.as_str().into()),
169 ),
170 (
171 "XDG_RUNTIME_DIR".into(),
172 Some(self.xdg_runtime_dir.as_str().into()),
173 ),
174 ("PGPASSFILE".into(), Some(self.pgpass_file.as_str().into())),
175 ];
176
177 env.push((
178 "TZDIR".into(),
179 self.tz_dir.as_ref().map(|dir| dir.as_str().into()),
180 ));
181
182 env.push(("TZ".into(), Some(self.timezone.clone())));
183
184 env
185 }
186}
187
188#[derive(Debug, Clone)]
189pub(super) struct XdgDirs {
190 pub(super) home: Utf8PathBuf,
191 pub(super) cache: Utf8PathBuf,
192 pub(super) runtime: Utf8PathBuf,
193}
194
195pub(super) fn prepare_timezone_env() -> BootstrapResult<TimezoneEnv> {
196 const DEFAULT_TIMEZONE: &str = "UTC";
197
198 let tz_dir = if let Some(dir) = env::var_os("TZDIR") {
199 let path = Utf8PathBuf::from_path_buf(PathBuf::from(dir)).map_err(
200 |_| -> crate::error::BootstrapError {
201 color_eyre::eyre::eyre!("TZDIR must be valid UTF-8").into()
202 },
203 )?;
204 if !path.exists() {
205 return Err(color_eyre::eyre::eyre!(
206 "time zone database not found at {}. Set TZDIR or install tzdata.",
207 path
208 )
209 .into());
210 }
211 Some(path)
212 } else {
213 discover_timezone_dir()?
214 };
215
216 let timezone = match env::var("TZ") {
217 Ok(value) if !value.trim().is_empty() => value,
218 Ok(_) | Err(std::env::VarError::NotPresent) => DEFAULT_TIMEZONE.to_owned(),
219 Err(std::env::VarError::NotUnicode(_)) => {
220 return Err(color_eyre::eyre::eyre!("TZ must be valid UTF-8").into());
221 }
222 };
223
224 Ok(TimezoneEnv {
225 dir: tz_dir,
226 zone: timezone,
227 })
228}
229
230fn discover_timezone_dir() -> BootstrapResult<Option<Utf8PathBuf>> {
231 #[cfg(unix)]
232 {
233 static CANDIDATES: [&str; 4] = [
234 "/usr/share/zoneinfo",
235 "/usr/lib/zoneinfo",
236 "/etc/zoneinfo",
237 "/share/zoneinfo",
238 ];
239
240 let candidate = CANDIDATES
241 .iter()
242 .map(Utf8Path::new)
243 .find(|path| path.exists())
244 .ok_or_else(|| -> crate::error::BootstrapError {
245 color_eyre::eyre::eyre!(
246 "time zone database not found. Set TZDIR or install tzdata."
247 )
248 .into()
249 })?;
250
251 Ok(Some(candidate.to_owned()))
252 }
253
254 #[cfg(not(unix))]
255 {
256 Ok(None)
257 }
258}