Skip to main content

initramfs_builder/initramfs/
cpio.rs

1use 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    /// Build a CPIO archive from a directory
35    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    /// Add a file or directory to the archive
57    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    /// Write the archive to a file
98    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        // Write trailer
107        self.write_trailer(writer)?;
108
109        Ok(())
110    }
111
112    /// Write a single entry in newc format
113    fn write_entry<W: Write>(&self, writer: &mut W, entry: &CpioEntry, ino: u32) -> Result<()> {
114        let namesize = entry.path.len() + 1; // +1 for null terminator
115        let filesize = entry.data.len();
116
117        // newc header format (110 bytes of ASCII hex)
118        let header = format!(
119            "{}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}",
120            "070701",         // magic
121            ino,              // inode
122            entry.mode,       // mode
123            entry.uid,        // uid
124            entry.gid,        // gid
125            entry.nlink,      // nlink
126            entry.mtime,      // mtime
127            filesize,         // filesize
128            entry.dev_major,  // dev major
129            entry.dev_minor,  // dev minor
130            entry.rdev_major, // rdev major
131            entry.rdev_minor, // rdev minor
132            namesize,         // namesize
133            0u32,             // checksum (always 0 for newc)
134        );
135
136        writer.write_all(header.as_bytes())?;
137        writer.write_all(entry.path.as_bytes())?;
138        writer.write_all(&[0])?; // null terminator
139
140        // Pad to 4-byte boundary after header+name
141        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        // Pad data to 4-byte boundary
148        let data_padding = (4 - (filesize % 4)) % 4;
149        writer.write_all(&vec![0u8; data_padding])?;
150
151        Ok(())
152    }
153
154    /// Write the TRAILER!!! entry
155    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        // Pad to 4-byte boundary
169        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    /// Get the number of entries
177    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); // 3 files + 1 directory
252    }
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(); // 3 bytes, needs padding
274
275        let archive = CpioArchive::from_directory(temp_dir.path()).unwrap();
276        let mut output = Vec::new();
277        archive.write_to(&mut output).unwrap();
278
279        // Output should be 4-byte aligned
280        assert_eq!(output.len() % 4, 0, "CPIO output should be 4-byte aligned");
281    }
282}