1pub mod traits {
24 #[cfg(doc)]
25 use std::{
26 fs, io,
27 path::{Path, PathBuf},
28 };
29
30
31 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
32 pub enum FileType {
33 File,
34 Dir,
35 Link,
36 }
37
38 pub trait Stat {
39 type Mode;
40 fn file_type(&self) -> Result<FileType, Self::Mode>;
41 }
42
43 pub trait DirectoryEntry {
44 type PathRef<'s>;
45 type OwnedPath;
46
47 fn name<'e>(&'e self) -> Self::PathRef<'e>;
48 fn owned_name(path: Self::PathRef<'_>) -> Self::OwnedPath;
49
50 type EagerMode;
53
54 fn eager_file_type(&self) -> Option<Result<FileType, Self::EagerMode>>;
56 }
57
58 pub trait DirectoryStream {
59 type DirEntry<'dir>: DirectoryEntry
61 where Self: 'dir;
62 type Err;
64
65 fn read_dir<'dir>(&'dir mut self) -> Result<Option<Self::DirEntry<'dir>>, Self::Err>;
66 }
67
68 pub trait VFS {
69 type Ctx<'vfs>
71 where Self: 'vfs;
72 type Err;
74
75 type PathRef<'s>;
78 type OwnedPath;
81 fn path_ref<'s>(p: &'s Self::OwnedPath) -> Self::PathRef<'s>;
82
83 fn initial_context<'vfs>(&'vfs self) -> Result<Self::Ctx<'vfs>, Self::Err>;
85 fn join_context_dir<'vfs>(
86 &'vfs self,
87 ctx: Self::Ctx<'vfs>,
88 rel: Self::PathRef<'_>,
89 ) -> Self::Ctx<'vfs>;
90 fn join_context_link<'vfs>(
91 &'vfs self,
92 ctx: Self::Ctx<'vfs>,
93 link_rel: Self::PathRef<'_>,
94 target: Self::PathRef<'_>,
95 ) -> Self::Ctx<'vfs>;
96
97 type Stat: Stat;
101
102 type File;
104 type FileOptions;
106
107 type Dir<'vfs>: DirectoryStream
109 where Self: 'vfs;
110 fn entry_rel<'vfs, 'dir, 's>(
111 name: <<<Self as VFS>::Dir<'vfs> as DirectoryStream>::DirEntry<'dir> as DirectoryEntry>::PathRef<'s>,
112 ) -> Self::PathRef<'s>;
113 fn entry_owned_rel<'vfs, 'dir>(
114 name: <<<Self as VFS>::Dir<'vfs> as DirectoryStream>::DirEntry<'dir> as DirectoryEntry>::OwnedPath,
115 ) -> Self::OwnedPath;
116
117 fn stat<'vfs>(
119 &'vfs self,
120 ctx: Self::Ctx<'vfs>,
121 rel: Self::PathRef<'_>,
122 ) -> Result<Self::Stat, Self::Err>;
123
124 fn read_link<'vfs>(
126 &'vfs self,
127 ctx: Self::Ctx<'vfs>,
128 rel: Self::PathRef<'_>,
129 ) -> Result<Self::OwnedPath, Self::Err>;
130
131 fn open_file<'vfs>(
136 &'vfs self,
137 ctx: Self::Ctx<'vfs>,
138 rel: Self::PathRef<'_>,
139 opts: Self::FileOptions,
140 ) -> Result<Self::File, Self::Err>;
141
142 fn open_dir<'vfs>(
144 &'vfs self,
145 ctx: Self::Ctx<'vfs>,
146 rel: Self::PathRef<'_>,
147 ) -> Result<Self::Dir<'vfs>, Self::Err>;
148 }
149}
150
151
152#[cfg(unix)]
153pub mod posix {}
154
155pub mod path_based {
156 use std::{
157 env, ffi, fs, io,
158 path::{Path, PathBuf},
159 };
160
161 use super::traits;
162
163 #[derive(Debug, Clone)]
164 #[repr(transparent)]
165 pub struct Stat(fs::Metadata);
166
167 impl traits::Stat for Stat {
168 type Mode = fs::FileType;
169 fn file_type(&self) -> Result<traits::FileType, Self::Mode> {
170 let ty = self.0.file_type();
171 if ty.is_symlink() {
172 return Ok(traits::FileType::Link);
173 }
174 if ty.is_file() {
175 return Ok(traits::FileType::File);
176 }
177 if ty.is_dir() {
178 return Ok(traits::FileType::Dir);
179 }
180 Err(ty)
181 }
182 }
183
184 cfg_if::cfg_if! {
185 if #[cfg(all(unix, feature = "nightly"))] {
186 #[derive(Debug)]
187 #[repr(transparent)]
188 pub struct DirectoryEntry(fs::DirEntry);
189
190 impl DirectoryEntry {
191 pub fn new(entry: fs::DirEntry) -> Self { Self(entry) }
192 }
193
194 impl traits::DirectoryEntry for DirectoryEntry {
195 type PathRef<'s> = &'s ffi::OsStr;
196 type OwnedPath = ffi::OsString;
197
198 fn name<'e>(&'e self) -> Self::PathRef<'e> {
199 use std::os::unix::fs::DirEntryExt2;
200 self.0.file_name_ref()
201 }
202 fn owned_name(path: Self::PathRef<'_>) -> Self::OwnedPath {
203 path.to_os_string()
204 }
205
206 type EagerMode = fs::FileType;
207
208 cfg_if::cfg_if! {
209 if #[cfg(any(
210 target_os = "solaris",
211 target_os = "illumos",
212 target_os = "haiku",
213 target_os = "vxworks",
214 target_os = "aix",
215 target_os = "nto",
216 target_os = "vita",
217 ))] {
218 fn eager_file_type(&self) -> Option<Result<traits::FileType, Self::EagerMode>> { None }
219 } else {
220 fn eager_file_type(&self) -> Option<Result<traits::FileType, Self::EagerMode>> {
221 let ty = self.0.file_type().unwrap();
222 if ty.is_symlink() {
223 return Some(Ok(traits::FileType::Link));
224 }
225 if ty.is_file() {
226 return Some(Ok(traits::FileType::File));
227 }
228 if ty.is_dir() {
229 return Some(Ok(traits::FileType::Dir));
230 }
231 Some(Err(ty))
232 }
233 }
234 }
235 }
236 } else {
237 #[derive(Debug)]
238 pub struct DirectoryEntry {
239 inner: fs::DirEntry,
240 name: ffi::OsString,
241 }
242
243 impl DirectoryEntry {
244 pub fn new(entry: fs::DirEntry) -> Self {
245 let name = entry.file_name();
246 Self {
247 inner: entry,
248 name,
249 }
250 }
251 }
252
253 impl traits::DirectoryEntry for DirectoryEntry {
254 type PathRef<'s> = &'s ffi::OsStr;
255 type OwnedPath = ffi::OsString;
256
257 fn name<'e>(&'e self) -> Self::PathRef<'e> {
258 &self.name
259 }
260 fn owned_name(path: Self::PathRef<'_>) -> Self::OwnedPath {
261 path.to_os_string()
262 }
263
264 type EagerMode = fs::FileType;
265
266 cfg_if::cfg_if! {
267 if #[cfg(any(
268 target_os = "solaris",
269 target_os = "illumos",
270 target_os = "haiku",
271 target_os = "vxworks",
272 target_os = "aix",
273 target_os = "nto",
274 target_os = "vita",
275 ))] {
276 fn eager_file_type(&self) -> Option<Result<traits::FileType, Self::EagerMode>> { None }
277 } else {
278 fn eager_file_type(&self) -> Option<Result<traits::FileType, Self::EagerMode>> {
279 let ty = self.inner.file_type().unwrap();
280 if ty.is_symlink() {
281 return Some(Ok(traits::FileType::Link));
282 }
283 if ty.is_file() {
284 return Some(Ok(traits::FileType::File));
285 }
286 if ty.is_dir() {
287 return Some(Ok(traits::FileType::Dir));
288 }
289 Some(Err(ty))
290 }
291 }
292 }
293 }
294 }
295 }
296
297 pub struct DirectoryStream {
298 inner: fs::ReadDir,
299 done: bool,
300 }
301
302 impl traits::DirectoryStream for DirectoryStream {
303 type DirEntry<'dir> = DirectoryEntry;
304 type Err = io::Error;
305
306 fn read_dir<'dir>(&'dir mut self) -> Result<Option<Self::DirEntry<'dir>>, Self::Err> {
307 if self.done {
308 return Ok(None);
309 }
310
311 loop {
312 use traits::DirectoryEntry as _;
313
314 let Some(result) = self.inner.next().transpose()? else {
315 self.done = true;
316 return Ok(None);
317 };
318 let entry = DirectoryEntry::new(result);
319 let name = entry.name();
320 if name == "." || name == ".." {
321 continue;
322 }
323 return Ok(Some(entry));
324 }
325 }
326 }
327
328 #[derive(Debug)]
329 pub struct VFS;
330
331 impl traits::VFS for VFS {
332 type Ctx<'vfs> = PathBuf;
333 type Err = io::Error;
334
335 type PathRef<'s> = &'s Path;
336 type OwnedPath = PathBuf;
337 fn path_ref<'s>(p: &'s Self::OwnedPath) -> Self::PathRef<'s> { p.as_ref() }
338
339 fn initial_context<'vfs>(&'vfs self) -> Result<Self::Ctx<'vfs>, Self::Err> {
340 env::current_dir()
341 }
342 fn join_context_dir<'vfs>(
343 &'vfs self,
344 ctx: Self::Ctx<'vfs>,
345 rel: Self::PathRef<'_>,
346 ) -> Self::Ctx<'vfs> {
347 ctx.join(rel)
348 }
349 fn join_context_link<'vfs>(
350 &'vfs self,
351 ctx: Self::Ctx<'vfs>,
352 _link_rel: Self::PathRef<'_>,
353 target: Self::PathRef<'_>,
354 ) -> Self::Ctx<'vfs> {
355 ctx.join(target)
356 }
357
358 type Stat = Stat;
359
360 type File = fs::File;
361 type FileOptions = fs::OpenOptions;
362
363 type Dir<'vfs> = DirectoryStream;
364 fn entry_rel<'vfs, 'dir, 's>(
365 name: <<<Self as traits::VFS>::Dir<'vfs> as traits::DirectoryStream>::DirEntry<'dir> as traits::DirectoryEntry>::PathRef<'s>,
366 ) -> Self::PathRef<'s> {
367 name.as_ref()
368 }
369 fn entry_owned_rel<'vfs, 'dir>(
370 name: <<<Self as traits::VFS>::Dir<'vfs> as traits::DirectoryStream>::DirEntry<'dir> as traits::DirectoryEntry>::OwnedPath,
371 ) -> Self::OwnedPath {
372 name.into()
373 }
374
375 fn stat<'vfs>(
376 &'vfs self,
377 ctx: Self::Ctx<'vfs>,
378 rel: Self::PathRef<'_>,
379 ) -> Result<Self::Stat, Self::Err> {
380 fs::symlink_metadata(ctx.join(rel)).map(Stat)
381 }
382
383 fn read_link<'vfs>(
384 &'vfs self,
385 ctx: Self::Ctx<'vfs>,
386 rel: Self::PathRef<'_>,
387 ) -> Result<Self::OwnedPath, Self::Err> {
388 fs::read_link(ctx.join(rel))
389 }
390
391 fn open_file<'vfs>(
392 &'vfs self,
393 ctx: Self::Ctx<'vfs>,
394 rel: Self::PathRef<'_>,
395 opts: Self::FileOptions,
396 ) -> Result<Self::File, Self::Err> {
397 opts.open(ctx.join(rel))
398 }
399
400 fn open_dir<'vfs>(
402 &'vfs self,
403 ctx: Self::Ctx<'vfs>,
404 rel: Self::PathRef<'_>,
405 ) -> Result<Self::Dir<'vfs>, Self::Err> {
406 fs::read_dir(ctx.join(rel)).map(|inner| DirectoryStream { inner, done: false })
407 }
408 }
409
410 #[cfg(test)]
411 mod test {
412 use tempdir::TempDir;
413
414 use super::*;
415
416
417 #[test]
418 fn file() -> io::Result<()> {
419 let td = TempDir::new("asdf")?;
420 fs::write(td.path().join("f.txt"), "asdf\n")?;
421
422 use traits::VFS as _;
423 let vfs = VFS;
424 let ctx = vfs.initial_context()?;
425 let ctx = vfs.join_context_dir(ctx, td.path());
426
427 use traits::Stat as _;
428 let stat = vfs.stat(ctx.clone(), Path::new("f.txt"))?;
429 assert_eq!(stat.file_type(), Ok(traits::FileType::File));
430
431 let mut opts = fs::OpenOptions::new();
432 opts.read(true);
433 let mut f = vfs.open_file(ctx, Path::new("f.txt"), opts)?;
434
435 use io::Read;
436 let mut s = String::new();
437 f.read_to_string(&mut s)?;
438 assert_eq!(s, "asdf\n");
439
440 Ok(())
441 }
442
443 #[test]
444 fn link() -> io::Result<()> {
445 let td = TempDir::new("asdf")?;
446 cfg_if::cfg_if! {
447 if #[cfg(unix)] {
448 std::os::unix::fs::symlink("wow.txt", td.path().join("l.txt"))?;
449 } else {
450 std::os::windows::fs::symlink_file("wow.txt", td.path().join("l.txt"))?;
451 }
452 }
453
454 use traits::VFS as _;
455 let vfs = VFS;
456 let ctx = vfs.initial_context()?;
457 let ctx = vfs.join_context_dir(ctx, td.path());
458
459 use traits::Stat as _;
460 let stat = vfs.stat(ctx.clone(), Path::new("l.txt"))?;
461 assert_eq!(stat.file_type(), Ok(traits::FileType::Link));
462
463 let target = vfs.read_link(ctx, Path::new("l.txt"))?;
464 assert_eq!(&target, Path::new("wow.txt"));
465
466 Ok(())
467 }
468
469 #[test]
470 fn dir() -> io::Result<()> {
471 let td = TempDir::new("asdf")?;
472 fs::write(td.path().join("f.txt"), "asdf\n")?;
473 fs::create_dir(td.path().join("a"))?;
474 fs::write(td.path().join("a/g.txt"), "asdf2\n")?;
475 use traits::VFS as _;
478 let vfs = VFS;
479 let ctx = vfs.initial_context()?;
480
481 use traits::Stat as _;
482 let stat = vfs.stat(ctx.clone(), td.path())?;
483 assert_eq!(stat.file_type(), Ok(traits::FileType::Dir));
484
485 let mut opts = fs::OpenOptions::new();
486 opts.read(true);
487
488 use io::Read;
489 use traits::{DirectoryEntry as _, DirectoryStream as _, Stat as _};
490 let mut dir = vfs.open_dir(ctx.clone(), td.path())?;
491 let ctx = vfs.join_context_dir(ctx, td.path());
492 while let Some(entry) = dir.read_dir()? {
493 let ty = entry
494 .eager_file_type()
495 .map(|r| r.unwrap())
496 .unwrap_or_else(|| {
497 vfs
498 .stat(ctx.clone(), VFS::entry_rel(entry.name()))
499 .unwrap()
500 .file_type()
501 .unwrap()
502 });
503 match VFS::path_ref(&VFS::entry_owned_rel(DirectoryEntry::owned_name(
504 entry.name(),
505 )))
506 .to_str()
507 .unwrap()
508 {
509 "f.txt" => {
510 assert_eq!(ty, traits::FileType::File);
511 let mut f = vfs.open_file(ctx.clone(), VFS::entry_rel(entry.name()), opts.clone())?;
512 let mut s = String::new();
513 f.read_to_string(&mut s)?;
514 assert_eq!(s, "asdf\n");
515 },
516 "a" => {
517 assert_eq!(ty, traits::FileType::Dir);
518
519 let mut dir = vfs.open_dir(ctx.clone(), VFS::entry_rel(entry.name()))?;
520 let ctx = vfs.join_context_dir(ctx.clone(), VFS::entry_rel(entry.name()));
521 while let Some(entry) = dir.read_dir()? {
522 let ty = entry
523 .eager_file_type()
524 .map(|r| r.unwrap())
525 .unwrap_or_else(|| {
526 vfs
527 .stat(ctx.clone(), VFS::entry_rel(entry.name()))
528 .unwrap()
529 .file_type()
530 .unwrap()
531 });
532 match DirectoryEntry::owned_name(entry.name()).to_str().unwrap() {
533 "g.txt" => {
534 assert_eq!(ty, traits::FileType::File);
535 let mut f =
536 vfs.open_file(ctx.clone(), VFS::entry_rel(entry.name()), opts.clone())?;
537 let mut s = String::new();
538 f.read_to_string(&mut s)?;
539 assert_eq!(s, "asdf2\n");
540 },
541 _ => unreachable!(),
542 }
543 }
544 },
545 _ => unreachable!(),
546 }
547 }
548
549 Ok(())
550 }
551 }
552}