use std::env;
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
pub fn non_empty_env_path(var: &str) -> Option<PathBuf> {
env::var(var)
.ok()
.map(|v| v.trim().to_string())
.filter(|s| !s.is_empty())
.map(PathBuf::from)
}
pub fn default_data_dir() -> PathBuf {
if let Some(srz_data) = non_empty_env_path("SRZ_DATA_DIR") {
return srz_data;
}
if let Some(xdg_data) = non_empty_env_path("XDG_DATA_HOME") {
return xdg_data.join("srz");
}
#[cfg(target_os = "windows")]
{
if let Some(local_app_data) = non_empty_env_path("LOCALAPPDATA") {
return local_app_data.join("srz").join("data");
}
}
if let Some(home) = non_empty_env_path("HOME") {
return home.join(".local").join("share").join("srz");
}
if let Some(user_profile) = non_empty_env_path("USERPROFILE") {
return user_profile.join(".local").join("share").join("srz");
}
std::env::temp_dir().join("srz-data")
}
pub fn set_private_dir_perms(path: &Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o700));
}
#[cfg(not(unix))]
let _ = path;
}
pub fn set_private_file_perms(path: &Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(path, fs::Permissions::from_mode(0o600));
}
#[cfg(not(unix))]
let _ = path;
}
pub fn create_private_dir_all(path: &Path) -> std::io::Result<()> {
if let Ok(meta) = fs::symlink_metadata(path)
&& meta.file_type().is_symlink()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"Refusing to create directory at symlink path: {}",
path.display()
),
));
}
fs::create_dir_all(path)?;
set_private_dir_perms(path);
Ok(())
}
pub fn open_private_file_for_write(path: &Path) -> std::io::Result<File> {
if let Ok(meta) = fs::symlink_metadata(path)
&& meta.file_type().is_symlink()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Refusing to open symlink for writing: {}", path.display()),
));
}
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let file = opts.open(path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = file.set_permissions(fs::Permissions::from_mode(0o600));
}
Ok(file)
}
pub fn write_private_file(path: &Path, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
if let Ok(meta) = fs::symlink_metadata(path)
&& meta.file_type().is_symlink()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Refusing to write to symlink: {}", path.display()),
));
}
let parent = path.parent().unwrap_or_else(|| Path::new("."));
if !parent.as_os_str().is_empty() && !parent.exists() {
create_private_dir_all(parent)?;
}
let mut temp = tempfile::Builder::new()
.prefix(".srz-tmp-")
.tempfile_in(parent)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = temp
.as_file()
.set_permissions(fs::Permissions::from_mode(0o600));
}
temp.write_all(contents.as_ref())?;
temp.flush()?;
temp.persist(path).map_err(|e| e.error)?;
set_private_file_perms(path);
Ok(())
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn test_non_empty_env_path() {
assert_eq!(non_empty_env_path("SRZ_NON_EXISTENT_VAR_XYZ"), None);
}
#[test]
fn test_default_data_dir() {
let dir = default_data_dir();
assert!(!dir.as_os_str().is_empty());
}
#[test]
fn test_create_and_write_private_file() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("subdir");
create_private_dir_all(&dir).unwrap();
assert!(dir.is_dir());
let file_path = dir.join("test_write.txt");
write_private_file(&file_path, b"test private content").unwrap();
let read_back = fs::read(&file_path).unwrap();
assert_eq!(read_back, b"test private content");
}
#[cfg(unix)]
#[test]
fn test_write_private_file_permissions() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
let file_path = tmp.path().join("test.txt");
write_private_file(&file_path, b"test private content").unwrap();
let meta = fs::metadata(&file_path).unwrap();
assert_eq!(meta.permissions().mode() & 0o777, 0o600);
}
#[cfg(unix)]
#[test]
fn test_fs_util_rejects_symlinks() {
use std::os::unix::fs::symlink;
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let target = dir.join("real_target.txt");
fs::write(&target, b"important data").unwrap();
let link = dir.join("symlink_file.txt");
symlink(&target, &link).unwrap();
let write_res = open_private_file_for_write(&link);
assert!(write_res.is_err());
assert_eq!(
write_res.err().unwrap().kind(),
std::io::ErrorKind::InvalidInput
);
let atomic_res = write_private_file(&link, b"hijack attempt");
assert!(atomic_res.is_err());
assert_eq!(
atomic_res.err().unwrap().kind(),
std::io::ErrorKind::InvalidInput
);
let link_dir = dir.join("symlink_dir");
symlink(&target, &link_dir).unwrap();
let dir_res = create_private_dir_all(&link_dir);
assert!(dir_res.is_err());
assert_eq!(
dir_res.err().unwrap().kind(),
std::io::ErrorKind::InvalidInput
);
}
}