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