1use 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
14pub type Result<T> = std::result::Result<T, FsPathError>;
16
17pub 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
23pub 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
29pub 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
35pub 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
41pub 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
47pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
49 let s = read_to_string(path)?;
52 serde_json::from_str(&s).map_err(|source| FsPathError::ReadJson { source, path: path.into() })
53}
54
55pub 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
64pub 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
71pub 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
89pub 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
98pub fn write_pretty_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
100 write_pretty_json(path, obj, create_file(path)?)
101}
102
103pub 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
125pub 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 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
138pub 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
144pub 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
158pub 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
176fn 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
192pub 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
199pub 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
205pub 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
211pub 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
217pub 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
223pub 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
229pub 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
262pub 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
272pub fn json_files(root: &Path) -> impl Iterator<Item = PathBuf> {
274 files_with_ext(root, "json")
275}
276
277pub 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}