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
219    // ========================================================================
220    // SharedContainerLayout tests
221    // ========================================================================
222
223    #[test]
224    fn test_container_layout_paths() {
225        let container = SharedContainerLayout::new("/test/shared/containers/main");
226
227        assert_eq!(
228            container.root().to_str().unwrap(),
229            "/test/shared/containers/main"
230        );
231        assert_eq!(
232            container.overlayfs_dir().to_str().unwrap(),
233            "/test/shared/containers/main/overlayfs"
234        );
235        assert_eq!(
236            container.upper_dir().to_str().unwrap(),
237            "/test/shared/containers/main/overlayfs/upper"
238        );
239        assert_eq!(
240            container.work_dir().to_str().unwrap(),
241            "/test/shared/containers/main/overlayfs/work"
242        );
243        assert_eq!(
244            container.rootfs_dir().to_str().unwrap(),
245            "/test/shared/containers/main/rootfs"
246        );
247    }
248
249    // ========================================================================
250    // SharedGuestLayout tests
251    // ========================================================================
252
253    #[test]
254    fn test_shared_guest_layout_paths() {
255        let layout = SharedGuestLayout::new("/test/shared");
256
257        assert_eq!(layout.base().to_str().unwrap(), "/test/shared");
258        assert_eq!(
259            layout.containers_dir().to_str().unwrap(),
260            "/test/shared/containers"
261        );
262    }
263
264    #[test]
265    fn test_shared_guest_layout_container() {
266        let layout = SharedGuestLayout::new("/test/shared");
267        let container = layout.container("main");
268
269        assert_eq!(
270            container.overlayfs_dir().to_str().unwrap(),
271            "/test/shared/containers/main/overlayfs"
272        );
273        assert_eq!(
274            container.rootfs_dir().to_str().unwrap(),
275            "/test/shared/containers/main/rootfs"
276        );
277    }
278
279    #[test]
280    fn test_shared_guest_layout_host_guest_identical() {
281        // Host and guest have identical structure under their respective bases
282        let host = SharedGuestLayout::new("/home/user/.boxlite/boxes/abc/mounts");
283        let guest = SharedGuestLayout::new("/run/boxlite/shared");
284
285        // Relative paths are identical
286        let host_rootfs_dir = host.container("main").rootfs_dir();
287        let guest_rootfs_dir = guest.container("main").rootfs_dir();
288        let host_rel = host_rootfs_dir.strip_prefix(host.base()).unwrap();
289        let guest_rel = guest_rootfs_dir.strip_prefix(guest.base()).unwrap();
290        assert_eq!(host_rel, guest_rel);
291    }
292}