use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Component, Path, PathBuf};
use super::Result;
use crate::local::error::LocalError;
#[cfg(unix)]
pub(super) fn enforce_private_cache_root(root: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt as _;
fs::set_permissions(root, fs::Permissions::from_mode(0o700)).map_err(|source| {
LocalError::Io {
operation: "restrict cache root to owner-only",
path: root.to_owned(),
source,
}
})?;
let mode = fs::symlink_metadata(root)
.map_err(|source| LocalError::Io {
operation: "inspect cache root permissions",
path: root.to_owned(),
source,
})?
.permissions()
.mode()
& 0o777;
if mode & 0o077 != 0 {
return Err(LocalError::CacheNotPrivate {
path: root.to_owned(),
reason: format!("filesystem mode {mode:o} still allows group/world access"),
});
}
Ok(())
}
#[cfg(windows)]
const BROAD_WINDOWS_PRINCIPALS: [&str; 5] = [
"Everyone:",
"Authenticated Users:",
"\\Users:",
"Todos:", "Jeder:", ];
#[cfg(windows)]
pub(super) fn enforce_private_cache_root(root: &Path) -> Result<()> {
let account = current_windows_account(root)?;
set_owner_only_windows_dacl(root, &account)?;
verify_private_windows_dacl(root)
}
#[cfg(windows)]
fn current_windows_account(root: &Path) -> Result<String> {
let Some(user) = std::env::var("USERNAME")
.ok()
.filter(|value| !value.trim().is_empty())
else {
return Err(LocalError::CacheNotPrivate {
path: root.to_owned(),
reason: "USERNAME is not set, cannot restrict the cache DACL".to_owned(),
});
};
Ok(
match std::env::var("USERDOMAIN")
.ok()
.filter(|value| !value.trim().is_empty())
{
Some(domain) => format!("{domain}\\{user}"),
None => user,
},
)
}
#[cfg(windows)]
fn set_owner_only_windows_dacl(root: &Path, account: &str) -> Result<()> {
let output = std::process::Command::new("icacls")
.arg(root)
.arg("/inheritance:r")
.arg("/grant:r")
.arg(format!("{account}:(OI)(CI)F"))
.output()
.map_err(|source| LocalError::Io {
operation: "run icacls to restrict cache DACL",
path: root.to_owned(),
source,
})?;
if !output.status.success() {
return Err(LocalError::CacheNotPrivate {
path: root.to_owned(),
reason: format!(
"icacls restriction failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
),
});
}
Ok(())
}
#[cfg(windows)]
fn verify_private_windows_dacl(root: &Path) -> Result<()> {
let output = std::process::Command::new("icacls")
.arg(root)
.output()
.map_err(|source| LocalError::Io {
operation: "run icacls to verify cache DACL",
path: root.to_owned(),
source,
})?;
if !output.status.success() {
return Err(LocalError::CacheNotPrivate {
path: root.to_owned(),
reason: "could not read back the cache DACL to verify it".to_owned(),
});
}
let listing = String::from_utf8_lossy(&output.stdout);
for principal in BROAD_WINDOWS_PRINCIPALS {
if listing.contains(principal) {
return Err(LocalError::CacheNotPrivate {
path: root.to_owned(),
reason: format!(
"a broad principal ({}) still has DACL access",
principal.trim_end_matches(':')
),
});
}
}
Ok(())
}
pub(super) fn safe_relative_path(path: &Path) -> bool {
!path.as_os_str().is_empty()
&& path
.components()
.all(|component| matches!(component, Component::Normal(_)))
}
pub(super) fn part_path(path: &Path) -> PathBuf {
let mut name = path.as_os_str().to_owned();
name.push(".part");
PathBuf::from(name)
}
pub(super) fn ensure_cache_directory(root: &Path, directory: &Path) -> Result<()> {
if directory == root {
fs::create_dir_all(root).map_err(|source| LocalError::Io {
operation: "create cache directory",
path: root.to_owned(),
source,
})?;
return validate_tree_path(root, root);
}
validate_tree_path(root, directory)?;
let relative = confined_relative(root, directory)?;
let mut current = root.to_owned();
for component in relative.components() {
current.push(component.as_os_str());
match fs::symlink_metadata(¤t) {
Ok(metadata) => {
if is_link_or_reparse(&metadata) || !metadata.is_dir() {
return Err(LocalError::UnsafeCachePath { path: current });
}
}
Err(source) if source.kind() == io::ErrorKind::NotFound => {
if let Err(source) = fs::create_dir(¤t)
&& source.kind() != io::ErrorKind::AlreadyExists
{
return Err(LocalError::Io {
operation: "create cache directory",
path: current.clone(),
source,
});
}
validate_tree_path(root, ¤t)?;
}
Err(source) => {
return Err(LocalError::Io {
operation: "inspect cache directory",
path: current,
source,
});
}
}
}
Ok(())
}
pub(super) fn remove_cache_entry(root: &Path, path: &Path) -> Result<()> {
validate_tree_path(root, path)?;
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(source) => {
return Err(LocalError::Io {
operation: "inspect cache entry",
path: path.to_owned(),
source,
});
}
};
if is_link_or_reparse(&metadata) {
return Err(LocalError::UnsafeCachePath {
path: path.to_owned(),
});
}
let result = if metadata.is_dir() {
fs::remove_dir_all(path)
} else {
fs::remove_file(path)
};
result.map_err(|source| LocalError::Io {
operation: "remove cache entry",
path: path.to_owned(),
source,
})
}
pub(super) fn rename_confined(root: &Path, source: &Path, destination: &Path) -> Result<()> {
validate_tree_path(root, source)?;
validate_tree_path(root, destination)?;
fs::rename(source, destination).map_err(|error| LocalError::Io {
operation: "atomically install artifact",
path: destination.to_owned(),
source: error,
})
}
pub(super) fn validate_cache_path(root: &Path, path: &Path) -> Result<()> {
validate_tree_path(root, path)
}
pub(super) fn validate_tree_path(root: &Path, path: &Path) -> Result<()> {
let relative = confined_relative(root, path)?;
let root_metadata = fs::symlink_metadata(root).map_err(|source| LocalError::Io {
operation: "inspect cache root",
path: root.to_owned(),
source,
})?;
if is_link_or_reparse(&root_metadata) || !root_metadata.is_dir() {
return Err(LocalError::UnsafeCachePath {
path: root.to_owned(),
});
}
let mut current = root.to_owned();
let components: Vec<_> = relative.components().collect();
for (index, component) in components.iter().enumerate() {
current.push(component.as_os_str());
match fs::symlink_metadata(¤t) {
Ok(metadata) => {
if is_link_or_reparse(&metadata)
|| (index + 1 != components.len() && !metadata.is_dir())
{
return Err(LocalError::UnsafeCachePath { path: current });
}
}
Err(source) if source.kind() == io::ErrorKind::NotFound => break,
Err(source) => {
return Err(LocalError::Io {
operation: "inspect cache path",
path: current,
source,
});
}
}
}
Ok(())
}
fn confined_relative<'a>(root: &Path, path: &'a Path) -> Result<&'a Path> {
let relative = path
.strip_prefix(root)
.map_err(|_| LocalError::UnsafeCachePath {
path: path.to_owned(),
})?;
if !relative.as_os_str().is_empty() && !safe_relative_path(relative) {
return Err(LocalError::UnsafeCachePath {
path: path.to_owned(),
});
}
Ok(relative)
}
#[cfg(windows)]
fn is_link_or_reparse(metadata: &fs::Metadata) -> bool {
use std::os::windows::fs::MetadataExt as _;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
}
#[cfg(not(windows))]
fn is_link_or_reparse(metadata: &fs::Metadata) -> bool {
metadata.file_type().is_symlink()
}
pub(super) fn write_synced(path: &Path, contents: &[u8]) -> Result<()> {
let mut file = File::create(path).map_err(|source| LocalError::Io {
operation: "create install marker",
path: path.to_owned(),
source,
})?;
file.write_all(contents).map_err(|source| LocalError::Io {
operation: "write install marker",
path: path.to_owned(),
source,
})?;
file.sync_all().map_err(|source| LocalError::Io {
operation: "sync install marker",
path: path.to_owned(),
source,
})
}