Skip to main content
KeyOS API Reference

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