]> git.proxmox.com Git - proxmox-backup.git/blob - src/pxar/extract.rs
switch to external pxar and fuse crates
[proxmox-backup.git] / src / pxar / extract.rs
1 //! Code for extraction of pxar contents onto the file system.
2
3 use std::convert::TryFrom;
4 use std::ffi::{CStr, CString, OsStr};
5 use std::io;
6 use std::os::unix::ffi::OsStrExt;
7 use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
8 use std::path::Path;
9
10 use anyhow::{bail, format_err, Error};
11 use nix::dir::Dir;
12 use nix::fcntl::OFlag;
13 use nix::sys::stat::Mode;
14
15 use pathpatterns::{MatchEntry, MatchList, MatchType};
16 use pxar::format::Device;
17 use pxar::Metadata;
18
19 use proxmox::c_result;
20 use proxmox::tools::fs::{create_path, CreateOptions};
21
22 use crate::pxar::dir_stack::PxarDirStack;
23 use crate::pxar::flags;
24 use crate::pxar::metadata;
25
26 struct Extractor<'a> {
27 /// FIXME: use bitflags!() for feature_flags
28 feature_flags: u64,
29 allow_existing_dirs: bool,
30 callback: &'a mut dyn FnMut(&Path),
31 dir_stack: PxarDirStack,
32 }
33
34 impl<'a> Extractor<'a> {
35 fn with_flag(&self, flag: u64) -> bool {
36 flag == (self.feature_flags & flag)
37 }
38 }
39
40 pub fn extract_archive<T, F>(
41 mut decoder: pxar::decoder::Decoder<T>,
42 destination: &Path,
43 match_list: &[MatchEntry],
44 feature_flags: u64,
45 allow_existing_dirs: bool,
46 mut callback: F,
47 ) -> Result<(), Error>
48 where
49 T: pxar::decoder::SeqRead,
50 F: FnMut(&Path),
51 {
52 // we use this to keep track of our directory-traversal
53 decoder.enable_goodbye_entries(true);
54
55 let root = decoder
56 .next()
57 .ok_or_else(|| format_err!("found empty pxar archive"))?
58 .map_err(|err| format_err!("error reading pxar archive: {}", err))?;
59
60 if !root.is_dir() {
61 bail!("pxar archive does not start with a directory entry!");
62 }
63
64 create_path(
65 &destination,
66 None,
67 Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
68 )
69 .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
70
71 let dir = Dir::open(
72 destination,
73 OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
74 Mode::empty(),
75 )
76 .map_err(|err| format_err!("unable to open target directory {:?}: {}", destination, err,))?;
77
78 let mut extractor = Extractor {
79 feature_flags,
80 allow_existing_dirs,
81 callback: &mut callback,
82 dir_stack: PxarDirStack::new(dir, root.metadata().clone()),
83 };
84
85 let mut match_stack = Vec::new();
86 let mut current_match = true;
87 while let Some(entry) = decoder.next() {
88 use pxar::EntryKind;
89
90 let entry = entry.map_err(|err| format_err!("error reading pxar archive: {}", err))?;
91
92 let file_name_os = entry.file_name();
93
94 // safety check: a file entry in an archive must never contain slashes:
95 if file_name_os.as_bytes().contains(&b'/') {
96 bail!("archive file entry contains slashes, which is invalid and a security concern");
97 }
98
99 let file_name = CString::new(file_name_os.as_bytes())
100 .map_err(|_| format_err!("encountered file name with null-bytes"))?;
101
102 let metadata = entry.metadata();
103
104 let match_result = match_list.matches(
105 entry.path().as_os_str().as_bytes(),
106 Some(metadata.file_type() as u32),
107 );
108
109 let did_match = match match_result {
110 Some(MatchType::Include) => true,
111 Some(MatchType::Exclude) => false,
112 None => current_match,
113 };
114 match (did_match, entry.kind()) {
115 (_, EntryKind::Directory) => {
116 extractor.callback(entry.path());
117
118 extractor
119 .dir_stack
120 .push(file_name_os.to_owned(), metadata.clone())?;
121
122 if current_match && match_result != Some(MatchType::Exclude) {
123 // We're currently in a positive match and this directory does not match an
124 // exclude entry, so make sure it is created:
125 let _ = extractor
126 .dir_stack
127 .last_dir_fd(extractor.allow_existing_dirs)
128 .map_err(|err| {
129 format_err!("error creating entry {:?}: {}", file_name_os, err)
130 })?;
131 }
132
133 // We're starting a new directory, push our old matching state and replace it with
134 // our new one:
135 match_stack.push(current_match);
136 current_match = did_match;
137
138 Ok(())
139 }
140 (_, EntryKind::GoodbyeTable) => {
141 // go up a directory
142 let dir = extractor
143 .dir_stack
144 .pop()
145 .map_err(|err| format_err!("unexpected end of directory entry: {}", err))?
146 .ok_or_else(|| format_err!("broken pxar archive (directory stack underrun)"))?;
147 // We left a directory, also get back our previous matching state. This is in sync
148 // with `dir_stack` so this should never be empty except for the final goodbye
149 // table, in which case we get back to the default of `true`.
150 current_match = match_stack.pop().unwrap_or(true);
151
152 if let Some(fd) = dir.try_as_raw_fd() {
153 metadata::apply(extractor.feature_flags, dir.metadata(), fd, &file_name)
154 } else {
155 Ok(())
156 }
157 }
158 (true, EntryKind::Symlink(link)) => {
159 extractor.callback(entry.path());
160 extractor.extract_symlink(&file_name, metadata, link.as_ref())
161 }
162 (true, EntryKind::Hardlink(link)) => {
163 extractor.callback(entry.path());
164 extractor.extract_hardlink(&file_name, metadata, link.as_os_str())
165 }
166 (true, EntryKind::Device(dev)) => {
167 if extractor.with_flag(flags::WITH_DEVICE_NODES) {
168 extractor.callback(entry.path());
169 extractor.extract_device(&file_name, metadata, dev)
170 } else {
171 Ok(())
172 }
173 }
174 (true, EntryKind::Fifo) => {
175 if extractor.with_flag(flags::WITH_FIFOS) {
176 extractor.callback(entry.path());
177 extractor.extract_special(&file_name, metadata, 0)
178 } else {
179 Ok(())
180 }
181 }
182 (true, EntryKind::Socket) => {
183 if extractor.with_flag(flags::WITH_SOCKETS) {
184 extractor.callback(entry.path());
185 extractor.extract_special(&file_name, metadata, 0)
186 } else {
187 Ok(())
188 }
189 }
190 (true, EntryKind::File { size, .. }) => extractor.extract_file(
191 &file_name,
192 metadata,
193 *size,
194 &mut decoder.contents().ok_or_else(|| {
195 format_err!("found regular file entry without contents in archive")
196 })?,
197 ),
198 (false, _) => Ok(()), // skip this
199 }
200 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
201 }
202
203 if !extractor.dir_stack.is_empty() {
204 bail!("unexpected eof while decoding pxar archive");
205 }
206
207 Ok(())
208 }
209
210 impl<'a> Extractor<'a> {
211 fn parent_fd(&mut self) -> Result<RawFd, Error> {
212 self.dir_stack.last_dir_fd(self.allow_existing_dirs)
213 }
214
215 fn callback(&mut self, path: &Path) {
216 (self.callback)(path)
217 }
218
219 fn extract_symlink(
220 &mut self,
221 file_name: &CStr,
222 metadata: &Metadata,
223 link: &OsStr,
224 ) -> Result<(), Error> {
225 let parent = self.parent_fd()?;
226 nix::unistd::symlinkat(link, Some(parent), file_name)?;
227 metadata::apply_at(self.feature_flags, metadata, parent, file_name)
228 }
229
230 fn extract_hardlink(
231 &mut self,
232 file_name: &CStr,
233 _metadata: &Metadata, // for now we don't use this because hardlinks don't need it...
234 link: &OsStr,
235 ) -> Result<(), Error> {
236 crate::pxar::tools::assert_relative_path(link)?;
237
238 let parent = self.parent_fd()?;
239 let root = self.dir_stack.root_dir_fd()?;
240 let target = CString::new(link.as_bytes())?;
241 nix::unistd::linkat(
242 Some(root),
243 target.as_c_str(),
244 Some(parent),
245 file_name,
246 nix::unistd::LinkatFlags::NoSymlinkFollow,
247 )?;
248
249 Ok(())
250 }
251
252 fn extract_device(
253 &mut self,
254 file_name: &CStr,
255 metadata: &Metadata,
256 device: &Device,
257 ) -> Result<(), Error> {
258 self.extract_special(file_name, metadata, device.to_dev_t())
259 }
260
261 fn extract_special(
262 &mut self,
263 file_name: &CStr,
264 metadata: &Metadata,
265 device: libc::dev_t,
266 ) -> Result<(), Error> {
267 let mode = metadata.stat.mode;
268 let mode = u32::try_from(mode).map_err(|_| {
269 format_err!(
270 "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
271 mode,
272 mode,
273 )
274 })?;
275 let parent = self.parent_fd()?;
276 unsafe { c_result!(libc::mknodat(parent, file_name.as_ptr(), mode, device)) }
277 .map_err(|err| format_err!("failed to create device node: {}", err))?;
278
279 metadata::apply_at(self.feature_flags, metadata, parent, file_name)
280 }
281
282 fn extract_file(
283 &mut self,
284 file_name: &CStr,
285 metadata: &Metadata,
286 size: u64,
287 contents: &mut dyn io::Read,
288 ) -> Result<(), Error> {
289 let parent = self.parent_fd()?;
290 let mut file = unsafe {
291 std::fs::File::from_raw_fd(nix::fcntl::openat(
292 parent,
293 file_name,
294 OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_CLOEXEC,
295 Mode::from_bits(0o600).unwrap(),
296 )?)
297 };
298
299 let extracted = io::copy(&mut *contents, &mut file)?;
300 if size != extracted {
301 bail!("extracted {} bytes of a file of {} bytes", extracted, size);
302 }
303
304 metadata::apply(self.feature_flags, metadata, file.as_raw_fd(), file_name)
305 }
306 }