use std::fs;
use std::io::Write;
use std::path::Path;
use crate::error::Result;
pub fn write_file_restricted_new(path: &Path, data: &[u8]) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)?;
file.write_all(data)?;
}
#[cfg(not(unix))]
{
fs::write(path, data)?;
}
Ok(())
}
pub fn write_file_restricted(path: &Path, data: &[u8]) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)?;
file.write_all(data)?;
file.set_permissions(fs::Permissions::from_mode(0o600))?;
}
#[cfg(not(unix))]
{
fs::write(path, data)?;
}
Ok(())
}
pub fn refuse_symlink(path: &Path, action: &str) -> Result<()> {
if let Ok(meta) = fs::symlink_metadata(path)
&& meta.file_type().is_symlink()
{
return Err(crate::error::Error::Other(format!(
"refusing to {action} symlink: {}",
path.display()
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn write_file_restricted_downgrades_mode_on_existing_file() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("pre-existing.env");
fs::write(&target, b"initial").unwrap();
fs::set_permissions(&target, fs::Permissions::from_mode(0o644)).unwrap();
assert_eq!(
fs::metadata(&target).unwrap().permissions().mode() & 0o777,
0o644
);
write_file_restricted(&target, b"secret").unwrap();
let mode = fs::metadata(&target).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "existing file must be downgraded to 0o600");
assert_eq!(fs::read(&target).unwrap(), b"secret");
}
#[test]
fn refuse_symlink_error_phrasing() {
#[cfg(unix)]
{
let dir = tempfile::tempdir().unwrap();
let real = dir.path().join("real");
fs::write(&real, b"x").unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink(&real, &link).unwrap();
let err = refuse_symlink(&link, "apply").unwrap_err().to_string();
assert!(
err.contains("refusing to apply symlink:"),
"expected canonical phrasing, got: {err}"
);
}
}
}