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}