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 report = {
52 let writer = std::io::BufWriter::new(stage.as_file_mut());
53 let (_, report) = self.write_to(writer, plan)?;
54 report
55 };
56 stage.as_file().sync_all().map_err(|e| io(&self.path, e))?;
57 stage
58 .persist(&self.path)
59 .map_err(|e| io(&self.path, e.error))?;
60 Ok(report)
61 }
62
63 pub fn write_to<W: Write>(&self, writer: W, plan: &BundlePlan) -> Result<(W, TarReport)> {
65 let mut writer = tar::Builder::new(writer);
66 writer.mode(tar::HeaderMode::Complete);
69
70 let mut report = TarReport::default();
71 let mtime = super::copy::source_date_epoch_secs()?;
72
73 for entry in &plan.files {
74 entry.assert_well_formed();
75 let name = archive_name(&entry.destination)?;
76
77 let mut header = pinned_header(entry.mode, mtime);
78
79 match entry.kind {
80 PlannedFileKind::Directory => {
81 header.set_entry_type(EntryType::Directory);
82 writer
83 .append_data(&mut header, format!("{name}/"), std::io::empty())
84 .map_err(|e| io(&self.path, e))?;
85 report.directories += 1;
86 }
87 PlannedFileKind::Symlink => {
88 let target = entry
89 .link_target
90 .clone()
91 .expect("validated symlinks have a target");
92 header.set_entry_type(EntryType::Symlink);
93 writer
94 .append_link(&mut header, &name, &target)
95 .map_err(|e| io(&self.path, e))?;
96 report.symlinks += 1;
97 }
98 _ => {
99 header.set_entry_type(EntryType::Regular);
100 append_regular(&mut writer, &mut header, &name, entry, &self.path)?;
101 report.files += 1;
102 report.bytes += entry.size;
103 }
104 }
105 }
106
107 let entries = report.files + report.directories + report.symlinks;
108 assert_eq!(
109 entries as usize,
110 plan.files.len(),
111 "every entry is archived"
112 );
113 writer.finish().map_err(|e| io(&self.path, e))?;
114 let mut writer = writer.into_inner().map_err(|e| io(&self.path, e))?;
115 writer.flush().map_err(|e| io(&self.path, e))?;
116 Ok((writer, report))
117 }
118}
119
120fn set_output_permissions(stage: &Path, destination: &Path) -> Result<()> {
121 use std::os::unix::fs::PermissionsExt;
122
123 let permissions = std::fs::metadata(destination)
124 .map(|metadata| metadata.permissions())
125 .unwrap_or_else(|_| std::fs::Permissions::from_mode(0o644));
126 std::fs::set_permissions(stage, permissions).map_err(|e| io(stage, e))
127}
128
129fn pinned_header(mode: u32, mtime: u64) -> Header {
131 let mut header = Header::new_gnu();
132 header.set_uid(0);
133 header.set_gid(0);
134 header.set_mtime(mtime);
135 header.set_mode(mode);
136 header.set_size(0);
137 header
138}
139
140fn append_regular<W: Write>(
141 writer: &mut tar::Builder<W>,
142 header: &mut Header,
143 name: &str,
144 entry: &PlannedFile,
145 archive: &Path,
146) -> Result<()> {
147 match (&entry.content, &entry.source) {
148 (Some(content), None) => {
149 assert_eq!(content.len() as u64, entry.size);
150 header.set_size(content.len() as u64);
151 writer
152 .append_data(header, name, content.as_slice())
153 .map_err(|e| io(archive, e))
154 }
155 (None, Some(source)) => {
156 let file = std::fs::File::open(source).map_err(|e| io(source, e))?;
157 let mut reader = HashingReader::new(std::io::BufReader::new(file));
158 header.set_size(entry.size);
159 let append_result = writer
160 .append_data(header, name, &mut reader)
161 .map_err(|e| io(archive, e));
162 let drain_result =
166 std::io::copy(&mut reader, &mut std::io::sink()).map_err(|e| io(source, e));
167 let (digest, size) = reader.finish();
168 let expected = entry
169 .sha256
170 .as_ref()
171 .expect("validated regular files have a digest");
172 ensure_matches_plan(source, expected, entry.size, digest, size)?;
173 append_result?;
174 drain_result?;
175 Ok(())
176 }
177 _ => unreachable!("validated regular files have exactly one content source"),
178 }
179}
180
181fn archive_name(destination: &Path) -> Result<String> {
183 let normalized = crate::paths::normalize_absolute(destination);
184 let relative = normalized.strip_prefix("/").unwrap_or(&normalized);
185 relative
186 .to_str()
187 .map(str::to_string)
188 .filter(|name| !name.is_empty())
189 .ok_or_else(|| Error::PathEscape {
190 path: destination.to_path_buf(),
191 kind: "archive",
192 })
193}