use std::path::{Path, PathBuf};
use crate::error::SessionError;
use crate::session_user::SessionUser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StickyBitPolicy {
#[default]
Strict,
Warn,
Disabled,
}
pub fn openjd_temp_dir(base_dir: Option<&Path>) -> Result<PathBuf, SessionError> {
let dir = match base_dir {
Some(p) => p.to_path_buf(),
None => default_openjd_dir(),
};
std::fs::create_dir_all(&dir).map_err(|e| SessionError::TempDir {
path: dir.clone(),
source: e,
})?;
Ok(dir)
}
fn default_openjd_dir() -> PathBuf {
#[cfg(unix)]
{
std::env::temp_dir().join("OpenJD")
}
#[cfg(windows)]
{
openjd_dir_from_programdata(std::env::var("PROGRAMDATA").ok())
}
}
#[cfg(windows)]
fn openjd_dir_from_programdata(programdata: Option<String>) -> PathBuf {
let program_data = programdata.unwrap_or_else(|| {
log::warn!(
target: "openjd.sessions",
"Environment variable \"PROGRAMDATA\" is not set. \
Creating session working directories under C:\\ProgramData"
);
r"C:\ProgramData".to_string()
});
PathBuf::from(program_data).join("Amazon").join("OpenJD")
}
#[cfg(unix)]
pub fn find_missing_sticky_bit(root_dir: &Path) -> Option<PathBuf> {
use std::os::unix::fs::MetadataExt;
const S_IWOTH: u32 = 0o002;
const S_ISVTX: u32 = 0o1000;
for parent in root_dir.ancestors().skip(1) {
if let Ok(meta) = std::fs::metadata(parent) {
let mode = meta.mode();
if (mode & S_IWOTH) != 0 && (mode & S_ISVTX) == 0 {
return Some(parent.to_path_buf());
}
}
}
None
}
pub struct TempDir {
path: PathBuf,
cleaned_up: bool,
}
impl std::fmt::Debug for TempDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TempDir")
.field("path", &self.path)
.field("cleaned_up", &self.cleaned_up)
.finish()
}
}
impl AsRef<std::path::Path> for TempDir {
fn as_ref(&self) -> &std::path::Path {
&self.path
}
}
impl TempDir {
pub fn new(
dir: Option<&Path>,
prefix: Option<&str>,
_user: Option<&dyn SessionUser>,
) -> Result<Self, SessionError> {
let parent = match dir {
Some(d) => d.to_path_buf(),
None => openjd_temp_dir(None)?,
};
let prefix = prefix.unwrap_or("");
let suffix = random_hex();
let name = format!("{prefix}{suffix}");
let path = parent.join(name);
std::fs::create_dir(&path).map_err(|e| SessionError::TempDir {
path: path.clone(),
source: e,
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = if let Some(u) = _user.filter(|u| !u.is_process_user()) {
if let Ok(Some(grp)) = nix::unistd::Group::from_name(u.group()) {
nix::unistd::chown(&path, None, Some(grp.gid)).map_err(|e| {
SessionError::PathPermissions {
path: path.display().to_string(),
reason: format!(
"Could not change ownership (error: {e}). Please ensure that uid {} is a member of group {}.",
nix::unistd::geteuid(), u.group()
),
}
})?;
}
0o770
} else {
0o700
};
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).map_err(
|e| SessionError::TempDir {
path: path.clone(),
source: e,
},
)?;
}
#[cfg(windows)]
{
if let Some(u) = _user.filter(|u| !u.is_process_user()) {
if let Ok(process_user) = crate::win32::get_process_user() {
if let Err(e) = crate::win32_permissions::set_permissions(
&path.to_string_lossy(),
&[process_user.as_str()],
&[u.user()],
&[],
) {
return Err(SessionError::PathPermissions {
path: path.display().to_string(),
reason: e.to_string(),
});
}
}
}
}
Ok(Self {
path,
cleaned_up: false,
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn cleanup(&mut self) -> Result<(), SessionError> {
if self.cleaned_up {
return Ok(());
}
self.cleaned_up = true;
std::fs::remove_dir_all(&self.path).map_err(|e| SessionError::TempDir {
path: self.path.clone(),
source: e,
})
}
}
impl Drop for TempDir {
fn drop(&mut self) {
if !self.cleaned_up {
let _ = std::fs::remove_dir_all(&self.path);
}
}
}
fn random_hex() -> String {
uuid::Uuid::new_v4().simple().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tempdir_debug() {
let td = TempDir::new(None, None, None).unwrap();
let dbg = format!("{td:?}");
assert!(dbg.contains("TempDir"));
assert!(dbg.contains("cleaned_up: false"));
}
#[test]
fn tempdir_as_ref_path() {
let td = TempDir::new(None, None, None).unwrap();
let p: &std::path::Path = td.as_ref();
assert_eq!(p, td.path());
}
#[cfg(unix)]
#[test]
fn find_missing_sticky_bit_detects_world_writable_without_sticky() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path().join("world_writable");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o777)).unwrap();
let child = dir.join("child");
std::fs::create_dir(&child).unwrap();
let result = find_missing_sticky_bit(&child);
assert_eq!(result, Some(dir));
}
#[cfg(unix)]
#[test]
fn find_missing_sticky_bit_none_when_sticky_set() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path().join("sticky_dir");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o1777)).unwrap();
let child = dir.join("child");
std::fs::create_dir(&child).unwrap();
let result = find_missing_sticky_bit(&child);
assert_eq!(result, None);
}
#[cfg(windows)]
#[test]
fn openjd_dir_from_programdata_warns_when_unset() {
testing_logger::setup();
let dir = openjd_dir_from_programdata(None);
assert_eq!(dir, PathBuf::from(r"C:\ProgramData\Amazon\OpenJD"));
testing_logger::validate(|captured_logs| {
assert!(
captured_logs.iter().any(|log| {
log.level == log::Level::Warn && log.body.contains("PROGRAMDATA")
}),
"Expected a warning about PROGRAMDATA not being set"
);
});
}
#[cfg(windows)]
#[test]
fn openjd_dir_from_programdata_uses_provided_value() {
testing_logger::setup();
let dir = openjd_dir_from_programdata(Some(r"D:\ProgramData".to_string()));
assert_eq!(dir, PathBuf::from(r"D:\ProgramData\Amazon\OpenJD"));
testing_logger::validate(|captured_logs| {
assert!(
!captured_logs
.iter()
.any(|log| log.level == log::Level::Warn),
"Expected no warning when PROGRAMDATA is provided"
);
});
}
#[cfg(windows)]
#[test]
fn openjd_temp_dir_honors_base_dir_override() {
let custom_root = std::env::temp_dir().join("OpenJDBaseDirTest");
let _ = std::fs::remove_dir_all(&custom_root);
let openjd_dir = custom_root.join("Amazon").join("OpenJD");
let dir = openjd_temp_dir(Some(&openjd_dir))
.expect("openjd_temp_dir should succeed with override");
assert_eq!(
dir, openjd_dir,
"openjd_temp_dir should return the path it was given"
);
assert!(dir.exists(), "openjd_temp_dir should create the directory");
assert!(
custom_root.join("Amazon").exists(),
"missing parents should be created as a side effect"
);
let _ = std::fs::remove_dir_all(&custom_root);
}
}