elfpak_core/rootfs/
archive.rs1use crate::{
10 error::{Error, Result, io},
11 hash::{HashingReader, ensure_matches_plan},
12 plan::{BundlePlan, PlannedFile, PlannedFileKind},
13};
14use std::{
15 io::Write,
16 path::{Path, PathBuf},
17};
18use tar::{EntryType, Header};
19
20#[derive(Debug)]
21pub struct TarBuilder {
22 path: PathBuf,
23}
24
25#[derive(Debug, Default, Clone, Copy)]
26pub struct TarReport {
27 pub files: u32,
28 pub directories: u32,
29 pub symlinks: u32,
30 pub bytes: u64,
32}
33
34impl TarBuilder {
35 pub fn new(path: impl Into<PathBuf>) -> TarBuilder {
36 TarBuilder { path: path.into() }
37 }
38
39 pub fn apply(&self, plan: &BundlePlan) -> Result<TarReport> {
40 let parent = self
41 .path
42 .parent()
43 .filter(|parent| !parent.as_os_str().is_empty())
44 .unwrap_or_else(|| Path::new("."));
45 std::fs::create_dir_all(parent).map_err(|e| io(parent, e))?;
46 let mut stage = tempfile::Builder::new()
47 .prefix(".elfpak-tar-")
48 .tempfile_in(parent)
49 .map_err(|e| io(parent, e))?;
50 set_output_permissions(stage.path(), &self.path)?;
51 let mut writer = tar::Builder::new(std::io::BufWriter::new(stage.as_file_mut()));
52 writer.mode(tar::HeaderMode::Complete);
55
56 let mut report = TarReport::default();
57 let mtime = super::copy::source_date_epoch_secs()?;
58
59 for entry in &plan.files {
60 entry.assert_well_formed();
61 let name = archive_name(&entry.destination)?;
62
63 let mut header = pinned_header(entry.mode, mtime);
64
65 match entry.kind {
66 PlannedFileKind::Directory => {
67 header.set_entry_type(EntryType::Directory);
68 writer
69 .append_data(&mut header, format!("{name}/"), std::io::empty())
70 .map_err(|e| io(&self.path, e))?;
71 report.directories += 1;
72 }
73 PlannedFileKind::Symlink => {
74 let target = entry
75 .link_target
76 .clone()
77 .expect("validated symlinks have a target");
78 header.set_entry_type(EntryType::Symlink);
79 writer
80 .append_link(&mut header, &name, &target)
81 .map_err(|e| io(&self.path, e))?;
82 report.symlinks += 1;
83 }
84 _ => {
85 header.set_entry_type(EntryType::Regular);
86 append_regular(&mut writer, &mut header, &name, entry, &self.path)?;
87 report.files += 1;
88 report.bytes += entry.size;
89 }
90 }
91 }
92
93 writer.finish().map_err(|e| io(&self.path, e))?;
94 writer
95 .into_inner()
96 .map_err(|e| io(&self.path, e))?
97 .flush()
98 .map_err(|e| io(&self.path, e))?;
99 stage.as_file().sync_all().map_err(|e| io(&self.path, e))?;
100
101 let entries = report.files + report.directories + report.symlinks;
102 assert_eq!(
103 entries as usize,
104 plan.files.len(),
105 "every entry is archived"
106 );
107 stage
108 .persist(&self.path)
109 .map_err(|e| io(&self.path, e.error))?;
110 Ok(report)
111 }
112}
113
114fn set_output_permissions(stage: &Path, destination: &Path) -> Result<()> {
115 use std::os::unix::fs::PermissionsExt;
116
117 let permissions = std::fs::metadata(destination)
118 .map(|metadata| metadata.permissions())
119 .unwrap_or_else(|_| std::fs::Permissions::from_mode(0o644));
120 std::fs::set_permissions(stage, permissions).map_err(|e| io(stage, e))
121}
122
123fn pinned_header(mode: u32, mtime: u64) -> Header {
125 let mut header = Header::new_gnu();
126 header.set_uid(0);
127 header.set_gid(0);
128 header.set_mtime(mtime);
129 header.set_mode(mode);
130 header.set_size(0);
131 header
132}
133
134fn append_regular<W: Write>(
135 writer: &mut tar::Builder<W>,
136 header: &mut Header,
137 name: &str,
138 entry: &PlannedFile,
139 archive: &Path,
140) -> Result<()> {
141 match (&entry.content, &entry.source) {
142 (Some(content), None) => {
143 assert_eq!(content.len() as u64, entry.size);
144 header.set_size(content.len() as u64);
145 writer
146 .append_data(header, name, content.as_slice())
147 .map_err(|e| io(archive, e))
148 }
149 (None, Some(source)) => {
150 let file = std::fs::File::open(source).map_err(|e| io(source, e))?;
151 let mut reader = HashingReader::new(std::io::BufReader::new(file));
152 header.set_size(entry.size);
153 let append_result = writer
154 .append_data(header, name, &mut reader)
155 .map_err(|e| io(archive, e));
156 let drain_result =
160 std::io::copy(&mut reader, &mut std::io::sink()).map_err(|e| io(source, e));
161 let (digest, size) = reader.finish();
162 let expected = entry
163 .sha256
164 .as_ref()
165 .expect("validated regular files have a digest");
166 ensure_matches_plan(source, expected, entry.size, digest, size)?;
167 append_result?;
168 drain_result?;
169 Ok(())
170 }
171 _ => unreachable!("validated regular files have exactly one content source"),
172 }
173}
174
175fn archive_name(destination: &Path) -> Result<String> {
177 let normalized = crate::paths::normalize_absolute(destination);
178 let relative = normalized.strip_prefix("/").unwrap_or(&normalized);
179 relative
180 .to_str()
181 .map(str::to_string)
182 .filter(|name| !name.is_empty())
183 .ok_or_else(|| Error::PathEscape {
184 path: destination.to_path_buf(),
185 kind: "archive",
186 })
187}