initramfs_builder/initramfs/
cpio.rs1use anyhow::{Context, Result};
2use std::fs::{self};
3use std::io::Write;
4use std::os::unix::fs::{MetadataExt, PermissionsExt};
5use std::path::Path;
6use tracing::debug;
7use walkdir::WalkDir;
8
9pub struct CpioArchive {
10 entries: Vec<CpioEntry>,
11}
12
13struct CpioEntry {
14 path: String,
15 mode: u32,
16 uid: u32,
17 gid: u32,
18 nlink: u32,
19 mtime: u32,
20 data: Vec<u8>,
21 dev_major: u32,
22 dev_minor: u32,
23 rdev_major: u32,
24 rdev_minor: u32,
25}
26
27impl CpioArchive {
28 pub fn new() -> Self {
29 Self {
30 entries: Vec::new(),
31 }
32 }
33
34 pub fn from_directory(root: &Path) -> Result<Self> {
36 let mut archive = Self::new();
37
38 for entry in WalkDir::new(root).follow_links(false) {
39 let entry = entry?;
40 let full_path = entry.path();
41
42 let rel_path = full_path.strip_prefix(root).unwrap_or(full_path);
43
44 if rel_path.as_os_str().is_empty() {
45 continue;
46 }
47
48 let archive_path = format!("{}", rel_path.display());
49
50 archive.add_path(full_path, &archive_path)?;
51 }
52
53 Ok(archive)
54 }
55
56 fn add_path(&mut self, source_path: &Path, archive_path: &str) -> Result<()> {
58 let metadata = fs::symlink_metadata(source_path)
59 .with_context(|| format!("Failed to read metadata for {:?}", source_path))?;
60
61 let file_type = metadata.file_type();
62 let mode = metadata.permissions().mode();
63
64 let data = if file_type.is_file() {
65 fs::read(source_path)?
66 } else if file_type.is_symlink() {
67 let target = fs::read_link(source_path)?;
68 target.to_string_lossy().as_bytes().to_vec()
69 } else {
70 Vec::new()
71 };
72
73 debug!(
74 "Adding to cpio: {} (mode: {:o}, size: {})",
75 archive_path,
76 mode,
77 data.len()
78 );
79
80 self.entries.push(CpioEntry {
81 path: archive_path.to_string(),
82 mode,
83 uid: metadata.uid(),
84 gid: metadata.gid(),
85 nlink: metadata.nlink() as u32,
86 mtime: metadata.mtime() as u32,
87 data,
88 dev_major: 0,
89 dev_minor: 0,
90 rdev_major: 0,
91 rdev_minor: 0,
92 });
93
94 Ok(())
95 }
96
97 pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
99 let mut ino = 1u32;
100
101 for entry in &self.entries {
102 self.write_entry(writer, entry, ino)?;
103 ino += 1;
104 }
105
106 self.write_trailer(writer)?;
108
109 Ok(())
110 }
111
112 fn write_entry<W: Write>(&self, writer: &mut W, entry: &CpioEntry, ino: u32) -> Result<()> {
114 let namesize = entry.path.len() + 1; let filesize = entry.data.len();
116
117 let header = format!(
119 "{}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}",
120 "070701", ino, entry.mode, entry.uid, entry.gid, entry.nlink, entry.mtime, filesize, entry.dev_major, entry.dev_minor, entry.rdev_major, entry.rdev_minor, namesize, 0u32, );
135
136 writer.write_all(header.as_bytes())?;
137 writer.write_all(entry.path.as_bytes())?;
138 writer.write_all(&[0])?; let header_plus_name = 110 + namesize;
142 let padding = (4 - (header_plus_name % 4)) % 4;
143 writer.write_all(&vec![0u8; padding])?;
144
145 writer.write_all(&entry.data)?;
146
147 let data_padding = (4 - (filesize % 4)) % 4;
149 writer.write_all(&vec![0u8; data_padding])?;
150
151 Ok(())
152 }
153
154 fn write_trailer<W: Write>(&self, writer: &mut W) -> Result<()> {
156 let trailer_name = "TRAILER!!!";
157 let namesize = trailer_name.len() + 1;
158
159 let header = format!(
160 "{}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}",
161 "070701", 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, namesize, 0
162 );
163
164 writer.write_all(header.as_bytes())?;
165 writer.write_all(trailer_name.as_bytes())?;
166 writer.write_all(&[0])?;
167
168 let header_plus_name = 110 + namesize;
170 let padding = (4 - (header_plus_name % 4)) % 4;
171 writer.write_all(&vec![0u8; padding])?;
172
173 Ok(())
174 }
175
176 pub fn len(&self) -> usize {
178 self.entries.len()
179 }
180
181 pub fn is_empty(&self) -> bool {
182 self.entries.is_empty()
183 }
184}
185
186impl Default for CpioArchive {
187 fn default() -> Self {
188 Self::new()
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use std::fs;
196 use tempfile::TempDir;
197
198 #[test]
199 fn test_empty_archive() {
200 let archive = CpioArchive::new();
201 assert!(archive.is_empty());
202 assert_eq!(archive.len(), 0);
203 }
204
205 #[test]
206 fn test_archive_from_directory() {
207 let temp_dir = TempDir::new().unwrap();
208 let file_path = temp_dir.path().join("test.txt");
209 fs::write(&file_path, b"hello world").unwrap();
210
211 let archive = CpioArchive::from_directory(temp_dir.path()).unwrap();
212 assert_eq!(archive.len(), 1);
213 }
214
215 #[test]
216 fn test_cpio_header_magic() {
217 let temp_dir = TempDir::new().unwrap();
218 let file_path = temp_dir.path().join("test.txt");
219 fs::write(&file_path, b"test").unwrap();
220
221 let archive = CpioArchive::from_directory(temp_dir.path()).unwrap();
222 let mut output = Vec::new();
223 archive.write_to(&mut output).unwrap();
224
225 let header = String::from_utf8_lossy(&output[..6]);
226 assert_eq!(header, "070701", "CPIO header should start with newc magic");
227 }
228
229 #[test]
230 fn test_cpio_trailer() {
231 let archive = CpioArchive::new();
232 let mut output = Vec::new();
233 archive.write_to(&mut output).unwrap();
234
235 let output_str = String::from_utf8_lossy(&output);
236 assert!(
237 output_str.contains("TRAILER!!!"),
238 "Archive should end with TRAILER!!!"
239 );
240 }
241
242 #[test]
243 fn test_multiple_files() {
244 let temp_dir = TempDir::new().unwrap();
245 fs::write(temp_dir.path().join("a.txt"), b"aaa").unwrap();
246 fs::write(temp_dir.path().join("b.txt"), b"bbb").unwrap();
247 fs::create_dir(temp_dir.path().join("subdir")).unwrap();
248 fs::write(temp_dir.path().join("subdir/c.txt"), b"ccc").unwrap();
249
250 let archive = CpioArchive::from_directory(temp_dir.path()).unwrap();
251 assert_eq!(archive.len(), 4); }
253
254 #[test]
255 fn test_symlink_handling() {
256 let temp_dir = TempDir::new().unwrap();
257 let target = temp_dir.path().join("target.txt");
258 let link = temp_dir.path().join("link.txt");
259 fs::write(&target, b"target content").unwrap();
260
261 #[cfg(unix)]
262 std::os::unix::fs::symlink(&target, &link).unwrap();
263
264 let archive = CpioArchive::from_directory(temp_dir.path()).unwrap();
265
266 #[cfg(unix)]
267 assert_eq!(archive.len(), 2);
268 }
269
270 #[test]
271 fn test_output_alignment() {
272 let temp_dir = TempDir::new().unwrap();
273 fs::write(temp_dir.path().join("odd.txt"), b"123").unwrap(); let archive = CpioArchive::from_directory(temp_dir.path()).unwrap();
276 let mut output = Vec::new();
277 archive.write_to(&mut output).unwrap();
278
279 assert_eq!(output.len() % 4, 0, "CPIO output should be 4-byte aligned");
281 }
282}