1use flate2::write::GzEncoder;
11use flate2::Compression;
12use std::io::Write;
13
14use crate::error::KernelError;
15
16const CPIO_NEWC_MAGIC: &str = "070701";
18
19pub fn default_init_script(services: &[&str]) -> String {
22 let mut script = String::from(
23 r#"#!/bin/sh
24# RVF initramfs init script
25# Generated by rvf-kernel
26
27set -e
28
29echo "RVF initramfs booting..."
30
31# Mount essential filesystems
32mount -t proc proc /proc
33mount -t sysfs sysfs /sys
34mount -t devtmpfs devtmpfs /dev
35mkdir -p /dev/pts /dev/shm
36mount -t devpts devpts /dev/pts
37mount -t tmpfs tmpfs /dev/shm
38mount -t tmpfs tmpfs /tmp
39mount -t tmpfs tmpfs /run
40
41# Set hostname
42hostname rvf
43
44# Configure loopback
45ip link set lo up
46
47# Configure primary network interface
48for iface in eth0 ens3 enp0s3; do
49 if [ -d "/sys/class/net/$iface" ]; then
50 ip link set "$iface" up
51 # Try DHCP if udhcpc is available
52 if command -v udhcpc >/dev/null 2>&1; then
53 udhcpc -i "$iface" -s /etc/udhcpc/simple.script -q &
54 fi
55 break
56 fi
57done
58
59# Set up minimal /etc
60echo "root:x:0:0:root:/root:/bin/sh" > /etc/passwd
61echo "root:x:0:" > /etc/group
62echo "nameserver 8.8.8.8" > /etc/resolv.conf
63echo "rvf" > /etc/hostname
64
65"#,
66 );
67
68 for service in services {
70 script.push_str(&format!(
71 "# Start service: {service}\n\
72 echo \"Starting {service}...\"\n"
73 ));
74 match *service {
75 "sshd" | "dropbear" => {
76 script.push_str(
77 "mkdir -p /etc/dropbear\n\
78 if command -v dropbear >/dev/null 2>&1; then\n\
79 dropbear -R -F -E -p 2222 &\n\
80 elif command -v sshd >/dev/null 2>&1; then\n\
81 mkdir -p /etc/ssh\n\
82 ssh-keygen -A 2>/dev/null || true\n\
83 /usr/sbin/sshd -p 2222\n\
84 fi\n",
85 );
86 }
87 "rvf-server" => {
88 script.push_str(
89 "if command -v rvf-server >/dev/null 2>&1; then\n\
90 rvf-server --listen 0.0.0.0:8080 &\n\
91 fi\n",
92 );
93 }
94 other => {
95 script.push_str(&format!(
96 "if command -v {other} >/dev/null 2>&1; then\n\
97 {other} &\n\
98 fi\n"
99 ));
100 }
101 }
102 script.push('\n');
103 }
104
105 script.push_str(
106 "echo \"RVF initramfs ready.\"\n\
107 \n\
108 # Drop to shell or wait\n\
109 exec /bin/sh\n",
110 );
111
112 script
113}
114
115pub struct CpioBuilder {
117 data: Vec<u8>,
119 next_ino: u32,
121}
122
123impl CpioBuilder {
124 pub fn new() -> Self {
126 Self {
127 data: Vec::with_capacity(64 * 1024),
128 next_ino: 1,
129 }
130 }
131
132 pub fn add_dir(&mut self, path: &str) {
137 self.add_entry(path, 0o040755, &[]);
138 }
139
140 pub fn add_file(&mut self, path: &str, mode: u32, content: &[u8]) {
145 self.add_entry(path, mode, content);
146 }
147
148 pub fn add_symlink(&mut self, path: &str, target: &str) {
152 self.add_entry(path, 0o120777, target.as_bytes());
153 }
154
155 pub fn add_device(&mut self, path: &str, mode: u32, devmajor: u32, devminor: u32) {
160 let ino = self.next_ino;
161 self.next_ino += 1;
162
163 let name_with_nul = format!("{path}\0");
164 let name_len = name_with_nul.len() as u32;
165 let filesize = 0u32;
166
167 let header = format!(
168 "{CPIO_NEWC_MAGIC}\
169 {ino:08X}\
170 {mode:08X}\
171 {uid:08X}\
172 {gid:08X}\
173 {nlink:08X}\
174 {mtime:08X}\
175 {filesize:08X}\
176 {devmajor:08X}\
177 {devminor:08X}\
178 {rdevmajor:08X}\
179 {rdevminor:08X}\
180 {namesize:08X}\
181 {check:08X}",
182 uid = 0u32,
183 gid = 0u32,
184 nlink = 1u32,
185 mtime = 0u32,
186 rdevmajor = devmajor,
187 rdevminor = devminor,
188 namesize = name_len,
189 check = 0u32,
190 );
191
192 self.data.extend_from_slice(header.as_bytes());
193 self.data.extend_from_slice(name_with_nul.as_bytes());
194 pad4(&mut self.data);
195 }
197
198 pub fn finish(mut self) -> Vec<u8> {
202 self.add_entry("TRAILER!!!", 0, &[]);
203 self.data
204 }
205
206 pub fn finish_gzipped(self) -> Result<Vec<u8>, KernelError> {
210 let raw = self.finish();
211 let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
212 encoder
213 .write_all(&raw)
214 .map_err(|e| KernelError::CompressionFailed(e.to_string()))?;
215 encoder
216 .finish()
217 .map_err(|e| KernelError::CompressionFailed(e.to_string()))
218 }
219
220 fn add_entry(&mut self, path: &str, mode: u32, content: &[u8]) {
221 let ino = self.next_ino;
222 self.next_ino += 1;
223
224 let name_with_nul = format!("{path}\0");
225 let name_len = name_with_nul.len() as u32;
226 let filesize = content.len() as u32;
227
228 let header = format!(
230 "{CPIO_NEWC_MAGIC}\
231 {ino:08X}\
232 {mode:08X}\
233 {uid:08X}\
234 {gid:08X}\
235 {nlink:08X}\
236 {mtime:08X}\
237 {filesize:08X}\
238 {devmajor:08X}\
239 {devminor:08X}\
240 {rdevmajor:08X}\
241 {rdevminor:08X}\
242 {namesize:08X}\
243 {check:08X}",
244 uid = 0u32,
245 gid = 0u32,
246 nlink = if mode & 0o040000 != 0 { 2u32 } else { 1u32 },
247 mtime = 0u32,
248 devmajor = 0u32,
249 devminor = 0u32,
250 rdevmajor = 0u32,
251 rdevminor = 0u32,
252 namesize = name_len,
253 check = 0u32,
254 );
255
256 debug_assert_eq!(header.len(), 110);
257
258 self.data.extend_from_slice(header.as_bytes());
259 self.data.extend_from_slice(name_with_nul.as_bytes());
260 pad4(&mut self.data);
261
262 if !content.is_empty() {
263 self.data.extend_from_slice(content);
264 pad4(&mut self.data);
265 }
266 }
267}
268
269impl Default for CpioBuilder {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275fn pad4(data: &mut Vec<u8>) {
277 let rem = data.len() % 4;
278 if rem != 0 {
279 let padding = 4 - rem;
280 data.extend(std::iter::repeat_n(0u8, padding));
281 }
282}
283
284pub fn build_initramfs(
294 services: &[&str],
295 extra_binaries: &[(&str, &[u8])],
296) -> Result<Vec<u8>, KernelError> {
297 let mut cpio = CpioBuilder::new();
298
299 let dirs = [
301 ".", "bin", "sbin", "etc", "etc/udhcpc", "dev", "proc", "sys",
302 "tmp", "var", "var/log", "var/run", "run", "root", "lib",
303 "usr", "usr/bin", "usr/sbin", "usr/lib", "mnt", "opt",
304 ];
305 for dir in &dirs {
306 cpio.add_dir(dir);
307 }
308
309 cpio.add_device("dev/console", 0o020600, 5, 1);
311 cpio.add_device("dev/ttyS0", 0o020660, 4, 64);
312 cpio.add_device("dev/null", 0o020666, 1, 3);
313 cpio.add_device("dev/zero", 0o020666, 1, 5);
314 cpio.add_device("dev/urandom", 0o020444, 1, 9);
315
316 let init_script = default_init_script(services);
318 cpio.add_file("init", 0o100755, init_script.as_bytes());
319
320 let udhcpc_script = r#"#!/bin/sh
322case "$1" in
323 bound|renew)
324 ip addr add "$ip/$mask" dev "$interface"
325 if [ -n "$router" ]; then
326 ip route add default via "$router"
327 fi
328 if [ -n "$dns" ]; then
329 echo "nameserver $dns" > /etc/resolv.conf
330 fi
331 ;;
332esac
333"#;
334 cpio.add_file(
335 "etc/udhcpc/simple.script",
336 0o100755,
337 udhcpc_script.as_bytes(),
338 );
339
340 for (path, content) in extra_binaries {
342 cpio.add_file(path, 0o100755, content);
343 }
344
345 cpio.finish_gzipped()
346}
347
348pub fn parse_cpio_entries(data: &[u8]) -> Result<Vec<(String, u32, u32)>, KernelError> {
353 let mut entries = Vec::new();
354 let mut offset = 0;
355
356 loop {
357 if offset + 110 > data.len() {
358 break;
359 }
360
361 let header = &data[offset..offset + 110];
362 let header_str = std::str::from_utf8(header)
363 .map_err(|_| KernelError::InitramfsBuildFailed("invalid cpio header encoding".into()))?;
364
365 if &header_str[..6] != CPIO_NEWC_MAGIC {
367 return Err(KernelError::InitramfsBuildFailed(format!(
368 "invalid cpio magic at offset {offset}: {:?}",
369 &header_str[..6]
370 )));
371 }
372
373 let mode = u32::from_str_radix(&header_str[14..22], 16)
374 .map_err(|_| KernelError::InitramfsBuildFailed("invalid mode".into()))?;
375 let filesize = u32::from_str_radix(&header_str[54..62], 16)
376 .map_err(|_| KernelError::InitramfsBuildFailed("invalid filesize".into()))?;
377 let namesize = u32::from_str_radix(&header_str[94..102], 16)
378 .map_err(|_| KernelError::InitramfsBuildFailed("invalid namesize".into()))?;
379
380 let name_start = offset + 110;
382 let name_end = name_start + namesize as usize;
383 if name_end > data.len() {
384 break;
385 }
386
387 let name_bytes = &data[name_start..name_end];
388 let name = std::str::from_utf8(name_bytes)
390 .map_err(|_| KernelError::InitramfsBuildFailed("invalid name encoding".into()))?
391 .trim_end_matches('\0')
392 .to_string();
393
394 if name == "TRAILER!!!" {
395 break;
396 }
397
398 let after_name = align4(name_end);
400 let data_end = after_name + filesize as usize;
402 let next_entry = if filesize > 0 {
403 align4(data_end)
404 } else {
405 after_name
406 };
407
408 entries.push((name, mode, filesize));
409 offset = next_entry;
410 }
411
412 Ok(entries)
413}
414
415fn align4(val: usize) -> usize {
416 (val + 3) & !3
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn cpio_builder_produces_valid_archive() {
425 let mut cpio = CpioBuilder::new();
426 cpio.add_dir("bin");
427 cpio.add_file("init", 0o100755, b"#!/bin/sh\necho hello\n");
428 cpio.add_symlink("bin/sh", "/bin/busybox");
429 let archive = cpio.finish();
430
431 assert!(archive.starts_with(b"070701"));
433
434 let entries = parse_cpio_entries(&archive).expect("parse should succeed");
436 assert_eq!(entries.len(), 3);
437
438 assert_eq!(entries[0].0, "bin");
440 assert_eq!(entries[0].1 & 0o040000, 0o040000); assert_eq!(entries[1].0, "init");
444 assert_eq!(entries[1].1 & 0o100000, 0o100000); assert_eq!(entries[1].1 & 0o755, 0o755); assert_eq!(entries[1].2, 21); assert_eq!(entries[2].0, "bin/sh");
450 assert_eq!(entries[2].1 & 0o120000, 0o120000); }
452
453 #[test]
454 fn cpio_archive_ends_with_trailer() {
455 let cpio = CpioBuilder::new();
456 let archive = cpio.finish();
457 let as_str = String::from_utf8_lossy(&archive);
458 assert!(as_str.contains("TRAILER!!!"));
459 }
460
461 #[test]
462 fn build_initramfs_produces_gzipped_cpio() {
463 let result = build_initramfs(&["sshd"], &[]).expect("build should succeed");
464
465 assert_eq!(result[0], 0x1F);
467 assert_eq!(result[1], 0x8B);
468
469 use flate2::read::GzDecoder;
471 use std::io::Read;
472 let mut decoder = GzDecoder::new(&result[..]);
473 let mut decompressed = Vec::new();
474 decoder
475 .read_to_end(&mut decompressed)
476 .expect("decompress should succeed");
477
478 let entries = parse_cpio_entries(&decompressed).expect("parse should succeed");
480
481 assert!(entries.len() >= 20, "expected at least 20 entries, got {}", entries.len());
483
484 let init_entry = entries.iter().find(|(name, _, _)| name == "init");
486 assert!(init_entry.is_some(), "must have /init");
487
488 let dir_names: Vec<&str> = entries
490 .iter()
491 .filter(|(_, mode, _)| mode & 0o040000 != 0)
492 .map(|(name, _, _)| name.as_str())
493 .collect();
494 assert!(dir_names.contains(&"bin"));
495 assert!(dir_names.contains(&"etc"));
496 assert!(dir_names.contains(&"proc"));
497 assert!(dir_names.contains(&"sys"));
498 assert!(dir_names.contains(&"dev"));
499 }
500
501 #[test]
502 fn build_initramfs_with_extra_binaries() {
503 let fake_binary = b"\x7FELF fake binary content";
504 let result = build_initramfs(
505 &["rvf-server"],
506 &[("bin/rvf-server", fake_binary)],
507 )
508 .expect("build should succeed");
509
510 use flate2::read::GzDecoder;
512 use std::io::Read;
513 let mut decoder = GzDecoder::new(&result[..]);
514 let mut decompressed = Vec::new();
515 decoder.read_to_end(&mut decompressed).unwrap();
516
517 let entries = parse_cpio_entries(&decompressed).unwrap();
518 let binary_entry = entries.iter().find(|(name, _, _)| name == "bin/rvf-server");
519 assert!(binary_entry.is_some(), "must have bin/rvf-server");
520 assert_eq!(binary_entry.unwrap().2, fake_binary.len() as u32);
521 }
522
523 #[test]
524 fn default_init_script_mounts_filesystems() {
525 let script = default_init_script(&[]);
526 assert!(script.contains("mount -t proc proc /proc"));
527 assert!(script.contains("mount -t sysfs sysfs /sys"));
528 assert!(script.contains("mount -t devtmpfs devtmpfs /dev"));
529 }
530
531 #[test]
532 fn default_init_script_includes_services() {
533 let script = default_init_script(&["sshd", "rvf-server"]);
534 assert!(script.contains("dropbear") || script.contains("sshd"));
535 assert!(script.contains("rvf-server"));
536 }
537
538 #[test]
539 fn cpio_header_is_110_bytes() {
540 let mut cpio = CpioBuilder::new();
541 cpio.add_file("x", 0o100644, b"");
542 let archive = cpio.finish();
543 let header_str = std::str::from_utf8(&archive[..110]).unwrap();
545 assert!(header_str.starts_with(CPIO_NEWC_MAGIC));
546 }
547
548 #[test]
549 fn device_nodes_are_parseable() {
550 let mut cpio = CpioBuilder::new();
551 cpio.add_device("dev/null", 0o020666, 1, 3);
552 let archive = cpio.finish();
553 let entries = parse_cpio_entries(&archive).unwrap();
554 assert_eq!(entries.len(), 1);
555 assert_eq!(entries[0].0, "dev/null");
556 assert_eq!(entries[0].1 & 0o020000, 0o020000);
558 }
559}