]> git.proxmox.com Git - proxmox-backup.git/blob - src/pxar/extract.rs
clippy: remove unnecessary clones
[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, OsString};
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 use std::sync::{Arc, Mutex};
10
11 use anyhow::{bail, format_err, Error};
12 use nix::dir::Dir;
13 use nix::fcntl::OFlag;
14 use nix::sys::stat::Mode;
15
16 use pathpatterns::{MatchEntry, MatchList, MatchType};
17 use pxar::format::Device;
18 use pxar::Metadata;
19
20 use proxmox::c_result;
21 use proxmox::tools::fs::{create_path, CreateOptions};
22
23 use crate::pxar::dir_stack::PxarDirStack;
24 use crate::pxar::metadata;
25 use crate::pxar::Flags;
26
27 pub fn extract_archive<T, F>(
28 mut decoder: pxar::decoder::Decoder<T>,
29 destination: &Path,
30 match_list: &[MatchEntry],
31 extract_match_default: bool,
32 feature_flags: Flags,
33 allow_existing_dirs: bool,
34 mut callback: F,
35 on_error: Option<Box<dyn FnMut(Error) -> Result<(), Error> + Send>>,
36 ) -> Result<(), Error>
37 where
38 T: pxar::decoder::SeqRead,
39 F: FnMut(&Path),
40 {
41 // we use this to keep track of our directory-traversal
42 decoder.enable_goodbye_entries(true);
43
44 let root = decoder
45 .next()
46 .ok_or_else(|| format_err!("found empty pxar archive"))?
47 .map_err(|err| format_err!("error reading pxar archive: {}", err))?;
48
49 if !root.is_dir() {
50 bail!("pxar archive does not start with a directory entry!");
51 }
52
53 create_path(
54 &destination,
55 None,
56 Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
57 )
58 .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
59
60 let dir = Dir::open(
61 destination,
62 OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
63 Mode::empty(),
64 )
65 .map_err(|err| format_err!("unable to open target directory {:?}: {}", destination, err,))?;
66
67 let mut extractor = Extractor::new(
68 dir,
69 root.metadata().clone(),
70 allow_existing_dirs,
71 feature_flags,
72 );
73
74 if let Some(on_error) = on_error {
75 extractor.on_error(on_error);
76 }
77
78 let mut match_stack = Vec::new();
79 let mut err_path_stack = vec![OsString::from("/")];
80 let mut current_match = extract_match_default;
81 while let Some(entry) = decoder.next() {
82 use pxar::EntryKind;
83
84 let entry = entry.map_err(|err| format_err!("error reading pxar archive: {}", err))?;
85
86 let file_name_os = entry.file_name();
87
88 // safety check: a file entry in an archive must never contain slashes:
89 if file_name_os.as_bytes().contains(&b'/') {
90 bail!("archive file entry contains slashes, which is invalid and a security concern");
91 }
92
93 let file_name = CString::new(file_name_os.as_bytes())
94 .map_err(|_| format_err!("encountered file name with null-bytes"))?;
95
96 let metadata = entry.metadata();
97
98 extractor.set_path(entry.path().as_os_str().to_owned());
99
100 let match_result = match_list.matches(
101 entry.path().as_os_str().as_bytes(),
102 Some(metadata.file_type() as u32),
103 );
104
105 let did_match = match match_result {
106 Some(MatchType::Include) => true,
107 Some(MatchType::Exclude) => false,
108 None => current_match,
109 };
110 match (did_match, entry.kind()) {
111 (_, EntryKind::Directory) => {
112 callback(entry.path());
113
114 let create = current_match && match_result != Some(MatchType::Exclude);
115 extractor
116 .enter_directory(file_name_os.to_owned(), metadata.clone(), create)
117 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
118
119 // We're starting a new directory, push our old matching state and replace it with
120 // our new one:
121 match_stack.push(current_match);
122 current_match = did_match;
123
124 // When we hit the goodbye table we'll try to apply metadata to the directory, but
125 // the Goodbye entry will not contain the path, so push it to our path stack for
126 // error messages:
127 err_path_stack.push(extractor.clone_path());
128
129 Ok(())
130 }
131 (_, EntryKind::GoodbyeTable) => {
132 // go up a directory
133
134 extractor.set_path(err_path_stack.pop().ok_or_else(|| {
135 format_err!(
136 "error at entry {:?}: unexpected end of directory",
137 file_name_os
138 )
139 })?);
140
141 extractor
142 .leave_directory()
143 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
144
145 // We left a directory, also get back our previous matching state. This is in sync
146 // with `dir_stack` so this should never be empty except for the final goodbye
147 // table, in which case we get back to the default of `true`.
148 current_match = match_stack.pop().unwrap_or(true);
149
150 Ok(())
151 }
152 (true, EntryKind::Symlink(link)) => {
153 callback(entry.path());
154 extractor.extract_symlink(&file_name, metadata, link.as_ref())
155 }
156 (true, EntryKind::Hardlink(link)) => {
157 callback(entry.path());
158 extractor.extract_hardlink(&file_name, link.as_os_str())
159 }
160 (true, EntryKind::Device(dev)) => {
161 if extractor.contains_flags(Flags::WITH_DEVICE_NODES) {
162 callback(entry.path());
163 extractor.extract_device(&file_name, metadata, dev)
164 } else {
165 Ok(())
166 }
167 }
168 (true, EntryKind::Fifo) => {
169 if extractor.contains_flags(Flags::WITH_FIFOS) {
170 callback(entry.path());
171 extractor.extract_special(&file_name, metadata, 0)
172 } else {
173 Ok(())
174 }
175 }
176 (true, EntryKind::Socket) => {
177 if extractor.contains_flags(Flags::WITH_SOCKETS) {
178 callback(entry.path());
179 extractor.extract_special(&file_name, metadata, 0)
180 } else {
181 Ok(())
182 }
183 }
184 (true, EntryKind::File { size, .. }) => extractor.extract_file(
185 &file_name,
186 metadata,
187 *size,
188 &mut decoder.contents().ok_or_else(|| {
189 format_err!("found regular file entry without contents in archive")
190 })?,
191 ),
192 (false, _) => Ok(()), // skip this
193 }
194 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
195 }
196
197 if !extractor.dir_stack.is_empty() {
198 bail!("unexpected eof while decoding pxar archive");
199 }
200
201 Ok(())
202 }
203
204 /// Common state for file extraction.
205 pub(crate) struct Extractor {
206 feature_flags: Flags,
207 allow_existing_dirs: bool,
208 dir_stack: PxarDirStack,
209
210 /// For better error output we need to track the current path in the Extractor state.
211 current_path: Arc<Mutex<OsString>>,
212
213 /// Error callback. Includes `current_path` in the reformatted error, should return `Ok` to
214 /// continue extracting or the passed error as `Err` to bail out.
215 on_error: Box<dyn FnMut(Error) -> Result<(), Error> + Send>,
216 }
217
218 impl Extractor {
219 /// Create a new extractor state for a target directory.
220 pub fn new(
221 root_dir: Dir,
222 metadata: Metadata,
223 allow_existing_dirs: bool,
224 feature_flags: Flags,
225 ) -> Self {
226 Self {
227 dir_stack: PxarDirStack::new(root_dir, metadata),
228 allow_existing_dirs,
229 feature_flags,
230 current_path: Arc::new(Mutex::new(OsString::new())),
231 on_error: Box::new(|err| Err(err)),
232 }
233 }
234
235 /// We call this on errors. The error will be reformatted to include `current_path`. The
236 /// callback should decide whether this error was fatal (simply return it) to bail out early,
237 /// or log/remember/accumulate errors somewhere and return `Ok(())` in its place to continue
238 /// extracting.
239 pub fn on_error(&mut self, mut on_error: Box<dyn FnMut(Error) -> Result<(), Error> + Send>) {
240 let path = Arc::clone(&self.current_path);
241 self.on_error = Box::new(move |err: Error| -> Result<(), Error> {
242 on_error(format_err!("error at {:?}: {}", path.lock().unwrap(), err))
243 });
244 }
245
246 pub fn set_path(&mut self, path: OsString) {
247 *self.current_path.lock().unwrap() = path;
248 }
249
250 pub fn clone_path(&self) -> OsString {
251 self.current_path.lock().unwrap().clone()
252 }
253
254 /// When encountering a directory during extraction, this is used to keep track of it. If
255 /// `create` is true it is immediately created and its metadata will be updated once we leave
256 /// it. If `create` is false it will only be created if it is going to have any actual content.
257 pub fn enter_directory(
258 &mut self,
259 file_name: OsString,
260 metadata: Metadata,
261 create: bool,
262 ) -> Result<(), Error> {
263 self.dir_stack.push(file_name, metadata)?;
264
265 if create {
266 self.dir_stack.create_last_dir(self.allow_existing_dirs)?;
267 }
268
269 Ok(())
270 }
271
272 /// When done with a directory we can apply its metadata if it has been created.
273 pub fn leave_directory(&mut self) -> Result<(), Error> {
274 let dir = self
275 .dir_stack
276 .pop()
277 .map_err(|err| format_err!("unexpected end of directory entry: {}", err))?
278 .ok_or_else(|| format_err!("broken pxar archive (directory stack underrun)"))?;
279
280 if let Some(fd) = dir.try_as_borrowed_fd() {
281 metadata::apply(
282 self.feature_flags,
283 dir.metadata(),
284 fd.as_raw_fd(),
285 &CString::new(dir.file_name().as_bytes())?,
286 &mut self.on_error,
287 )
288 .map_err(|err| format_err!("failed to apply directory metadata: {}", err))?;
289 }
290
291 Ok(())
292 }
293
294 fn contains_flags(&self, flag: Flags) -> bool {
295 self.feature_flags.contains(flag)
296 }
297
298 fn parent_fd(&mut self) -> Result<RawFd, Error> {
299 self.dir_stack
300 .last_dir_fd(self.allow_existing_dirs)
301 .map(|d| d.as_raw_fd())
302 .map_err(|err| format_err!("failed to get parent directory file descriptor: {}", err))
303 }
304
305 pub fn extract_symlink(
306 &mut self,
307 file_name: &CStr,
308 metadata: &Metadata,
309 link: &OsStr,
310 ) -> Result<(), Error> {
311 let parent = self.parent_fd()?;
312 nix::unistd::symlinkat(link, Some(parent), file_name)?;
313 metadata::apply_at(
314 self.feature_flags,
315 metadata,
316 parent,
317 file_name,
318 &mut self.on_error,
319 )
320 }
321
322 pub fn extract_hardlink(&mut self, file_name: &CStr, link: &OsStr) -> Result<(), Error> {
323 crate::pxar::tools::assert_relative_path(link)?;
324
325 let parent = self.parent_fd()?;
326 let root = self.dir_stack.root_dir_fd()?;
327 let target = CString::new(link.as_bytes())?;
328 nix::unistd::linkat(
329 Some(root.as_raw_fd()),
330 target.as_c_str(),
331 Some(parent),
332 file_name,
333 nix::unistd::LinkatFlags::NoSymlinkFollow,
334 )?;
335
336 Ok(())
337 }
338
339 pub fn extract_device(
340 &mut self,
341 file_name: &CStr,
342 metadata: &Metadata,
343 device: &Device,
344 ) -> Result<(), Error> {
345 self.extract_special(file_name, metadata, device.to_dev_t())
346 }
347
348 pub fn extract_special(
349 &mut self,
350 file_name: &CStr,
351 metadata: &Metadata,
352 device: libc::dev_t,
353 ) -> Result<(), Error> {
354 let mode = metadata.stat.mode;
355 let mode = u32::try_from(mode).map_err(|_| {
356 format_err!(
357 "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
358 mode,
359 mode,
360 )
361 })?;
362 let parent = self.parent_fd()?;
363 unsafe { c_result!(libc::mknodat(parent, file_name.as_ptr(), mode, device)) }
364 .map_err(|err| format_err!("failed to create device node: {}", err))?;
365
366 metadata::apply_at(
367 self.feature_flags,
368 metadata,
369 parent,
370 file_name,
371 &mut self.on_error,
372 )
373 }
374
375 pub fn extract_file(
376 &mut self,
377 file_name: &CStr,
378 metadata: &Metadata,
379 size: u64,
380 contents: &mut dyn io::Read,
381 ) -> Result<(), Error> {
382 let parent = self.parent_fd()?;
383 let mut file = unsafe {
384 std::fs::File::from_raw_fd(
385 nix::fcntl::openat(
386 parent,
387 file_name,
388 OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_WRONLY | OFlag::O_CLOEXEC,
389 Mode::from_bits(0o600).unwrap(),
390 )
391 .map_err(|err| format_err!("failed to create file {:?}: {}", file_name, err))?,
392 )
393 };
394
395 metadata::apply_initial_flags(
396 self.feature_flags,
397 metadata,
398 file.as_raw_fd(),
399 &mut self.on_error,
400 )?;
401
402 let extracted = io::copy(&mut *contents, &mut file)
403 .map_err(|err| format_err!("failed to copy file contents: {}", err))?;
404 if size != extracted {
405 bail!("extracted {} bytes of a file of {} bytes", extracted, size);
406 }
407
408 metadata::apply(
409 self.feature_flags,
410 metadata,
411 file.as_raw_fd(),
412 file_name,
413 &mut self.on_error,
414 )
415 }
416
417 pub async fn async_extract_file<T: tokio::io::AsyncRead + Unpin>(
418 &mut self,
419 file_name: &CStr,
420 metadata: &Metadata,
421 size: u64,
422 contents: &mut T,
423 ) -> Result<(), Error> {
424 let parent = self.parent_fd()?;
425 let mut file = tokio::fs::File::from_std(unsafe {
426 std::fs::File::from_raw_fd(
427 nix::fcntl::openat(
428 parent,
429 file_name,
430 OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_WRONLY | OFlag::O_CLOEXEC,
431 Mode::from_bits(0o600).unwrap(),
432 )
433 .map_err(|err| format_err!("failed to create file {:?}: {}", file_name, err))?,
434 )
435 });
436
437 metadata::apply_initial_flags(
438 self.feature_flags,
439 metadata,
440 file.as_raw_fd(),
441 &mut self.on_error,
442 )?;
443
444 let extracted = tokio::io::copy(&mut *contents, &mut file)
445 .await
446 .map_err(|err| format_err!("failed to copy file contents: {}", err))?;
447 if size != extracted {
448 bail!("extracted {} bytes of a file of {} bytes", extracted, size);
449 }
450
451 metadata::apply(
452 self.feature_flags,
453 metadata,
454 file.as_raw_fd(),
455 file_name,
456 &mut self.on_error,
457 )
458 }
459 }