use std::{
env,
ffi::{OsStr, OsString},
fs, io,
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
sync::LazyLock,
};
use crate::{error, warn};
pub(crate) static NULL_PATH: &str = "/dev/null";
pub(crate) const ILLEGAL_CHARACTERS: [(u8, char); 1] = [(b'/', '⁄')];
pub(crate) fn sanitize_string(os_str: impl AsRef<OsStr>) -> OsString {
let os_str = os_str.as_ref();
let bytes = os_str.as_encoded_bytes();
let mut buf = Vec::with_capacity(bytes.len());
for byte in bytes {
if let Some(replacement) = ILLEGAL_CHARACTERS
.iter()
.find_map(|(illegal, replacement)| (illegal == byte).then_some(replacement))
{
let mut char_buf = [0u8; 4];
buf.extend(replacement.encode_utf8(&mut char_buf).as_bytes());
} else {
buf.push(*byte);
}
}
unsafe { OsString::from_encoded_bytes_unchecked(buf) }
}
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 {
11
}
});
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(base_dirs) = directories::BaseDirs::new() {
let home = base_dirs.home_dir();
dirs.push(home.to_path_buf());
dirs.push(home.join(".ssh"));
dirs.push(home.join(".gnupg"));
dirs.push(base_dirs.data_dir().to_path_buf());
dirs.push(base_dirs.config_dir().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: Option<Vec<PathBuf>> =
env::var_os("PROTECTED_DIRS").map(|v| env::split_paths(&v).collect());
let mut dirs = Vec::with_capacity({
if let Some(user_paths) = &user_paths {
user_paths.len()
} else {
0
}
});
if let Some(user_paths) = user_paths {
dirs.extend(user_paths);
} else {
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)
}
#[cfg(test)]
mod tests {
#[test]
fn sanitize_str_replaces_chars() {
use std::ffi::OsString;
use crate::paths::sys::sanitize_os_str;
let os_str = OsString::from("/");
let sanitized = sanitize_os_str(os_str);
assert_eq!(sanitized, OsString::from("⁄"))
}
}