Skip to main content

kaizen/core/
safe_fs.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! File opens that never follow a final symlink on supported platforms.
3
4use std::fs::{File, OpenOptions};
5use std::io::{self, Write};
6use std::path::Path;
7
8pub fn create_new(path: &Path) -> io::Result<File> {
9    let mut options = OpenOptions::new();
10    options.write(true).create_new(true);
11    no_follow(&mut options);
12    options.open(path)
13}
14
15pub fn append(path: &Path) -> io::Result<File> {
16    reject_alias(path)?;
17    let mut options = OpenOptions::new();
18    options.create(true).append(true);
19    no_follow(&mut options);
20    options.open(path)
21}
22
23pub fn read_write(path: &Path) -> io::Result<File> {
24    reject_alias(path)?;
25    let mut options = OpenOptions::new();
26    options.create(true).read(true).write(true);
27    no_follow(&mut options);
28    options.open(path)
29}
30
31pub fn write_atomic(path: &Path, content: &[u8]) -> io::Result<()> {
32    reject_alias(path)?;
33    let parent = path
34        .parent()
35        .ok_or_else(|| io::Error::other("path has no parent"))?;
36    std::fs::create_dir_all(parent)?;
37    let mut tmp = tempfile::NamedTempFile::new_in(parent)?;
38    tmp.write_all(content)?;
39    tmp.as_file().sync_all().ok();
40    tmp.persist(path).map_err(|error| error.error)?;
41    Ok(())
42}
43
44pub fn reject_alias(path: &Path) -> io::Result<()> {
45    if std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
46        return Err(io::Error::new(
47            io::ErrorKind::PermissionDenied,
48            "refusing to write through symlink",
49        ));
50    }
51    reject_hardlink(path)
52}
53
54#[cfg(unix)]
55pub fn reject_hardlink(path: &Path) -> io::Result<()> {
56    use std::os::unix::fs::MetadataExt;
57    match std::fs::symlink_metadata(path) {
58        Ok(metadata) if metadata.is_file() && metadata.nlink() > 1 => Err(io::Error::new(
59            io::ErrorKind::PermissionDenied,
60            "refusing to write through hard link",
61        )),
62        Ok(_) => Ok(()),
63        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
64        Err(error) => Err(error),
65    }
66}
67
68#[cfg(not(unix))]
69pub fn reject_hardlink(_path: &Path) -> io::Result<()> {
70    Ok(())
71}
72
73#[cfg(unix)]
74fn no_follow(options: &mut OpenOptions) {
75    use std::os::unix::fs::OpenOptionsExt;
76    options.custom_flags(libc::O_NOFOLLOW);
77}
78
79#[cfg(not(unix))]
80fn no_follow(_options: &mut OpenOptions) {}