1use anyhow::{Context, Result};
8use camino::Utf8Path;
9use cap_std_ext::dirext::{CapStdExtDirExt, WalkConfiguration};
10use fn_error_context::context;
11use ostree_ext::ostree;
12use std::collections::HashMap;
13use std::fs::File;
14use std::io::{self, Write};
15use std::ops::ControlFlow;
16
17use crate::cli::ExportFormat;
18
19#[derive(Debug, Default)]
21struct ExportOptions {
22 kernel_in_boot: bool,
24 disable_selinux: bool,
26}
27
28#[context("Exporting container")]
30pub(crate) async fn export(
31 format: &ExportFormat,
32 target_path: &Utf8Path,
33 output_path: Option<&Utf8Path>,
34 kernel_in_boot: bool,
35 disable_selinux: bool,
36) -> Result<()> {
37 use cap_std_ext::cap_std;
38 use cap_std_ext::cap_std::fs::Dir;
39
40 let options = ExportOptions {
41 kernel_in_boot,
42 disable_selinux,
43 };
44
45 let root_dir = Dir::open_ambient_dir(target_path, cap_std::ambient_authority())
46 .with_context(|| format!("Failed to open directory: {}", target_path))?;
47
48 match format {
49 ExportFormat::Tar => export_tar(&root_dir, output_path, &options).await,
50 }
51}
52
53#[context("Exporting to tar")]
55async fn export_tar(
56 root_dir: &cap_std_ext::cap_std::fs::Dir,
57 output_path: Option<&Utf8Path>,
58 options: &ExportOptions,
59) -> Result<()> {
60 let output: Box<dyn Write> = match output_path {
61 Some(path) => {
62 let file = File::create(path)
63 .with_context(|| format!("Failed to create output file: {}", path))?;
64 Box::new(file)
65 }
66 None => Box::new(io::stdout()),
67 };
68
69 let mut tar_builder = tar::Builder::new(output);
70 export_filesystem(&mut tar_builder, root_dir, options)?;
71 tar_builder.finish().context("Finalizing tar archive")?;
72
73 Ok(())
74}
75
76fn export_filesystem<W: Write>(
77 tar_builder: &mut tar::Builder<W>,
78 root_dir: &cap_std_ext::cap_std::fs::Dir,
79 options: &ExportOptions,
80) -> Result<()> {
81 let sepolicy = if options.disable_selinux {
86 None
87 } else {
88 crate::lsm::new_sepolicy_at(root_dir)?
89 };
90
91 export_filesystem_walk(tar_builder, root_dir, sepolicy.as_ref())?;
92
93 if options.kernel_in_boot {
94 handle_kernel_relocation(tar_builder, root_dir)?;
95 }
96
97 Ok(())
98}
99
100fn tar_header_from_meta(
102 entry_type: tar::EntryType,
103 size: u64,
104 meta: &cap_std_ext::cap_std::fs::Metadata,
105) -> tar::Header {
106 use cap_std_ext::cap_primitives::fs::{MetadataExt, PermissionsExt};
107
108 let mut header = tar::Header::new_gnu();
109 header.set_entry_type(entry_type);
110 header.set_size(size);
111 header.set_mode(meta.permissions().mode() & !libc::S_IFMT);
112 header.set_uid(meta.uid() as u64);
113 header.set_gid(meta.gid() as u64);
114 header
115}
116
117fn tar_header_dir_root() -> tar::Header {
119 let mut header = tar::Header::new_gnu();
120 header.set_entry_type(tar::EntryType::Directory);
121 header.set_size(0);
122 header.set_mode(0o755);
123 header.set_uid(0);
124 header.set_gid(0);
125 header
126}
127
128const SKIP_PATHS: &[&str] = &["sysroot/ostree"];
131
132fn export_filesystem_walk<W: Write>(
133 tar_builder: &mut tar::Builder<W>,
134 root_dir: &cap_std_ext::cap_std::fs::Dir,
135 sepolicy: Option<&ostree::SePolicy>,
136) -> Result<()> {
137 use std::path::Path;
138
139 let mut hardlinks: HashMap<(u64, u64), std::path::PathBuf> = HashMap::new();
143
144 let walk_config = WalkConfiguration::default()
146 .noxdev()
147 .path_base(Path::new("/"));
148
149 root_dir.walk(&walk_config, |entry| -> std::io::Result<ControlFlow<()>> {
150 let path = entry.path;
151
152 if path == Path::new("/") {
156 return Ok(ControlFlow::Continue(()));
157 }
158
159 let relative_path = path.strip_prefix("/").unwrap_or(path);
161
162 if relative_path == Path::new("") {
164 return Ok(ControlFlow::Continue(()));
165 }
166
167 for skip_path in SKIP_PATHS {
169 if relative_path.starts_with(skip_path) {
170 if entry.file_type.is_dir() {
172 return Ok(ControlFlow::Break(()));
173 }
174 return Ok(ControlFlow::Continue(()));
175 }
176 }
177
178 let file_type = entry.file_type;
179 if file_type.is_dir() {
180 add_directory_to_tar_from_walk(tar_builder, entry.dir, path, relative_path, sepolicy)
181 .map_err(std::io::Error::other)?;
182 } else if file_type.is_file() {
183 add_file_to_tar_from_walk(
184 tar_builder,
185 entry.dir,
186 entry.filename,
187 path,
188 relative_path,
189 sepolicy,
190 &mut hardlinks,
191 )
192 .map_err(std::io::Error::other)?;
193 } else if file_type.is_symlink() {
194 add_symlink_to_tar_from_walk(
195 tar_builder,
196 entry.dir,
197 entry.filename,
198 path,
199 relative_path,
200 sepolicy,
201 )
202 .map_err(std::io::Error::other)?;
203 } else {
204 return Err(std::io::Error::other(format!(
205 "Unsupported file type: {}",
206 relative_path.display()
207 )));
208 }
209
210 Ok(ControlFlow::Continue(()))
211 })?;
212
213 Ok(())
214}
215
216fn add_directory_to_tar_from_walk<W: Write>(
217 tar_builder: &mut tar::Builder<W>,
218 dir: &cap_std_ext::cap_std::fs::Dir,
219 absolute_path: &std::path::Path,
220 relative_path: &std::path::Path,
221 sepolicy: Option<&ostree::SePolicy>,
222) -> Result<()> {
223 use cap_std_ext::cap_primitives::fs::PermissionsExt;
224
225 let metadata = dir.dir_metadata()?;
226 let mut header = tar_header_from_meta(tar::EntryType::Directory, 0, &metadata);
227
228 if let Some(policy) = sepolicy {
229 let label = compute_selinux_label(policy, absolute_path, metadata.permissions().mode())?;
230 add_selinux_pax_extension(tar_builder, &label)?;
231 }
232
233 tar_builder
234 .append_data(&mut header, relative_path, &mut std::io::empty())
235 .with_context(|| format!("Failed to add directory: {}", relative_path.display()))?;
236
237 Ok(())
238}
239
240fn add_file_to_tar_from_walk<W: Write>(
241 tar_builder: &mut tar::Builder<W>,
242 dir: &cap_std_ext::cap_std::fs::Dir,
243 filename: &std::ffi::OsStr,
244 absolute_path: &std::path::Path,
245 relative_path: &std::path::Path,
246 sepolicy: Option<&ostree::SePolicy>,
247 hardlinks: &mut HashMap<(u64, u64), std::path::PathBuf>,
248) -> Result<()> {
249 use cap_std_ext::cap_primitives::fs::{MetadataExt, PermissionsExt};
250 use std::path::Path;
251
252 let filename_path = Path::new(filename);
253 let metadata = dir.metadata(filename_path)?;
254
255 let nlink = metadata.nlink();
259 if nlink > 1 {
260 let key = (metadata.dev(), metadata.ino());
261 if let Some(first_path) = hardlinks.get(&key) {
262 let mut header = tar_header_from_meta(tar::EntryType::Link, 0, &metadata);
264
265 if let Some(policy) = sepolicy {
266 let label =
267 compute_selinux_label(policy, absolute_path, metadata.permissions().mode())?;
268 add_selinux_pax_extension(tar_builder, &label)?;
269 }
270
271 tar_builder
272 .append_link(&mut header, relative_path, first_path)
273 .with_context(|| format!("Failed to add hardlink: {}", relative_path.display()))?;
274 return Ok(());
275 } else {
276 hardlinks.insert(key, relative_path.to_path_buf());
278 }
279 }
280
281 let mut header = tar_header_from_meta(tar::EntryType::Regular, metadata.len(), &metadata);
283
284 if let Some(policy) = sepolicy {
285 let label = compute_selinux_label(policy, absolute_path, metadata.permissions().mode())?;
286 add_selinux_pax_extension(tar_builder, &label)?;
287 }
288
289 let mut file = dir.open(filename_path)?;
290 tar_builder
291 .append_data(&mut header, relative_path, &mut file)
292 .with_context(|| format!("Failed to add file: {}", relative_path.display()))?;
293
294 Ok(())
295}
296
297fn add_symlink_to_tar_from_walk<W: Write>(
298 tar_builder: &mut tar::Builder<W>,
299 dir: &cap_std_ext::cap_std::fs::Dir,
300 filename: &std::ffi::OsStr,
301 absolute_path: &std::path::Path,
302 relative_path: &std::path::Path,
303 sepolicy: Option<&ostree::SePolicy>,
304) -> Result<()> {
305 use cap_std_ext::cap_primitives::fs::PermissionsExt;
306 use std::path::Path;
307
308 let filename_path = Path::new(filename);
309 let link_target = dir
310 .read_link_contents(filename_path)
311 .with_context(|| format!("Failed to read symlink: {:?}", filename))?;
312 let metadata = dir.symlink_metadata(filename_path)?;
313 let mut header = tar_header_from_meta(tar::EntryType::Symlink, 0, &metadata);
314
315 if let Some(policy) = sepolicy {
316 let symlink_mode = libc::S_IFLNK | (metadata.permissions().mode() & !libc::S_IFMT);
318 let label = compute_selinux_label(policy, absolute_path, symlink_mode)?;
319 add_selinux_pax_extension(tar_builder, &label)?;
320 }
321
322 tar_builder
323 .append_link(&mut header, relative_path, &link_target)
324 .with_context(|| format!("Failed to add symlink: {}", relative_path.display()))?;
325
326 Ok(())
327}
328
329fn handle_kernel_relocation<W: Write>(
331 tar_builder: &mut tar::Builder<W>,
332 root_dir: &cap_std_ext::cap_std::fs::Dir,
333) -> Result<()> {
334 use crate::kernel::KernelType;
335
336 let kernel_info = match crate::kernel::find_kernel(root_dir)? {
337 Some(kernel) => kernel,
338 None => return Ok(()),
339 };
340
341 append_dir_entry(tar_builder, "boot")?;
342 append_dir_entry(tar_builder, "boot/grub2")?;
343
344 if kernel_info.kernel.unified {
346 return Ok(());
347 }
348
349 if let KernelType::Vmlinuz { path, initramfs } = &kernel_info.k_type {
351 let version = &kernel_info.kernel.version;
352
353 if root_dir.try_exists(path)? {
355 let metadata = root_dir.metadata(path)?;
356 let mut header =
357 tar_header_from_meta(tar::EntryType::Regular, metadata.len(), &metadata);
358 let mut file = root_dir.open(path)?;
359 let boot_path = format!("boot/vmlinuz-{}", version);
360 tar_builder
361 .append_data(&mut header, &boot_path, &mut file)
362 .with_context(|| format!("Failed to add kernel: {}", boot_path))?;
363 }
364
365 if root_dir.try_exists(initramfs)? {
367 let metadata = root_dir.metadata(initramfs)?;
368 let mut header =
369 tar_header_from_meta(tar::EntryType::Regular, metadata.len(), &metadata);
370 let mut file = root_dir.open(initramfs)?;
371 let boot_path = format!("boot/initramfs-{}.img", version);
372 tar_builder
373 .append_data(&mut header, &boot_path, &mut file)
374 .with_context(|| format!("Failed to add initramfs: {}", boot_path))?;
375 }
376 }
377
378 Ok(())
379}
380
381fn append_dir_entry<W: Write>(tar_builder: &mut tar::Builder<W>, path: &str) -> Result<()> {
382 let mut header = tar_header_dir_root();
383 tar_builder
384 .append_data(&mut header, path, &mut std::io::empty())
385 .with_context(|| format!("Failed to create {} directory", path))?;
386 Ok(())
387}
388
389fn compute_selinux_label(
390 policy: &ostree::SePolicy,
391 path: &std::path::Path,
392 mode: u32,
393) -> Result<String> {
394 use camino::Utf8Path;
395
396 let path_str = path
398 .to_str()
399 .ok_or_else(|| anyhow::anyhow!("Non-UTF8 path not supported: {:?}", path))?;
400 let utf8_path = Utf8Path::new(path_str);
401
402 let label = crate::lsm::require_label(policy, utf8_path, mode)?;
403 Ok(label.to_string())
404}
405
406fn add_selinux_pax_extension<W: Write>(
407 tar_builder: &mut tar::Builder<W>,
408 selinux_context: &str,
409) -> Result<()> {
410 tar_builder
411 .append_pax_extensions([("SCHILY.xattr.security.selinux", selinux_context.as_bytes())])
412 .context("Failed to add SELinux PAX extension")?;
413 Ok(())
414}