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 super::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 pinned_header(mode: u32, mtime: u64) -> Header {
122 let mut header = Header::new_gnu();
123 header.set_uid(0);
124 header.set_gid(0);
125 header.set_mtime(mtime);
126 header.set_mode(mode);
127 header.set_size(0);
128 header
129}
130
131fn append_regular<W: Write>(
132 writer: &mut tar::Builder<W>,
133 header: &mut Header,
134 name: &str,
135 entry: &PlannedFile,
136 archive: &Path,
137) -> Result<()> {
138 match (&entry.content, &entry.source) {
139 (Some(content), None) => {
140 assert_eq!(content.len() as u64, entry.size);
141 header.set_size(content.len() as u64);
142 writer
143 .append_data(header, name, content.as_slice())
144 .map_err(|e| io(archive, e))
145 }
146 (None, Some(source)) => {
147 let file = std::fs::File::open(source).map_err(|e| io(source, e))?;
148 let mut reader = HashingReader::new(std::io::BufReader::new(file));
149 header.set_size(entry.size);
150 let append_result = writer
151 .append_data(header, name, &mut reader)
152 .map_err(|e| io(archive, e));
153 let drain_result =
157 std::io::copy(&mut reader, &mut std::io::sink()).map_err(|e| io(source, e));
158 let (digest, size) = reader.finish();
159 let expected = entry
160 .sha256
161 .as_ref()
162 .expect("validated regular files have a digest");
163 ensure_matches_plan(source, expected, entry.size, digest, size)?;
164 append_result?;
165 drain_result?;
166 Ok(())
167 }
168 _ => unreachable!("validated regular files have exactly one content source"),
169 }
170}
171
172fn archive_name(destination: &Path) -> Result<String> {
178 let normalized = crate::paths::normalize_absolute(destination);
179 let relative = normalized.strip_prefix("/").unwrap_or(&normalized);
180 if relative.as_os_str().is_empty() {
181 return Err(Error::PathEscape {
182 path: destination.to_path_buf(),
183 kind: "archive",
184 });
185 }
186 relative
187 .to_str()
188 .map(str::to_string)
189 .ok_or_else(|| Error::Config {
190 message: format!(
191 "`{}` is not valid Unicode and cannot be named in a tar or OCI archive; \
192 directory output has no such restriction",
193 destination.display()
194 ),
195 })
196}