1use crate::PetriInitrd;
7use crate::TempOrPersistentPath;
8use anyhow::Context;
9use std::io::Write;
10use std::path::Path;
11use std::sync::Arc;
12
13pub struct InitrdFile<'a> {
15 pub name: &'a str,
17 pub contents: &'a [u8],
19 pub permissions: u32,
21}
22
23impl PetriInitrd {
24 pub fn prepare(
33 base: &Path,
34 files: &[InitrdFile<'_>],
35 rdinit: &str,
36 scratch_dir: Option<&Path>,
37 ) -> anyhow::Result<Self> {
38 anyhow::ensure!(
39 rdinit.starts_with('/') && !rdinit.chars().any(|c| c.is_control() || c.is_whitespace()),
40 "rdinit must be an absolute path without whitespace or control characters"
41 );
42 let mut data = fs_err::read(base).context("reading base initrd")?;
43 for file in files {
44 anyhow::ensure!(
45 !file.name.is_empty()
46 && !file.name.starts_with('/')
47 && !file.name.contains(['\0', '\\'])
48 && file
49 .name
50 .split('/')
51 .all(|part| !matches!(part, "" | "." | "..")),
52 "invalid initrd file path: {:?}",
53 file.name
54 );
55 anyhow::ensure!(
56 file.permissions & !0o7777 == 0,
57 "invalid initrd file permissions"
58 );
59 data = initrd_cpio::inject_into_initrd(
60 &data,
61 file.name,
62 file.contents,
63 0o100000 | file.permissions,
64 )
65 .with_context(|| format!("injecting {} into initrd", file.name))?;
66 }
67 let mut builder = tempfile::Builder::new();
68 builder.prefix(".petri-initrd-").suffix(".gz");
69 let mut output = match scratch_dir {
70 Some(dir) => builder.tempfile_in(dir),
71 None => builder.tempfile(),
72 }
73 .context("creating prepared initrd")?;
74 output.write_all(&data).context("writing prepared initrd")?;
75 Ok(Self {
76 path: TempOrPersistentPath::Temp(Arc::new(output.into_temp_path())),
77 rdinit_param: rdinit.into(),
78 })
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use std::io::Read;
86 use test_with_tracing::test;
87
88 #[test]
89 fn prepares_explicit_files_and_owns_temporary_output() -> anyhow::Result<()> {
90 let temp = tempfile::tempdir()?;
91 let base = temp.path().join("base.gz");
92 let mut trailer = format!(
93 "070701{}0000000b00000000TRAILER!!!\0",
94 "00000000".repeat(11)
95 )
96 .into_bytes();
97 trailer.resize(trailer.len().div_ceil(4) * 4, 0);
98 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
99 encoder.write_all(&trailer)?;
100 let original = encoder.finish()?;
101 fs_err::write(&base, &original)?;
102 let files = [
103 InitrdFile {
104 name: "pipette",
105 contents: b"agent",
106 permissions: 0o755,
107 },
108 InitrdFile {
109 name: "init.sh",
110 contents: b"exec /pipette",
111 permissions: 0o755,
112 },
113 InitrdFile {
114 name: "ca.pem",
115 contents: b"explicit CA bundle",
116 permissions: 0o644,
117 },
118 ];
119 let first = PetriInitrd::prepare(&base, &files, "/init.sh", Some(temp.path()))?;
120 let second = PetriInitrd::prepare(&base, &files, "/init.sh", None)?;
121 assert_eq!(first.path.as_ref().parent(), Some(temp.path()));
122 assert_eq!(
123 second.path.as_ref().parent(),
124 Some(std::env::temp_dir().as_path())
125 );
126 assert_ne!(first.path.as_ref(), second.path.as_ref());
127 assert_eq!(first.rdinit_param, "/init.sh");
128 assert_eq!(fs_err::read(&base)?, original);
129 let mut archive = Vec::new();
130 flate2::read::GzDecoder::new(fs_err::File::open(first.path.as_ref())?)
131 .read_to_end(&mut archive)?;
132 let mut offset = 0;
133 for file in files {
134 assert_eq!(&archive[offset..offset + 6], b"070701");
135 let field = |start, end| {
136 u32::from_str_radix(
137 std::str::from_utf8(&archive[offset + start..offset + end]).unwrap(),
138 16,
139 )
140 .unwrap() as usize
141 };
142 assert_eq!(field(14, 22), (0o100000 | file.permissions) as usize);
143 let size = field(54, 62);
144 let name_size = field(94, 102);
145 assert_eq!(
146 &archive[offset + 110..offset + 110 + name_size - 1],
147 file.name.as_bytes()
148 );
149 let content_offset = offset + (110 + name_size).div_ceil(4) * 4;
150 assert_eq!(
151 &archive[content_offset..content_offset + size],
152 file.contents
153 );
154 offset = content_offset + size.div_ceil(4) * 4;
155 }
156 let path = first.path.as_ref().to_path_buf();
157 let owner = first.clone();
158 drop(first);
159 assert!(path.exists());
160 drop(owner);
161 assert!(!path.exists());
162 for name in ["../escape", "/absolute", "bad\0name"] {
163 assert!(
164 PetriInitrd::prepare(
165 &base,
166 &[InitrdFile {
167 name,
168 contents: b"",
169 permissions: 0o644
170 }],
171 "/init.sh",
172 Some(temp.path()),
173 )
174 .is_err()
175 );
176 }
177 Ok(())
178 }
179}