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 serde::{de::DeserializeOwned, Serialize};
5use std::{
6    fs::{self, File},
7    io::{BufWriter, Write},
8    path::{Component, Path, PathBuf},
9};
10
11/// The [`fs`](self) result type.
12pub type Result<T> = std::result::Result<T, FsPathError>;
13
14/// Wrapper for [`File::create`].
15pub fn create_file(path: impl AsRef<Path>) -> Result<fs::File> {
16    let path = path.as_ref();
17    File::create(path).map_err(|err| FsPathError::create_file(err, path))
18}
19
20/// Wrapper for [`std::fs::remove_file`].
21pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
22    let path = path.as_ref();
23    fs::remove_file(path).map_err(|err| FsPathError::remove_file(err, path))
24}
25
26/// Wrapper for [`std::fs::read`].
27pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
28    let path = path.as_ref();
29    fs::read(path).map_err(|err| FsPathError::read(err, path))
30}
31
32/// Wrapper for [`std::fs::read_link`].
33pub fn read_link(path: impl AsRef<Path>) -> Result<PathBuf> {
34    let path = path.as_ref();
35    fs::read_link(path).map_err(|err| FsPathError::read_link(err, path))
36}
37
38/// Wrapper for [`std::fs::read_to_string`].
39pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
40    let path = path.as_ref();
41    fs::read_to_string(path).map_err(|err| FsPathError::read(err, path))
42}
43
44/// Reads the JSON file and deserialize it into the provided type.
45pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
46    // read the file into a byte array first
47    // https://github.com/serde-rs/json/issues/160
48    let s = read_to_string(path)?;
49    serde_json::from_str(&s).map_err(|source| FsPathError::ReadJson { source, path: path.into() })
50}
51
52/// Writes the object as a JSON object.
53pub fn write_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
54    let file = create_file(path)?;
55    let mut writer = BufWriter::new(file);
56    serde_json::to_writer(&mut writer, obj)
57        .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
58    writer.flush().map_err(|e| FsPathError::write(e, path))
59}
60
61/// Writes the object as a pretty JSON object.
62pub fn write_pretty_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
63    let file = create_file(path)?;
64    let mut writer = BufWriter::new(file);
65    serde_json::to_writer_pretty(&mut writer, obj)
66        .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
67    writer.flush().map_err(|e| FsPathError::write(e, path))
68}
69
70/// Wrapper for `std::fs::write`
71pub fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
72    let path = path.as_ref();
73    fs::write(path, contents).map_err(|err| FsPathError::write(err, path))
74}
75
76/// Wrapper for `std::fs::copy`
77pub fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<u64> {
78    let from = from.as_ref();
79    let to = to.as_ref();
80    fs::copy(from, to).map_err(|err| FsPathError::copy(err, from, to))
81}
82
83/// Wrapper for `std::fs::create_dir`
84pub fn create_dir(path: impl AsRef<Path>) -> Result<()> {
85    let path = path.as_ref();
86    fs::create_dir(path).map_err(|err| FsPathError::create_dir(err, path))
87}
88
89/// Wrapper for `std::fs::create_dir_all`
90pub fn create_dir_all(path: impl AsRef<Path>) -> Result<()> {
91    let path = path.as_ref();
92    fs::create_dir_all(path).map_err(|err| FsPathError::create_dir(err, path))
93}
94
95/// Wrapper for `std::fs::remove_dir`
96pub fn remove_dir(path: impl AsRef<Path>) -> Result<()> {
97    let path = path.as_ref();
98    fs::remove_dir(path).map_err(|err| FsPathError::remove_dir(err, path))
99}
100
101/// Wrapper for `std::fs::remove_dir_all`
102pub fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
103    let path = path.as_ref();
104    fs::remove_dir_all(path).map_err(|err| FsPathError::remove_dir(err, path))
105}
106
107/// Wrapper for `std::fs::File::open`
108pub fn open(path: impl AsRef<Path>) -> Result<fs::File> {
109    let path = path.as_ref();
110    fs::File::open(path).map_err(|err| FsPathError::open(err, path))
111}
112
113/// Normalize a path, removing things like `.` and `..`.
114///
115/// NOTE: This does not return symlinks and does not touch the filesystem at all (unlike
116/// [`std::fs::canonicalize`])
117///
118/// ref: <https://github.com/rust-lang/cargo/blob/9ded34a558a900563b0acf3730e223c649cf859d/crates/cargo-util/src/paths.rs#L81>
119pub fn normalize_path(path: &Path) -> PathBuf {
120    let mut components = path.components().peekable();
121    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
122        components.next();
123        PathBuf::from(c.as_os_str())
124    } else {
125        PathBuf::new()
126    };
127
128    for component in components {
129        match component {
130            Component::Prefix(..) => unreachable!(),
131            Component::RootDir => {
132                ret.push(component.as_os_str());
133            }
134            Component::CurDir => {}
135            Component::ParentDir => {
136                ret.pop();
137            }
138            Component::Normal(c) => {
139                ret.push(c);
140            }
141        }
142    }
143    ret
144}
145
146/// Returns an iterator over all files with the given extension under the `root` dir.
147pub fn files_with_ext<'a>(root: &Path, ext: &'a str) -> impl Iterator<Item = PathBuf> + 'a {
148    walkdir::WalkDir::new(root)
149        .sort_by_file_name()
150        .into_iter()
151        .filter_map(walkdir::Result::ok)
152        .filter(|e| e.file_type().is_file() && e.path().extension() == Some(ext.as_ref()))
153        .map(walkdir::DirEntry::into_path)
154}
155
156/// Returns an iterator over all JSON files under the `root` dir.
157pub fn json_files(root: &Path) -> impl Iterator<Item = PathBuf> {
158    files_with_ext(root, "json")
159}
160
161/// Canonicalize a path, returning an error if the path does not exist.
162///
163/// Mainly useful to apply canonicalization to paths obtained from project files but still error
164/// properly instead of flattening the errors.
165pub fn canonicalize_path(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
166    dunce::canonicalize(path)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_normalize_path() {
175        let p = Path::new("/a/../file.txt");
176        let normalized = normalize_path(p);
177        assert_eq!(normalized, PathBuf::from("/file.txt"));
178    }
179}