fs/
adapter.rs

1// SPDX-FileCopyrightText: 2025 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::io::{Read, Seek, Write};
5
6use server::{CheckedPermissions, MessageAllowed};
7
8use crate::{messages::*, DirEntry, Error, FileSystem, Location, Metadata, OpenFlags};
9
10/// Marker trait that bundles all basic filesystem permissions.
11/// Corresponds to the `fs-generic` permission template in `permission_templates.toml`.
12pub trait BasicFsPermissions:
13    CheckedPermissions
14    + MessageAllowed<OpenDirMessage>
15    + MessageAllowed<OpenFileMessage>
16    + MessageAllowed<CloseFile>
17    + MessageAllowed<CloseDir>
18    + MessageAllowed<CreateDirMessage>
19    + MessageAllowed<ReadFile>
20    + MessageAllowed<SeekFile>
21    + MessageAllowed<WriteFile>
22    + MessageAllowed<TruncateFile>
23    + MessageAllowed<SetLen>
24    + MessageAllowed<GetMetadata>
25    + MessageAllowed<NextEntry>
26    + MessageAllowed<Flush>
27    + MessageAllowed<FlushFs>
28    + MessageAllowed<Remove>
29    + MessageAllowed<Rename>
30    + MessageAllowed<AtomicCopy>
31    + MessageAllowed<AsyncRead>
32    + MessageAllowed<AsyncWrite>
33    + MessageAllowed<AsyncCopyBlock>
34    + MessageAllowed<SubscribeFilesystemEvent>
35{
36}
37
38impl<P> BasicFsPermissions for P where
39    P: CheckedPermissions
40        + MessageAllowed<OpenDirMessage>
41        + MessageAllowed<OpenFileMessage>
42        + MessageAllowed<CloseFile>
43        + MessageAllowed<CloseDir>
44        + MessageAllowed<CreateDirMessage>
45        + MessageAllowed<ReadFile>
46        + MessageAllowed<SeekFile>
47        + MessageAllowed<WriteFile>
48        + MessageAllowed<TruncateFile>
49        + MessageAllowed<SetLen>
50        + MessageAllowed<GetMetadata>
51        + MessageAllowed<NextEntry>
52        + MessageAllowed<Flush>
53        + MessageAllowed<FlushFs>
54        + MessageAllowed<Remove>
55        + MessageAllowed<Rename>
56        + MessageAllowed<AtomicCopy>
57        + MessageAllowed<AsyncRead>
58        + MessageAllowed<AsyncWrite>
59        + MessageAllowed<AsyncCopyBlock>
60        + MessageAllowed<SubscribeFilesystemEvent>
61{
62}
63
64/// Abstraction over filesystem operations for testing and generic code.
65///
66/// - [`FileSystem`]: actual keyos fs server
67/// - `FsTest`: uses temporary directories (test-only)
68pub trait FsAdapter {
69    type File: FileAdapter<Self::Permissions>;
70    type Permissions: CheckedPermissions;
71    type DirIter: Iterator<Item = Result<DirEntry, Error>>;
72
73    fn create_dir(&self, path: &str, location: Location) -> Result<(), Error>
74    where
75        Self::Permissions: MessageAllowed<CreateDirMessage>,
76        Self::Permissions: MessageAllowed<CloseDir>;
77
78    fn remove(&self, path: &str, location: Location) -> Result<(), Error>
79    where
80        Self::Permissions: MessageAllowed<Remove>;
81
82    fn atomic_copy(
83        &self,
84        src: &str,
85        dest: &str,
86        rename: Option<String>,
87        location: Location,
88    ) -> Result<(), Error>
89    where
90        Self::Permissions: MessageAllowed<AtomicCopy>;
91
92    fn open_file(&self, path: &str, location: Location, flags: OpenFlags) -> Result<Self::File, Error>
93    where
94        Self::Permissions: MessageAllowed<OpenFileMessage>,
95        Self::Permissions: MessageAllowed<CloseFile>;
96
97    fn open_dir(&self, path: &str, location: Location) -> Result<Self::DirIter, Error>
98    where
99        Self::Permissions: MessageAllowed<OpenDirMessage>,
100        Self::Permissions: MessageAllowed<CloseDir>,
101        Self::Permissions: MessageAllowed<NextEntry>;
102
103    fn metadata(&self, path: &str, location: Location) -> Result<Metadata, Error>
104    where
105        Self::Permissions: MessageAllowed<GetMetadata>;
106
107    fn rename(&self, src: &str, dest: &str, location: Location) -> Result<(), Error>
108    where
109        Self::Permissions: MessageAllowed<Rename>;
110
111    fn flush(&mut self, location: Location) -> Result<(), Error>
112    where
113        Self::Permissions: MessageAllowed<FlushFs>;
114
115    fn walk_dir(&self, path: &str, location: Location) -> Result<DirWalker<Self>, Error>
116    where
117        Self: Clone,
118        Self::Permissions: MessageAllowed<OpenDirMessage>,
119        Self::Permissions: MessageAllowed<CloseDir>,
120        Self::Permissions: MessageAllowed<NextEntry>,
121    {
122        DirWalker::new(self.clone(), path, location)
123    }
124
125    fn ensure_parent_dir_exists(&self, path: &str, location: Location) -> Result<(), Error>
126    where
127        Self::Permissions: MessageAllowed<CreateDirMessage>,
128        Self::Permissions: MessageAllowed<CloseDir>,
129    {
130        crate::ensure_parent_dir_exists_impl(|dir| self.create_dir(dir, location), path)
131    }
132
133    fn remove_if_exists(&self, path: &str, location: Location) -> Result<(), Error>
134    where
135        Self::Permissions: MessageAllowed<Remove>,
136    {
137        match self.remove(path, location) {
138            Ok(_) => Ok(()),
139            Err(Error::FileNotFound) => Ok(()),
140            Err(e) => Err(e),
141        }
142    }
143}
144
145impl<P> FsAdapter for FileSystem<P>
146where
147    P: CheckedPermissions
148        + MessageAllowed<CloseFile>
149        + MessageAllowed<CloseDir>
150        + MessageAllowed<NextEntry>
151        + MessageAllowed<ReadFile>
152        + MessageAllowed<WriteFile>
153        + MessageAllowed<Flush>
154        + MessageAllowed<SeekFile>,
155{
156    type DirIter = DirIterator<P>;
157    type File = crate::File<P>;
158    type Permissions = P;
159
160    fn create_dir(&self, path: &str, location: Location) -> Result<(), Error>
161    where
162        P: MessageAllowed<CreateDirMessage>,
163        P: MessageAllowed<CloseDir>,
164    {
165        Ok(self.create_dir(path, location).map(|_| ())?)
166    }
167
168    fn remove(&self, path: &str, location: Location) -> Result<(), Error>
169    where
170        P: MessageAllowed<Remove>,
171    {
172        Ok(self.remove(path, location)?)
173    }
174
175    fn atomic_copy(
176        &self,
177        src: &str,
178        dest: &str,
179        rename: Option<String>,
180        location: Location,
181    ) -> Result<(), Error>
182    where
183        P: MessageAllowed<AtomicCopy>,
184    {
185        Ok(self.atomic_copy(src, dest, rename, location)?)
186    }
187
188    fn open_file(&self, path: &str, location: Location, flags: OpenFlags) -> Result<Self::File, Error>
189    where
190        P: MessageAllowed<OpenFileMessage>,
191        P: MessageAllowed<CloseFile>,
192    {
193        Ok(self.open_file(path, location, flags)?)
194    }
195
196    fn open_dir(&self, path: &str, location: Location) -> Result<Self::DirIter, Error>
197    where
198        P: MessageAllowed<OpenDirMessage>,
199        P: MessageAllowed<CloseDir>,
200        P: MessageAllowed<NextEntry>,
201    {
202        let dir = self.open_dir(path, location)?;
203        Ok(DirIterator { dir })
204    }
205
206    fn metadata(&self, path: &str, location: Location) -> Result<Metadata, Error>
207    where
208        Self::Permissions: MessageAllowed<GetMetadata>,
209    {
210        Ok(self.metadata(path, location)?)
211    }
212
213    fn rename(&self, src: &str, dest: &str, location: Location) -> Result<(), Error>
214    where
215        P: MessageAllowed<Rename>,
216    {
217        Ok(self.rename(src, dest, location)?)
218    }
219
220    fn flush(&mut self, location: Location) -> Result<(), Error>
221    where
222        P: MessageAllowed<FlushFs>,
223    {
224        Ok(FileSystem::flush(self, location)?)
225    }
226}
227
228pub trait FileAdapter<P: CheckedPermissions>: Read + Write + Seek {
229    fn metadata(&self) -> Result<Metadata, Error>
230    where
231        P: MessageAllowed<GetMetadata>;
232
233    fn truncate(&mut self) -> Result<(), Error>
234    where
235        P: MessageAllowed<TruncateFile>;
236
237    fn set_mtime(&mut self, datetime: crate::DateTime) -> Result<(), Error>
238    where
239        P: MessageAllowed<SetMtime>;
240
241    fn copy_block_to(&mut self, to: &mut Self, len: usize) -> Result<usize, Error>
242    where
243        P: MessageAllowed<AsyncCopyBlock>;
244}
245
246impl<P> FileAdapter<P> for crate::File<P>
247where
248    P: CheckedPermissions
249        + MessageAllowed<CloseFile>
250        + MessageAllowed<ReadFile>
251        + MessageAllowed<WriteFile>
252        + MessageAllowed<Flush>
253        + MessageAllowed<SeekFile>,
254{
255    fn metadata(&self) -> Result<Metadata, Error>
256    where
257        P: MessageAllowed<GetMetadata>,
258    {
259        self.metadata()
260    }
261
262    fn truncate(&mut self) -> Result<(), Error>
263    where
264        P: MessageAllowed<TruncateFile>,
265    {
266        self.truncate()
267    }
268
269    fn set_mtime(&mut self, datetime: crate::DateTime) -> Result<(), Error>
270    where
271        P: MessageAllowed<SetMtime>,
272    {
273        self.set_mtime(datetime)
274    }
275
276    fn copy_block_to(&mut self, to: &mut Self, len: usize) -> Result<usize, Error>
277    where
278        P: MessageAllowed<AsyncCopyBlock>,
279    {
280        self.copy_block_to(to, len)
281    }
282}
283
284pub struct DirIterator<P: CheckedPermissions + MessageAllowed<CloseDir>> {
285    dir: crate::Dir<P>,
286}
287
288impl<P: CheckedPermissions + MessageAllowed<CloseDir>> Iterator for DirIterator<P>
289where
290    P: MessageAllowed<NextEntry>,
291{
292    type Item = Result<DirEntry, Error>;
293
294    fn next(&mut self) -> Option<Self::Item> {
295        match self.dir.next_entry() {
296            Ok(Some(entry)) => Some(Ok(entry)),
297            Ok(None) => None,
298            Err(e) => Some(Err(e)),
299        }
300    }
301}
302
303pub struct DirWalker<F>
304where
305    F: FsAdapter,
306    F::Permissions: MessageAllowed<CloseDir> + MessageAllowed<OpenDirMessage> + MessageAllowed<NextEntry>,
307{
308    fs: F,
309    /// current directory being iterated
310    current_iter: Option<F::DirIter>,
311    /// current path prefix (e.g. "subdir/nested")
312    current_path: String,
313    /// stack of not-yet-visited directory paths to traverse
314    stack: Vec<String>,
315    location: Location,
316}
317
318impl<F> DirWalker<F>
319where
320    F: FsAdapter,
321    F::Permissions: MessageAllowed<CloseDir> + MessageAllowed<OpenDirMessage> + MessageAllowed<NextEntry>,
322{
323    pub fn new(fs: F, path: impl Into<String>, location: Location) -> Result<Self, Error> {
324        let path = path.into();
325        let current_iter = fs.open_dir(&path, location)?;
326
327        Ok(Self { fs, current_iter: Some(current_iter), current_path: path, stack: Vec::new(), location })
328    }
329}
330
331impl<F> Iterator for DirWalker<F>
332where
333    F: FsAdapter,
334    F::Permissions: MessageAllowed<CloseDir> + MessageAllowed<OpenDirMessage> + MessageAllowed<NextEntry>,
335{
336    type Item = Result<(String, DirEntry), Error>;
337
338    fn next(&mut self) -> Option<Self::Item> {
339        loop {
340            if let Some(iter) = &mut self.current_iter {
341                match iter.next() {
342                    Some(Ok(entry)) => {
343                        if entry.name == "." || entry.name == ".." {
344                            continue;
345                        }
346
347                        let full_path = if self.current_path.is_empty() || self.current_path == "/" {
348                            entry.name.clone()
349                        } else {
350                            format!("{}/{}", self.current_path.trim_end_matches('/'), entry.name)
351                        };
352
353                        if entry.is_dir {
354                            self.stack.push(full_path.clone());
355                        }
356
357                        return Some(Ok((full_path, entry)));
358                    }
359                    Some(Err(e)) => {
360                        return Some(Err(e));
361                    }
362                    None => {
363                        self.current_iter = None;
364                    }
365                }
366            }
367
368            // current iterator exhausted, pop next directory from stack
369            if let Some(next_path) = self.stack.pop() {
370                match self.fs.open_dir(&next_path, self.location) {
371                    Ok(iter) => {
372                        self.current_path = next_path;
373                        self.current_iter = Some(iter);
374                    }
375                    Err(e) => {
376                        return Some(Err(e));
377                    }
378                }
379            } else {
380                return None;
381            }
382        }
383    }
384}
385
386#[cfg(feature = "test")]
387pub mod test_utils {
388    use std::collections::HashMap;
389    use std::marker::PhantomData;
390    use std::path::PathBuf;
391    use std::sync::Arc;
392
393    use chrono::{DateTime, Datelike, Local, Timelike};
394    use server::AllPermissions;
395
396    use super::*;
397
398    /// Wrapper for std::fs::File that implements FileAdapter.
399    pub struct TestFile<P: CheckedPermissions> {
400        file: std::fs::File,
401        _phantom: PhantomData<P>,
402    }
403
404    impl<P: CheckedPermissions> TestFile<P> {
405        pub fn new(file: std::fs::File) -> Self { Self { file, _phantom: PhantomData } }
406    }
407
408    impl<P: CheckedPermissions> Read for TestFile<P> {
409        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { self.file.read(buf) }
410    }
411
412    impl<P: CheckedPermissions> Write for TestFile<P> {
413        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.file.write(buf) }
414
415        fn flush(&mut self) -> std::io::Result<()> { self.file.flush() }
416    }
417
418    impl<P: CheckedPermissions> Seek for TestFile<P> {
419        fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { self.file.seek(pos) }
420    }
421
422    impl<P: CheckedPermissions> FileAdapter<P> for TestFile<P> {
423        fn metadata(&self) -> Result<Metadata, Error>
424        where
425            P: MessageAllowed<GetMetadata>,
426        {
427            let metadata: Metadata = self.file.metadata()?.try_into()?;
428            Ok(metadata)
429        }
430
431        fn truncate(&mut self) -> Result<(), Error>
432        where
433            P: MessageAllowed<TruncateFile>,
434        {
435            let pos = self.file.stream_position()?;
436            self.file.set_len(pos)?;
437            Ok(())
438        }
439
440        fn set_mtime(&mut self, datetime: crate::DateTime) -> Result<(), Error>
441        where
442            P: MessageAllowed<SetMtime>,
443        {
444            use chrono::{Local, TimeZone};
445
446            let datetime_local = Local
447                .with_ymd_and_hms(
448                    datetime.date.year as i32,
449                    datetime.date.month as u32,
450                    datetime.date.day as u32,
451                    datetime.time.hour as u32,
452                    datetime.time.min as u32,
453                    datetime.time.sec as u32,
454                )
455                .single()
456                .ok_or(Error::Io)?;
457            let system_time: std::time::SystemTime = datetime_local.into();
458
459            self.file.set_modified(system_time)?;
460            Ok(())
461        }
462
463        fn copy_block_to(&mut self, to: &mut Self, len: usize) -> Result<usize, Error>
464        where
465            P: MessageAllowed<AsyncCopyBlock>,
466        {
467            use std::io::{Read, Write};
468
469            let mut buf = vec![0u8; len];
470            let bytes_read = self.file.read(&mut buf)?;
471            to.file.write_all(&buf[..bytes_read])?;
472            Ok(bytes_read)
473        }
474    }
475
476    pub struct TestDirIterator {
477        entries: std::vec::IntoIter<std::result::Result<std::fs::DirEntry, std::io::Error>>,
478    }
479
480    impl Iterator for TestDirIterator {
481        type Item = Result<DirEntry, Error>;
482
483        fn next(&mut self) -> Option<Self::Item> {
484            use chrono::Local;
485
486            self.entries.next().map(|entry| {
487                let entry = entry?;
488                let metadata = entry.metadata()?;
489                let modified: chrono::DateTime<Local> = metadata.modified()?.into();
490                let modified = modified.into();
491
492                Ok(DirEntry {
493                    name: entry.file_name().to_string_lossy().to_string(),
494                    modified,
495                    len: metadata.len(),
496                    is_dir: metadata.is_dir(),
497                    is_file: metadata.is_file(),
498                })
499            })
500        }
501    }
502
503    /// Test implementation of `FsAdapter` using temporary directories.
504    #[derive(Clone)]
505    pub struct FsTest {
506        _temp_dir: Arc<tempfile::TempDir>,
507        roots: Arc<HashMap<Location, PathBuf>>,
508    }
509
510    impl Default for FsTest {
511        fn default() -> Self {
512            let temp_dir = tempfile::TempDir::new().unwrap();
513            let base = temp_dir.path();
514
515            let mut roots = HashMap::new();
516            roots.insert(Location::EncryptedRoot, base.join("encrypted"));
517            roots.insert(Location::System, base.join("system"));
518            roots.insert(Location::SystemAppData, base.join(crate::SYSTEM_STATE_ROOT));
519            roots.insert(Location::CommonAssets, base.join("common"));
520            roots.insert(Location::AppData, base.join("appdata"));
521            roots.insert(Location::Usb, base.join("usb"));
522            roots.insert(Location::User, base.join("user"));
523            roots.insert(Location::Boot, base.join("boot"));
524            roots.insert(Location::AppResources, base.join("app-resources"));
525
526            for path in roots.values() {
527                std::fs::create_dir_all(path).unwrap();
528            }
529
530            Self { _temp_dir: Arc::new(temp_dir), roots: Arc::new(roots) }
531        }
532    }
533
534    impl FsTest {
535        fn root(&self, location: Location) -> &PathBuf { self.roots.get(&location).unwrap() }
536
537        pub fn write_file(&self, path: &str, contents: &[u8], location: Location) {
538            let parts: Vec<&str> = path.rsplitn(2, '/').collect();
539            if parts.len() == 2 {
540                self.create_dir(parts[1], location).unwrap();
541            }
542            let mut file = self.open_file(path, location, OpenFlags::CREATE).unwrap();
543            file.write_all(contents).unwrap();
544        }
545
546        pub fn read_file_contents(&self, path: &str, location: Location) -> Result<Vec<u8>, Error> {
547            let mut file = self.open_file(path, location, OpenFlags::READ_ONLY)?;
548            let mut contents = Vec::new();
549            file.read_to_end(&mut contents)?;
550            Ok(contents)
551        }
552
553        /// Print a tree view of the filesystem at a given location.
554        /// Useful for debugging tests.
555        pub fn print_tree(&self, location: Location) {
556            let root = self.root(location);
557            println!("-----");
558            self.print_tree_recursive(root, 0, None);
559            println!("-----");
560        }
561
562        /// Print a tree view with a maximum depth.
563        pub fn print_tree_with_depth(&self, location: Location, max_depth: usize) {
564            let root = self.root(location);
565            println!("-----");
566            self.print_tree_recursive(root, 0, Some(max_depth));
567            println!("-----");
568        }
569
570        fn print_tree_recursive(&self, dir_path: &std::path::Path, depth: usize, max_depth: Option<usize>) {
571            if let Some(max) = max_depth {
572                if depth >= max {
573                    return;
574                }
575            }
576
577            let Ok(entries) = std::fs::read_dir(dir_path) else {
578                return;
579            };
580
581            for entry in entries.flatten() {
582                let name = entry.file_name();
583                let name_str = name.to_string_lossy();
584
585                for _ in 0..depth {
586                    print!("\t");
587                }
588                println!("{name_str}");
589
590                if entry.path().is_dir() {
591                    self.print_tree_recursive(&entry.path(), depth + 1, max_depth);
592                }
593            }
594        }
595    }
596
597    impl TryFrom<std::fs::Metadata> for Metadata {
598        type Error = std::io::Error;
599
600        fn try_from(metadata: std::fs::Metadata) -> Result<Self, Self::Error> {
601            let created: DateTime<Local> = metadata.created()?.into();
602            let accessed: DateTime<Local> = metadata.accessed()?.into();
603            let modified: DateTime<Local> = metadata.modified()?.into();
604
605            let accessed_date = accessed.date_naive();
606            Ok(crate::Metadata {
607                is_dir: metadata.is_dir(),
608                size: metadata.len(),
609                created: created.into(),
610                accessed: crate::Date {
611                    year: accessed_date.year() as u16,
612                    month: accessed_date.month() as u16,
613                    day: accessed_date.day() as u16,
614                },
615                modified: modified.into(),
616            })
617        }
618    }
619
620    impl FsAdapter for FsTest {
621        type DirIter = TestDirIterator;
622        type File = TestFile<AllPermissions>;
623        type Permissions = AllPermissions;
624
625        fn create_dir(&self, path: &str, location: Location) -> Result<(), Error> {
626            let root = self.root(location);
627            std::fs::create_dir_all(root.join(path.trim_start_matches('/')))?;
628            Ok(())
629        }
630
631        fn remove(&self, path: &str, location: Location) -> Result<(), Error> {
632            let root = self.root(location);
633            let full_path = root.join(path.trim_start_matches('/'));
634            if full_path.is_dir() {
635                std::fs::remove_dir_all(&full_path)?;
636            } else {
637                std::fs::remove_file(&full_path)?;
638            }
639            Ok(())
640        }
641
642        fn atomic_copy(
643            &self,
644            src: &str,
645            dest: &str,
646            rename: Option<String>,
647            location: Location,
648        ) -> Result<(), Error> {
649            fn copy_recursive(src: &std::path::Path, dest: &std::path::Path) -> Result<(), Error> {
650                if src.is_dir() {
651                    std::fs::create_dir(dest)?;
652                    for entry in std::fs::read_dir(src)? {
653                        let entry = entry?;
654                        copy_recursive(&entry.path(), &dest.join(entry.file_name()))?;
655                    }
656                } else {
657                    std::fs::copy(src, dest)?;
658                }
659                Ok(())
660            }
661            let root = self.root(location);
662            let src_path = root.join(src.trim_start_matches('/'));
663            let dest_path = root.join(dest.trim_start_matches('/'));
664
665            if !dest_path.exists() {
666                return Err(Error::FileNotFound);
667            }
668
669            let final_dest = if let Some(new_name) = rename {
670                dest_path.join(new_name)
671            } else {
672                dest_path.join(src_path.file_name().unwrap())
673            };
674
675            copy_recursive(&src_path, &final_dest)
676        }
677
678        fn open_file(&self, path: &str, location: Location, flags: OpenFlags) -> Result<Self::File, Error> {
679            let root = self.root(location);
680            let full_path = root.join(path.trim_start_matches('/'));
681
682            let file = std::fs::OpenOptions::new()
683                .read(flags.read)
684                .write(flags.write)
685                .create(flags.create)
686                .truncate(flags.create)
687                .open(&full_path)?;
688
689            Ok(TestFile::new(file))
690        }
691
692        fn open_dir(&self, path: &str, location: Location) -> Result<Self::DirIter, Error> {
693            let root = self.root(location);
694            let full_path = root.join(path.trim_start_matches('/'));
695
696            let entries: Vec<_> = std::fs::read_dir(&full_path)?.collect();
697
698            Ok(TestDirIterator { entries: entries.into_iter() })
699        }
700
701        fn rename(&self, src: &str, dest: &str, location: Location) -> Result<(), Error> {
702            let root = self.root(location);
703            let src_path = root.join(src.trim_start_matches('/'));
704            let dest_path = root.join(dest.trim_start_matches('/'));
705            std::fs::rename(&src_path, &dest_path)?;
706            Ok(())
707        }
708
709        fn metadata(&self, path: &str, location: Location) -> Result<Metadata, Error>
710        where
711            Self::Permissions: MessageAllowed<GetMetadata>,
712        {
713            let root = self.root(location);
714            Ok(std::fs::metadata(root.join(path.trim_start_matches('/')))?.try_into()?)
715        }
716
717        fn flush(&mut self, _location: Location) -> Result<(), Error> { Ok(()) }
718    }
719
720    impl From<chrono::DateTime<Local>> for crate::DateTime {
721        fn from(dt: chrono::DateTime<Local>) -> Self {
722            crate::DateTime {
723                date: crate::Date { year: dt.year() as u16, month: dt.month() as u16, day: dt.day() as u16 },
724                time: crate::Time {
725                    hour: dt.hour() as u16,
726                    min: dt.minute() as u16,
727                    sec: dt.second() as u16,
728                    millis: (dt.nanosecond() / 1_000_000) as u16,
729                },
730            }
731        }
732    }
733
734    #[cfg(test)]
735    mod tests {
736        use super::*;
737
738        #[test]
739        fn test_dir_walker() {
740            let fs = FsTest::default();
741            let location = Location::AppData;
742
743            fs.write_file("root.txt", b"root", location);
744            fs.write_file("dir1/file1.txt", b"file1", location);
745            fs.write_file("dir1/file2.txt", b"file2", location);
746            fs.write_file("dir1/subdir/file3.txt", b"file3", location);
747            fs.write_file("dir2/file4.txt", b"file4", location);
748
749            let walker = fs.walk_dir("/", location).unwrap();
750            let mut paths: Vec<String> = walker.map(|r| r.unwrap().0).collect();
751            paths.sort();
752
753            let expected = vec![
754                "dir1",
755                "dir1/file1.txt",
756                "dir1/file2.txt",
757                "dir1/subdir",
758                "dir1/subdir/file3.txt",
759                "dir2",
760                "dir2/file4.txt",
761                "root.txt",
762            ];
763
764            assert_eq!(paths, expected);
765        }
766    }
767}