1use 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, Read, Seek, SeekFrom, Write},
9 path::{Component, Path, PathBuf},
10};
11
12#[cfg(unix)]
13use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
14
15pub type Result<T> = std::result::Result<T, FsPathError>;
17
18pub fn create_file(path: impl AsRef<Path>) -> Result<fs::File> {
20 let path = path.as_ref();
21 File::create(path).map_err(|err| FsPathError::create_file(err, path))
22}
23
24pub fn remove_file(path: impl AsRef<Path>) -> Result<()> {
26 let path = path.as_ref();
27 fs::remove_file(path).map_err(|err| FsPathError::remove_file(err, path))
28}
29
30pub fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
32 let path = path.as_ref();
33 fs::read(path).map_err(|err| FsPathError::read(err, path))
34}
35
36pub fn read_link(path: impl AsRef<Path>) -> Result<PathBuf> {
38 let path = path.as_ref();
39 fs::read_link(path).map_err(|err| FsPathError::read_link(err, path))
40}
41
42pub fn read_to_string(path: impl AsRef<Path>) -> Result<String> {
44 let path = path.as_ref();
45 fs::read_to_string(path).map_err(|err| FsPathError::read(err, path))
46}
47
48pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
50 let s = read_to_string(path)?;
53 serde_json::from_str(&s).map_err(|source| FsPathError::ReadJson { source, path: path.into() })
54}
55
56pub fn read_json_gzip_file<T: DeserializeOwned>(path: &Path) -> Result<T> {
58 let file = open(path)?;
59 let reader = BufReader::new(file);
60 let decoder = GzDecoder::new(reader);
61 serde_json::from_reader(decoder)
62 .map_err(|source| FsPathError::ReadJson { source, path: path.into() })
63}
64
65pub fn locked_read_to_string(path: impl AsRef<Path>) -> Result<String> {
67 let path = path.as_ref();
68 let contents = locked_read(path)?;
69 String::from_utf8(contents).map_err(|err| FsPathError::read(std::io::Error::other(err), path))
70}
71
72pub fn locked_read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
74 let path = path.as_ref();
75 let mut file =
76 fs::OpenOptions::new().read(true).open(path).map_err(|err| FsPathError::open(err, path))?;
77 file.lock_shared().map_err(|err| FsPathError::lock(err, path))?;
78 let contents = read_inner(path, &mut file)?;
79 file.unlock().map_err(|err| FsPathError::unlock(err, path))?;
80 Ok(contents)
81}
82
83fn read_inner(path: &Path, file: &mut File) -> Result<Vec<u8>> {
84 let file_len = file.metadata().map_err(|err| FsPathError::open(err, path))?.len() as usize;
85 let mut buffer = Vec::with_capacity(file_len);
86 file.read_to_end(&mut buffer).map_err(|err| FsPathError::read(err, path))?;
87 Ok(buffer)
88}
89
90pub fn write_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
92 let file = create_file(path)?;
93 let mut writer = BufWriter::new(file);
94 serde_json::to_writer(&mut writer, obj)
95 .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
96 writer.flush().map_err(|e| FsPathError::write(e, path))
97}
98
99pub fn write_pretty_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
101 write_pretty_json(path, obj, create_file(path)?)
102}
103
104pub fn write_sensitive_json_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
106 let mut options = File::options();
107 options.write(true).create(true).truncate(false);
109 #[cfg(unix)]
110 options.mode(0o600);
111
112 let file = options.open(path).map_err(|err| FsPathError::create_file(err, path))?;
113 #[cfg(unix)]
114 file.set_permissions(fs::Permissions::from_mode(0o600))
115 .map_err(|err| FsPathError::write(err, path))?;
116 file.set_len(0).map_err(|err| FsPathError::write(err, path))?;
117
118 write_pretty_json(path, obj, file)
119}
120
121fn write_pretty_json<T: Serialize>(path: &Path, obj: &T, file: File) -> Result<()> {
122 let mut writer = BufWriter::new(file);
123 serde_json::to_writer_pretty(&mut writer, obj)
124 .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
125 writer.flush().map_err(|e| FsPathError::write(e, path))
126}
127
128pub fn write_json_gzip_file<T: Serialize>(path: &Path, obj: &T) -> Result<()> {
130 let file = create_file(path)?;
131 let writer = BufWriter::new(file);
132 let mut encoder = GzEncoder::new(writer, Compression::default());
133 serde_json::to_writer(&mut encoder, obj)
134 .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?;
135 let mut inner_writer = encoder.finish().map_err(|e| FsPathError::write(e, path))?;
137 inner_writer.flush().map_err(|e| FsPathError::write(e, path))?;
138 Ok(())
139}
140
141pub fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
143 let path = path.as_ref();
144 fs::write(path, contents).map_err(|err| FsPathError::write(err, path))
145}
146
147pub fn locked_write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
149 let path = path.as_ref();
150 let mut file = fs::OpenOptions::new()
151 .write(true)
152 .create(true)
153 .truncate(false)
154 .open(path)
155 .map_err(|err| FsPathError::open(err, path))?;
156 file.lock().map_err(|err| FsPathError::lock(err, path))?;
157 file.set_len(0).map_err(|err| FsPathError::write(err, path))?;
158 file.write_all(contents.as_ref()).map_err(|err| FsPathError::write(err, path))?;
159 file.unlock().map_err(|err| FsPathError::unlock(err, path))
160}
161
162pub fn locked_write_line(path: impl AsRef<Path>, line: &str) -> Result<()> {
164 let path = path.as_ref();
165 if cfg!(windows) {
166 return locked_write_line_windows(path, line);
167 }
168
169 let mut file = std::fs::OpenOptions::new()
170 .append(true)
171 .create(true)
172 .open(path)
173 .map_err(|err| FsPathError::open(err, path))?;
174
175 file.lock().map_err(|err| FsPathError::lock(err, path))?;
176 writeln!(file, "{line}").map_err(|err| FsPathError::write(err, path))?;
177 file.unlock().map_err(|err| FsPathError::unlock(err, path))
178}
179
180fn locked_write_line_windows(path: &Path, line: &str) -> Result<()> {
182 let mut file = std::fs::OpenOptions::new()
183 .write(true)
184 .truncate(false)
185 .create(true)
186 .open(path)
187 .map_err(|err| FsPathError::open(err, path))?;
188 file.lock().map_err(|err| FsPathError::lock(err, path))?;
189
190 file.seek(SeekFrom::End(0)).map_err(|err| FsPathError::write(err, path))?;
191 writeln!(file, "{line}").map_err(|err| FsPathError::write(err, path))?;
192
193 file.unlock().map_err(|err| FsPathError::unlock(err, path))
194}
195
196pub fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<u64> {
198 let from = from.as_ref();
199 let to = to.as_ref();
200 fs::copy(from, to).map_err(|err| FsPathError::copy(err, from, to))
201}
202
203pub fn create_dir(path: impl AsRef<Path>) -> Result<()> {
205 let path = path.as_ref();
206 fs::create_dir(path).map_err(|err| FsPathError::create_dir(err, path))
207}
208
209pub fn create_dir_all(path: impl AsRef<Path>) -> Result<()> {
211 let path = path.as_ref();
212 fs::create_dir_all(path).map_err(|err| FsPathError::create_dir(err, path))
213}
214
215pub fn remove_dir(path: impl AsRef<Path>) -> Result<()> {
217 let path = path.as_ref();
218 fs::remove_dir(path).map_err(|err| FsPathError::remove_dir(err, path))
219}
220
221pub fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
223 let path = path.as_ref();
224 fs::remove_dir_all(path).map_err(|err| FsPathError::remove_dir(err, path))
225}
226
227pub fn open(path: impl AsRef<Path>) -> Result<fs::File> {
229 let path = path.as_ref();
230 fs::File::open(path).map_err(|err| FsPathError::open(err, path))
231}
232
233pub fn normalize_path(path: &Path) -> PathBuf {
240 let mut components = path.components().peekable();
241 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
242 components.next();
243 PathBuf::from(c.as_os_str())
244 } else {
245 PathBuf::new()
246 };
247
248 for component in components {
249 match component {
250 Component::Prefix(..) => unreachable!(),
251 Component::RootDir => {
252 ret.push(component.as_os_str());
253 }
254 Component::CurDir => {}
255 Component::ParentDir => {
256 ret.pop();
257 }
258 Component::Normal(c) => {
259 ret.push(c);
260 }
261 }
262 }
263 ret
264}
265
266pub fn files_with_ext<'a>(root: &Path, ext: &'a str) -> impl Iterator<Item = PathBuf> + 'a {
268 walkdir::WalkDir::new(root)
269 .sort_by_file_name()
270 .into_iter()
271 .filter_map(walkdir::Result::ok)
272 .filter(|e| e.file_type().is_file() && e.path().extension() == Some(ext.as_ref()))
273 .map(walkdir::DirEntry::into_path)
274}
275
276pub fn json_files(root: &Path) -> impl Iterator<Item = PathBuf> {
278 files_with_ext(root, "json")
279}
280
281pub fn canonicalize_path(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
286 dunce::canonicalize(path)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use std::{sync::mpsc, thread, time::Duration};
293
294 #[cfg(unix)]
295 #[test]
296 fn test_write_sensitive_json_file_permissions() {
297 let dir = tempfile::tempdir().unwrap();
298 for name in ["new", "existing"] {
299 let path = dir.path().join(name);
300 if name == "existing" {
301 fs::write(&path, []).unwrap();
302 fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
303 }
304
305 write_sensitive_json_file(&path, &()).unwrap();
306 assert_eq!(fs::metadata(path).unwrap().permissions().mode() & 0o777, 0o600);
307 }
308 }
309
310 #[test]
311 fn test_normalize_path() {
312 let p = Path::new("/a/../file.txt");
313 let normalized = normalize_path(p);
314 assert_eq!(normalized, PathBuf::from("/file.txt"));
315 }
316
317 #[test]
318 fn test_locked_write_waits_before_truncating() {
319 let dir = tempfile::tempdir().unwrap();
320 let path = dir.path().join("locked.txt");
321 fs::write(&path, b"original contents").unwrap();
322 let mut reader = File::open(&path).unwrap();
323 reader.lock_shared().unwrap();
324
325 let (started_tx, started_rx) = mpsc::channel();
326 let (finished_tx, finished_rx) = mpsc::channel();
327 let writer_path = path.clone();
328 let writer = thread::spawn(move || {
329 started_tx.send(()).unwrap();
330 let result = locked_write(writer_path, b"new");
331 finished_tx.send(()).unwrap();
332 result
333 });
334 started_rx.recv().unwrap();
335 let pending = finished_rx.recv_timeout(Duration::from_millis(100));
337 let mut contents = Vec::new();
338 let read_result = reader.read_to_end(&mut contents);
339 let unlock_result = reader.unlock();
340 drop(reader);
341 writer.join().unwrap().unwrap();
342
343 unlock_result.unwrap();
344 assert_eq!(pending, Err(mpsc::RecvTimeoutError::Timeout));
345 read_result.unwrap();
346 assert_eq!(contents, b"original contents");
347 assert_eq!(fs::read(&path).unwrap(), b"new");
348 }
349}