use crate::host::Access;
use crate::service::Permit;
use std::fs::{File, OpenOptions};
use std::io;
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt};
use std::path::Path;
use std::time::SystemTime;
pub(crate) fn create_private_dir(_: Permit, path: &Path) -> io::Result<()> {
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(path)
}
pub(crate) fn create_private(_: Permit, path: &Path) -> io::Result<File> {
OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)
}
pub(crate) fn open_private_append(_: Permit, path: &Path) -> io::Result<File> {
OpenOptions::new()
.create(true)
.append(true)
.mode(0o600)
.open(path)
}
pub(crate) fn open_private_lock(_: Permit, path: &Path) -> io::Result<File> {
OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.mode(0o600)
.open(path)
}
pub(crate) fn copy_access(_: Permit, existing: &Path, temp: &Path) -> io::Result<()> {
match std::fs::symlink_metadata(existing) {
Ok(found) if !found.file_type().is_symlink() => {
let mode = found.permissions().mode() & 0o777;
std::fs::set_permissions(temp, std::fs::Permissions::from_mode(mode))
}
_ => Ok(()),
}
}
pub(crate) fn sync_dir(dir: &Path) {
let _ = File::open(dir).and_then(|d| d.sync_all());
}
pub(crate) fn touch_dir(_: Permit, path: &Path, at: SystemTime) -> io::Result<()> {
File::open(path)?.set_modified(at)
}
pub(crate) fn create_dir_all(_: Permit, path: &Path) -> io::Result<()> {
std::fs::create_dir_all(path)
}
pub(crate) fn create_dir(_: Permit, path: &Path) -> io::Result<()> {
std::fs::create_dir(path)
}
pub(crate) fn copy(_: Permit, from: &Path, to: &Path) -> io::Result<u64> {
std::fs::copy(from, to)
}
pub(crate) fn rename(_: Permit, from: &Path, to: &Path) -> io::Result<()> {
std::fs::rename(from, to)
}
pub(crate) fn remove_file(_: Permit, path: &Path) -> io::Result<()> {
std::fs::remove_file(path)
}
pub(crate) fn remove_dir(_: Permit, path: &Path) -> io::Result<()> {
std::fs::remove_dir(path)
}
pub(crate) fn remove_dir_all(_: Permit, path: &Path) -> io::Result<()> {
std::fs::remove_dir_all(path)
}
pub fn access(path: &Path) -> Option<Access> {
let mode = std::fs::metadata(path).ok()?.permissions().mode() & 0o777;
Some(Access {
shared: mode & 0o077 != 0,
described: format!("mode {mode:o}"),
})
}
#[cfg(any(test, feature = "test-support"))]
pub mod testing {
use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
fn set(path: &Path, mode: u32) -> io::Result<()> {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
}
pub fn open_to_others(path: &Path) -> io::Result<()> {
set(path, 0o644)
}
pub fn make_private(path: &Path) -> io::Result<()> {
set(path, private_mode(path)?)
}
pub fn is_private(path: &Path) -> io::Result<bool> {
let mode = std::fs::metadata(path)?.permissions().mode() & 0o777;
Ok(mode == private_mode(path)?)
}
fn private_mode(path: &Path) -> io::Result<u32> {
Ok(if std::fs::metadata(path)?.is_dir() {
0o700
} else {
0o600
})
}
pub fn read_only_for_owner(path: &Path) -> io::Result<()> {
set(path, 0o400)
}
pub fn deny_reading(path: &Path) -> io::Result<()> {
set(path, 0o000)
}
pub fn make_runnable(path: &Path) -> io::Result<()> {
set(path, 0o755)
}
pub fn deny_running(path: &Path) -> io::Result<()> {
set(path, 0o644)
}
pub fn deny_changes(dir: &Path) -> io::Result<()> {
set(dir, 0o555)
}
pub fn allow_changes(dir: &Path) -> io::Result<()> {
set(dir, 0o755)
}
pub fn link(target: &Path, at: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(target, at)
}
pub fn link_dir(target: &Path, at: &Path) -> io::Result<()> {
std::os::unix::fs::symlink(target, at)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"pitboard-host-fs-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
dir
}
fn mode(p: &Path) -> u32 {
std::fs::metadata(p).unwrap().permissions().mode() & 0o777
}
#[test]
fn created_directories_are_private_and_existing_ones_keep_their_mode() {
let scratch = scratch("dirs");
let permit = Permit::for_a_test();
create_private_dir(permit, &scratch.join("nested")).unwrap();
assert_eq!(
mode(&scratch),
0o700,
"a created parent must be private too"
);
assert_eq!(mode(&scratch.join("nested")), 0o700);
std::fs::set_permissions(&scratch, std::fs::Permissions::from_mode(0o755)).unwrap();
create_private_dir(permit, &scratch).unwrap();
assert_eq!(
mode(&scratch),
0o755,
"a directory the user named is theirs to set"
);
std::fs::remove_dir_all(&scratch).unwrap();
}
#[test]
fn a_file_created_private_is_private_however_the_umask_is_set() {
use std::io::Write;
let dir = scratch("file");
let permit = Permit::for_a_test();
create_private_dir(permit, &dir).unwrap();
let (fresh, history, lock) = (
dir.join(".state.json.1.0.pitboard"),
dir.join("history.jsonl"),
dir.join("state.lock"),
);
create_private(permit, &fresh).unwrap();
open_private_append(permit, &history).unwrap();
open_private_lock(permit, &lock).unwrap();
for made in [&fresh, &history, &lock] {
assert_eq!(
access(made),
Some(Access {
shared: false,
described: "mode 600".into()
}),
"{}",
made.display()
);
}
for there in [&history, &lock] {
std::fs::write(there, "one\n").unwrap();
testing::open_to_others(there).unwrap();
assert!(access(there).unwrap().shared);
}
writeln!(open_private_append(permit, &history).unwrap(), "two").unwrap();
open_private_lock(permit, &lock).unwrap();
assert_eq!(std::fs::read_to_string(&history).unwrap(), "one\ntwo\n");
assert_eq!(std::fs::read_to_string(&lock).unwrap(), "one\n");
for there in [&history, &lock] {
assert_eq!(
mode(there),
0o644,
"a file already there keeps its own mode"
);
}
assert!(create_private(permit, &fresh).is_err(), "a file is there");
let planted = dir.join(".usage.json.1.0.pitboard");
testing::link(&dir.join("elsewhere"), &planted).unwrap();
assert!(create_private(permit, &planted).is_err(), "a link is there");
assert!(
!dir.join("elsewhere").exists(),
"and nothing is written through it"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn each_way_a_test_sets_a_file_up_does_what_it_says() {
let dir = scratch("testing");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("file");
std::fs::write(&file, "").unwrap();
let gives = |set: fn(&Path) -> io::Result<()>, expected: u32| {
set(&file).unwrap();
assert_eq!(mode(&file), expected);
};
gives(testing::open_to_others, 0o644);
gives(testing::make_private, 0o600);
gives(testing::read_only_for_owner, 0o400);
gives(testing::deny_reading, 0o000);
gives(testing::make_runnable, 0o755);
gives(testing::deny_running, 0o644);
assert!(!testing::is_private(&file).unwrap());
testing::make_private(&file).unwrap();
assert!(testing::is_private(&file).unwrap());
let folder = dir.join("folder");
std::fs::create_dir(&folder).unwrap();
testing::make_private(&folder).unwrap();
assert_eq!(mode(&folder), 0o700, "a directory's own private mode");
assert!(testing::is_private(&folder).unwrap());
testing::deny_changes(&folder).unwrap();
assert_eq!(mode(&folder), 0o555);
testing::allow_changes(&folder).unwrap();
assert_eq!(mode(&folder), 0o755);
assert!(!testing::is_private(&folder).unwrap());
testing::link(Path::new("file"), &dir.join("to-file")).unwrap();
testing::link_dir(&folder, &dir.join("to-folder")).unwrap();
assert_eq!(
std::fs::read_link(dir.join("to-file")).unwrap(),
Path::new("file")
);
assert!(
dir.join("to-file").is_file(),
"read from the link's own directory"
);
assert!(dir.join("to-folder").is_dir());
std::fs::remove_dir_all(&dir).unwrap();
}
}