use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use super::slots::{SLOT_DIR_NAME, SlotDir, current_uid};
#[derive(Debug, thiserror::Error)]
#[error(
"no build-lease store is usable: {canonical}: {canonical_error}; fallback {fallback}: \
{fallback_error}. Repair: {}",
self.repair()
)]
#[non_exhaustive]
pub struct StoreError {
pub canonical: PathBuf,
pub canonical_error: String,
pub fallback: PathBuf,
pub fallback_error: String,
}
impl StoreError {
#[must_use]
pub fn repair(&self) -> String {
format!(
"make {0} a directory you own with no group or other write permission \
(`mkdir -p {0} && chmod 700 {0}`), moving aside any file in its place",
self.canonical.display()
)
}
}
#[must_use]
pub fn canonical_path(home: &Path) -> PathBuf {
home.join(".trusty-mpm").join(SLOT_DIR_NAME)
}
#[must_use]
pub fn fallback_path() -> PathBuf {
#[cfg(debug_assertions)]
if let Some(dir) = std::env::var_os("TRUSTY_MPM_TEST_BUILD_SLOT_FALLBACK") {
return PathBuf::from(dir);
}
fixed_fallback_path()
}
#[must_use]
pub fn fixed_fallback_path() -> PathBuf {
PathBuf::from("/tmp").join(format!("trusty-mpm-{SLOT_DIR_NAME}-{}", current_uid()))
}
impl SlotDir {
pub fn resolve(home: Option<&Path>) -> Result<Self, StoreError> {
Self::resolve_in(home, &fallback_path())
}
pub fn resolve_in(home: Option<&Path>, fallback: &Path) -> Result<Self, StoreError> {
let canonical = home.map_or_else(
|| PathBuf::from("~/.trusty-mpm").join(SLOT_DIR_NAME),
canonical_path,
);
let canonical_error = match home {
None => "no home directory".to_string(),
Some(_) => match open_private_dir(&canonical, true) {
Ok(dir) => return Ok(dir),
Err(err) => err,
},
};
match open_private_dir(fallback, false) {
Ok(dir) => Ok(dir.into_fallback(format!("{}: {canonical_error}", canonical.display()))),
Err(fallback_error) => Err(StoreError {
canonical,
canonical_error,
fallback: fallback.to_path_buf(),
fallback_error,
}),
}
}
}
fn open_private_dir(path: &Path, recursive: bool) -> Result<SlotDir, String> {
let mut builder = std::fs::DirBuilder::new();
builder.recursive(recursive).mode(0o700);
match builder.create(path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(err) => return Err(err.to_string()),
}
let meta = if recursive {
std::fs::metadata(path)
} else {
std::fs::symlink_metadata(path)
}
.map_err(|err| err.to_string())?;
if !meta.is_dir() {
return Err("not a directory".to_string());
}
if meta.uid() != current_uid() {
return Err(format!(
"owned by uid {}, not this user's uid {}",
meta.uid(),
current_uid()
));
}
if meta.mode() & 0o022 != 0 {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).map_err(|err| {
format!(
"mode {:o} is group- or other-writable and could not be narrowed: {err}",
meta.mode() & 0o777
)
})?;
}
SlotDir::at(path).map_err(|err| err.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_broken_home_falls_back_to_the_fixed_per_uid_store() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(tmp.path().join(".trusty-mpm"), "x").expect("a file, not a dir");
let fallback = tmp.path().join("fixed");
let slots = SlotDir::resolve_in(Some(tmp.path()), &fallback).expect("fallback");
assert_eq!(slots.path(), fallback);
let why = slots.fallback_reason().expect("marked as the fallback");
assert!(why.contains(".trusty-mpm/build-slots"), "{why}");
let good = tempfile::tempdir().expect("tempdir");
let slots = SlotDir::resolve_in(Some(good.path()), &fallback).expect("canonical");
assert_eq!(slots.path(), canonical_path(good.path()));
assert!(slots.fallback_reason().is_none());
}
#[test]
fn a_store_error_names_both_paths_and_the_repair() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(tmp.path().join(".trusty-mpm"), "x").expect("file");
std::fs::write(tmp.path().join("blocker"), "x").expect("file");
let err = SlotDir::resolve_in(Some(tmp.path()), &tmp.path().join("blocker/sub"))
.expect_err("neither is usable");
let text = err.to_string();
for needle in [
".trusty-mpm/build-slots",
"blocker/sub",
"Repair:",
"chmod 700",
] {
assert!(text.contains(needle), "{needle}: {text}");
}
assert!(
SlotDir::resolve_in(None, &tmp.path().join("blocker/sub")).is_err(),
"no home never means ./"
);
}
#[test]
fn a_writable_by_others_store_is_narrowed() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("fallback");
std::fs::create_dir(&dir).expect("mkdir");
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o777)).expect("chmod");
std::fs::write(tmp.path().join(".trusty-mpm"), "x").expect("file");
SlotDir::resolve_in(Some(tmp.path()), &dir).expect("owned by us, so narrowed");
let mode = std::fs::metadata(&dir).expect("stat").permissions().mode();
assert_eq!(mode & 0o777, 0o700, "{mode:o}");
}
#[test]
fn the_fallback_path_ignores_tmpdir() {
let expected = PathBuf::from(format!("/tmp/trusty-mpm-build-slots-{}", current_uid()));
assert_eq!(fixed_fallback_path(), expected);
}
}