use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime};
pub fn restrict_to_owner(path: &Path) -> io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
}
#[cfg(windows)]
{
if let Err(e) = restrict_windows(path) {
tracing::warn!(
error = %e,
path = %path.display(),
"could not restrict file to its owner; it keeps the directory's inherited ACL"
);
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
{
let _ = path;
Ok(())
}
}
pub fn write_executable(path: &Path, body: &str) -> io::Result<()> {
write_atomic(path, body, 0o755)
}
pub fn write_readable(path: &Path, body: &str) -> io::Result<()> {
write_atomic(path, body, 0o644)
}
fn write_atomic(path: &Path, body: &str, mode: u32) -> io::Result<()> {
let (tmp, mut file) = open_temp_exclusive(path)?;
file.write_all(body.as_bytes())?;
file.sync_all()?;
drop(file);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) = std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(mode)) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
}
#[cfg(not(unix))]
let _ = mode;
if let Err(e) = std::fs::rename(&tmp, path) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
sync_parent(path);
Ok(())
}
fn open_temp_exclusive(path: &Path) -> io::Result<(PathBuf, std::fs::File)> {
let file_name = path.file_name().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("'{}' has no file name to write", path.display()),
)
})?;
let mut tmp_name = file_name.to_os_string();
tmp_name.push(".openlatch-tmp");
let tmp = path.with_file_name(tmp_name);
let open_exclusive = || {
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)
};
let file = match open_exclusive() {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
std::fs::remove_file(&tmp)?;
open_exclusive()?
}
Err(e) => return Err(e),
};
Ok((tmp, file))
}
fn sync_parent(path: &Path) {
#[cfg(unix)]
{
if let Some(parent) = path.parent() {
if let Ok(dir) = std::fs::File::open(parent) {
let _ = dir.sync_all();
}
}
}
#[cfg(not(unix))]
let _ = path;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileFingerprint {
size: u64,
modified: Option<SystemTime>,
identity: Option<(u64, u64)>,
digest: Option<[u8; 32]>,
}
impl FileFingerprint {
pub fn size(&self) -> u64 {
self.size
}
}
pub fn fingerprint(path: &Path) -> io::Result<FileFingerprint> {
let meta = std::fs::metadata(path)?;
#[cfg(unix)]
let identity = {
use std::os::unix::fs::MetadataExt;
Some((meta.dev(), meta.ino()))
};
#[cfg(not(unix))]
let identity = None;
#[cfg(unix)]
let digest = None;
#[cfg(not(unix))]
let digest = {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(std::fs::read(path)?);
Some(<[u8; 32]>::from(hasher.finalize()))
};
Ok(FileFingerprint {
size: meta.len(),
modified: meta.modified().ok(),
identity,
digest,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReplaceOutcome {
Replaced,
Contended,
}
pub fn replace_preserving(
path: &Path,
body: &[u8],
expected: &FileFingerprint,
) -> io::Result<ReplaceOutcome> {
replace_inner(path, body, Some(expected))
}
pub fn write_preserving_mode(path: &Path, body: &[u8]) -> io::Result<()> {
replace_inner(path, body, None).map(|_| ())
}
fn replace_inner(
path: &Path,
body: &[u8],
expected: Option<&FileFingerprint>,
) -> io::Result<ReplaceOutcome> {
let (tmp, mut file) = open_temp_exclusive(path)?;
let written = file.write_all(body).and_then(|()| file.sync_all());
drop(file);
let outcome = written.and_then(|()| commit_replace(path, &tmp, expected));
match outcome {
Ok(ReplaceOutcome::Replaced) => sync_parent(path),
_ => {
let _ = std::fs::remove_file(&tmp);
}
}
outcome
}
fn commit_replace(
path: &Path,
tmp: &Path,
expected: Option<&FileFingerprint>,
) -> io::Result<ReplaceOutcome> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(path) {
Ok(meta) => std::fs::set_permissions(
tmp,
std::fs::Permissions::from_mode(meta.permissions().mode() & 0o7777),
)?,
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
}
if let Some(expected) = expected {
match fingerprint(path) {
Ok(now) if now == *expected => {}
Ok(_) => return Ok(ReplaceOutcome::Contended),
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(ReplaceOutcome::Contended),
Err(e) => return Err(e),
}
}
std::fs::rename(tmp, path)?;
Ok(ReplaceOutcome::Replaced)
}
const LOCK_WAIT: Duration = Duration::from_secs(2);
pub fn with_lockfile<T>(
lock: &Path,
stale_after: Duration,
f: impl FnOnce() -> T,
) -> io::Result<T> {
let started = Instant::now();
loop {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(lock)
{
Ok(mut file) => {
let _ = write!(file, "{}", std::process::id());
drop(file);
let _held = LockGuard(lock);
return Ok(f());
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
let stale = std::fs::metadata(lock)
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.elapsed().ok())
.is_some_and(|age| age >= stale_after);
if stale {
let _ = std::fs::remove_file(lock);
} else if started.elapsed() >= LOCK_WAIT {
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
format!("'{}' is held by another process", lock.display()),
));
} else {
std::thread::sleep(Duration::from_millis(10));
}
}
Err(e) => return Err(e),
}
}
}
struct LockGuard<'a>(&'a Path);
impl Drop for LockGuard<'_> {
fn drop(&mut self) {
let _ = std::fs::remove_file(self.0);
}
}
#[cfg(windows)]
fn restrict_windows(path: &Path) -> io::Result<()> {
use std::process::{Command, Stdio};
const OWNER_RIGHTS_SID: &str = "*S-1-3-4:(F)";
let output = Command::new("icacls")
.arg(path)
.args(["/inheritance:r", "/grant:r", OWNER_RIGHTS_SID])
.stdin(Stdio::null())
.output()?;
if output.status.success() {
return Ok(());
}
let detail = String::from_utf8_lossy(&output.stdout);
Err(io::Error::other(format!(
"icacls failed to restrict '{}' to its owner ({}): {}",
path.display(),
output.status,
detail.trim()
)))
}
#[cfg(test)]
mod tests {
use super::*;
fn write_secret(dir: &Path, name: &str) -> std::path::PathBuf {
let path = dir.join(name);
std::fs::write(&path, b"secret").unwrap();
path
}
#[test]
fn restricting_keeps_the_file_readable_by_this_process() {
let dir = tempfile::tempdir().unwrap();
let path = write_secret(dir.path(), "token");
restrict_to_owner(&path).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"secret");
}
#[test]
#[cfg(unix)]
fn restricting_a_missing_file_is_an_error() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("nope");
assert!(restrict_to_owner(&missing).is_err());
}
#[test]
#[cfg(unix)]
fn unix_sets_mode_0600() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = write_secret(dir.path(), "token");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
restrict_to_owner(&path).unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "expected 0600, got {mode:o}");
}
#[test]
fn write_executable_writes_the_body() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("PreToolUse");
write_executable(&path, "#!/bin/sh\nprintf '{}'\n").unwrap();
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
"#!/bin/sh\nprintf '{}'\n"
);
}
#[test]
#[cfg(unix)]
fn write_executable_sets_mode_0755() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("PreToolUse");
write_executable(&path, "#!/bin/sh\n").unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o755, "expected 0755, got {mode:o}");
}
#[test]
fn write_executable_replaces_an_existing_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("PreToolUse");
std::fs::write(&path, "old").unwrap();
write_executable(&path, "new").unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "new");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o755);
}
}
#[test]
fn write_executable_leaves_no_temp_file_behind() {
let dir = tempfile::tempdir().unwrap();
for name in ["PreToolUse", "PreToolUse.ps1"] {
let path = dir.path().join(name);
write_executable(&path, "body").unwrap();
assert_eq!(std::fs::read_to_string(&path).unwrap(), "body");
}
let mut left: Vec<String> = std::fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
left.sort();
assert_eq!(
left,
vec!["PreToolUse".to_string(), "PreToolUse.ps1".to_string()],
"a temp file survived, or an extension was replaced instead of appended"
);
}
#[cfg(unix)]
#[test]
fn write_executable_never_follows_a_planted_temp_symlink() {
let dir = tempfile::tempdir().unwrap();
let victim = dir.path().join("victim-do-not-touch");
std::fs::write(&victim, "PRECIOUS").unwrap();
let path = dir.path().join("PreToolUse");
let tmp = dir.path().join("PreToolUse.openlatch-tmp");
std::os::unix::fs::symlink(&victim, &tmp).unwrap();
write_executable(&path, "shim body").expect("the write itself still succeeds");
assert_eq!(
std::fs::read_to_string(&victim).unwrap(),
"PRECIOUS",
"the planted symlink was followed and the victim file was overwritten"
);
assert_eq!(std::fs::read_to_string(&path).unwrap(), "shim body");
assert!(
!tmp.exists(),
"the temp path must not survive the write, symlink or not"
);
}
#[test]
fn write_executable_recovers_from_a_stale_temp_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("PreToolUse");
std::fs::write(dir.path().join("PreToolUse.openlatch-tmp"), "crashed run").unwrap();
write_executable(&path, "fresh body").expect("a stale temp file is recoverable");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "fresh body");
}
#[test]
fn replace_aborts_when_the_file_changed_under_it() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("globalState.json");
std::fs::write(&path, "{\"a\":1}").unwrap();
let seen = fingerprint(&path).unwrap();
let theirs = dir.path().join("editor-tmp");
std::fs::write(&theirs, "{\"a\":2}").unwrap();
std::fs::rename(&theirs, &path).unwrap();
let outcome = replace_preserving(&path, b"{\"ours\":true}", &seen).unwrap();
assert_eq!(outcome, ReplaceOutcome::Contended);
assert_eq!(std::fs::read_to_string(&path).unwrap(), "{\"a\":2}");
assert!(!dir.path().join("globalState.json.openlatch-tmp").exists());
let now = fingerprint(&path).unwrap();
assert_eq!(
replace_preserving(&path, b"{\"ours\":true}", &now).unwrap(),
ReplaceOutcome::Replaced
);
assert_eq!(std::fs::read_to_string(&path).unwrap(), "{\"ours\":true}");
}
#[test]
fn replace_treats_a_vanished_file_as_contended() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("providers.json");
std::fs::write(&path, "{}").unwrap();
let seen = fingerprint(&path).unwrap();
std::fs::remove_file(&path).unwrap();
assert_eq!(
replace_preserving(&path, b"{}", &seen).unwrap(),
ReplaceOutcome::Contended
);
assert!(!path.exists());
}
#[test]
#[cfg(unix)]
fn replace_preserves_mode_0600() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("providers.json");
std::fs::write(&path, "{}").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
let seen = fingerprint(&path).unwrap();
replace_preserving(&path, b"{\"x\":1}", &seen).unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "expected 0600, got {mode:o}");
write_preserving_mode(&path, b"{\"x\":2}").unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "expected 0600, got {mode:o}");
}
#[cfg(unix)]
#[test]
fn replace_never_follows_a_planted_temp_symlink() {
let dir = tempfile::tempdir().unwrap();
let victim = dir.path().join("victim-do-not-touch");
std::fs::write(&victim, "PRECIOUS").unwrap();
let path = dir.path().join("globalState.json");
std::fs::write(&path, "{}").unwrap();
std::os::unix::fs::symlink(&victim, dir.path().join("globalState.json.openlatch-tmp"))
.unwrap();
let seen = fingerprint(&path).unwrap();
replace_preserving(&path, b"{\"x\":1}", &seen).unwrap();
assert_eq!(std::fs::read_to_string(&victim).unwrap(), "PRECIOUS");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "{\"x\":1}");
}
#[test]
fn with_lockfile_excludes_a_second_holder() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("records.lock");
let inner = with_lockfile(&lock, Duration::from_secs(30), || {
assert!(lock.exists(), "the lock is held inside the section");
with_lockfile(&lock, Duration::from_secs(30), || ())
})
.unwrap();
let err = inner.expect_err("a second holder waits, then gives up");
assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
assert!(!lock.exists(), "the lock is released when the section ends");
}
#[test]
fn a_stale_lock_is_recovered() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("records.lock");
std::fs::write(&lock, "12345").unwrap();
let ran = with_lockfile(&lock, Duration::ZERO, || true).unwrap();
assert!(ran);
assert!(!lock.exists());
}
#[test]
fn write_executable_fails_when_the_directory_is_missing() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nope").join("PreToolUse");
assert!(write_executable(&path, "body").is_err());
}
#[test]
#[cfg(windows)]
fn windows_leaves_only_an_owner_ace() {
let dir = tempfile::tempdir().unwrap();
let path = write_secret(dir.path(), "token");
restrict_to_owner(&path).unwrap();
let out = std::process::Command::new("icacls")
.arg(&path)
.output()
.expect("icacls runs");
let acl = String::from_utf8_lossy(&out.stdout);
assert!(
acl.contains("OWNER RIGHTS") || acl.contains("S-1-3-4"),
"owner ACE missing from DACL: {acl}"
);
assert!(
!acl.contains("(I)"),
"inherited ACEs survived, secret may be readable by others: {acl}"
);
assert!(
!acl.contains("\\Users:") && !acl.contains("BUILTIN\\Users"),
"Users group still has access: {acl}"
);
}
}