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