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}