util/
file.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8use std::fs::File;
9use std::fs::OpenOptions;
10use std::io;
11use std::path::Path;
12
13#[cfg(unix)]
14use once_cell::sync::Lazy;
15
16use crate::errors::IOContext;
17
18#[cfg(unix)]
19static UMASK: Lazy<u32> = Lazy::new(|| unsafe {
20    let umask = libc::umask(0);
21    libc::umask(umask);
22    #[allow(clippy::useless_conversion)] // mode_t is u16 on mac and u32 on linux
23    umask.into()
24});
25
26#[cfg(unix)]
27pub fn apply_umask(mode: u32) -> u32 {
28    mode & !*UMASK
29}
30
31pub fn atomic_write(path: &Path, op: impl FnOnce(&mut File) -> io::Result<()>) -> io::Result<File> {
32    atomicfile::atomic_write(path, 0o644, false, op).path_context("error atomic writing file", path)
33}
34
35/// Open a path for atomic writing.
36pub fn atomic_open(path: &Path) -> io::Result<atomicfile::AtomicFile> {
37    atomicfile::AtomicFile::open(path, 0o644, false).path_context("error atomic opening file", path)
38}
39
40pub fn open(path: impl AsRef<Path>, mode: &str) -> io::Result<File> {
41    let path = path.as_ref();
42
43    let mut opts = OpenOptions::new();
44    for opt in mode.chars() {
45        match opt {
46            'r' => opts.read(true),
47            'w' => opts.write(true),
48            'a' => opts.append(true),
49            'c' => opts.create(true),
50            't' => opts.truncate(true),
51            'x' => opts.create_new(true),
52            _ => {
53                return Err(io::Error::new(
54                    io::ErrorKind::Other,
55                    format!("invalid open() mode {}", opt),
56                ))
57                .path_context("error opening file", path);
58            }
59        };
60    }
61
62    opts.open(path).path_context("error opening file", path)
63}
64
65pub fn create(path: impl AsRef<Path>) -> io::Result<File> {
66    open(path, "wct")
67}
68
69pub fn exists(path: impl AsRef<Path>) -> io::Result<Option<std::fs::Metadata>> {
70    match std::fs::metadata(path.as_ref()) {
71        Ok(m) => Ok(Some(m)),
72        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
73        Err(err) => Err(err).path_context("error reading file", path.as_ref()),
74    }
75}
76
77pub fn unlink_if_exists(path: impl AsRef<Path>) -> io::Result<()> {
78    match std::fs::remove_file(path.as_ref()) {
79        Ok(()) => Ok(()),
80        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
81        Err(err) => Err(err).path_context("error deleting file", path.as_ref()),
82    }
83}
84
85pub fn read_to_string_if_exists(path: impl AsRef<Path>) -> io::Result<Option<String>> {
86    match std::fs::read_to_string(path.as_ref()) {
87        Ok(contents) => Ok(Some(contents)),
88        Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
89        Err(err) => Err(err).path_context("error reading file", path.as_ref()),
90    }
91}
92
93#[cfg(test)]
94mod test {
95    use anyhow::Result;
96    use tempfile::tempdir;
97
98    use super::*;
99
100    #[test]
101    fn test_open_context() -> Result<()> {
102        let dir = tempdir()?;
103
104        let path = dir.path().join("doesnt").join("exist");
105        let err_str = format!("{}", open(&path, "cwa").unwrap_err());
106
107        // Make sure error contains path.
108        assert!(err_str.contains(path.display().to_string().as_str()));
109
110        // And the original error.
111        let orig_err = format!("{}", std::fs::File::open(&path).unwrap_err());
112        assert!(err_str.contains(&orig_err));
113
114        Ok(())
115    }
116}