use std::{
env, fs, io,
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
sync::LazyLock,
};
use crate::{error, paths::env_var_path, warn};
pub fn home_dir() -> Option<&'static Path> {
HOME.as_deref()
}
static HOME: LazyLock<Option<PathBuf>> = LazyLock::new(env::home_dir);
pub fn xdg_data_home() -> Option<&'static Path> {
XDG_DATA_HOME.as_deref()
}
static XDG_DATA_HOME: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
env_var_path("XDG_DATA_HOME")
.or_else(|| env::home_dir().map(|h| h.join(".local/share")))
.and_then(|a| if a.is_relative() { None } else { Some(a) })
});
pub fn xdg_config_home() -> Option<&'static Path> {
XDG_CONFIG_HOME.as_deref()
}
static XDG_CONFIG_HOME: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
env_var_path("XDG_CONFIG_HOME")
.or_else(|| env::home_dir().map(|h| h.join(".config")))
.and_then(|a| if a.is_relative() { None } else { Some(a) })
});
pub fn xdg_state_home() -> Option<&'static Path> {
XDG_STATE_HOME.as_deref()
}
static XDG_STATE_HOME: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
env_var_path("XDG_STATE_HOME")
.or_else(|| env::home_dir().map(|h| h.join(".local/state")))
.and_then(|a| if a.is_relative() { None } else { Some(a) })
});
pub fn xdg_cache_home() -> Option<&'static Path> {
XDG_CACHE_HOME.as_deref()
}
static XDG_CACHE_HOME: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
env_var_path("XDG_CACHE_HOME")
.or_else(|| env::home_dir().map(|h| h.join(".cache")))
.and_then(|a| if a.is_relative() { None } else { Some(a) })
});
pub fn protected_paths() -> &'static [PathBuf] {
&PROTECTED_PATHS
}
static PROTECTED_PATHS: LazyLock<Box<[PathBuf]>> = LazyLock::new(|| {
let user_paths: Option<Vec<PathBuf>> =
env::var_os("PROTECTED_PATHS").map(|v| env::split_paths(&v).collect());
let mut dirs = Vec::with_capacity({
1 + if let Some(user_paths) = &user_paths {
user_paths.len()
} else {
16
}
});
dirs.push(PathBuf::from("/"));
if let Some(user_paths) = user_paths {
dirs.extend(user_paths);
} else {
dirs.push(PathBuf::from("/usr/bin"));
dirs.push(PathBuf::from("/usr/lib"));
dirs.push(PathBuf::from("/home"));
dirs.push(PathBuf::from("/etc"));
dirs.push(PathBuf::from("/sys"));
if let Some(home) = env::home_dir() {
dirs.push(home.join("Desktop"));
dirs.push(home.join("Documents"));
dirs.push(home.join("Music"));
dirs.push(home.join("Pictures"));
dirs.push(home.join("Videos"));
dirs.push(home.join(".ssh"));
dirs.push(home.join(".gnupg"));
dirs.push(home);
}
if let Some(path) = xdg_config_home() {
dirs.push(path.to_path_buf());
};
if let Some(path) = xdg_data_home() {
dirs.push(path.to_path_buf());
};
if let Some(path) = xdg_state_home() {
dirs.push(path.to_path_buf());
}
}
dirs.into_boxed_slice()
});
pub fn protected_directories() -> &'static [PathBuf] {
&PROTECTED_DIRECTORIES
}
static PROTECTED_DIRECTORIES: LazyLock<Box<[PathBuf]>> = LazyLock::new(|| {
let user_paths: Vec<PathBuf> = env::var_os("PROTECTED_DIRS")
.map(|v| env::split_paths(&v).collect())
.unwrap_or_default();
let mut dirs = Vec::with_capacity(
user_paths.len()
+ if cfg!(feature = "extra-protected-paths") {
1
} else {
0
},
);
dirs.extend(user_paths);
#[cfg(feature = "extra-protected-paths")]
{
dirs.push(PathBuf::from("/boot"));
}
dirs.into_boxed_slice()
});
pub fn unprotected_devices() -> &'static [u64] {
&UNPROTECTED_DEVICES
}
static UNPROTECTED_DEVICES: LazyLock<Box<[u64]>> = LazyLock::new(|| {
let user_devices = match env::var("UNPROTECTED_DEVICES") {
Ok(v) => Some(v),
Err(env::VarError::NotPresent) => None,
Err(env::VarError::NotUnicode(os_string)) => {
warn!(
"UNPROTECTED_DEVICES is not readable, reverting to defaults. Reason: Expected UTF-8 string of devices ID's separated by ':', found: {os_string}",
os_string = os_string.display()
);
None
}
};
let unprotected_devices = user_devices
.and_then(|s| {
let parsed: Result<Vec<u64>, _> = s.split(':').map(|dev| dev.parse()).collect();
if let Err(err) = &parsed {
warn!(
"UNPROTECTED_DEVICES is not parseable, reverting to defaults. Reason: {err}"
)
}
parsed.ok()
})
.unwrap_or_else(|| {
["/", "/home", "/tmp"].into_iter().filter_map(|path| match fs::symlink_metadata(path) {
Ok(meta) => Some(meta.dev()),
Err(err) => {
error!("Could not protect {path} by default: {err}{non_root}", non_root = if path != "/" {"\n(This is fine if '{path}' is not on a separate filesystem than '/')"} else {""});
None
},
}).collect()
});
unprotected_devices.into_boxed_slice()
});
pub fn is_protected_device(device: u64) -> bool {
!UNPROTECTED_DEVICES.contains(&device)
}
pub fn get_mountpoints(path: impl AsRef<Path>) -> io::Result<Vec<(PathBuf, u64)>> {
let path = path.as_ref();
let mut mountpoints = Vec::new();
let mut latest_dev = path.symlink_metadata()?.dev();
let mut current = path;
while let Some(parent) = current.parent() {
let parent_meta = parent.symlink_metadata()?;
if parent_meta.dev() != latest_dev {
mountpoints.push((current.to_owned(), latest_dev));
latest_dev = parent_meta.dev();
}
current = parent
}
if path.is_absolute() {
mountpoints.push((current.to_owned(), latest_dev));
}
mountpoints.reverse();
Ok(mountpoints)
}