Skip to main content

a7n_platform/
fs.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3//! File helpers for secrets and trust data.
4
5use std::fs::{self, File, OpenOptions};
6use std::io::{self, Write};
7use std::path::{Path, PathBuf};
8
9/// Per-user daemon log path.
10///
11/// # Errors
12/// Returns an error if the user data directory cannot be found.
13pub fn log_path() -> io::Result<PathBuf> {
14    directories::ProjectDirs::from("", "", "apeirogon")
15        .map(|dirs| dirs.data_local_dir().join("apeirogond.log"))
16        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no home directory found"))
17}
18
19/// Opens an append-only log with user-only permissions.
20///
21/// # Errors
22/// Returns any directory, permission or file I/O error.
23pub fn append_user_only(path: &Path) -> io::Result<File> {
24    if let Some(parent) = path.parent() {
25        fs::create_dir_all(parent)?;
26    }
27    let mut options = OpenOptions::new();
28    options.create(true).append(true);
29    #[cfg(unix)]
30    {
31        use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
32        options.mode(USER_ONLY_MODE);
33        let file = options.open(path)?;
34        file.set_permissions(fs::Permissions::from_mode(USER_ONLY_MODE))?;
35        Ok(file)
36    }
37    #[cfg(not(unix))]
38    options.open(path)
39}
40
41/// Permission bits of a user-only file on unix.
42#[cfg(unix)]
43const USER_ONLY_MODE: u32 = 0o600;
44
45/// Writes `bytes` to `path` atomically: a temp file in the same directory is
46/// created user-only, synced, then renamed over `path`.
47///
48/// # Errors
49/// Returns any I/O error from creating, writing or renaming the file.
50pub fn write_user_only(path: &Path, bytes: &[u8]) -> io::Result<()> {
51    let mut temp_name = path.file_name().unwrap_or_default().to_os_string();
52    temp_name.push(".tmp");
53    let temp_path = path.with_file_name(temp_name);
54
55    let result = write_temp(&temp_path, bytes).and_then(|()| fs::rename(&temp_path, path));
56    if result.is_err() {
57        // Best effort: the original error is the one worth reporting.
58        let _ = fs::remove_file(&temp_path);
59    }
60    result
61}
62
63fn write_temp(temp_path: &Path, bytes: &[u8]) -> io::Result<()> {
64    let mut file = create_user_only(temp_path)?;
65    file.write_all(bytes)?;
66    file.sync_all()
67}
68
69#[cfg(unix)]
70fn create_user_only(path: &Path) -> io::Result<File> {
71    use std::os::unix::fs::OpenOptionsExt;
72    OpenOptions::new()
73        .write(true)
74        .create(true)
75        .truncate(true)
76        .mode(USER_ONLY_MODE)
77        .open(path)
78}
79
80// ponytail: no explicit ACL; the per-user data directory is already user-only
81// by its inherited ACL. Tighten if the key ever moves outside the profile.
82#[cfg(not(unix))]
83fn create_user_only(path: &Path) -> io::Result<File> {
84    OpenOptions::new()
85        .write(true)
86        .create(true)
87        .truncate(true)
88        .open(path)
89}
90
91#[cfg(all(test, unix))]
92mod tests {
93    use super::*;
94    use std::os::unix::fs::PermissionsExt;
95
96    #[test]
97    fn write_user_only_new_file_is_user_only() {
98        // Arrange
99        let dir = tempfile::tempdir().unwrap();
100        let path = dir.path().join("secret");
101
102        // Act
103        write_user_only(&path, b"key").unwrap();
104
105        // Assert
106        let mode = fs::metadata(&path).unwrap().permissions().mode();
107        assert_eq!(mode & 0o777, USER_ONLY_MODE);
108    }
109}
110
111/// Compares open file identities, including across rename and rotation.
112///
113/// # Errors
114/// Returns any metadata query error.
115#[cfg(unix)]
116pub fn same_file(left: &File, right: &File) -> io::Result<bool> {
117    use std::os::unix::fs::MetadataExt;
118    let (left, right) = (left.metadata()?, right.metadata()?);
119    Ok(left.dev() == right.dev() && left.ino() == right.ino())
120}
121
122/// Compares open file identities, including across rename and rotation.
123///
124/// # Errors
125/// Returns any metadata query error.
126#[cfg(windows)]
127pub fn same_file(left: &File, right: &File) -> io::Result<bool> {
128    use std::os::windows::io::AsRawHandle;
129    use windows_sys::Win32::Storage::FileSystem::{
130        BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
131    };
132    fn identity(file: &File) -> io::Result<(u32, u32, u32)> {
133        let mut info = BY_HANDLE_FILE_INFORMATION::default();
134        // SAFETY: file owns a live handle and info is writable for the call.
135        if unsafe { GetFileInformationByHandle(file.as_raw_handle().cast(), &raw mut info) } == 0 {
136            return Err(io::Error::last_os_error());
137        }
138        Ok((
139            info.dwVolumeSerialNumber,
140            info.nFileIndexHigh,
141            info.nFileIndexLow,
142        ))
143    }
144    Ok(identity(left)? == identity(right)?)
145}