a3s_box_runtime/rootfs/
builder.rs1use std::fs;
8use std::path::PathBuf;
9
10use a3s_box_core::error::{BoxError, Result};
11
12use super::layout::GuestLayout;
13
14pub struct RootfsBuilder {
16 rootfs_path: PathBuf,
18
19 layout: GuestLayout,
21}
22
23impl RootfsBuilder {
24 pub fn new(rootfs_path: impl Into<PathBuf>) -> Self {
26 Self {
27 rootfs_path: rootfs_path.into(),
28 layout: GuestLayout::default(),
29 }
30 }
31
32 pub fn with_layout(mut self, layout: GuestLayout) -> Self {
34 self.layout = layout;
35 self
36 }
37
38 pub fn build(&self) -> Result<()> {
40 tracing::info!(
41 rootfs = %self.rootfs_path.display(),
42 "Building guest rootfs"
43 );
44
45 fs::create_dir_all(&self.rootfs_path).map_err(|e| {
47 BoxError::BuildError(format!(
48 "Failed to create rootfs directory {}: {}",
49 self.rootfs_path.display(),
50 e
51 ))
52 })?;
53
54 self.create_directories()?;
56
57 self.create_essential_files()?;
59
60 tracing::info!("Guest rootfs built successfully");
61 Ok(())
62 }
63
64 fn create_directories(&self) -> Result<()> {
66 for dir in self.layout.required_dirs() {
67 let full_path = self.rootfs_path.join(dir.trim_start_matches('/'));
68 fs::create_dir_all(&full_path).map_err(|e| {
69 BoxError::BuildError(format!(
70 "Failed to create directory {}: {}",
71 full_path.display(),
72 e
73 ))
74 })?;
75 tracing::debug!(dir = %full_path.display(), "Created directory");
76 }
77 Ok(())
78 }
79
80 fn create_essential_files(&self) -> Result<()> {
82 let passwd_content =
84 "root:x:0:0:root:/root:/bin/sh\nnobody:x:65534:65534:nobody:/:/bin/false\n";
85 self.write_file("etc/passwd", passwd_content)?;
86
87 let group_content = "root:x:0:\nnogroup:x:65534:\n";
89 self.write_file("etc/group", group_content)?;
90
91 let hosts_content = "127.0.0.1\tlocalhost\n::1\t\tlocalhost\n";
93 self.write_file("etc/hosts", hosts_content)?;
94
95 let resolv_content = "nameserver 8.8.8.8\nnameserver 8.8.4.4\n";
97 self.write_file("etc/resolv.conf", resolv_content)?;
98
99 let nsswitch_content = "passwd: files\ngroup: files\nhosts: files dns\n";
101 self.write_file("etc/nsswitch.conf", nsswitch_content)?;
102
103 Ok(())
104 }
105
106 fn write_file(&self, relative_path: &str, content: &str) -> Result<()> {
108 let full_path = self.rootfs_path.join(relative_path);
109
110 if let Some(parent) = full_path.parent() {
112 fs::create_dir_all(parent).map_err(|e| {
113 BoxError::BuildError(format!("Failed to create parent directory: {}", e))
114 })?;
115 }
116
117 fs::write(&full_path, content).map_err(|e| {
118 BoxError::BuildError(format!("Failed to write {}: {}", full_path.display(), e))
119 })?;
120
121 tracing::debug!(path = %full_path.display(), "Created file");
122 Ok(())
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use tempfile::TempDir;
130
131 #[test]
132 fn test_rootfs_builder_creates_directories() {
133 let temp_dir = TempDir::new().unwrap();
134 let rootfs_path = temp_dir.path().join("rootfs");
135
136 let builder = RootfsBuilder::new(&rootfs_path);
137 builder.build().unwrap();
138
139 assert!(rootfs_path.join("dev").exists());
141 assert!(rootfs_path.join("proc").exists());
142 assert!(rootfs_path.join("sys").exists());
143 assert!(rootfs_path.join("tmp").exists());
144 assert!(rootfs_path.join("etc").exists());
145 assert!(rootfs_path.join("workspace").exists());
146 }
147
148 #[test]
149 fn test_rootfs_builder_creates_essential_files() {
150 let temp_dir = TempDir::new().unwrap();
151 let rootfs_path = temp_dir.path().join("rootfs");
152
153 let builder = RootfsBuilder::new(&rootfs_path);
154 builder.build().unwrap();
155
156 assert!(rootfs_path.join("etc/passwd").exists());
158 assert!(rootfs_path.join("etc/group").exists());
159 assert!(rootfs_path.join("etc/hosts").exists());
160 assert!(rootfs_path.join("etc/resolv.conf").exists());
161
162 let passwd = fs::read_to_string(rootfs_path.join("etc/passwd")).unwrap();
164 assert!(passwd.contains("root:x:0:0"));
165 }
166
167 #[test]
168 fn test_rootfs_builder_essential_files_content() {
169 let temp_dir = TempDir::new().unwrap();
170 let rootfs_path = temp_dir.path().join("rootfs");
171
172 RootfsBuilder::new(&rootfs_path).build().unwrap();
173
174 let passwd = fs::read_to_string(rootfs_path.join("etc/passwd")).unwrap();
176 assert!(passwd.contains("root:x:0:0:root:/root:/bin/sh"));
177 assert!(passwd.contains("nobody:x:65534:65534"));
178
179 let group = fs::read_to_string(rootfs_path.join("etc/group")).unwrap();
181 assert!(group.contains("root:x:0:"));
182 assert!(group.contains("nogroup:x:65534:"));
183
184 let hosts = fs::read_to_string(rootfs_path.join("etc/hosts")).unwrap();
186 assert!(hosts.contains("127.0.0.1"));
187 assert!(hosts.contains("localhost"));
188
189 let resolv = fs::read_to_string(rootfs_path.join("etc/resolv.conf")).unwrap();
191 assert!(resolv.contains("nameserver"));
192
193 let nsswitch = fs::read_to_string(rootfs_path.join("etc/nsswitch.conf")).unwrap();
195 assert!(nsswitch.contains("passwd: files"));
196 assert!(nsswitch.contains("hosts: files dns"));
197 }
198
199 #[test]
200 fn test_rootfs_builder_with_custom_layout() {
201 let temp_dir = TempDir::new().unwrap();
202 let rootfs_path = temp_dir.path().join("rootfs");
203
204 let custom_layout = GuestLayout {
205 workspace_dir: "/work",
206 tmp_dir: "/tmp",
207 run_dir: "/run",
208 };
209
210 let builder = RootfsBuilder::new(&rootfs_path).with_layout(custom_layout);
211 builder.build().unwrap();
212
213 assert!(rootfs_path.join("work").exists());
215 }
216
217 #[test]
218 fn test_rootfs_builder_idempotent() {
219 let temp_dir = TempDir::new().unwrap();
220 let rootfs_path = temp_dir.path().join("rootfs");
221
222 let builder = RootfsBuilder::new(&rootfs_path);
223
224 builder.build().unwrap();
226 builder.build().unwrap();
227
228 assert!(rootfs_path.join("etc/passwd").exists());
230 assert!(rootfs_path.join("dev").exists());
231 }
232}