Skip to main content

boxlite_shared/
layout.rs

1//! Filesystem layout definitions shared between host and guest.
2//!
3//! This module provides layout structs for the shared filesystem pattern:
4//! - `SharedGuestLayout`: Layout for the shared directory (virtiofs mount)
5//! - `SharedContainerLayout`: Per-container directory layout within shared/
6//!
7//! Lives in boxlite-shared so both host and guest can use these definitions.
8
9use std::path::{Path, PathBuf};
10
11// ============================================================================
12// CONSTANTS
13// ============================================================================
14
15/// Shared filesystem directory names.
16pub mod dirs {
17    /// Host preparation directory (host writes here)
18    pub const MOUNTS: &str = "mounts";
19
20    /// Guest-visible directory (bind mount target, read-only on Linux)
21    pub const SHARED: &str = "shared";
22
23    /// Containers subdirectory
24    pub const CONTAINERS: &str = "containers";
25
26    /// Container rootfs directory name (all rootfs strategies mount here)
27    pub const ROOTFS: &str = "rootfs";
28
29    /// Overlayfs directory name (contains upper/ and work/)
30    pub const OVERLAYFS: &str = "overlayfs";
31
32    /// Overlayfs upper directory name
33    pub const UPPER: &str = "upper";
34
35    /// Overlayfs work directory name
36    pub const WORK: &str = "work";
37
38    /// Overlayfs diff directory name (contains image layers)
39    pub const DIFF: &str = "diff";
40
41    /// Layers directory name (virtiofs source for image layers)
42    pub const LAYERS: &str = "layers";
43
44    /// Volumes directory name (contains user volumes)
45    pub const VOLUMES: &str = "volumes";
46}
47
48/// Guest base path (FHS-compliant).
49pub const GUEST_BASE: &str = "/run/boxlite";
50
51// ============================================================================
52// SHARED CONTAINER LAYOUT (per-container directories)
53// ============================================================================
54
55/// Per-container directory layout within the shared filesystem.
56///
57/// Represents the directory structure for a single container:
58/// ```text
59/// {root}/                    # shared/containers/{cid}/
60/// ├── overlayfs/
61/// │   ├── diff/              # Image layers (lower dirs for overlayfs)
62/// │   ├── upper/             # Overlayfs upper (writable layer)
63/// │   └── work/              # Overlayfs work directory
64/// ├── rootfs/                # All rootfs strategies mount here
65/// └── volumes/               # User volumes (virtiofs mounts)
66///     ├── {volume-name-1}/
67///     └── {volume-name-2}/
68/// ```
69#[derive(Clone, Debug)]
70pub struct SharedContainerLayout {
71    root: PathBuf,
72}
73
74impl SharedContainerLayout {
75    /// Create a container layout with the given root path.
76    pub fn new(root: impl Into<PathBuf>) -> Self {
77        Self { root: root.into() }
78    }
79
80    /// Root directory of this container: shared/containers/{cid}
81    pub fn root(&self) -> &Path {
82        &self.root
83    }
84
85    /// Overlayfs directory: {root}/overlayfs
86    pub fn overlayfs_dir(&self) -> PathBuf {
87        self.root.join(dirs::OVERLAYFS)
88    }
89
90    /// Upper directory: {root}/overlayfs/upper
91    ///
92    /// Writable layer for overlayfs.
93    pub fn upper_dir(&self) -> PathBuf {
94        self.overlayfs_dir().join(dirs::UPPER)
95    }
96
97    /// Work directory: {root}/overlayfs/work
98    ///
99    /// Overlayfs work directory.
100    pub fn work_dir(&self) -> PathBuf {
101        self.overlayfs_dir().join(dirs::WORK)
102    }
103
104    /// Diff directory: {root}/overlayfs/diff
105    ///
106    /// Contains image layers (lower dirs for overlayfs).
107    pub fn diff_dir(&self) -> PathBuf {
108        self.overlayfs_dir().join(dirs::DIFF)
109    }
110
111    /// Rootfs directory: {root}/rootfs
112    ///
113    /// All rootfs strategies (merged, overlayfs, disk image) mount here.
114    /// Guest bind mounts /run/boxlite/{cid}/rootfs/ to this location.
115    pub fn rootfs_dir(&self) -> PathBuf {
116        self.root.join(dirs::ROOTFS)
117    }
118
119    /// Volumes directory: {root}/volumes
120    ///
121    /// Base directory for user volume mounts.
122    pub fn volumes_dir(&self) -> PathBuf {
123        self.root.join(dirs::VOLUMES)
124    }
125
126    /// Specific volume directory: {root}/volumes/{volume_name}
127    ///
128    /// Convention-based path for a specific user volume.
129    /// Both host and guest use this to construct volume mount paths.
130    pub fn volume_dir(&self, volume_name: &str) -> PathBuf {
131        self.volumes_dir().join(volume_name)
132    }
133
134    /// Layers directory: {root}/layers
135    ///
136    /// Source directory for image layers (virtiofs mount point).
137    /// Guest bind-mounts from here to diff_dir for overlayfs.
138    pub fn layers_dir(&self) -> PathBuf {
139        self.root.join(dirs::LAYERS)
140    }
141
142    /// Prepare container directories.
143    pub fn prepare(&self) -> std::io::Result<()> {
144        std::fs::create_dir_all(self.upper_dir())?;
145        std::fs::create_dir_all(self.work_dir())?;
146        std::fs::create_dir_all(self.rootfs_dir())?;
147        std::fs::create_dir_all(self.volumes_dir())?;
148        Ok(())
149    }
150}
151
152// ============================================================================
153// SHARED GUEST LAYOUT (shared directory root)
154// ============================================================================
155
156/// Shared directory layout - identical structure on host and guest.
157///
158/// This struct represents the directory structure under:
159/// - Host: `~/.boxlite/boxes/{box-id}/mounts/`
160/// - Guest: `/run/boxlite/shared/`
161///
162/// The structure is:
163/// ```text
164/// {base}/
165/// └── containers/
166///     └── {cid}/              # SharedContainerLayout
167///         ├── overlayfs/{upper,work}
168///         └── rootfs/
169/// ```
170///
171/// # Example
172///
173/// ```
174/// use boxlite_shared::layout::SharedGuestLayout;
175///
176/// // Host usage
177/// let host_layout = SharedGuestLayout::new("/home/user/.boxlite/boxes/abc123/mounts");
178///
179/// // Guest usage
180/// let guest_layout = SharedGuestLayout::new("/run/boxlite/shared");
181///
182/// // Both have identical container paths relative to base
183/// let host_container = host_layout.container("main");
184/// let guest_container = guest_layout.container("main");
185/// assert!(host_container.rootfs_dir().ends_with("containers/main/rootfs"));
186/// assert!(guest_container.rootfs_dir().ends_with("containers/main/rootfs"));
187/// ```
188#[derive(Clone, Debug)]
189pub struct SharedGuestLayout {
190    base: PathBuf,
191}
192
193impl SharedGuestLayout {
194    /// Create a shared layout with the given base path.
195    pub fn new(base: impl Into<PathBuf>) -> Self {
196        Self { base: base.into() }
197    }
198
199    /// Base directory of this shared layout.
200    pub fn base(&self) -> &Path {
201        &self.base
202    }
203
204    /// Containers directory: {base}/containers
205    pub fn containers_dir(&self) -> PathBuf {
206        self.base.join(dirs::CONTAINERS)
207    }
208
209    /// Get layout for a specific container.
210    pub fn container(&self, container_id: &str) -> SharedContainerLayout {
211        SharedContainerLayout::new(self.containers_dir().join(container_id))
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use proptest::prelude::*;
219
220    // ========================================================================
221    // SharedContainerLayout tests
222    // ========================================================================
223
224    #[test]
225    fn test_container_layout_paths() {
226        let container = SharedContainerLayout::new("/test/shared/containers/main");
227
228        assert_eq!(
229            container.root().to_str().unwrap(),
230            "/test/shared/containers/main"
231        );
232        assert_eq!(
233            container.overlayfs_dir().to_str().unwrap(),
234            "/test/shared/containers/main/overlayfs"
235        );
236        assert_eq!(
237            container.upper_dir().to_str().unwrap(),
238            "/test/shared/containers/main/overlayfs/upper"
239        );
240        assert_eq!(
241            container.work_dir().to_str().unwrap(),
242            "/test/shared/containers/main/overlayfs/work"
243        );
244        assert_eq!(
245            container.rootfs_dir().to_str().unwrap(),
246            "/test/shared/containers/main/rootfs"
247        );
248    }
249
250    // ========================================================================
251    // SharedGuestLayout tests
252    // ========================================================================
253
254    #[test]
255    fn test_shared_guest_layout_paths() {
256        let layout = SharedGuestLayout::new("/test/shared");
257
258        assert_eq!(layout.base().to_str().unwrap(), "/test/shared");
259        assert_eq!(
260            layout.containers_dir().to_str().unwrap(),
261            "/test/shared/containers"
262        );
263    }
264
265    #[test]
266    fn test_shared_guest_layout_container() {
267        let layout = SharedGuestLayout::new("/test/shared");
268        let container = layout.container("main");
269
270        assert_eq!(
271            container.overlayfs_dir().to_str().unwrap(),
272            "/test/shared/containers/main/overlayfs"
273        );
274        assert_eq!(
275            container.rootfs_dir().to_str().unwrap(),
276            "/test/shared/containers/main/rootfs"
277        );
278    }
279
280    #[test]
281    fn test_shared_guest_layout_host_guest_identical() {
282        // Host and guest have identical structure under their respective bases
283        let host = SharedGuestLayout::new("/home/user/.boxlite/boxes/abc/mounts");
284        let guest = SharedGuestLayout::new("/run/boxlite/shared");
285
286        // Relative paths are identical
287        let host_rootfs_dir = host.container("main").rootfs_dir();
288        let guest_rootfs_dir = guest.container("main").rootfs_dir();
289        let host_rel = host_rootfs_dir.strip_prefix(host.base()).unwrap();
290        let guest_rel = guest_rootfs_dir.strip_prefix(guest.base()).unwrap();
291        assert_eq!(host_rel, guest_rel);
292    }
293
294    // ========================================================================
295    // Property-based tests
296    // ========================================================================
297
298    proptest! {
299        #[test]
300        fn prop_all_container_paths_under_root(
301            base in "[a-z/]{1,30}",
302            cid in "[a-zA-Z0-9]{1,20}"
303        ) {
304            let layout = SharedGuestLayout::new(&base);
305            let container = layout.container(&cid);
306
307            // Every generated path must be a child of the container root
308            let root = container.root().to_path_buf();
309            prop_assert!(container.overlayfs_dir().starts_with(&root));
310            prop_assert!(container.upper_dir().starts_with(&root));
311            prop_assert!(container.work_dir().starts_with(&root));
312            prop_assert!(container.diff_dir().starts_with(&root));
313            prop_assert!(container.rootfs_dir().starts_with(&root));
314            prop_assert!(container.volumes_dir().starts_with(&root));
315            prop_assert!(container.layers_dir().starts_with(&root));
316        }
317
318        #[test]
319        fn prop_volume_dir_under_volumes(
320            base in "[a-z/]{1,30}",
321            cid in "[a-zA-Z0-9]{1,20}",
322            vol in "[a-zA-Z0-9_-]{1,20}"
323        ) {
324            let layout = SharedGuestLayout::new(&base);
325            let container = layout.container(&cid);
326            let volume_path = container.volume_dir(&vol);
327            prop_assert!(volume_path.starts_with(container.volumes_dir()));
328        }
329
330        #[test]
331        fn prop_host_guest_relative_paths_identical(
332            host_base in "/[a-z]{1,10}(/[a-z]{1,10}){0,3}",
333            guest_base in "/[a-z]{1,10}(/[a-z]{1,10}){0,3}",
334            cid in "[a-zA-Z0-9]{1,10}"
335        ) {
336            let host = SharedGuestLayout::new(&host_base);
337            let guest = SharedGuestLayout::new(&guest_base);
338
339            let host_rootfs = host.container(&cid).rootfs_dir();
340            let guest_rootfs = guest.container(&cid).rootfs_dir();
341
342            let host_rel = host_rootfs.strip_prefix(host.base()).unwrap();
343            let guest_rel = guest_rootfs.strip_prefix(guest.base()).unwrap();
344            prop_assert_eq!(host_rel, guest_rel);
345        }
346    }
347}