virtual_fs/
overlay_fs.rs

1use std::{
2    collections::HashSet,
3    fmt::Debug,
4    io::{self, SeekFrom},
5    path::{Path, PathBuf},
6    pin::Pin,
7    sync::Arc,
8    task::{Context, Poll},
9};
10
11use futures::future::BoxFuture;
12use replace_with::replace_with_or_abort;
13use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf};
14
15use crate::{
16    FileOpener, FileSystem, FileSystems, FsError, Metadata, OpenOptions, OpenOptionsConfig,
17    ReadDir, VirtualFile, ops,
18};
19
20fn unlink_overlay_path<P>(primary: &Arc<P>, path: &Path) -> Result<(), FsError>
21where
22    P: FileSystem + ?Sized,
23{
24    // Create the whiteout before removing from primary. If whiteout creation
25    // fails we abort early so the lower-layer entry never becomes visible.
26    // Removing from primary first and then failing to create the whiteout
27    // would cause the secondary file to re-appear.
28    match ops::create_white_out(primary, path) {
29        Ok(()) | Err(FsError::AlreadyExists) => {}
30        Err(e) => return Err(e),
31    }
32
33    match primary.remove_file(path) {
34        // File was not in primary (only in a secondary) – the whiteout alone
35        // is sufficient to suppress it.
36        Err(e) if should_continue(e) => Ok(()),
37        other => other,
38    }
39}
40
41struct SecondaryFile<P> {
42    path: PathBuf,
43    primary: Arc<P>,
44    inner: Box<dyn VirtualFile + Send + Sync>,
45}
46
47impl<P> Debug for SecondaryFile<P>
48where
49    P: FileSystem + 'static,
50{
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("SecondaryFile").finish()
53    }
54}
55
56impl<P> VirtualFile for SecondaryFile<P>
57where
58    P: FileSystem + 'static,
59{
60    fn last_accessed(&self) -> u64 {
61        self.inner.last_accessed()
62    }
63
64    fn last_modified(&self) -> u64 {
65        self.inner.last_modified()
66    }
67
68    fn created_time(&self) -> u64 {
69        self.inner.created_time()
70    }
71
72    fn set_times(&mut self, atime: Option<u64>, mtime: Option<u64>) -> crate::Result<()> {
73        self.inner.set_times(atime, mtime)
74    }
75
76    fn size(&self) -> u64 {
77        self.inner.size()
78    }
79
80    fn set_len(&mut self, new_size: u64) -> crate::Result<()> {
81        self.inner.set_len(new_size)
82    }
83
84    fn unlink(&mut self) -> crate::Result<()> {
85        // A SecondaryFile is only created when the primary has no binding for
86        // this path at open time. Any file currently at this path in primary is
87        // a different, independently-created object. Only create a whiteout to
88        // hide the secondary entry; never remove from primary.
89        match ops::create_white_out(&self.primary, &self.path) {
90            Ok(()) => Ok(()),
91            // The whiteout already exists: the path was already deleted from the
92            // overlay's perspective, so report it as not found.
93            Err(FsError::AlreadyExists) => Err(FsError::EntryNotFound),
94            Err(e) => Err(e),
95        }
96    }
97
98    fn is_open(&self) -> bool {
99        self.inner.is_open()
100    }
101
102    fn get_special_fd(&self) -> Option<u32> {
103        self.inner.get_special_fd()
104    }
105
106    fn write_from_mmap(&mut self, offset: u64, len: u64) -> std::io::Result<()> {
107        self.inner.write_from_mmap(offset, len)
108    }
109
110    fn as_owned_buffer(&self) -> Option<shared_buffer::OwnedBuffer> {
111        self.inner.as_owned_buffer()
112    }
113
114    fn poll_read_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
115        Pin::new(self.inner.as_mut()).poll_read_ready(cx)
116    }
117
118    fn poll_write_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
119        Pin::new(self.inner.as_mut()).poll_write_ready(cx)
120    }
121}
122
123impl<P> AsyncRead for SecondaryFile<P>
124where
125    P: FileSystem + 'static,
126{
127    fn poll_read(
128        mut self: Pin<&mut Self>,
129        cx: &mut Context<'_>,
130        buf: &mut ReadBuf<'_>,
131    ) -> Poll<io::Result<()>> {
132        Pin::new(self.inner.as_mut()).poll_read(cx, buf)
133    }
134}
135
136impl<P> AsyncWrite for SecondaryFile<P>
137where
138    P: FileSystem + 'static,
139{
140    fn poll_write(
141        mut self: Pin<&mut Self>,
142        cx: &mut Context<'_>,
143        buf: &[u8],
144    ) -> Poll<io::Result<usize>> {
145        Pin::new(self.inner.as_mut()).poll_write(cx, buf)
146    }
147
148    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
149        Pin::new(self.inner.as_mut()).poll_flush(cx)
150    }
151
152    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
153        Pin::new(self.inner.as_mut()).poll_shutdown(cx)
154    }
155
156    fn poll_write_vectored(
157        mut self: Pin<&mut Self>,
158        cx: &mut Context<'_>,
159        bufs: &[io::IoSlice<'_>],
160    ) -> Poll<io::Result<usize>> {
161        Pin::new(self.inner.as_mut()).poll_write_vectored(cx, bufs)
162    }
163
164    fn is_write_vectored(&self) -> bool {
165        self.inner.is_write_vectored()
166    }
167}
168
169impl<P> AsyncSeek for SecondaryFile<P>
170where
171    P: FileSystem + 'static,
172{
173    fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
174        Pin::new(self.inner.as_mut()).start_seek(position)
175    }
176
177    fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
178        Pin::new(self.inner.as_mut()).poll_complete(cx)
179    }
180}
181
182/// A primary filesystem and chain of secondary filesystems that are overlaid
183/// on top of each other.
184///
185/// # Precedence
186///
187/// The [`OverlayFileSystem`] will execute operations based on precedence.
188///
189///
190/// Most importantly, this means earlier filesystems can shadow files and
191/// directories that have a lower precedence.
192///
193///# Examples
194///
195/// Something useful to know is that the [`FileSystems`] trait is implemented
196/// for both arrays and tuples.
197///
198/// For example, if you want to create a [`crate::FileSystem`] which will
199/// create files in-memory while still being able to read from the host, you
200/// might do something like this:
201///
202/// ```rust
203/// use virtual_fs::{
204///     mem_fs::FileSystem as MemFS,
205///     host_fs::FileSystem as HostFS,
206///     OverlayFileSystem,
207/// };
208///
209/// let runtime = tokio::runtime::Builder::new_current_thread()
210///     .enable_all()
211///     .build()
212///     .unwrap();
213/// let _guard = runtime.enter();
214///
215/// let fs = OverlayFileSystem::new(MemFS::default(), [HostFS::new(tokio::runtime::Handle::current(), "/").unwrap()]);
216///
217/// // This also has the benefit of storing the two values in-line with no extra
218/// // overhead or indirection.
219/// assert_eq!(
220///     std::mem::size_of_val(&fs),
221///     std::mem::size_of::<(MemFS, HostFS)>(),
222/// );
223/// ```
224///
225/// A more complex example is
226#[derive(Clone, PartialEq, Eq)]
227pub struct OverlayFileSystem<P, S> {
228    primary: Arc<P>,
229    secondaries: S,
230}
231
232impl<P, S> OverlayFileSystem<P, S>
233where
234    P: FileSystem + Send + Sync + 'static,
235    S: for<'a> FileSystems<'a> + Send + Sync + 'static,
236{
237    /// Create a new [`FileSystem`] using a primary [`crate::FileSystem`] and a
238    /// chain of secondary [`FileSystems`].
239    pub fn new(primary: P, secondaries: S) -> Self {
240        OverlayFileSystem {
241            primary: Arc::new(primary),
242            secondaries,
243        }
244    }
245
246    /// Get a reference to the primary filesystem.
247    pub fn primary(&self) -> &P {
248        &self.primary
249    }
250
251    /// Get a reference to the secondary filesystems.
252    pub fn secondaries(&self) -> &S {
253        &self.secondaries
254    }
255
256    /// Get a mutable reference to the secondary filesystems.
257    pub fn secondaries_mut(&mut self) -> &mut S {
258        &mut self.secondaries
259    }
260
261    fn permission_error_or_not_found(&self, path: &Path) -> Result<(), FsError> {
262        for fs in self.secondaries.filesystems() {
263            if ops::exists(fs, path) {
264                return Err(FsError::PermissionDenied);
265            }
266        }
267
268        Err(FsError::EntryNotFound)
269    }
270}
271
272impl<P, S> FileSystem for OverlayFileSystem<P, S>
273where
274    P: FileSystem + Send + 'static,
275    S: for<'a> FileSystems<'a> + Send + Sync + 'static,
276    for<'a> <<S as FileSystems<'a>>::Iter as IntoIterator>::IntoIter: Send,
277{
278    fn readlink(&self, path: &Path) -> crate::Result<PathBuf> {
279        // Whiteout files can not be read, they are just markers
280        if ops::is_white_out(path).is_some() {
281            return Err(FsError::EntryNotFound);
282        }
283
284        // Check if the file is in the primary
285        match self.primary.readlink(path) {
286            Ok(meta) => return Ok(meta),
287            Err(e) if should_continue(e) => {}
288            Err(e) => return Err(e),
289        }
290
291        // There might be a whiteout, search for this
292        if ops::has_white_out(&self.primary, path) {
293            return Err(FsError::EntryNotFound);
294        }
295
296        // Otherwise scan the secondaries
297        for fs in self.secondaries.filesystems() {
298            match fs.readlink(path) {
299                Err(e) if should_continue(e) => continue,
300                other => return other,
301            }
302        }
303
304        Err(FsError::EntryNotFound)
305    }
306
307    fn read_dir(&self, path: &Path) -> Result<ReadDir, FsError> {
308        let mut entries = Vec::new();
309        let mut had_at_least_one_success = false;
310        let mut white_outs = HashSet::new();
311
312        let filesystems = std::iter::once(&self.primary as &(dyn FileSystem + Send))
313            .chain(self.secondaries().filesystems());
314
315        for fs in filesystems {
316            match fs.read_dir(path) {
317                Ok(r) => {
318                    for entry in r {
319                        let entry = entry?;
320
321                        // White out entries block any later entries in the secondaries
322                        // unless the entry has comes before the white out, thus the order
323                        // that the file systems are parsed is important to this logic.
324                        if let Some(path) = entry.is_white_out() {
325                            tracing::trace!(
326                                path=%path.display(),
327                                "Found whiteout file",
328                            );
329                            white_outs.insert(path);
330                            continue;
331                        } else if white_outs.contains(&entry.path) {
332                            tracing::trace!(
333                                path=%path.display(),
334                                "Skipping path because a whiteout exists",
335                            );
336                            continue;
337                        }
338
339                        entries.push(entry);
340                    }
341                    had_at_least_one_success = true;
342                }
343                Err(e) if should_continue(e) => continue,
344                Err(e) => return Err(e),
345            }
346        }
347
348        if had_at_least_one_success {
349            // Make sure later entries are removed in favour of earlier ones.
350            // Note: this sort is guaranteed to be stable, meaning filesystems
351            // "higher up" the chain will be further towards the start and kept
352            // when deduplicating.
353            entries.sort_by(|a, b| a.path.cmp(&b.path));
354            entries.dedup_by(|a, b| a.path == b.path);
355
356            Ok(ReadDir::new(entries))
357        } else {
358            Err(FsError::BaseNotDirectory)
359        }
360    }
361
362    fn create_dir(&self, path: &Path) -> Result<(), FsError> {
363        // You can not create directories that use the whiteout prefix
364        if ops::is_white_out(path).is_some() {
365            return Err(FsError::InvalidInput);
366        }
367
368        // It could be the case that the directory was earlier hidden in the secondaries
369        // by a whiteout file, hence we need to make sure those are cleared out.
370        ops::remove_white_out(self.primary.as_ref(), path);
371
372        // Make sure the parent tree is in place on the primary, this is to cover the
373        // scenario where the secondaries has a parent structure that is not yet in the
374        // primary and the primary needs it to create a sub-directory
375        if let Some(parent) = path.parent()
376            && self.read_dir(parent).is_ok()
377        {
378            ops::create_dir_all(&self.primary, parent).ok();
379        }
380
381        // Create the directory in the primary
382        match self.primary.create_dir(path) {
383            Err(e) if should_continue(e) => {}
384            other => return other,
385        }
386
387        self.permission_error_or_not_found(path)
388    }
389
390    fn create_symlink(&self, source: &Path, target: &Path) -> Result<(), FsError> {
391        if ops::is_white_out(target).is_some() {
392            return Err(FsError::InvalidInput);
393        }
394
395        ops::remove_white_out(self.primary.as_ref(), target);
396
397        if let Some(parent) = target.parent()
398            && self.read_dir(parent).is_ok()
399        {
400            ops::create_dir_all(&self.primary, parent).ok();
401        }
402
403        self.primary.create_symlink(source, target)
404    }
405
406    fn hard_link(&self, source: &Path, target: &Path) -> Result<(), FsError> {
407        if ops::is_white_out(source).is_some() || ops::is_white_out(target).is_some() {
408            return Err(FsError::InvalidInput);
409        }
410
411        ops::remove_white_out(self.primary.as_ref(), target);
412
413        if let Some(parent) = target.parent()
414            && self.read_dir(parent).is_ok()
415        {
416            ops::create_dir_all(&self.primary, parent).ok();
417        }
418
419        self.primary.hard_link(source, target)
420    }
421
422    fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
423        // Whiteout files can not be removed, instead the original directory
424        // must be removed or recreated.
425        if ops::is_white_out(path).is_some() {
426            tracing::trace!(
427                path=%path.display(),
428                "Unable to remove a whited out directory",
429            );
430            return Err(FsError::EntryNotFound);
431        }
432
433        // If the directory is contained in a secondary file system then we need to create a
434        // whiteout file so that it is suppressed and is no longer returned in `readdir` calls.
435
436        let had_at_least_one_success = self.secondaries.filesystems().into_iter().any(|fs| {
437            fs.read_dir(path).is_ok() && ops::create_white_out(&self.primary, path).is_ok()
438        });
439
440        // Attempt to remove it from the primary, if this succeeds then we may have also
441        // added the whiteout file in the earlier step, but are required in this case to
442        // properly delete the directory.
443        match self.primary.remove_dir(path) {
444            Err(e) if should_continue(e) => {}
445            other => return other,
446        }
447
448        if had_at_least_one_success {
449            return Ok(());
450        }
451        self.permission_error_or_not_found(path)
452    }
453
454    fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<(), FsError>> {
455        let from = from.to_owned();
456        let to = to.to_owned();
457        Box::pin(async move {
458            // Whiteout files can not be renamed
459            if ops::is_white_out(&from).is_some() {
460                tracing::trace!(
461                    from=%from.display(),
462                    to=%to.display(),
463                    "Attempting to rename a file that was whited out"
464                );
465                return Err(FsError::EntryNotFound);
466            }
467            // You can not rename a file into a whiteout file
468            if ops::is_white_out(&to).is_some() {
469                tracing::trace!(
470                    from=%from.display(),
471                    to=%to.display(),
472                    "Attempting to rename a file into a whiteout file"
473                );
474                return Err(FsError::InvalidInput);
475            }
476
477            // We attempt to rename the file or directory in the primary
478            // if this succeeds then we also need to ensure the white out
479            // files are created where we need them, so we do not immediately
480            // return until that is done
481            let mut had_at_least_one_success = false;
482            match self.primary.rename(&from, &to).await {
483                Err(e) if should_continue(e) => {}
484                Ok(()) => {
485                    had_at_least_one_success = true;
486                }
487                other => return other,
488            }
489
490            // If we have not yet renamed the file it may still reside in
491            // the secondaries, in which case we need to copy it to the
492            // primary rather than rename it
493            if !had_at_least_one_success {
494                for fs in self.secondaries.filesystems() {
495                    if fs.metadata(&from).is_ok() {
496                        ops::copy_reference_ext(fs, &self.primary, &from, &to).await?;
497                        had_at_least_one_success = true;
498                        break;
499                    }
500                }
501            }
502
503            // If the rename operation was a success then we need to update any
504            // whiteout files on the primary before we return success.
505            if had_at_least_one_success {
506                for fs in self.secondaries.filesystems() {
507                    if fs.metadata(&from).is_ok() {
508                        tracing::trace!(
509                            path=%from.display(),
510                            "Creating a whiteout for the file that was renamed",
511                        );
512                        ops::create_white_out(&self.primary, &from).ok();
513                        break;
514                    }
515                }
516                ops::remove_white_out(&self.primary, &to);
517                return Ok(());
518            }
519
520            // Otherwise we are in a failure scenario
521            self.permission_error_or_not_found(&from)
522        })
523    }
524
525    fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
526        // Whiteout files can not be read, they are just markers
527        if ops::is_white_out(path).is_some() {
528            return Err(FsError::EntryNotFound);
529        }
530
531        // Check if the file is in the primary
532        match self.primary.metadata(path) {
533            Ok(meta) => return Ok(meta),
534            Err(e) if should_continue(e) => {}
535            Err(e) => return Err(e),
536        }
537
538        // There might be a whiteout, search for this
539        if ops::has_white_out(&self.primary, path) {
540            return Err(FsError::EntryNotFound);
541        }
542
543        // Otherwise scan the secondaries
544        for fs in self.secondaries.filesystems() {
545            match fs.metadata(path) {
546                Err(e) if should_continue(e) => continue,
547                other => return other,
548            }
549        }
550
551        Err(FsError::EntryNotFound)
552    }
553
554    fn symlink_metadata(&self, path: &Path) -> crate::Result<Metadata> {
555        // Whiteout files can not be read, they are just markers
556        if ops::is_white_out(path).is_some() {
557            return Err(FsError::EntryNotFound);
558        }
559
560        // Check if the file is in the primary
561        match self.primary.symlink_metadata(path) {
562            Ok(meta) => return Ok(meta),
563            Err(e) if should_continue(e) => {}
564            Err(e) => return Err(e),
565        }
566
567        // There might be a whiteout, search for this
568        if ops::has_white_out(&self.primary, path) {
569            return Err(FsError::EntryNotFound);
570        }
571
572        // Otherwise scan the secondaries
573        for fs in self.secondaries.filesystems() {
574            match fs.symlink_metadata(path) {
575                Err(e) if should_continue(e) => continue,
576                other => return other,
577            }
578        }
579
580        Err(FsError::EntryNotFound)
581    }
582
583    fn remove_file(&self, path: &Path) -> Result<(), FsError> {
584        // It is not possible to delete whiteout files directly, instead
585        // one must delete the original file
586        if ops::is_white_out(path).is_some() {
587            return Err(FsError::InvalidInput);
588        }
589
590        // If the file is contained in a secondary then then we need to create a
591        // whiteout file so that it is suppressed.
592        let had_at_least_one_success = self.secondaries.filesystems().into_iter().any(|fs| {
593            fs.metadata(path).is_ok() && ops::create_white_out(&self.primary, path).is_ok()
594        });
595
596        // Attempt to remove it from the primary
597        match self.primary.remove_file(path) {
598            Err(e) if should_continue(e) => {}
599            other => return other,
600        }
601
602        if had_at_least_one_success {
603            return Ok(());
604        }
605        self.permission_error_or_not_found(path)
606    }
607
608    fn new_open_options(&self) -> OpenOptions<'_> {
609        OpenOptions::new(self)
610    }
611}
612
613impl<P, S> FileOpener for OverlayFileSystem<P, S>
614where
615    P: FileSystem + Send + 'static,
616    S: for<'a> FileSystems<'a> + Send + Sync + 'static,
617    for<'a> <<S as FileSystems<'a>>::Iter as IntoIterator>::IntoIter: Send,
618{
619    fn open(
620        &self,
621        path: &Path,
622        conf: &OpenOptionsConfig,
623    ) -> Result<Box<dyn VirtualFile + Send + Sync + 'static>, FsError> {
624        // Whiteout files can not be read, they are just markers
625        if ops::is_white_out(path).is_some() {
626            tracing::trace!(
627                path=%path.display(),
628                options=?conf,
629                "Whiteout files can't be opened",
630            );
631            return Err(FsError::InvalidInput);
632        }
633
634        // Check if the file is in the primary (without actually creating it) as
635        // when the file is in the primary it takes preference over any of file
636        {
637            let mut conf = conf.clone();
638            conf.create = false;
639            conf.create_new = false;
640            match self.primary.new_open_options().options(conf).open(path) {
641                Err(e) if should_continue(e) => {}
642                other => return other,
643            }
644        }
645
646        // In the scenario that we are creating the file then there is
647        // special handling that will ensure its setup correctly
648        if conf.create_new {
649            // When the secondary has the directory structure but the primary
650            // does not then we need to make sure we create all the structure
651            // in the primary
652            if let Some(parent) = path.parent() {
653                if ops::exists(self, parent) {
654                    // We create the directory structure on the primary so that
655                    // the new file can be created, this will make it override
656                    // whatever is in the primary
657                    ops::create_dir_all(&self.primary, parent)?;
658                } else {
659                    return Err(FsError::EntryNotFound);
660                }
661            }
662
663            // Remove any whiteout
664            ops::remove_white_out(&self.primary, path);
665
666            // Create the file in the primary
667            return self
668                .primary
669                .new_open_options()
670                .options(conf.clone())
671                .open(path);
672        }
673
674        // There might be a whiteout, search for this and if its found then
675        // we are done as the secondary file or directory has been earlier
676        // deleted via a white out (when the create flag is set then
677        // the white out marker is ignored)
678        if !conf.create && ops::has_white_out(&self.primary, path) {
679            tracing::trace!(
680                path=%path.display(),
681                "The file has been whited out",
682            );
683            return Err(FsError::EntryNotFound);
684        }
685
686        // Determine if a mutation will be possible with the opened file
687        let require_mutations = conf.append || conf.write || conf.create_new | conf.truncate;
688
689        // If the file is on a secondary then we should open it
690        if !ops::has_white_out(&self.primary, path) {
691            for fs in self.secondaries.filesystems() {
692                let mut sub_conf = conf.clone();
693                sub_conf.create = false;
694                sub_conf.create_new = false;
695                sub_conf.append = false;
696                sub_conf.truncate = false;
697                match fs.new_open_options().options(sub_conf.clone()).open(path) {
698                    Err(e) if should_continue(e) => continue,
699                    Ok(file) if require_mutations => {
700                        // If the file was opened with the ability to mutate then we need
701                        // to return a copy on write emulation so that the file can be
702                        // copied from the secondary to the primary in the scenario that
703                        // it is edited
704                        return open_copy_on_write(path, conf, &self.primary, file);
705                    }
706                    Ok(file) => {
707                        return Ok(Box::new(SecondaryFile {
708                            path: path.to_path_buf(),
709                            primary: Arc::clone(&self.primary),
710                            inner: file,
711                        }));
712                    }
713                    Err(err) => return Err(err),
714                }
715            }
716        }
717
718        // If we are creating the file then do so
719        if conf.create {
720            // Create the parent structure and remove any whiteouts
721            if let Some(parent) = path.parent()
722                && ops::exists(self, parent)
723            {
724                ops::create_dir_all(&self.primary, parent)?;
725            }
726            ops::remove_white_out(&self.primary, path);
727
728            // Create the file in the primary
729            return self
730                .primary
731                .new_open_options()
732                .options(conf.clone())
733                .open(path);
734        }
735
736        // The file does not exist anywhere
737        Err(FsError::EntryNotFound)
738    }
739}
740
741fn open_copy_on_write<P>(
742    path: &Path,
743    conf: &OpenOptionsConfig,
744    primary: &Arc<P>,
745    file: Box<dyn VirtualFile + Send + Sync>,
746) -> Result<Box<dyn VirtualFile + Send + Sync>, FsError>
747where
748    P: FileSystem,
749{
750    struct CopyOnWriteFile<P> {
751        path: PathBuf,
752        primary: Arc<P>,
753        state: CowState,
754        readable: bool,
755        append: bool,
756        new_size: Option<u64>,
757        unlinked: bool,
758    }
759    enum CowState {
760        // The original file is still open and can be accessed for all
761        // read operations
762        ReadOnly(Box<dyn VirtualFile + Send + Sync>),
763        // The copy has started but first we need to get the cursor
764        // position within the source file so that it can be restored
765        SeekingGet(Box<dyn VirtualFile + Send + Sync>),
766        // Now we have the original starting cursor location we need
767        // to move the position of the read to the start of the source
768        // file
769        SeekingSet {
770            original_offset: u64,
771            src: Box<dyn VirtualFile + Send + Sync>,
772        },
773        // We are now copying the data in parts held in the buffer piece
774        // by piece until the original file is completely copied
775        Copying {
776            original_offset: u64,
777            buf: Vec<u8>,
778            buf_pos: usize,
779            dst: Box<dyn VirtualFile + Send + Sync>,
780            src: Box<dyn VirtualFile + Send + Sync>,
781        },
782        // After copying the file we need to seek the position back
783        // to its original location on the newly copied file
784        SeekingRestore {
785            dst: Box<dyn VirtualFile + Send + Sync>,
786        },
787        // We have copied the file and can use all the normal operations
788        Copied(Box<dyn VirtualFile + Send + Sync>),
789        // An error occurred during the copy operation and we are now in a
790        // failed state, after the error is consumed it will reset back
791        // to the original file
792        Error {
793            err: io::Error,
794            src: Box<dyn VirtualFile + Send + Sync>,
795        },
796    }
797    impl CowState {
798        fn as_ref(&self) -> &(dyn VirtualFile + Send + Sync) {
799            match self {
800                Self::ReadOnly(inner) => inner.as_ref(),
801                Self::SeekingGet(inner) => inner.as_ref(),
802                Self::SeekingSet { src, .. } => src.as_ref(),
803                Self::Copying { src, .. } => src.as_ref(),
804                Self::SeekingRestore { dst, .. } => dst.as_ref(),
805                Self::Copied(inner) => inner.as_ref(),
806                Self::Error { src, .. } => src.as_ref(),
807            }
808        }
809        fn as_mut(&mut self) -> &mut (dyn VirtualFile + Send + Sync) {
810            match self {
811                Self::ReadOnly(inner) => inner.as_mut(),
812                Self::SeekingGet(inner) => inner.as_mut(),
813                Self::SeekingSet { src, .. } => src.as_mut(),
814                Self::Copying { src, .. } => src.as_mut(),
815                Self::SeekingRestore { dst, .. } => dst.as_mut(),
816                Self::Copied(inner) => inner.as_mut(),
817                Self::Error { src, .. } => src.as_mut(),
818            }
819        }
820    }
821
822    impl<P> CopyOnWriteFile<P>
823    where
824        P: FileSystem + 'static,
825    {
826        fn poll_copy_progress(&mut self, cx: &mut Context) -> Poll<io::Result<()>> {
827            // Enter a loop until we go pending
828            let mut again = true;
829            while again {
830                again = false;
831
832                // The state machine is updated during the poll operation
833                replace_with_or_abort(&mut self.state, |state| match state {
834                    // We record the current position of the file so that it can be
835                    // restored after the copy-on-write is finished
836                    CowState::SeekingGet(mut src) => {
837                        match Pin::new(src.as_mut()).poll_complete(cx) {
838                            Poll::Ready(Ok(offset)) => {
839                                if let Err(err) =
840                                    Pin::new(src.as_mut()).start_seek(SeekFrom::Start(0))
841                                {
842                                    return CowState::Error { err, src };
843                                }
844                                again = true;
845                                CowState::SeekingSet {
846                                    original_offset: offset,
847                                    src,
848                                }
849                            }
850                            Poll::Ready(Err(err)) => CowState::Error { err, src },
851                            Poll::Pending => CowState::SeekingGet(src),
852                        }
853                    }
854
855                    // We complete the seek operation to the start of the source file
856                    CowState::SeekingSet {
857                        original_offset,
858                        mut src,
859                    } => {
860                        match Pin::new(src.as_mut()).poll_complete(cx).map_ok(|_| ()) {
861                            Poll::Ready(Ok(())) => {
862                                if self.unlinked {
863                                    again = true;
864                                    return CowState::Copying {
865                                        original_offset,
866                                        buf: Vec::new(),
867                                        buf_pos: 0,
868                                        dst: Box::new(crate::BufferFile::default()),
869                                        src,
870                                    };
871                                }
872
873                                // Remove the whiteout, create the parent structure and open
874                                // the new file on the primary
875                                if let Some(parent) = self.path.parent() {
876                                    ops::create_dir_all(&self.primary, parent).ok();
877                                }
878                                let mut had_white_out = false;
879                                if ops::has_white_out(&self.primary, &self.path) {
880                                    ops::remove_white_out(&self.primary, &self.path);
881                                    had_white_out = true;
882                                }
883                                let dst = self
884                                    .primary
885                                    .new_open_options()
886                                    .create(true)
887                                    .read(self.readable)
888                                    .write(true)
889                                    .truncate(true)
890                                    .open(&self.path);
891                                match dst {
892                                    Ok(dst) if had_white_out => {
893                                        again = true;
894                                        CowState::Copied(dst)
895                                    }
896                                    Ok(dst) => {
897                                        again = true;
898                                        CowState::Copying {
899                                            original_offset,
900                                            buf: Vec::new(),
901                                            buf_pos: 0,
902                                            src,
903                                            dst,
904                                        }
905                                    }
906                                    Err(err) => CowState::Error {
907                                        err: err.into(),
908                                        src,
909                                    },
910                                }
911                            }
912                            Poll::Ready(Err(err)) => CowState::Error { err, src },
913                            Poll::Pending => CowState::SeekingSet {
914                                original_offset,
915                                src,
916                            },
917                        }
918                    }
919                    // We are now copying all the data on blocks
920                    CowState::Copying {
921                        mut src,
922                        mut dst,
923                        mut buf,
924                        mut buf_pos,
925                        original_offset,
926                    } => {
927                        loop {
928                            // We are either copying more data from the source
929                            // or we are copying the data to the destination
930                            if buf_pos < buf.len() {
931                                let dst_pinned = Pin::new(dst.as_mut());
932                                match dst_pinned.poll_write(cx, &buf[buf_pos..]) {
933                                    Poll::Ready(Ok(0)) => {}
934                                    Poll::Ready(Ok(amt)) => {
935                                        buf_pos += amt;
936                                        continue;
937                                    }
938                                    Poll::Ready(Err(err)) => {
939                                        return CowState::Error { err, src };
940                                    }
941                                    Poll::Pending => {}
942                                }
943                            } else {
944                                buf.resize_with(8192, || 0);
945                                buf_pos = 8192;
946                                let mut read_buf = ReadBuf::new(&mut buf);
947                                match Pin::new(src.as_mut()).poll_read(cx, &mut read_buf) {
948                                    Poll::Ready(Ok(())) if read_buf.filled().is_empty() => {
949                                        again = true;
950
951                                        if self.append {
952                                            // When we append then we leave the cursor at the
953                                            // end of the file
954                                            return CowState::Copied(dst);
955                                        } else {
956                                            // No more data exists to be read so we now move on to
957                                            // restoring the cursor back to the original position
958                                            if let Err(err) = Pin::new(dst.as_mut())
959                                                .start_seek(SeekFrom::Start(original_offset))
960                                            {
961                                                return CowState::Error { err, src };
962                                            }
963                                            return CowState::SeekingRestore { dst };
964                                        }
965                                    }
966                                    Poll::Ready(Ok(())) => {
967                                        // There is more data to be processed
968                                        let new_len = read_buf.filled().len();
969                                        unsafe { buf.set_len(new_len) };
970                                        buf_pos = 0;
971                                        continue;
972                                    }
973                                    Poll::Ready(Err(err)) => return CowState::Error { err, src },
974                                    Poll::Pending => {}
975                                }
976                            }
977                            return CowState::Copying {
978                                original_offset,
979                                buf,
980                                buf_pos,
981                                src,
982                                dst,
983                            };
984                        }
985                    }
986                    // Now once the restoration of the seek position completes we set the copied state
987                    CowState::SeekingRestore { mut dst } => {
988                        match Pin::new(dst.as_mut()).poll_complete(cx) {
989                            Poll::Ready(_) => {
990                                // If we have changed the length then set it
991                                if let Some(new_size) = self.new_size.take() {
992                                    dst.set_len(new_size).ok();
993                                }
994                                CowState::Copied(dst)
995                            }
996                            Poll::Pending => CowState::SeekingRestore { dst },
997                        }
998                    }
999                    s => s,
1000                });
1001            }
1002
1003            // Determine what response to give based off the state, when an error occurs
1004            // this will be returned and the copy-on-write will be reset
1005            let mut ret = Poll::Pending;
1006            replace_with_or_abort(&mut self.state, |state| match state {
1007                CowState::ReadOnly(src) => {
1008                    ret = Poll::Ready(Ok(()));
1009                    CowState::ReadOnly(src)
1010                }
1011                CowState::Copied(src) => {
1012                    ret = Poll::Ready(Ok(()));
1013                    CowState::Copied(src)
1014                }
1015                CowState::Error { err, src } => {
1016                    ret = Poll::Ready(Err(err));
1017                    CowState::ReadOnly(src)
1018                }
1019                state => {
1020                    ret = Poll::Pending;
1021                    state
1022                }
1023            });
1024            ret
1025        }
1026
1027        fn poll_copy_start_and_progress(&mut self, cx: &mut Context) -> Poll<io::Result<()>> {
1028            replace_with_or_abort(&mut self.state, |state| match state {
1029                CowState::ReadOnly(inner) => {
1030                    tracing::trace!("COW file touched, starting file clone");
1031                    CowState::SeekingGet(inner)
1032                }
1033                state => state,
1034            });
1035            self.poll_copy_progress(cx)
1036        }
1037    }
1038
1039    impl<P> Debug for CopyOnWriteFile<P>
1040    where
1041        P: FileSystem + 'static,
1042    {
1043        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1044            f.debug_struct("CopyOnWriteFile").finish()
1045        }
1046    }
1047
1048    impl<P> VirtualFile for CopyOnWriteFile<P>
1049    where
1050        P: FileSystem + 'static,
1051    {
1052        fn last_accessed(&self) -> u64 {
1053            self.state.as_ref().last_accessed()
1054        }
1055
1056        fn last_modified(&self) -> u64 {
1057            self.state.as_ref().last_modified()
1058        }
1059
1060        fn created_time(&self) -> u64 {
1061            self.state.as_ref().created_time()
1062        }
1063
1064        fn size(&self) -> u64 {
1065            self.state.as_ref().size()
1066        }
1067
1068        fn set_len(&mut self, new_size: u64) -> crate::Result<()> {
1069            self.new_size = Some(new_size);
1070            replace_with_or_abort(&mut self.state, |state| match state {
1071                CowState::Copied(mut file) => {
1072                    file.set_len(new_size).ok();
1073                    CowState::Copied(file)
1074                }
1075                state => {
1076                    if self.unlinked {
1077                        return match state {
1078                            CowState::ReadOnly(inner) => CowState::SeekingGet(inner),
1079                            state => state,
1080                        };
1081                    }
1082
1083                    // in the scenario where the length is set but the file is not
1084                    // polled then we need to make sure we create a file properly
1085                    if let Some(parent) = self.path.parent() {
1086                        ops::create_dir_all(&self.primary, parent).ok();
1087                    }
1088                    let dst = self
1089                        .primary
1090                        .new_open_options()
1091                        .create(true)
1092                        .write(true)
1093                        .open(&self.path);
1094                    if let Ok(mut file) = dst {
1095                        file.set_len(new_size).ok();
1096                    }
1097                    state
1098                }
1099            });
1100            Ok(())
1101        }
1102
1103        fn unlink(&mut self) -> crate::Result<()> {
1104            match &self.state {
1105                CowState::Copied(_) => {
1106                    // The COW copy has landed in primary – it is our file, so it
1107                    // is safe to remove it and create a whiteout for the secondary.
1108                    unlink_overlay_path(&self.primary, &self.path)?;
1109                }
1110                _ => {
1111                    // The file has not yet been written to primary. Only create a
1112                    // whiteout to hide the secondary entry; do NOT remove anything
1113                    // from primary, because whatever is currently bound to this
1114                    // path may be an independently-created file.
1115                    match ops::create_white_out(&self.primary, &self.path) {
1116                        Ok(()) => {}
1117                        // The whiteout already exists: the path was already deleted
1118                        // from the overlay's perspective.
1119                        Err(FsError::AlreadyExists) => return Err(FsError::EntryNotFound),
1120                        Err(e) => return Err(e),
1121                    }
1122                }
1123            }
1124            self.unlinked = true;
1125            Ok(())
1126        }
1127
1128        fn poll_read_ready(
1129            mut self: Pin<&mut Self>,
1130            cx: &mut std::task::Context<'_>,
1131        ) -> Poll<std::io::Result<usize>> {
1132            match self.poll_copy_progress(cx) {
1133                Poll::Ready(Ok(())) => {}
1134                Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
1135                Poll::Pending => return Poll::Pending,
1136            }
1137            Pin::new(self.state.as_mut()).poll_read_ready(cx)
1138        }
1139
1140        fn poll_write_ready(
1141            mut self: Pin<&mut Self>,
1142            cx: &mut std::task::Context<'_>,
1143        ) -> Poll<std::io::Result<usize>> {
1144            match self.poll_copy_progress(cx) {
1145                Poll::Ready(Ok(())) => {}
1146                Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
1147                Poll::Pending => return Poll::Pending,
1148            }
1149            Pin::new(self.state.as_mut()).poll_write_ready(cx)
1150        }
1151    }
1152
1153    impl<P> AsyncWrite for CopyOnWriteFile<P>
1154    where
1155        P: FileSystem + 'static,
1156    {
1157        fn poll_write(
1158            mut self: Pin<&mut Self>,
1159            cx: &mut std::task::Context<'_>,
1160            buf: &[u8],
1161        ) -> Poll<Result<usize, std::io::Error>> {
1162            match self.poll_copy_start_and_progress(cx) {
1163                Poll::Pending => return Poll::Pending,
1164                Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
1165                Poll::Ready(Ok(())) => {}
1166            }
1167            Pin::new(self.state.as_mut()).poll_write(cx, buf)
1168        }
1169
1170        fn poll_write_vectored(
1171            mut self: Pin<&mut Self>,
1172            cx: &mut Context<'_>,
1173            bufs: &[io::IoSlice<'_>],
1174        ) -> Poll<Result<usize, io::Error>> {
1175            match self.poll_copy_start_and_progress(cx) {
1176                Poll::Pending => return Poll::Pending,
1177                Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
1178                Poll::Ready(Ok(())) => {}
1179            }
1180            Pin::new(self.state.as_mut()).poll_write_vectored(cx, bufs)
1181        }
1182
1183        fn poll_flush(
1184            mut self: Pin<&mut Self>,
1185            cx: &mut std::task::Context<'_>,
1186        ) -> Poll<Result<(), std::io::Error>> {
1187            match self.poll_copy_progress(cx) {
1188                Poll::Ready(Ok(())) => {}
1189                Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
1190                Poll::Pending => return Poll::Pending,
1191            }
1192            // The file may actually be read-only and not support flush operations
1193            // at all, and there's nothing to flush in read-only state anyway.
1194            match self.state {
1195                CowState::ReadOnly(_) => Poll::Ready(Ok(())),
1196                _ => Pin::new(self.state.as_mut()).poll_flush(cx),
1197            }
1198        }
1199
1200        fn poll_shutdown(
1201            mut self: Pin<&mut Self>,
1202            cx: &mut std::task::Context<'_>,
1203        ) -> Poll<Result<(), std::io::Error>> {
1204            match self.poll_copy_progress(cx) {
1205                Poll::Ready(Ok(())) => {}
1206                Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
1207                Poll::Pending => return Poll::Pending,
1208            }
1209            // Same deal as flush above
1210            match self.state {
1211                CowState::ReadOnly(_) => Poll::Ready(Ok(())),
1212                _ => Pin::new(self.state.as_mut()).poll_shutdown(cx),
1213            }
1214        }
1215    }
1216
1217    impl<P> AsyncRead for CopyOnWriteFile<P>
1218    where
1219        P: FileSystem + 'static,
1220    {
1221        fn poll_read(
1222            mut self: Pin<&mut Self>,
1223            cx: &mut std::task::Context<'_>,
1224            buf: &mut tokio::io::ReadBuf<'_>,
1225        ) -> Poll<std::io::Result<()>> {
1226            match self.poll_copy_progress(cx) {
1227                Poll::Ready(Ok(())) => {}
1228                p => return p,
1229            }
1230            Pin::new(self.state.as_mut()).poll_read(cx, buf)
1231        }
1232    }
1233
1234    impl<P> AsyncSeek for CopyOnWriteFile<P>
1235    where
1236        P: FileSystem + 'static,
1237    {
1238        fn start_seek(
1239            mut self: Pin<&mut Self>,
1240            position: std::io::SeekFrom,
1241        ) -> std::io::Result<()> {
1242            match &mut self.state {
1243                CowState::ReadOnly(file)
1244                | CowState::SeekingGet(file)
1245                | CowState::Error { src: file, .. }
1246                | CowState::Copied(file)
1247                | CowState::SeekingRestore { dst: file, .. } => {
1248                    Pin::new(file.as_mut()).start_seek(position)
1249                }
1250                CowState::SeekingSet {
1251                    original_offset,
1252                    src,
1253                    ..
1254                }
1255                | CowState::Copying {
1256                    original_offset,
1257                    src,
1258                    ..
1259                } => {
1260                    *original_offset = match position {
1261                        SeekFrom::Current(delta) => original_offset
1262                            .checked_add_signed(delta)
1263                            .unwrap_or(*original_offset),
1264                        SeekFrom::Start(pos) => pos,
1265                        SeekFrom::End(pos) => src
1266                            .size()
1267                            .checked_add_signed(pos)
1268                            .unwrap_or(*original_offset),
1269                    };
1270                    Ok(())
1271                }
1272            }
1273        }
1274
1275        fn poll_complete(
1276            mut self: Pin<&mut Self>,
1277            cx: &mut std::task::Context<'_>,
1278        ) -> Poll<std::io::Result<u64>> {
1279            match self.poll_copy_progress(cx) {
1280                Poll::Ready(Ok(())) => {}
1281                Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
1282                Poll::Pending => return Poll::Pending,
1283            }
1284            Pin::new(self.state.as_mut()).poll_complete(cx)
1285        }
1286    }
1287
1288    tracing::trace!(
1289        path=%path.display(),
1290        options=?conf,
1291        "Opening the file in copy-on-write mode",
1292    );
1293    Ok(Box::new(CopyOnWriteFile::<P> {
1294        path: path.to_path_buf(),
1295        primary: primary.clone(),
1296        state: CowState::ReadOnly(file),
1297        readable: conf.read,
1298        append: conf.append,
1299        new_size: None,
1300        unlinked: false,
1301    }))
1302}
1303
1304impl<P, S> Debug for OverlayFileSystem<P, S>
1305where
1306    P: FileSystem,
1307    S: for<'a> FileSystems<'a>,
1308{
1309    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1310        struct IterFilesystems<'a, S>(&'a S);
1311        impl<S> Debug for IterFilesystems<'_, S>
1312        where
1313            S: for<'b> FileSystems<'b>,
1314        {
1315            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1316                let mut f = f.debug_list();
1317
1318                for fs in self.0.filesystems() {
1319                    f.entry(&fs);
1320                }
1321
1322                f.finish()
1323            }
1324        }
1325
1326        f.debug_struct("OverlayFileSystem")
1327            .field("primary", &self.primary)
1328            .field("secondaries", &IterFilesystems(&self.secondaries))
1329            .finish()
1330    }
1331}
1332
1333fn should_continue(e: FsError) -> bool {
1334    // HACK: We shouldn't really be ignoring FsError::BaseNotDirectory, but
1335    // it's needed because the mem_fs::FileSystem doesn't return
1336    // FsError::EntryNotFound when an intermediate directory doesn't exist
1337    // (i.e. the "/path/to" in "/path/to/file.txt").
1338    matches!(
1339        e,
1340        FsError::EntryNotFound | FsError::InvalidInput | FsError::BaseNotDirectory
1341    )
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346    use std::path::PathBuf;
1347
1348    use super::*;
1349    use crate::mem_fs::FileSystem as MemFS;
1350    use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
1351
1352    #[test]
1353    fn overlay_read_dir_rebases_mounted_entries() {
1354        let primary = MemFS::default();
1355        ops::create_dir_all(&primary, "/app").unwrap();
1356
1357        let volume = MemFS::default();
1358        ops::create_dir_all(&volume, "/themes/twentytwentyfour").unwrap();
1359
1360        let container = MemFS::default();
1361        ops::create_dir_all(&container, "/app/wp-content/themes/twentytwentyfour").unwrap();
1362
1363        let volume: Arc<dyn FileSystem + Send + Sync> = Arc::new(volume);
1364        primary
1365            .mount(
1366                PathBuf::from("/app/wp-content"),
1367                &volume,
1368                PathBuf::from("/"),
1369            )
1370            .unwrap();
1371
1372        let overlay = OverlayFileSystem::new(primary, [container]);
1373
1374        let mut entries: Vec<_> = overlay
1375            .read_dir(Path::new("/app/wp-content/themes"))
1376            .unwrap()
1377            .map(|entry| entry.unwrap().path)
1378            .collect();
1379        entries.sort();
1380
1381        assert_eq!(
1382            entries,
1383            vec![PathBuf::from("/app/wp-content/themes/twentytwentyfour")],
1384        );
1385    }
1386
1387    #[tokio::test]
1388    async fn remove_directory() {
1389        let primary = MemFS::default();
1390        let secondary = MemFS::default();
1391        let first = Path::new("/first");
1392        let second = Path::new("/second");
1393        let file_txt = second.join("file.txt");
1394        let third = Path::new("/third");
1395        primary.create_dir(first).unwrap();
1396        primary.create_dir(second).unwrap();
1397        primary
1398            .new_open_options()
1399            .create(true)
1400            .write(true)
1401            .open(&file_txt)
1402            .unwrap()
1403            .write_all(b"Hello, World!")
1404            .await
1405            .unwrap();
1406        secondary.create_dir(third).unwrap();
1407
1408        let overlay = OverlayFileSystem::new(primary, [secondary]);
1409
1410        // Delete a folder on the primary filesystem
1411        overlay.remove_dir(first).unwrap();
1412        assert_eq!(
1413            overlay.primary().metadata(first).unwrap_err(),
1414            FsError::EntryNotFound,
1415            "Deleted from primary"
1416        );
1417        assert!(!ops::exists(&overlay.secondaries[0], second));
1418
1419        // Directory on the primary fs isn't empty
1420        assert_eq!(
1421            overlay.remove_dir(second).unwrap_err(),
1422            FsError::DirectoryNotEmpty,
1423        );
1424
1425        // Try to remove something on one of the overlay filesystems
1426        assert_eq!(overlay.remove_dir(third), Ok(()));
1427
1428        // It should no longer exist
1429        assert_eq!(overlay.metadata(third).unwrap_err(), FsError::EntryNotFound);
1430
1431        assert!(ops::exists(&overlay.secondaries[0], third));
1432    }
1433
1434    #[tokio::test]
1435    async fn open_files() {
1436        let primary = MemFS::default();
1437        let secondary = MemFS::default();
1438        ops::create_dir_all(&primary, "/primary").unwrap();
1439        ops::touch(&primary, "/primary/read.txt").unwrap();
1440        ops::touch(&primary, "/primary/write.txt").unwrap();
1441        ops::create_dir_all(&secondary, "/secondary").unwrap();
1442        ops::touch(&secondary, "/secondary/read.txt").unwrap();
1443        ops::touch(&secondary, "/secondary/write.txt").unwrap();
1444        ops::create_dir_all(&secondary, "/primary").unwrap();
1445        ops::write(&secondary, "/primary/read.txt", "This is shadowed")
1446            .await
1447            .unwrap();
1448
1449        let fs = OverlayFileSystem::new(primary, [secondary]);
1450
1451        // Any new files will be created on the primary fs
1452        let _ = fs
1453            .new_open_options()
1454            .create(true)
1455            .write(true)
1456            .open("/new.txt")
1457            .unwrap();
1458        assert!(ops::exists(&fs.primary, "/new.txt"));
1459        assert!(!ops::exists(&fs.secondaries[0], "/new.txt"));
1460
1461        // You can open a file for reading and writing on the primary fs
1462        let _ = fs
1463            .new_open_options()
1464            .create(false)
1465            .write(true)
1466            .read(true)
1467            .open("/primary/write.txt")
1468            .unwrap();
1469
1470        // Files on the primary should always shadow the secondary
1471        let content = ops::read_to_string(&fs, "/primary/read.txt").await.unwrap();
1472        assert_ne!(content, "This is shadowed");
1473    }
1474
1475    #[tokio::test]
1476    async fn open_secondary_file_preserves_owned_buffer_access() {
1477        let primary = MemFS::default();
1478        let secondary = MemFS::default();
1479        ops::create_dir_all(&secondary, "/secondary").unwrap();
1480        secondary
1481            .insert_ro_file(
1482                "/secondary/buffer.txt".as_ref(),
1483                b"overlay-buffer".to_vec().into(),
1484            )
1485            .unwrap();
1486
1487        let fs = OverlayFileSystem::new(primary, [secondary]);
1488
1489        let file = fs
1490            .new_open_options()
1491            .read(true)
1492            .open("/secondary/buffer.txt")
1493            .unwrap();
1494
1495        let buffer = file
1496            .as_owned_buffer()
1497            .expect("secondary wrapper should preserve inner owned-buffer access");
1498        assert_eq!(buffer.as_slice(), b"overlay-buffer");
1499    }
1500
1501    #[tokio::test]
1502    async fn create_file_that_looks_like_it_is_in_a_secondary_filesystem_folder() {
1503        let primary = MemFS::default();
1504        let secondary = MemFS::default();
1505        ops::create_dir_all(&secondary, "/path/to/").unwrap();
1506        assert!(!ops::is_dir(&primary, "/path/to/"));
1507        let fs = OverlayFileSystem::new(primary, [secondary]);
1508
1509        ops::touch(&fs, "/path/to/file.txt").unwrap();
1510
1511        assert!(ops::is_dir(&fs.primary, "/path/to/"));
1512        assert!(ops::is_file(&fs.primary, "/path/to/file.txt"));
1513        assert!(!ops::is_file(&fs.secondaries[0], "/path/to/file.txt"));
1514    }
1515
1516    #[tokio::test]
1517    async fn listed_files_appear_overlaid() {
1518        let primary = MemFS::default();
1519        let secondary = MemFS::default();
1520        let secondary_overlaid = MemFS::default();
1521        ops::create_dir_all(&primary, "/primary").unwrap();
1522        ops::touch(&primary, "/primary/read.txt").unwrap();
1523        ops::touch(&primary, "/primary/write.txt").unwrap();
1524        ops::create_dir_all(&secondary, "/secondary").unwrap();
1525        ops::touch(&secondary, "/secondary/read.txt").unwrap();
1526        ops::touch(&secondary, "/secondary/write.txt").unwrap();
1527        // This second "secondary" filesystem should share the same folders as
1528        // the first one.
1529        ops::create_dir_all(&secondary_overlaid, "/secondary").unwrap();
1530        ops::touch(&secondary_overlaid, "/secondary/overlayed.txt").unwrap();
1531
1532        let fs = OverlayFileSystem::new(primary, [secondary, secondary_overlaid]);
1533
1534        let paths: Vec<_> = ops::walk(&fs, "/").map(|entry| entry.path()).collect();
1535        assert_eq!(
1536            paths,
1537            vec![
1538                PathBuf::from("/secondary"),
1539                PathBuf::from("/secondary/write.txt"),
1540                PathBuf::from("/secondary/read.txt"),
1541                PathBuf::from("/secondary/overlayed.txt"),
1542                PathBuf::from("/primary"),
1543                PathBuf::from("/primary/write.txt"),
1544                PathBuf::from("/primary/read.txt"),
1545            ]
1546        );
1547    }
1548
1549    #[tokio::test]
1550    async fn open_secondary_fs_files_in_write_mode() {
1551        let primary = MemFS::default();
1552        let secondary = MemFS::default();
1553        ops::create_dir_all(&secondary, "/secondary").unwrap();
1554        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1555            .await
1556            .unwrap();
1557
1558        let fs = OverlayFileSystem::new(primary, [secondary]);
1559
1560        let mut f = fs
1561            .new_open_options()
1562            .write(true)
1563            .read(true)
1564            .open("/secondary/file.txt")
1565            .unwrap();
1566        // reading is fine
1567        let mut buf = String::new();
1568        f.read_to_string(&mut buf).await.unwrap();
1569        assert_eq!(buf, "Hello, World!");
1570        f.seek(SeekFrom::Start(0)).await.unwrap();
1571        // next we will write a new set of bytes
1572        f.set_len(0).unwrap();
1573        assert_eq!(f.write(b"Hi").await.unwrap(), 2);
1574        // Same with flushing
1575        assert_eq!(f.flush().await.unwrap(), ());
1576
1577        // if we now read it then the data should be different
1578        buf = String::new();
1579        f.seek(SeekFrom::Start(0)).await.unwrap();
1580        f.read_to_string(&mut buf).await.unwrap();
1581        assert_eq!(buf, "Hi");
1582        drop(f);
1583
1584        // including if we open it again
1585        let mut f = fs
1586            .new_open_options()
1587            .read(true)
1588            .open("/secondary/file.txt")
1589            .unwrap();
1590        buf = String::new();
1591        f.read_to_string(&mut buf).await.unwrap();
1592        assert_eq!(buf, "Hi");
1593    }
1594
1595    #[tokio::test]
1596    async fn open_secondary_fs_files_unlink() {
1597        let primary = MemFS::default();
1598        let secondary = MemFS::default();
1599        ops::create_dir_all(&secondary, "/secondary").unwrap();
1600        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1601            .await
1602            .unwrap();
1603
1604        let fs = OverlayFileSystem::new(primary, [secondary]);
1605
1606        fs.metadata(Path::new("/secondary/file.txt")).unwrap();
1607
1608        // Now delete the file and make sure its not found
1609        fs.remove_file(Path::new("/secondary/file.txt")).unwrap();
1610        assert_eq!(
1611            fs.metadata(Path::new("/secondary/file.txt")).unwrap_err(),
1612            FsError::EntryNotFound
1613        )
1614    }
1615
1616    #[tokio::test]
1617    async fn open_secondary_fs_without_cow() {
1618        let primary = MemFS::default();
1619        let secondary = MemFS::default();
1620        ops::create_dir_all(&secondary, "/secondary").unwrap();
1621        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1622            .await
1623            .unwrap();
1624
1625        let fs = OverlayFileSystem::new(primary, [secondary]);
1626
1627        let mut f = fs
1628            .new_open_options()
1629            .create(true)
1630            .read(true)
1631            .open(Path::new("/secondary/file.txt"))
1632            .unwrap();
1633        assert_eq!(f.size() as usize, 13);
1634
1635        let mut buf = String::new();
1636        f.read_to_string(&mut buf).await.unwrap();
1637        assert_eq!(buf, "Hello, World!");
1638
1639        // it should not be in the primary and nor should the secondary folder
1640        assert!(!ops::is_dir(&fs.primary, "/secondary"));
1641        assert!(!ops::is_file(&fs.primary, "/secondary/file.txt"));
1642        assert!(ops::is_dir(&fs.secondaries[0], "/secondary"));
1643        assert!(ops::is_file(&fs.secondaries[0], "/secondary/file.txt"));
1644    }
1645
1646    #[tokio::test]
1647    async fn unlink_open_secondary_fs_without_cow_keeps_handle_alive() {
1648        let primary = MemFS::default();
1649        let secondary = MemFS::default();
1650        ops::create_dir_all(&secondary, "/secondary").unwrap();
1651        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1652            .await
1653            .unwrap();
1654
1655        let fs = OverlayFileSystem::new(primary, [secondary]);
1656
1657        let mut f = fs
1658            .new_open_options()
1659            .read(true)
1660            .open(Path::new("/secondary/file.txt"))
1661            .unwrap();
1662
1663        f.unlink().unwrap();
1664        assert_eq!(
1665            fs.metadata(Path::new("/secondary/file.txt")).unwrap_err(),
1666            FsError::EntryNotFound
1667        );
1668        assert!(ops::is_file(&fs.primary, "/secondary/.wh.file.txt"));
1669        assert!(!ops::is_file(&fs.primary, "/secondary/file.txt"));
1670        assert!(ops::is_file(&fs.secondaries[0], "/secondary/file.txt"));
1671
1672        let mut buf = String::new();
1673        f.read_to_string(&mut buf).await.unwrap();
1674        assert_eq!(buf, "Hello, World!");
1675    }
1676
1677    #[tokio::test]
1678    async fn unlink_open_secondary_fs_without_cow_returns_not_found_when_whiteout_already_exists() {
1679        let primary = MemFS::default();
1680        let secondary = MemFS::default();
1681        ops::create_dir_all(&secondary, "/secondary").unwrap();
1682        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1683            .await
1684            .unwrap();
1685
1686        let fs = OverlayFileSystem::new(primary, [secondary]);
1687
1688        let mut f = fs
1689            .new_open_options()
1690            .read(true)
1691            .open(Path::new("/secondary/file.txt"))
1692            .unwrap();
1693
1694        // The path was already deleted via another route before this handle
1695        // called unlink(). The handle should see EntryNotFound, not Ok(()).
1696        ops::create_white_out(&fs.primary, "/secondary/file.txt").unwrap();
1697        assert_eq!(f.unlink(), Err(FsError::EntryNotFound));
1698    }
1699
1700    #[tokio::test]
1701    async fn unlink_open_secondary_fs_without_cow_preserves_whiteout_creation_error() {
1702        let primary = MemFS::default();
1703        let secondary = MemFS::default();
1704        ops::write(&primary, "/secondary", b"not a directory")
1705            .await
1706            .unwrap();
1707        ops::create_dir_all(&secondary, "/secondary").unwrap();
1708        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1709            .await
1710            .unwrap();
1711
1712        let fs = OverlayFileSystem::new(primary, [secondary]);
1713
1714        let mut f = fs
1715            .new_open_options()
1716            .read(true)
1717            .open(Path::new("/secondary/file.txt"))
1718            .unwrap();
1719
1720        assert_eq!(f.unlink(), Err(FsError::BaseNotDirectory));
1721    }
1722
1723    #[tokio::test]
1724    async fn create_and_append_secondary_fs_with_cow() {
1725        let primary = MemFS::default();
1726        let secondary = MemFS::default();
1727        ops::create_dir_all(&secondary, "/secondary").unwrap();
1728        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1729            .await
1730            .unwrap();
1731
1732        let fs = OverlayFileSystem::new(primary, [secondary]);
1733
1734        let mut f = fs
1735            .new_open_options()
1736            .create(true)
1737            .append(true)
1738            .read(true)
1739            .open(Path::new("/secondary/file.txt"))
1740            .unwrap();
1741        assert_eq!(f.size() as usize, 13);
1742
1743        f.write_all(b"asdf").await.unwrap();
1744        assert_eq!(f.size() as usize, 17);
1745
1746        f.seek(SeekFrom::Start(0)).await.unwrap();
1747
1748        let mut buf = String::new();
1749        f.read_to_string(&mut buf).await.unwrap();
1750        assert_eq!(buf, "Hello, World!asdf");
1751
1752        // Now lets check the file systems under
1753        let f = fs
1754            .primary
1755            .new_open_options()
1756            .create(true)
1757            .append(true)
1758            .read(true)
1759            .open(Path::new("/secondary/file.txt"))
1760            .unwrap();
1761        assert_eq!(f.size() as usize, 17);
1762        let f = fs.secondaries[0]
1763            .new_open_options()
1764            .create(true)
1765            .append(true)
1766            .read(true)
1767            .open(Path::new("/secondary/file.txt"))
1768            .unwrap();
1769        assert_eq!(f.size() as usize, 13);
1770
1771        // it should now exist in both the primary and secondary
1772        assert!(ops::is_dir(&fs.primary, "/secondary"));
1773        assert!(ops::is_file(&fs.primary, "/secondary/file.txt"));
1774        assert!(ops::is_dir(&fs.secondaries[0], "/secondary"));
1775        assert!(ops::is_file(&fs.secondaries[0], "/secondary/file.txt"));
1776    }
1777
1778    #[tokio::test]
1779    async fn unlink_open_secondary_fs_with_cow_does_not_recreate_path() {
1780        let primary = MemFS::default();
1781        let secondary = MemFS::default();
1782        ops::create_dir_all(&secondary, "/secondary").unwrap();
1783        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1784            .await
1785            .unwrap();
1786
1787        let fs = OverlayFileSystem::new(primary, [secondary]);
1788
1789        let mut f = fs
1790            .new_open_options()
1791            .append(true)
1792            .read(true)
1793            .open(Path::new("/secondary/file.txt"))
1794            .unwrap();
1795
1796        f.unlink().unwrap();
1797        assert_eq!(
1798            fs.metadata(Path::new("/secondary/file.txt")).unwrap_err(),
1799            FsError::EntryNotFound
1800        );
1801        assert!(ops::is_file(&fs.primary, "/secondary/.wh.file.txt"));
1802        assert!(!ops::is_file(&fs.primary, "/secondary/file.txt"));
1803        assert!(ops::is_file(&fs.secondaries[0], "/secondary/file.txt"));
1804
1805        f.write_all(b"asdf").await.unwrap();
1806        f.seek(SeekFrom::Start(0)).await.unwrap();
1807
1808        let mut buf = String::new();
1809        f.read_to_string(&mut buf).await.unwrap();
1810        assert_eq!(buf, "Hello, World!asdf");
1811
1812        assert_eq!(
1813            fs.metadata(Path::new("/secondary/file.txt")).unwrap_err(),
1814            FsError::EntryNotFound
1815        );
1816        assert!(ops::is_file(&fs.primary, "/secondary/.wh.file.txt"));
1817        assert!(!ops::is_file(&fs.primary, "/secondary/file.txt"));
1818        assert!(ops::is_file(&fs.secondaries[0], "/secondary/file.txt"));
1819    }
1820
1821    /// Regression test: unlinking a handle opened from the secondary must not
1822    /// destroy a file that was created in the primary at the same path *after*
1823    /// the handle was opened.
1824    #[tokio::test]
1825    async fn unlink_secondary_handle_does_not_delete_later_primary_file() {
1826        let primary = MemFS::default();
1827        let secondary = MemFS::default();
1828        ops::create_dir_all(&secondary, "/dir").unwrap();
1829        ops::write(&secondary, "/dir/file.txt", b"secondary")
1830            .await
1831            .unwrap();
1832
1833        let fs = OverlayFileSystem::new(primary, [secondary]);
1834
1835        // Open from secondary (read-only → SecondaryFile).
1836        let mut f = fs
1837            .new_open_options()
1838            .read(true)
1839            .open(Path::new("/dir/file.txt"))
1840            .unwrap();
1841
1842        // A new file is created at the same path in primary AFTER the handle
1843        // was opened.
1844        ops::create_dir_all(&fs.primary, "/dir").unwrap();
1845        ops::write(&fs.primary, "/dir/file.txt", b"primary replacement")
1846            .await
1847            .unwrap();
1848
1849        // Unlinking the old handle should only whiteout the secondary entry,
1850        // never delete the new primary file.
1851        f.unlink().unwrap();
1852
1853        // The whiteout must exist.
1854        assert!(ops::is_file(&fs.primary, "/dir/.wh.file.txt"));
1855        // The newer primary file must still be intact.
1856        assert!(ops::is_file(&fs.primary, "/dir/file.txt"));
1857        assert_eq!(
1858            ops::read_to_string(&fs.primary, "/dir/file.txt")
1859                .await
1860                .unwrap(),
1861            "primary replacement"
1862        );
1863    }
1864
1865    /// Regression test: same correctness requirement for a COW handle that has
1866    /// not yet been flushed to primary when unlink() is called.
1867    #[tokio::test]
1868    async fn unlink_cow_handle_does_not_delete_later_primary_file() {
1869        let primary = MemFS::default();
1870        let secondary = MemFS::default();
1871        ops::create_dir_all(&secondary, "/dir").unwrap();
1872        ops::write(&secondary, "/dir/file.txt", b"secondary")
1873            .await
1874            .unwrap();
1875
1876        let fs = OverlayFileSystem::new(primary, [secondary]);
1877
1878        // Open with write access → CopyOnWriteFile (still in ReadOnly state,
1879        // no write has happened yet so nothing is in primary).
1880        let mut f = fs
1881            .new_open_options()
1882            .write(true)
1883            .read(true)
1884            .open(Path::new("/dir/file.txt"))
1885            .unwrap();
1886
1887        // A new file is created at the same path in primary AFTER the handle
1888        // was opened.
1889        ops::create_dir_all(&fs.primary, "/dir").unwrap();
1890        ops::write(&fs.primary, "/dir/file.txt", b"primary replacement")
1891            .await
1892            .unwrap();
1893
1894        // Unlink before any write → COW copy has not started, so the primary
1895        // file we just created must not be deleted.
1896        f.unlink().unwrap();
1897
1898        assert!(ops::is_file(&fs.primary, "/dir/.wh.file.txt"));
1899        assert!(ops::is_file(&fs.primary, "/dir/file.txt"));
1900        assert_eq!(
1901            ops::read_to_string(&fs.primary, "/dir/file.txt")
1902                .await
1903                .unwrap(),
1904            "primary replacement"
1905        );
1906    }
1907
1908    #[tokio::test]
1909    async fn unlink_file_from_secondary_fs() {
1910        let primary = MemFS::default();
1911        let secondary = MemFS::default();
1912        ops::create_dir_all(&secondary, "/secondary").unwrap();
1913        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1914            .await
1915            .unwrap();
1916
1917        let fs = OverlayFileSystem::new(primary, [secondary]);
1918
1919        fs.remove_file(Path::new("/secondary/file.txt")).unwrap();
1920        assert_eq!(ops::exists(&fs, Path::new("/secondary/file.txt")), false);
1921
1922        assert!(ops::is_file(&fs.primary, "/secondary/.wh.file.txt"));
1923        assert!(ops::is_file(&fs.secondaries[0], "/secondary/file.txt"));
1924
1925        // Now create the file again after the unlink
1926        let mut f = fs
1927            .new_open_options()
1928            .create(true)
1929            .write(true)
1930            .read(true)
1931            .open(Path::new("/secondary/file.txt"))
1932            .unwrap();
1933        assert_eq!(f.size() as usize, 0);
1934        f.write_all(b"asdf").await.unwrap();
1935        assert_eq!(f.size() as usize, 4);
1936
1937        // The whiteout should be gone and new file exist
1938        assert!(!ops::is_file(&fs.primary, "/secondary/.wh.file.txt"));
1939        assert!(ops::is_file(&fs.primary, "/secondary/file.txt"));
1940        assert!(ops::is_file(&fs.secondaries[0], "/secondary/file.txt"));
1941    }
1942
1943    #[tokio::test]
1944    async fn rmdir_from_secondary_fs() {
1945        let primary = MemFS::default();
1946        let secondary = MemFS::default();
1947        ops::create_dir_all(&secondary, "/secondary").unwrap();
1948
1949        let fs = OverlayFileSystem::new(primary, [secondary]);
1950
1951        assert!(ops::is_dir(&fs, "/secondary"));
1952        fs.remove_dir(Path::new("/secondary")).unwrap();
1953
1954        assert!(!ops::is_dir(&fs, "/secondary"));
1955        assert!(ops::is_file(&fs.primary, "/.wh.secondary"));
1956        assert!(ops::is_dir(&fs.secondaries[0], "/secondary"));
1957
1958        fs.create_dir(Path::new("/secondary")).unwrap();
1959        assert!(ops::is_dir(&fs, "/secondary"));
1960        assert!(ops::is_dir(&fs.primary, "/secondary"));
1961        assert!(!ops::is_file(&fs.primary, "/.wh.secondary"));
1962        assert!(ops::is_dir(&fs.secondaries[0], "/secondary"));
1963    }
1964
1965    #[tokio::test]
1966    async fn rmdir_sub_from_secondary_fs() {
1967        let primary = MemFS::default();
1968        let secondary = MemFS::default();
1969        ops::create_dir_all(&secondary, "/first/secondary").unwrap();
1970
1971        let fs = OverlayFileSystem::new(primary, [secondary]);
1972
1973        assert!(ops::is_dir(&fs, "/first/secondary"));
1974        fs.remove_dir(Path::new("/first/secondary")).unwrap();
1975
1976        assert!(!ops::is_dir(&fs, "/first/secondary"));
1977        assert!(ops::is_file(&fs.primary, "/first/.wh.secondary"));
1978        assert!(ops::is_dir(&fs.secondaries[0], "/first/secondary"));
1979
1980        fs.create_dir(Path::new("/first/secondary")).unwrap();
1981        assert!(ops::is_dir(&fs, "/first/secondary"));
1982        assert!(ops::is_dir(&fs.primary, "/first/secondary"));
1983        assert!(!ops::is_file(&fs.primary, "/first/.wh.secondary"));
1984        assert!(ops::is_dir(&fs.secondaries[0], "/first/secondary"));
1985    }
1986
1987    #[tokio::test]
1988    async fn create_new_secondary_fs_without_cow() {
1989        let primary = MemFS::default();
1990        let secondary = MemFS::default();
1991        ops::create_dir_all(&secondary, "/secondary").unwrap();
1992        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
1993            .await
1994            .unwrap();
1995
1996        let fs = OverlayFileSystem::new(primary, [secondary]);
1997
1998        let mut f = fs
1999            .new_open_options()
2000            .create_new(true)
2001            .read(true)
2002            .open(Path::new("/secondary/file.txt"))
2003            .unwrap();
2004        assert_eq!(f.size() as usize, 0);
2005
2006        let mut buf = String::new();
2007        f.read_to_string(&mut buf).await.unwrap();
2008        assert_eq!(buf, "");
2009
2010        // it should now exist in both the primary and secondary
2011        assert!(ops::is_dir(&fs.primary, "/secondary"));
2012        assert!(ops::is_file(&fs.primary, "/secondary/file.txt"));
2013        assert!(ops::is_dir(&fs.secondaries[0], "/secondary"));
2014        assert!(ops::is_file(&fs.secondaries[0], "/secondary/file.txt"));
2015    }
2016
2017    #[tokio::test]
2018    async fn open_secondary_fs_files_remove_dir() {
2019        let primary = MemFS::default();
2020        let secondary = MemFS::default();
2021        ops::create_dir_all(&secondary, "/secondary").unwrap();
2022
2023        let fs = OverlayFileSystem::new(primary, [secondary]);
2024
2025        fs.metadata(Path::new("/secondary")).unwrap();
2026
2027        // Now delete the file and make sure its not found
2028        fs.remove_dir(Path::new("/secondary")).unwrap();
2029        assert_eq!(
2030            fs.metadata(Path::new("/secondary")).unwrap_err(),
2031            FsError::EntryNotFound
2032        )
2033    }
2034
2035    /// Make sure files that are never written are not copied to the primary,
2036    /// even when opened with write permissions.
2037    /// Regression test for https://github.com/wasmerio/wasmer/issues/5445
2038    #[tokio::test]
2039    async fn test_overlayfs_readonly_files_not_copied() {
2040        let primary = MemFS::default();
2041        let secondary = MemFS::default();
2042        ops::create_dir_all(&secondary, "/secondary").unwrap();
2043        ops::write(&secondary, "/secondary/file.txt", b"Hello, World!")
2044            .await
2045            .unwrap();
2046
2047        let fs = OverlayFileSystem::new(primary, [secondary]);
2048
2049        {
2050            let mut f = fs
2051                .new_open_options()
2052                .read(true)
2053                .write(true)
2054                .open(Path::new("/secondary/file.txt"))
2055                .unwrap();
2056            let mut s = String::new();
2057            f.read_to_string(&mut s).await.unwrap();
2058            assert_eq!(s, "Hello, World!");
2059
2060            f.flush().await.unwrap();
2061            f.shutdown().await.unwrap();
2062        }
2063
2064        // Primary should not have the file
2065        assert!(!ops::is_file(&fs.primary, "/secondary/file.txt"));
2066    }
2067
2068    // OLD tests that used WebcFileSystem.
2069    // Should be re-implemented with WebcVolumeFs
2070    // #[tokio::test]
2071    // async fn wasi_runner_use_case() {
2072    //     // Set up some dummy files on the host
2073    //     let temp = TempDir::new().unwrap();
2074    //     let first = temp.path().join("first");
2075    //     let file_txt = first.join("file.txt");
2076    //     let second = temp.path().join("second");
2077    //     std::fs::create_dir_all(&first).unwrap();
2078    //     std::fs::write(&file_txt, b"First!").unwrap();
2079    //     std::fs::create_dir_all(&second).unwrap();
2080    //     // configure the union FS so things are saved in memory by default
2081    //     // (initialized with a set of unix-like folders), but certain folders
2082    //     // are first to the host.
2083    //     let primary = RootFileSystemBuilder::new().build();
2084    //     let host_fs: Arc<dyn FileSystem + Send + Sync> =
2085    //         Arc::new(crate::host_fs::FileSystem::default());
2086    //     let first_dirs = [(&first, "/first"), (&second, "/second")];
2087    //     for (host, guest) in first_dirs {
2088    //         primary
2089    //             .mount(PathBuf::from(guest), &host_fs, host.clone())
2090    //             .unwrap();
2091    //     }
2092    //     // Set up the secondary file systems
2093    //     let webc = WebCOwned::parse(Bytes::from_static(PYTHON), &ParseOptions::default()).unwrap();
2094    //     let webc = WebcFileSystem::init_all(Arc::new(webc));
2095    //
2096    //     let fs = OverlayFileSystem::new(primary, [webc]);
2097    //
2098    //     // We should get all the normal directories from rootfs (primary)
2099    //     assert!(ops::is_dir(&fs, "/lib"));
2100    //     assert!(ops::is_dir(&fs, "/bin"));
2101    //     assert!(ops::is_file(&fs, "/dev/stdin"));
2102    //     assert!(ops::is_file(&fs, "/dev/stdout"));
2103    //     // We also want to see files from the WEBC volumes (secondary)
2104    //     assert!(ops::is_dir(&fs, "/lib/python3.6"));
2105    //     assert!(ops::is_file(&fs, "/lib/python3.6/collections/__init__.py"));
2106    //     #[cfg(never)]
2107    //     {
2108    //         // files on a secondary fs aren't writable
2109    //         // TODO(Michael-F-Bryan): re-enable this if/when we fix
2110    //         // open_readonly_file_hack()
2111    //         assert_eq!(
2112    //             fs.new_open_options()
2113    //                 .append(true)
2114    //                 .open("/lib/python3.6/collections/__init__.py")
2115    //                 .unwrap_err(),
2116    //             FsError::PermissionDenied,
2117    //         );
2118    //     }
2119    //     // you are allowed to create files that look like they are in a secondary
2120    //     // folder, though
2121    //     ops::touch(&fs, "/lib/python3.6/collections/something-else.py").unwrap();
2122    //     // But it'll be on the primary filesystem, not the secondary one
2123    //     assert!(ops::is_file(
2124    //         &fs.primary,
2125    //         "/lib/python3.6/collections/something-else.py"
2126    //     ));
2127    //     assert!(!ops::is_file(
2128    //         &fs.secondaries[0],
2129    //         "/lib/python3.6/collections/something-else.py"
2130    //     ));
2131    //     // You can do the same thing with folders
2132    //     fs.create_dir("/lib/python3.6/something-else".as_ref())
2133    //         .unwrap();
2134    //     assert!(ops::is_dir(&fs.primary, "/lib/python3.6/something-else"));
2135    //     assert!(!ops::is_dir(
2136    //         &fs.secondaries[0],
2137    //         "/lib/python3.6/something-else"
2138    //     ));
2139    //     // It only works when you are directly inside an existing directory
2140    //     // on the secondary filesystem, though
2141    //     assert_eq!(
2142    //         ops::touch(&fs, "/lib/python3.6/collections/this/doesnt/exist.txt").unwrap_err(),
2143    //         FsError::EntryNotFound
2144    //     );
2145    //     // you should also be able to read files mounted from the host
2146    //     assert!(ops::is_dir(&fs, "/first"));
2147    //     assert!(ops::is_file(&fs, "/first/file.txt"));
2148    //     assert_eq!(
2149    //         ops::read_to_string(&fs, "/first/file.txt").await.unwrap(),
2150    //         "First!"
2151    //     );
2152    //     // Overwriting them is fine and we'll see the changes on the host
2153    //     ops::write(&fs, "/first/file.txt", "Updated").await.unwrap();
2154    //     assert_eq!(std::fs::read_to_string(&file_txt).unwrap(), "Updated");
2155    //     // The filesystem will see changes on the host that happened after it was
2156    //     // set up
2157    //     let another = second.join("another.txt");
2158    //     std::fs::write(&another, "asdf").unwrap();
2159    //     assert_eq!(
2160    //         ops::read_to_string(&fs, "/second/another.txt")
2161    //             .await
2162    //             .unwrap(),
2163    //         "asdf"
2164    //     );
2165    // }
2166    //
2167    // #[tokio::test]
2168    // async fn absolute_and_relative_paths_are_passed_through() {
2169    //     let python = Arc::new(load_webc(PYTHON));
2170    //
2171    //     // The underlying filesystem doesn't care about absolute/relative paths
2172    //     assert_eq!(python.read_dir("/lib".as_ref()).unwrap().count(), 4);
2173    //     assert_eq!(python.read_dir("lib".as_ref()).unwrap().count(), 4);
2174    //
2175    //     // read_dir() should be passed through to the primary
2176    //     let webc_primary =
2177    //         OverlayFileSystem::new(Arc::clone(&python), [crate::EmptyFileSystem::default()]);
2178    //     assert_same_directory_contents(&python, "/lib", &webc_primary);
2179    //     assert_same_directory_contents(&python, "lib", &webc_primary);
2180    //
2181    //     // read_dir() should also be passed through to the secondary
2182    //     let webc_secondary =
2183    //         OverlayFileSystem::new(crate::EmptyFileSystem::default(), [Arc::clone(&python)]);
2184    //     assert_same_directory_contents(&python, "/lib", &webc_secondary);
2185    //     assert_same_directory_contents(&python, "lib", &webc_secondary);
2186    //
2187    //     // It should be fine to overlay the root fs on top of our webc file
2188    //     let overlay_rootfs = OverlayFileSystem::new(
2189    //         RootFileSystemBuilder::default().build(),
2190    //         [Arc::clone(&python)],
2191    //     );
2192    //     assert_same_directory_contents(&python, "/lib", &overlay_rootfs);
2193    //     assert_same_directory_contents(&python, "lib", &overlay_rootfs);
2194    // }
2195    // #[track_caller]
2196    // fn assert_same_directory_contents(
2197    //     original: &dyn FileSystem,
2198    //     path: impl AsRef<Path>,
2199    //     candidate: &dyn FileSystem,
2200    // ) {
2201    //     let path = path.as_ref();
2202    //
2203    //     let original_entries: Vec<_> = original
2204    //         .read_dir(path)
2205    //         .unwrap()
2206    //         .map(|r| r.unwrap())
2207    //         .collect();
2208    //     let candidate_entries: Vec<_> = candidate
2209    //         .read_dir(path)
2210    //         .unwrap()
2211    //         .map(|r| r.unwrap())
2212    //         .collect();
2213    //
2214    //     assert_eq!(original_entries, candidate_entries);
2215    // }
2216}