Skip to main content

a3s_box_runtime/rootfs/
provider.rs

1//! Rootfs provider — abstracts how a rootfs directory is prepared for a box.
2//!
3//! Two built-in providers:
4//! - `CopyProvider` — full recursive copy (works everywhere, current default)
5//! - `OverlayProvider` — Linux overlayfs mount (near-instant, CoW)
6
7use std::path::{Path, PathBuf};
8
9use a3s_box_core::error::{BoxError, Result};
10
11/// Abstracts how a rootfs directory is prepared for a box from a cached lower layer.
12pub trait RootfsProvider: Send + Sync {
13    /// Prepare a rootfs at `box_dir` from the cached read-only layer at `cache_dir`.
14    /// Returns the path to use as `InstanceSpec.rootfs_path`.
15    fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf>;
16
17    /// Prepare an empty writable rootfs for an OCI cache miss.
18    fn prepare_empty(&self, box_dir: &Path) -> Result<PathBuf> {
19        let rootfs = box_dir.join("rootfs");
20        std::fs::create_dir_all(&rootfs).map_err(|error| {
21            BoxError::BuildError(format!(
22                "Failed to create rootfs {}: {error}",
23                rootfs.display()
24            ))
25        })?;
26        Ok(rootfs)
27    }
28
29    /// Cleanup after box stops.
30    ///
31    /// When `persistent` is true, the writable layer (overlay upper dir or copy
32    /// rootfs) is preserved on disk so changes survive the next start.
33    /// When false, the writable layer is wiped for a clean slate.
34    fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()>;
35
36    /// Human-readable name for logging.
37    fn name(&self) -> &'static str;
38}
39
40/// Full recursive copy provider — works on all platforms.
41///
42/// This is the original behavior: copies the entire cached rootfs into
43/// `box_dir/rootfs/`. Safe but slow for large images.
44pub struct CopyProvider;
45
46impl RootfsProvider for CopyProvider {
47    fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
48        let rootfs = box_dir.join("rootfs");
49        // Reuse existing rootfs when persistent and already populated
50        if rootfs.exists() {
51            tracing::info!(path = %rootfs.display(), "Reusing persistent rootfs");
52            return Ok(rootfs);
53        }
54        crate::cache::layer_cache::copy_dir_recursive(cache_dir, &rootfs)?;
55        Ok(rootfs)
56    }
57
58    fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()> {
59        if persistent {
60            tracing::info!("Persistent box: keeping rootfs on disk");
61            return Ok(());
62        }
63        let rootfs = box_dir.join("rootfs");
64        if rootfs.exists() {
65            std::fs::remove_dir_all(&rootfs).map_err(|e| {
66                BoxError::BuildError(format!(
67                    "Failed to remove rootfs {}: {}",
68                    rootfs.display(),
69                    e
70                ))
71            })?;
72        }
73        Ok(())
74    }
75
76    fn name(&self) -> &'static str {
77        "copy"
78    }
79}
80
81/// A copy provider backed by a case-sensitive APFS sparse image.
82///
83/// macOS commonly stores `~/.a3s` on case-insensitive APFS. Passing a normal
84/// host directory to libkrun as the guest root would then make Linux paths such
85/// as `/bin` and `/BIN` aliases. Each box therefore owns a sparse, dynamically
86/// allocated case-sensitive APFS image and exposes its mountpoint via virtiofs.
87#[cfg(target_os = "macos")]
88pub struct CaseSensitiveApfsProvider;
89
90#[cfg(target_os = "macos")]
91impl CaseSensitiveApfsProvider {
92    // v2 stores the Linux tree below a private directory inside the volume.
93    // APFS creates volume-management entries such as `.fseventsd` at the
94    // volume root; exposing that root to the guest both leaks host artifacts
95    // and can make recursive rootfs walks fail with EACCES.
96    const IMAGE_STEM: &'static str = "rootfs-apfs-v2";
97    const IMAGE_NAME: &'static str = "rootfs-apfs-v2.sparseimage";
98    const DATA_DIR: &'static str = ".a3s-rootfs";
99
100    fn clone_image(source: &Path, destination: &Path) -> Result<()> {
101        let output = std::process::Command::new("cp")
102            .arg("-c")
103            .arg(source)
104            .arg(destination)
105            .output()
106            .map_err(|error| {
107                BoxError::BuildError(format!("Failed to start APFS clone: {error}"))
108            })?;
109        if !output.status.success() {
110            return Err(BoxError::BuildError(format!(
111                "Failed to clone cached APFS rootfs {}: {}",
112                source.display(),
113                String::from_utf8_lossy(&output.stderr).trim()
114            )));
115        }
116        Ok(())
117    }
118
119    fn mount(&self, box_dir: &Path) -> Result<PathBuf> {
120        use std::process::Command;
121
122        std::fs::create_dir_all(box_dir).map_err(|error| {
123            BoxError::BuildError(format!(
124                "Failed to create box directory {}: {error}",
125                box_dir.display()
126            ))
127        })?;
128        let rootfs = box_dir.join("rootfs");
129        std::fs::create_dir_all(&rootfs).map_err(|error| {
130            BoxError::BuildError(format!(
131                "Failed to create APFS mountpoint {}: {error}",
132                rootfs.display()
133            ))
134        })?;
135        if super::is_mountpoint(&rootfs) {
136            return Self::data_dir(&rootfs);
137        }
138
139        let image = box_dir.join(Self::IMAGE_NAME);
140        if !image.exists() {
141            let stem = box_dir.join(Self::IMAGE_STEM);
142            let output = Command::new("hdiutil")
143                .args([
144                    "create",
145                    "-quiet",
146                    "-size",
147                    "64g",
148                    "-type",
149                    "SPARSE",
150                    "-fs",
151                    "Case-sensitive APFS",
152                    "-volname",
153                    "A3SRootfs",
154                ])
155                .arg(&stem)
156                .output()
157                .map_err(|error| {
158                    BoxError::BuildError(format!("Failed to start hdiutil create: {error}"))
159                })?;
160            if !output.status.success() {
161                return Err(BoxError::BuildError(format!(
162                    "Failed to create case-sensitive APFS rootfs image: {}",
163                    String::from_utf8_lossy(&output.stderr).trim()
164                )));
165            }
166        }
167
168        let output = Command::new("hdiutil")
169            .args([
170                "attach",
171                "-quiet",
172                "-nobrowse",
173                "-owners",
174                "on",
175                "-mountpoint",
176            ])
177            .arg(&rootfs)
178            .arg(&image)
179            .output()
180            .map_err(|error| {
181                BoxError::BuildError(format!("Failed to start hdiutil attach: {error}"))
182            })?;
183        if !output.status.success() {
184            return Err(BoxError::BuildError(format!(
185                "Failed to mount case-sensitive APFS rootfs image {}: {}",
186                image.display(),
187                String::from_utf8_lossy(&output.stderr).trim()
188            )));
189        }
190        if !super::is_mountpoint(&rootfs) {
191            return Err(BoxError::BuildError(format!(
192                "hdiutil did not mount the rootfs image at {}",
193                rootfs.display()
194            )));
195        }
196        Self::data_dir(&rootfs)
197    }
198
199    fn data_dir(mountpoint: &Path) -> Result<PathBuf> {
200        let data = mountpoint.join(Self::DATA_DIR);
201        std::fs::create_dir_all(&data).map_err(|error| {
202            BoxError::BuildError(format!(
203                "Failed to create APFS rootfs data directory {}: {error}",
204                data.display()
205            ))
206        })?;
207        Ok(data)
208    }
209}
210
211#[cfg(target_os = "macos")]
212impl RootfsProvider for CaseSensitiveApfsProvider {
213    fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
214        let image = box_dir.join(Self::IMAGE_NAME);
215        if cache_dir.is_file() && !image.exists() {
216            std::fs::create_dir_all(box_dir).map_err(BoxError::IoError)?;
217            Self::clone_image(cache_dir, &image)?;
218        }
219        let rootfs = self.mount(box_dir)?;
220        if cache_dir.is_file() {
221            return Ok(rootfs);
222        }
223        if std::fs::read_dir(&rootfs)
224            .map_err(|error| BoxError::BuildError(error.to_string()))?
225            .next()
226            .is_none()
227        {
228            crate::cache::layer_cache::copy_dir_recursive(cache_dir, &rootfs)?;
229        } else {
230            tracing::info!(path = %rootfs.display(), "Reusing persistent APFS rootfs");
231        }
232        Ok(rootfs)
233    }
234
235    fn prepare_empty(&self, box_dir: &Path) -> Result<PathBuf> {
236        self.mount(box_dir)
237    }
238
239    fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()> {
240        super::unmount_box_rootfs(&box_dir.join("rootfs"));
241        if !persistent {
242            let image = box_dir.join(Self::IMAGE_NAME);
243            if image.exists() {
244                std::fs::remove_file(&image).map_err(|error| {
245                    BoxError::BuildError(format!(
246                        "Failed to remove rootfs image {}: {error}",
247                        image.display()
248                    ))
249                })?;
250            }
251        }
252        Ok(())
253    }
254
255    fn name(&self) -> &'static str {
256        "case-sensitive-apfs"
257    }
258}
259
260/// Overlayfs provider — near-instant CoW mounts (Linux only).
261///
262/// Layout:
263/// ```text
264/// cache_dir/           ← lower (read-only, shared across boxes)
265/// box_dir/upper/       ← upper (per-box writes)
266/// box_dir/work/        ← overlayfs workdir
267/// box_dir/merged/      ← merged view → InstanceSpec.rootfs_path
268/// ```
269pub struct OverlayProvider;
270
271impl OverlayProvider {
272    fn lower_dir(box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
273        let rootfs = box_dir.join("rootfs");
274        match std::fs::read_dir(&rootfs) {
275            Ok(mut entries) => {
276                if entries.next().is_some() {
277                    // A cache miss builds the first generation directly in `rootfs`.
278                    // Once that generation has run, the cache will usually be warm.
279                    // Keep the original writable tree as the overlay lower instead
280                    // of switching the next generation to the immutable image cache;
281                    // otherwise persistent guest writes silently disappear on restart.
282                    Ok(rootfs)
283                } else {
284                    Ok(cache_dir.to_path_buf())
285                }
286            }
287            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
288                Ok(cache_dir.to_path_buf())
289            }
290            Err(error) => Err(BoxError::BuildError(format!(
291                "Failed to inspect existing rootfs {}: {error}",
292                rootfs.display()
293            ))),
294        }
295    }
296}
297
298impl RootfsProvider for OverlayProvider {
299    fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
300        let lower = Self::lower_dir(box_dir, cache_dir)?;
301        let upper = box_dir.join("upper");
302        let work = box_dir.join("work");
303        let merged = box_dir.join("merged");
304
305        for dir in [&upper, &work, &merged] {
306            std::fs::create_dir_all(dir).map_err(|e| {
307                BoxError::BuildError(format!(
308                    "Failed to create overlay dir {}: {}",
309                    dir.display(),
310                    e
311                ))
312            })?;
313        }
314
315        // Idempotent: a restart re-runs prepare(); without this guard each call
316        // stacks another overlay on `merged` (the leaked double/triple mounts).
317        if super::is_mountpoint(&merged) {
318            tracing::debug!(merged = %merged.display(), "Overlay already mounted; reusing");
319            return Ok(merged);
320        }
321
322        super::overlay::overlay_mount(&lower, &upper, &work, &merged)?;
323
324        tracing::info!(
325            lower = %lower.display(),
326            merged = %merged.display(),
327            "Overlay mount ready"
328        );
329
330        Ok(merged)
331    }
332
333    fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()> {
334        let merged = box_dir.join("merged");
335
336        if persistent {
337            // A retained upper is about to become the next generation's
338            // writable layer. Fully release every old mount before reuse;
339            // lazy detach can keep an old namespace writer alive and make the
340            // replacement generation observe stale rootfs state.
341            super::unmount_box_overlay_for_reuse(&merged)?;
342            // Keep both possible persistent generations: a cache-miss generation
343            // lives in `rootfs`, while later overlay writes live in `upper`.
344            // The next prepare mounts their union again.
345            tracing::info!("Persistent box: keeping rootfs and overlay upper on disk");
346            for dir_name in &["merged", "work"] {
347                let dir = box_dir.join(dir_name);
348                if dir.exists() {
349                    if let Err(e) = std::fs::remove_dir_all(&dir) {
350                        tracing::warn!(path = %dir.display(), error = %e, "Failed to remove overlay dir");
351                    }
352                }
353            }
354            return Ok(());
355        }
356
357        // A discarded rootfs can use bounded lazy unmount cleanup. It will
358        // never be mounted as a replacement generation.
359        super::unmount_box_overlay(&merged);
360
361        for dir_name in &["rootfs", "upper", "work", "merged"] {
362            let dir = box_dir.join(dir_name);
363            if dir.exists() {
364                if let Err(e) = std::fs::remove_dir_all(&dir) {
365                    tracing::warn!(
366                        path = %dir.display(),
367                        error = %e,
368                        "Failed to remove overlay dir"
369                    );
370                }
371            }
372        }
373
374        Ok(())
375    }
376
377    fn name(&self) -> &'static str {
378        "overlay"
379    }
380}
381
382/// Auto-detect the best available rootfs provider for the current platform.
383pub fn default_provider() -> Box<dyn RootfsProvider> {
384    #[cfg(target_os = "macos")]
385    {
386        tracing::info!("Using case-sensitive APFS rootfs provider");
387        Box::new(CaseSensitiveApfsProvider)
388    }
389
390    #[cfg(not(target_os = "macos"))]
391    {
392        if super::overlay::is_overlay_supported() {
393            tracing::info!("Using overlayfs rootfs provider");
394            return Box::new(OverlayProvider);
395        }
396
397        tracing::info!("Overlayfs not available, using copy provider");
398        Box::new(CopyProvider)
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use tempfile::TempDir;
406
407    fn make_sample_rootfs(dir: &Path) {
408        std::fs::create_dir_all(dir.join("etc")).unwrap();
409        std::fs::create_dir_all(dir.join("bin")).unwrap();
410        std::fs::write(dir.join("etc/hostname"), "testbox").unwrap();
411        std::fs::write(dir.join("bin/hello"), "#!/bin/sh\necho hi").unwrap();
412    }
413
414    #[test]
415    fn test_copy_provider_prepare() {
416        let tmp = TempDir::new().unwrap();
417        let cache_dir = tmp.path().join("cache");
418        let box_dir = tmp.path().join("box");
419        std::fs::create_dir_all(&cache_dir).unwrap();
420        std::fs::create_dir_all(&box_dir).unwrap();
421        make_sample_rootfs(&cache_dir);
422
423        let provider = CopyProvider;
424        let rootfs = provider.prepare(&box_dir, &cache_dir).unwrap();
425
426        assert_eq!(rootfs, box_dir.join("rootfs"));
427        assert!(rootfs.join("etc/hostname").exists());
428        assert_eq!(
429            std::fs::read_to_string(rootfs.join("etc/hostname")).unwrap(),
430            "testbox"
431        );
432        assert!(rootfs.join("bin/hello").exists());
433    }
434
435    #[test]
436    fn test_copy_provider_prepare_reuses_existing_rootfs_without_overwriting() {
437        let tmp = TempDir::new().unwrap();
438        let cache_dir = tmp.path().join("cache");
439        let box_dir = tmp.path().join("box");
440        let rootfs = box_dir.join("rootfs");
441        std::fs::create_dir_all(&cache_dir).unwrap();
442        std::fs::create_dir_all(rootfs.join("etc")).unwrap();
443        make_sample_rootfs(&cache_dir);
444        std::fs::write(rootfs.join("etc/hostname"), "persistent-host").unwrap();
445
446        let provider = CopyProvider;
447        let prepared = provider.prepare(&box_dir, &cache_dir).unwrap();
448
449        assert_eq!(prepared, rootfs);
450        assert_eq!(
451            std::fs::read_to_string(prepared.join("etc/hostname")).unwrap(),
452            "persistent-host"
453        );
454        assert!(
455            !prepared.join("bin/hello").exists(),
456            "existing persistent rootfs must not be overwritten from cache"
457        );
458    }
459
460    #[test]
461    fn test_copy_provider_cleanup() {
462        let tmp = TempDir::new().unwrap();
463        let cache_dir = tmp.path().join("cache");
464        let box_dir = tmp.path().join("box");
465        std::fs::create_dir_all(&cache_dir).unwrap();
466        std::fs::create_dir_all(&box_dir).unwrap();
467        make_sample_rootfs(&cache_dir);
468
469        let provider = CopyProvider;
470        let rootfs = provider.prepare(&box_dir, &cache_dir).unwrap();
471        assert!(rootfs.exists());
472
473        provider.cleanup(&box_dir, false).unwrap();
474        assert!(!rootfs.exists());
475    }
476
477    #[test]
478    fn test_copy_provider_cleanup_persistent_keeps_rootfs() {
479        let tmp = TempDir::new().unwrap();
480        let box_dir = tmp.path().join("box");
481        let rootfs = box_dir.join("rootfs");
482        std::fs::create_dir_all(rootfs.join("etc")).unwrap();
483        std::fs::write(rootfs.join("etc/hostname"), "kept").unwrap();
484
485        CopyProvider.cleanup(&box_dir, true).unwrap();
486
487        assert_eq!(
488            std::fs::read_to_string(rootfs.join("etc/hostname")).unwrap(),
489            "kept"
490        );
491    }
492
493    #[test]
494    fn test_copy_provider_cleanup_nonexistent() {
495        let tmp = TempDir::new().unwrap();
496        let provider = CopyProvider;
497        // Should not error on missing dir
498        provider.cleanup(tmp.path(), false).unwrap();
499    }
500
501    #[test]
502    fn test_copy_provider_name() {
503        assert_eq!(CopyProvider.name(), "copy");
504    }
505
506    #[test]
507    fn test_overlay_provider_name() {
508        assert_eq!(OverlayProvider.name(), "overlay");
509    }
510
511    #[test]
512    fn test_overlay_provider_uses_populated_rootfs_as_persistent_lower() {
513        let tmp = TempDir::new().unwrap();
514        let cache_dir = tmp.path().join("cache");
515        let box_dir = tmp.path().join("box");
516        let rootfs = box_dir.join("rootfs");
517        std::fs::create_dir_all(&cache_dir).unwrap();
518        std::fs::create_dir_all(&rootfs).unwrap();
519        std::fs::write(rootfs.join("restart-proof"), "generation-one").unwrap();
520
521        assert_eq!(
522            OverlayProvider::lower_dir(&box_dir, &cache_dir).unwrap(),
523            rootfs
524        );
525    }
526
527    #[test]
528    fn test_overlay_provider_ignores_empty_rootfs_as_lower() {
529        let tmp = TempDir::new().unwrap();
530        let cache_dir = tmp.path().join("cache");
531        let box_dir = tmp.path().join("box");
532        std::fs::create_dir_all(&cache_dir).unwrap();
533        std::fs::create_dir_all(box_dir.join("rootfs")).unwrap();
534
535        assert_eq!(
536            OverlayProvider::lower_dir(&box_dir, &cache_dir).unwrap(),
537            cache_dir
538        );
539    }
540
541    #[test]
542    fn test_overlay_provider_cleanup_persistent_keeps_rootfs_and_upper() {
543        let tmp = TempDir::new().unwrap();
544        let box_dir = tmp.path().join("box");
545        for dir in ["rootfs", "upper", "work", "merged"] {
546            std::fs::create_dir_all(box_dir.join(dir)).unwrap();
547        }
548        std::fs::write(box_dir.join("rootfs/restart-proof"), "generation-one").unwrap();
549        std::fs::write(box_dir.join("upper/data.txt"), "state").unwrap();
550        std::fs::write(box_dir.join("work/scratch.txt"), "work").unwrap();
551        std::fs::write(box_dir.join("merged/view.txt"), "merged").unwrap();
552
553        OverlayProvider.cleanup(&box_dir, true).unwrap();
554
555        assert_eq!(
556            std::fs::read_to_string(box_dir.join("upper/data.txt")).unwrap(),
557            "state"
558        );
559        assert_eq!(
560            std::fs::read_to_string(box_dir.join("rootfs/restart-proof")).unwrap(),
561            "generation-one"
562        );
563        assert!(!box_dir.join("work").exists());
564        assert!(!box_dir.join("merged").exists());
565    }
566
567    #[test]
568    fn test_overlay_provider_cleanup_nonpersistent_removes_all_overlay_dirs() {
569        let tmp = TempDir::new().unwrap();
570        let box_dir = tmp.path().join("box");
571        for dir in ["rootfs", "upper", "work", "merged"] {
572            std::fs::create_dir_all(box_dir.join(dir)).unwrap();
573            std::fs::write(box_dir.join(dir).join("file.txt"), "data").unwrap();
574        }
575
576        OverlayProvider.cleanup(&box_dir, false).unwrap();
577
578        assert!(!box_dir.join("rootfs").exists());
579        assert!(!box_dir.join("upper").exists());
580        assert!(!box_dir.join("work").exists());
581        assert!(!box_dir.join("merged").exists());
582    }
583
584    #[test]
585    fn test_default_provider_returns_something() {
586        let provider = default_provider();
587        // On any platform, we should get a provider
588        assert!(!provider.name().is_empty());
589    }
590
591    #[cfg(target_os = "macos")]
592    #[test]
593    fn case_sensitive_apfs_provider_preserves_distinct_names() {
594        use std::os::unix::fs::MetadataExt;
595
596        let tmp = TempDir::new().unwrap();
597        let box_dir = tmp.path().join("box");
598        let provider = CaseSensitiveApfsProvider;
599        let rootfs = provider.prepare_empty(&box_dir).unwrap();
600        std::fs::write(rootfs.join("Foo"), "upper").unwrap();
601        std::fs::write(rootfs.join("foo"), "lower").unwrap();
602
603        assert_eq!(
604            std::fs::read_to_string(rootfs.join("Foo")).unwrap(),
605            "upper"
606        );
607        assert_eq!(
608            std::fs::read_to_string(rootfs.join("foo")).unwrap(),
609            "lower"
610        );
611        assert_ne!(
612            std::fs::metadata(rootfs.join("Foo")).unwrap().ino(),
613            std::fs::metadata(rootfs.join("foo")).unwrap().ino()
614        );
615
616        provider.cleanup(&box_dir, false).unwrap();
617        assert!(!box_dir.join(CaseSensitiveApfsProvider::IMAGE_NAME).exists());
618    }
619
620    #[cfg(target_os = "linux")]
621    #[test]
622    fn test_overlay_provider_prepare_and_cleanup() {
623        if !super::super::overlay::is_overlay_supported() {
624            // Skip if overlay not available (e.g., in container without privileges)
625            return;
626        }
627
628        let tmp = TempDir::new().unwrap();
629        let cache_dir = tmp.path().join("cache");
630        let box_dir = tmp.path().join("box");
631        std::fs::create_dir_all(&cache_dir).unwrap();
632        std::fs::create_dir_all(&box_dir).unwrap();
633        make_sample_rootfs(&cache_dir);
634
635        let provider = OverlayProvider;
636        let merged = provider.prepare(&box_dir, &cache_dir).unwrap();
637
638        assert_eq!(merged, box_dir.join("merged"));
639        assert!(merged.join("etc/hostname").exists());
640        assert_eq!(
641            std::fs::read_to_string(merged.join("etc/hostname")).unwrap(),
642            "testbox"
643        );
644
645        // Write to merged — should go to upper
646        std::fs::write(merged.join("etc/newfile"), "overlay write").unwrap();
647        assert!(box_dir.join("upper/etc/newfile").exists());
648
649        provider.cleanup(&box_dir, false).unwrap();
650        assert!(!box_dir.join("merged").exists());
651        assert!(!box_dir.join("upper").exists());
652        assert!(!box_dir.join("work").exists());
653    }
654
655    #[cfg(target_os = "linux")]
656    #[test]
657    fn test_overlay_provider_persistent_cleanup_remounts_retained_upper() {
658        if !super::super::overlay::is_overlay_supported() {
659            return;
660        }
661
662        let tmp = TempDir::new().unwrap();
663        let cache_dir = tmp.path().join("cache");
664        let box_dir = tmp.path().join("box");
665        std::fs::create_dir_all(&cache_dir).unwrap();
666        std::fs::create_dir_all(&box_dir).unwrap();
667        make_sample_rootfs(&cache_dir);
668
669        let provider = OverlayProvider;
670        let first = provider.prepare(&box_dir, &cache_dir).unwrap();
671        std::fs::write(first.join("restart-proof"), "generation-one").unwrap();
672
673        provider.cleanup(&box_dir, true).unwrap();
674        assert_eq!(
675            std::fs::read_to_string(box_dir.join("upper/restart-proof")).unwrap(),
676            "generation-one"
677        );
678
679        let second = provider.prepare(&box_dir, &cache_dir).unwrap();
680        assert_eq!(
681            std::fs::read_to_string(second.join("restart-proof")).unwrap(),
682            "generation-one"
683        );
684
685        provider.cleanup(&box_dir, false).unwrap();
686    }
687}