openjd_sessions/
tempdir.rs1use std::path::{Path, PathBuf};
8
9use crate::error::SessionError;
10use crate::session_user::SessionUser;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum StickyBitPolicy {
19 #[default]
22 Strict,
23 Warn,
25 Disabled,
27}
28
29pub fn openjd_temp_dir(base_dir: Option<&Path>) -> Result<PathBuf, SessionError> {
43 let dir = match base_dir {
44 Some(p) => p.to_path_buf(),
45 None => default_openjd_dir(),
46 };
47
48 std::fs::create_dir_all(&dir).map_err(|e| SessionError::TempDir {
49 path: dir.clone(),
50 source: e,
51 })?;
52 Ok(dir)
53}
54
55fn default_openjd_dir() -> PathBuf {
61 #[cfg(unix)]
62 {
63 std::env::temp_dir().join("OpenJD")
64 }
65
66 #[cfg(windows)]
67 {
68 openjd_dir_from_programdata(std::env::var("PROGRAMDATA").ok())
69 }
70}
71
72#[cfg(windows)]
79fn openjd_dir_from_programdata(programdata: Option<String>) -> PathBuf {
80 let program_data = programdata.unwrap_or_else(|| {
81 log::warn!(
82 target: "openjd.sessions",
83 "Environment variable \"PROGRAMDATA\" is not set. \
84 Creating session working directories under C:\\ProgramData"
85 );
86 r"C:\ProgramData".to_string()
87 });
88 PathBuf::from(program_data).join("Amazon").join("OpenJD")
89}
90
91#[cfg(unix)]
95pub fn find_missing_sticky_bit(root_dir: &Path) -> Option<PathBuf> {
96 use std::os::unix::fs::MetadataExt;
97
98 const S_IWOTH: u32 = 0o002;
99 const S_ISVTX: u32 = 0o1000;
100
101 for parent in root_dir.ancestors().skip(1) {
102 if let Ok(meta) = std::fs::metadata(parent) {
103 let mode = meta.mode();
104 if (mode & S_IWOTH) != 0 && (mode & S_ISVTX) == 0 {
105 return Some(parent.to_path_buf());
106 }
107 }
108 }
109 None
110}
111
112pub struct TempDir {
127 path: PathBuf,
128 cleaned_up: bool,
129}
130
131impl std::fmt::Debug for TempDir {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("TempDir")
134 .field("path", &self.path)
135 .field("cleaned_up", &self.cleaned_up)
136 .finish()
137 }
138}
139
140impl AsRef<std::path::Path> for TempDir {
141 fn as_ref(&self) -> &std::path::Path {
142 &self.path
143 }
144}
145
146impl TempDir {
147 pub fn new(
153 dir: Option<&Path>,
154 prefix: Option<&str>,
155 _user: Option<&dyn SessionUser>,
156 ) -> Result<Self, SessionError> {
157 let parent = match dir {
158 Some(d) => d.to_path_buf(),
159 None => openjd_temp_dir(None)?,
160 };
161
162 let prefix = prefix.unwrap_or("");
163 let suffix = random_hex();
164 let name = format!("{prefix}{suffix}");
165 let path = parent.join(name);
166
167 std::fs::create_dir(&path).map_err(|e| SessionError::TempDir {
168 path: path.clone(),
169 source: e,
170 })?;
171
172 #[cfg(unix)]
173 {
174 use std::os::unix::fs::PermissionsExt;
175 let mode = if let Some(u) = _user.filter(|u| !u.is_process_user()) {
176 if let Ok(Some(grp)) = nix::unistd::Group::from_name(u.group()) {
179 nix::unistd::chown(&path, None, Some(grp.gid)).map_err(|e| {
180 SessionError::PathPermissions {
181 path: path.display().to_string(),
182 reason: format!(
183 "Could not change ownership (error: {e}). Please ensure that uid {} is a member of group {}.",
184 nix::unistd::geteuid(), u.group()
185 ),
186 }
187 })?;
188 }
189 0o770
190 } else {
191 0o700
192 };
193 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).map_err(
194 |e| SessionError::TempDir {
195 path: path.clone(),
196 source: e,
197 },
198 )?;
199 }
200
201 #[cfg(windows)]
203 {
204 if let Some(u) = _user.filter(|u| !u.is_process_user()) {
205 if let Ok(process_user) = crate::win32::get_process_user() {
206 if let Err(e) = crate::win32_permissions::set_permissions(
207 &path.to_string_lossy(),
208 &[process_user.as_str()],
209 &[u.user()],
210 &[],
211 ) {
212 return Err(SessionError::PathPermissions {
213 path: path.display().to_string(),
214 reason: e.to_string(),
215 });
216 }
217 }
218 }
219 }
220
221 Ok(Self {
222 path,
223 cleaned_up: false,
224 })
225 }
226
227 pub fn path(&self) -> &Path {
228 &self.path
229 }
230
231 pub fn cleanup(&mut self) -> Result<(), SessionError> {
233 if self.cleaned_up {
234 return Ok(());
235 }
236 self.cleaned_up = true;
237 std::fs::remove_dir_all(&self.path).map_err(|e| SessionError::TempDir {
238 path: self.path.clone(),
239 source: e,
240 })
241 }
242}
243
244impl Drop for TempDir {
245 fn drop(&mut self) {
246 if !self.cleaned_up {
247 let _ = std::fs::remove_dir_all(&self.path);
248 }
249 }
250}
251
252fn random_hex() -> String {
253 uuid::Uuid::new_v4().simple().to_string()
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn tempdir_debug() {
262 let td = TempDir::new(None, None, None).unwrap();
263 let dbg = format!("{td:?}");
264 assert!(dbg.contains("TempDir"));
265 assert!(dbg.contains("cleaned_up: false"));
266 }
267
268 #[test]
269 fn tempdir_as_ref_path() {
270 let td = TempDir::new(None, None, None).unwrap();
271 let p: &std::path::Path = td.as_ref();
272 assert_eq!(p, td.path());
273 }
274
275 #[cfg(unix)]
278 #[test]
279 fn find_missing_sticky_bit_detects_world_writable_without_sticky() {
280 use std::os::unix::fs::PermissionsExt;
281
282 let tmp = tempfile::TempDir::new().unwrap();
283 let dir = tmp.path().join("world_writable");
284 std::fs::create_dir(&dir).unwrap();
285 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o777)).unwrap();
286
287 let child = dir.join("child");
288 std::fs::create_dir(&child).unwrap();
289
290 let result = find_missing_sticky_bit(&child);
291 assert_eq!(result, Some(dir));
292 }
293
294 #[cfg(unix)]
297 #[test]
298 fn find_missing_sticky_bit_none_when_sticky_set() {
299 use std::os::unix::fs::PermissionsExt;
300
301 let tmp = tempfile::TempDir::new().unwrap();
302 let dir = tmp.path().join("sticky_dir");
303 std::fs::create_dir(&dir).unwrap();
304 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).unwrap();
305
306 let child = dir.join("child");
307 std::fs::create_dir(&child).unwrap();
308
309 let result = find_missing_sticky_bit(&child);
310 assert_eq!(result, None);
311 }
312
313 #[cfg(windows)]
320 #[test]
321 fn openjd_dir_from_programdata_warns_when_unset() {
322 testing_logger::setup();
323 let dir = openjd_dir_from_programdata(None);
324 assert_eq!(dir, PathBuf::from(r"C:\ProgramData\Amazon\OpenJD"));
325 testing_logger::validate(|captured_logs| {
326 assert!(
327 captured_logs.iter().any(|log| {
328 log.level == log::Level::Warn && log.body.contains("PROGRAMDATA")
329 }),
330 "Expected a warning about PROGRAMDATA not being set"
331 );
332 });
333 }
334
335 #[cfg(windows)]
338 #[test]
339 fn openjd_dir_from_programdata_uses_provided_value() {
340 testing_logger::setup();
341 let dir = openjd_dir_from_programdata(Some(r"D:\ProgramData".to_string()));
342 assert_eq!(dir, PathBuf::from(r"D:\ProgramData\Amazon\OpenJD"));
343 testing_logger::validate(|captured_logs| {
344 assert!(
345 !captured_logs
346 .iter()
347 .any(|log| log.level == log::Level::Warn),
348 "Expected no warning when PROGRAMDATA is provided"
349 );
350 });
351 }
352
353 #[cfg(windows)]
361 #[test]
362 fn openjd_temp_dir_honors_base_dir_override() {
363 let custom_root = std::env::temp_dir().join("OpenJDBaseDirTest");
364 let _ = std::fs::remove_dir_all(&custom_root);
366
367 let openjd_dir = custom_root.join("Amazon").join("OpenJD");
368 let dir = openjd_temp_dir(Some(&openjd_dir))
369 .expect("openjd_temp_dir should succeed with override");
370 assert_eq!(
371 dir, openjd_dir,
372 "openjd_temp_dir should return the path it was given"
373 );
374 assert!(dir.exists(), "openjd_temp_dir should create the directory");
375 assert!(
376 custom_root.join("Amazon").exists(),
377 "missing parents should be created as a side effect"
378 );
379
380 let _ = std::fs::remove_dir_all(&custom_root);
382 }
383}