Skip to main content

foundry_common/
fs.rs

1//! Contains various `std::fs` wrapper functions that also contain the target path in their errors.
2
3use crate::errors::FsPathError;
4use flate2::{Compression, read::GzDecoder, write::GzEncoder};
5use serde::{Serialize, de::DeserializeOwned};
6#[cfg(unix)]
7use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
8use std::{
9    fs::{self, File},
10    io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
11    path::{Component, Path, PathBuf},
12};
13
14/// The [`fs`](self) result type.
15pub type Result<T> = std::result::Result<T, FsPathError>;
16
17/// Wrapper for [`File::create`].
18pub fn create_file(path: impl AsRef<Path>) -> Result<fs::File> {
19    let path = path.as_ref();
20    File::create(path).map_err(|err| FsPathError::create_file(err, path))
21}
22
23/// Wrapper for [`std::fs::remove_file`].
24pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
25    let path = path.as_ref();
26    fs::remove_file(path).map_err(|err| FsPathError::remove_file(err, path))
27}
28
29/// Wrapper for [`std::fs::read`].
30pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
31    let path = path.as_ref();
32    fs::read(path).map_err(|err| FsPathError::read(err, path))
33}
34
35/// Wrapper for [`std::fs::read_link`].
36pub fn read_link(path: impl AsRef<Path>) -> Result<PathBuf> {
37    let path = path.as_ref();
38    fs::read_link(path).map_err(|err| FsPathError::read_link(err, path))
39}
40
41/// Wrapper for [`std::fs::read_to_string`].
42pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
43    let path = path.as_ref();
44    fs::read_to_string(path).map_err(|err| FsPathError::read(err, path))
45}
46
47/// Reads the JSON file and deserialize it into the provided type.
48pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
49    // read the file into a byte array first
50    // https://github.com/serde-rs/json/issues/160
51    let s = read_to_string(path)?;
52    serde_json::from_str(&s).map_err(|source| FsPathError::ReadJson { source, path: path.into() })
53}
54
55/// Reads and decodes the json gzip file, then deserialize it into the provided type.
56pub fn read_json_gzip_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
57    let file = open(path)?;
58    let reader = BufReader::new(file);
59    let decoder = GzDecoder::new(reader);
60    serde_json::from_reader(decoder)
61        .map_err(|source| FsPathError::ReadJson { source, path: path.into() })
62}
63
64/// Reads the entire contents of a locked shared file into a string.
65pub fn locked_read_to_string(path: impl AsRef<Path>) -> Result<String> {
66    let path = path.as_ref();
67    let contents = locked_read(path)?;
68    String::from_utf8(contents).map_err(|err| FsPathError::read(std::io::Error::other(err), path))
69}
70
71/// Reads the entire contents of a locked shared file into a bytes vector.
72pub fn locked_read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
73    let path = path.as_ref();
74    let mut file =
75        fs::OpenOptions::new().read(true).open(path).map_err(|err| FsPathError::open(err, path))?;
76    file.lock_shared().map_err(|err| FsPathError::lock(err, path))?;
77    let contents = read_inner(path, &mut file)?;
78    file.unlock().map_err(|err| FsPathError::unlock(err, path))?;
79    Ok(contents)
80}
81
82fn read_inner(path: &Path, file: &mut File) -> Result<Vec<u8>> {
83    let file_len = file.metadata().map_err(|err| FsPathError::open(err, path))?.len() as usize;
84    let mut buffer = Vec::with_capacity(file_len);
85    file.read_to_end(&mut buffer).map_err(|err| FsPathError::read(err, path))?;
86    Ok(buffer)
87}
88
89/// Writes the object as a JSON object.
90pub fn write_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
91    let file = create_file(path)?;
92    let mut writer = BufWriter::new(file);
93    serde_json::to_writer(&mut writer, obj)
94        .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
95    writer.flush().map_err(|e| FsPathError::write(e, path))
96}
97
98/// Writes the object as a pretty JSON object.
99pub fn write_pretty_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
100    write_pretty_json(path, obj, create_file(path)?)
101}
102
103/// Writes an object as pretty JSON with owner-only permissions on Unix.
104pub fn write_sensitive_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
105    let mut options = File::options();
106    options.write(true).create(true).truncate(true);
107    #[cfg(unix)]
108    options.mode(0o600);
109
110    let file = options.open(path).map_err(|err| FsPathError::create_file(err, path))?;
111    #[cfg(unix)]
112    file.set_permissions(fs::Permissions::from_mode(0o600))
113        .map_err(|err| FsPathError::write(err, path))?;
114
115    write_pretty_json(path, obj, file)
116}
117
118fn write_pretty_json<T: Serialize>(path: &Path, obj: &T, file: File) -> Result<()> {
119    let mut writer = BufWriter::new(file);
120    serde_json::to_writer_pretty(&mut writer, obj)
121        .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
122    writer.flush().map_err(|e| FsPathError::write(e, path))
123}
124
125/// Writes the object as a gzip compressed file.
126pub fn write_json_gzip_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
127    let file = create_file(path)?;
128    let writer = BufWriter::new(file);
129    let mut encoder = GzEncoder::new(writer, Compression::default());
130    serde_json::to_writer(&mut encoder, obj)
131        .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
132    // Ensure we surface any I/O errors on final gzip write and buffer flush.
133    let mut inner_writer = encoder.finish().map_err(|e| FsPathError::write(e, path))?;
134    inner_writer.flush().map_err(|e| FsPathError::write(e, path))?;
135    Ok(())
136}
137
138/// Wrapper for `std::fs::write`
139pub fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
140    let path = path.as_ref();
141    fs::write(path, contents).map_err(|err| FsPathError::write(err, path))
142}
143
144/// Writes all content in an exclusive locked file.
145pub fn locked_write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
146    let path = path.as_ref();
147    let mut file = fs::OpenOptions::new()
148        .write(true)
149        .create(true)
150        .truncate(true)
151        .open(path)
152        .map_err(|err| FsPathError::open(err, path))?;
153    file.lock().map_err(|err| FsPathError::lock(err, path))?;
154    file.write_all(contents.as_ref()).map_err(|err| FsPathError::write(err, path))?;
155    file.unlock().map_err(|err| FsPathError::unlock(err, path))
156}
157
158/// Writes a line in an exclusive locked file.
159pub fn locked_write_line(path: impl AsRef<Path>, line: &str) -> Result<()> {
160    let path = path.as_ref();
161    if cfg!(windows) {
162        return locked_write_line_windows(path, line);
163    }
164
165    let mut file = std::fs::OpenOptions::new()
166        .append(true)
167        .create(true)
168        .open(path)
169        .map_err(|err| FsPathError::open(err, path))?;
170
171    file.lock().map_err(|err| FsPathError::lock(err, path))?;
172    writeln!(file, "{line}").map_err(|err| FsPathError::write(err, path))?;
173    file.unlock().map_err(|err| FsPathError::unlock(err, path))
174}
175
176// Locking fails on Windows if the file is opened in append mode.
177fn locked_write_line_windows(path: &Path, line: &str) -> Result<()> {
178    let mut file = std::fs::OpenOptions::new()
179        .write(true)
180        .truncate(false)
181        .create(true)
182        .open(path)
183        .map_err(|err| FsPathError::open(err, path))?;
184    file.lock().map_err(|err| FsPathError::lock(err, path))?;
185
186    file.seek(SeekFrom::End(0)).map_err(|err| FsPathError::write(err, path))?;
187    writeln!(file, "{line}").map_err(|err| FsPathError::write(err, path))?;
188
189    file.unlock().map_err(|err| FsPathError::unlock(err, path))
190}
191
192/// Wrapper for `std::fs::copy`
193pub fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<u64> {
194    let from = from.as_ref();
195    let to = to.as_ref();
196    fs::copy(from, to).map_err(|err| FsPathError::copy(err, from, to))
197}
198
199/// Wrapper for `std::fs::create_dir`
200pub fn create_dir(path: impl AsRef<Path>) -> Result<()> {
201    let path = path.as_ref();
202    fs::create_dir(path).map_err(|err| FsPathError::create_dir(err, path))
203}
204
205/// Wrapper for `std::fs::create_dir_all`
206pub fn create_dir_all(path: impl AsRef<Path>) -> Result<()> {
207    let path = path.as_ref();
208    fs::create_dir_all(path).map_err(|err| FsPathError::create_dir(err, path))
209}
210
211/// Wrapper for `std::fs::remove_dir`
212pub fn remove_dir(path: impl AsRef<Path>) -> Result<()> {
213    let path = path.as_ref();
214    fs::remove_dir(path).map_err(|err| FsPathError::remove_dir(err, path))
215}
216
217/// Wrapper for `std::fs::remove_dir_all`
218pub fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
219    let path = path.as_ref();
220    fs::remove_dir_all(path).map_err(|err| FsPathError::remove_dir(err, path))
221}
222
223/// Wrapper for `std::fs::File::open`
224pub fn open(path: impl AsRef<Path>) -> Result<fs::File> {
225    let path = path.as_ref();
226    fs::File::open(path).map_err(|err| FsPathError::open(err, path))
227}
228
229/// Normalize a path, removing things like `.` and `..`.
230///
231/// NOTE: This does not return symlinks and does not touch the filesystem at all (unlike
232/// [`std::fs::canonicalize`])
233///
234/// ref: <https://github.com/rust-lang/cargo/blob/9ded34a558a900563b0acf3730e223c649cf859d/crates/cargo-util/src/paths.rs#L81>
235pub fn normalize_path(path: &Path) -> PathBuf {
236    let mut components = path.components().peekable();
237    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
238        components.next();
239        PathBuf::from(c.as_os_str())
240    } else {
241        PathBuf::new()
242    };
243
244    for component in components {
245        match component {
246            Component::Prefix(..) => unreachable!(),
247            Component::RootDir => {
248                ret.push(component.as_os_str());
249            }
250            Component::CurDir => {}
251            Component::ParentDir => {
252                ret.pop();
253            }
254            Component::Normal(c) => {
255                ret.push(c);
256            }
257        }
258    }
259    ret
260}
261
262/// Returns an iterator over all files with the given extension under the `root` dir.
263pub fn files_with_ext<'a>(root: &Path, ext: &'a str) -> impl Iterator<Item = PathBuf> + 'a {
264    walkdir::WalkDir::new(root)
265        .sort_by_file_name()
266        .into_iter()
267        .filter_map(walkdir::Result::ok)
268        .filter(|e| e.file_type().is_file() && e.path().extension() == Some(ext.as_ref()))
269        .map(walkdir::DirEntry::into_path)
270}
271
272/// Returns an iterator over all JSON files under the `root` dir.
273pub fn json_files(root: &Path) -> impl Iterator<Item = PathBuf> {
274    files_with_ext(root, "json")
275}
276
277/// Canonicalize a path, returning an error if the path does not exist.
278///
279/// Mainly useful to apply canonicalization to paths obtained from project files but still error
280/// properly instead of flattening the errors.
281pub fn canonicalize_path(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
282    dunce::canonicalize(path)
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[cfg(unix)]
290    #[test]
291    fn test_write_sensitive_json_file_permissions() {
292        let dir = tempfile::tempdir().unwrap();
293        for name in ["new", "existing"] {
294            let path = dir.path().join(name);
295            if name == "existing" {
296                fs::write(&path, []).unwrap();
297                fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
298            }
299
300            write_sensitive_json_file(&path, &()).unwrap();
301            assert_eq!(fs::metadata(path).unwrap().permissions().mode() & 0o777, 0o600);
302        }
303    }
304
305    #[test]
306    fn test_normalize_path() {
307        let p = Path::new("/a/../file.txt");
308        let normalized = normalize_path(p);
309        assert_eq!(normalized, PathBuf::from("/file.txt"));
310    }
311}