1 //! Code for extraction of pxar contents onto the file system.
3 use std
::convert
::TryFrom
;
4 use std
::ffi
::{CStr, CString, OsStr, OsString}
;
6 use std
::os
::unix
::ffi
::OsStrExt
;
7 use std
::os
::unix
::io
::{AsRawFd, FromRawFd, RawFd}
;
9 use std
::sync
::{Arc, Mutex}
;
11 use anyhow
::{bail, format_err, Error}
;
13 use nix
::fcntl
::OFlag
;
14 use nix
::sys
::stat
::Mode
;
16 use pathpatterns
::{MatchEntry, MatchList, MatchType}
;
17 use pxar
::format
::Device
;
20 use proxmox
::c_result
;
21 use proxmox
::tools
::fs
::{create_path, CreateOptions}
;
23 use crate::pxar
::dir_stack
::PxarDirStack
;
24 use crate::pxar
::metadata
;
25 use crate::pxar
::Flags
;
27 pub fn extract_archive
<T
, F
>(
28 mut decoder
: pxar
::decoder
::Decoder
<T
>,
30 match_list
: &[MatchEntry
],
31 extract_match_default
: bool
,
33 allow_existing_dirs
: bool
,
35 on_error
: Option
<Box
<dyn FnMut(Error
) -> Result
<(), Error
> + Send
>>,
36 ) -> Result
<(), Error
>
38 T
: pxar
::decoder
::SeqRead
,
41 // we use this to keep track of our directory-traversal
42 decoder
.enable_goodbye_entries(true);
46 .ok_or_else(|| format_err
!("found empty pxar archive"))?
47 .map_err(|err
| format_err
!("error reading pxar archive: {}", err
))?
;
50 bail
!("pxar archive does not start with a directory entry!");
56 Some(CreateOptions
::new().perm(Mode
::from_bits_truncate(0o700))),
58 .map_err(|err
| format_err
!("error creating directory {:?}: {}", destination
, err
))?
;
62 OFlag
::O_DIRECTORY
| OFlag
::O_CLOEXEC
,
65 .map_err(|err
| format_err
!("unable to open target directory {:?}: {}", destination
, err
,))?
;
67 let mut extractor
= Extractor
::new(
69 root
.metadata().clone(),
74 if let Some(on_error
) = on_error
{
75 extractor
.on_error(on_error
);
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() {
84 let entry
= entry
.map_err(|err
| format_err
!("error reading pxar archive: {}", err
))?
;
86 let file_name_os
= entry
.file_name();
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");
93 let file_name
= CString
::new(file_name_os
.as_bytes())
94 .map_err(|_
| format_err
!("encountered file name with null-bytes"))?
;
96 let metadata
= entry
.metadata();
98 extractor
.set_path(entry
.path().as_os_str().to_owned());
100 let match_result
= match_list
.matches(
101 entry
.path().as_os_str().as_bytes(),
102 Some(metadata
.file_type() as u32),
105 let did_match
= match match_result
{
106 Some(MatchType
::Include
) => true,
107 Some(MatchType
::Exclude
) => false,
108 None
=> current_match
,
110 match (did_match
, entry
.kind()) {
111 (_
, EntryKind
::Directory
) => {
112 callback(entry
.path());
114 let create
= current_match
&& match_result
!= Some(MatchType
::Exclude
);
116 .enter_directory(file_name_os
.to_owned(), metadata
.clone(), create
)
117 .map_err(|err
| format_err
!("error at entry {:?}: {}", file_name_os
, err
))?
;
119 // We're starting a new directory, push our old matching state and replace it with
121 match_stack
.push(current_match
);
122 current_match
= did_match
;
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
127 err_path_stack
.push(extractor
.clone_path());
131 (_
, EntryKind
::GoodbyeTable
) => {
134 extractor
.set_path(err_path_stack
.pop().ok_or_else(|| {
136 "error at entry {:?}: unexpected end of directory",
143 .map_err(|err
| format_err
!("error at entry {:?}: {}", file_name_os
, err
))?
;
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);
152 (true, EntryKind
::Symlink(link
)) => {
153 callback(entry
.path());
154 extractor
.extract_symlink(&file_name
, metadata
, link
.as_ref())
156 (true, EntryKind
::Hardlink(link
)) => {
157 callback(entry
.path());
158 extractor
.extract_hardlink(&file_name
, link
.as_os_str())
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
)
168 (true, EntryKind
::Fifo
) => {
169 if extractor
.contains_flags(Flags
::WITH_FIFOS
) {
170 callback(entry
.path());
171 extractor
.extract_special(&file_name
, metadata
, 0)
176 (true, EntryKind
::Socket
) => {
177 if extractor
.contains_flags(Flags
::WITH_SOCKETS
) {
178 callback(entry
.path());
179 extractor
.extract_special(&file_name
, metadata
, 0)
184 (true, EntryKind
::File { size, .. }
) => extractor
.extract_file(
188 &mut decoder
.contents().ok_or_else(|| {
189 format_err
!("found regular file entry without contents in archive")
192 (false, _
) => Ok(()), // skip this
194 .map_err(|err
| format_err
!("error at entry {:?}: {}", file_name_os
, err
))?
;
197 if !extractor
.dir_stack
.is_empty() {
198 bail
!("unexpected eof while decoding pxar archive");
204 /// Common state for file extraction.
205 pub(crate) struct Extractor
{
206 feature_flags
: Flags
,
207 allow_existing_dirs
: bool
,
208 dir_stack
: PxarDirStack
,
210 /// For better error output we need to track the current path in the Extractor state.
211 current_path
: Arc
<Mutex
<OsString
>>,
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
>,
219 /// Create a new extractor state for a target directory.
223 allow_existing_dirs
: bool
,
224 feature_flags
: Flags
,
227 dir_stack
: PxarDirStack
::new(root_dir
, metadata
),
230 current_path
: Arc
::new(Mutex
::new(OsString
::new())),
231 on_error
: Box
::new(|err
| Err(err
)),
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
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
))
246 pub fn set_path(&mut self, path
: OsString
) {
247 *self.current_path
.lock().unwrap() = path
;
250 pub fn clone_path(&self) -> OsString
{
251 self.current_path
.lock().unwrap().clone()
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(
262 ) -> Result
<(), Error
> {
263 self.dir_stack
.push(file_name
, metadata
)?
;
266 self.dir_stack
.create_last_dir(self.allow_existing_dirs
)?
;
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
> {
277 .map_err(|err
| format_err
!("unexpected end of directory entry: {}", err
))?
278 .ok_or_else(|| format_err
!("broken pxar archive (directory stack underrun)"))?
;
280 if let Some(fd
) = dir
.try_as_borrowed_fd() {
285 &CString
::new(dir
.file_name().as_bytes())?
,
288 .map_err(|err
| format_err
!("failed to apply directory metadata: {}", err
))?
;
294 fn contains_flags(&self, flag
: Flags
) -> bool
{
295 self.feature_flags
.contains(flag
)
298 fn parent_fd(&mut self) -> Result
<RawFd
, Error
> {
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
))
305 pub fn extract_symlink(
310 ) -> Result
<(), Error
> {
311 let parent
= self.parent_fd()?
;
312 nix
::unistd
::symlinkat(link
, Some(parent
), file_name
)?
;
322 pub fn extract_hardlink(&mut self, file_name
: &CStr
, link
: &OsStr
) -> Result
<(), Error
> {
323 crate::pxar
::tools
::assert_relative_path(link
)?
;
325 let parent
= self.parent_fd()?
;
326 let root
= self.dir_stack
.root_dir_fd()?
;
327 let target
= CString
::new(link
.as_bytes())?
;
329 Some(root
.as_raw_fd()),
333 nix
::unistd
::LinkatFlags
::NoSymlinkFollow
,
339 pub fn extract_device(
344 ) -> Result
<(), Error
> {
345 self.extract_special(file_name
, metadata
, device
.to_dev_t())
348 pub fn extract_special(
353 ) -> Result
<(), Error
> {
354 let mode
= metadata
.stat
.mode
;
355 let mode
= u32::try_from(mode
).map_err(|_
| {
357 "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
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
))?
;
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(
388 OFlag
::O_CREAT
| OFlag
::O_EXCL
| OFlag
::O_WRONLY
| OFlag
::O_CLOEXEC
,
389 Mode
::from_bits(0o600).unwrap(),
391 .map_err(|err
| format_err
!("failed to create file {:?}: {}", file_name
, err
))?
,
395 metadata
::apply_initial_flags(
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
);
417 pub async
fn async_extract_file
<T
: tokio
::io
::AsyncRead
+ Unpin
>(
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(
430 OFlag
::O_CREAT
| OFlag
::O_EXCL
| OFlag
::O_WRONLY
| OFlag
::O_CLOEXEC
,
431 Mode
::from_bits(0o600).unwrap(),
433 .map_err(|err
| format_err
!("failed to create file {:?}: {}", file_name
, err
))?
,
437 metadata
::apply_initial_flags(
444 let extracted
= tokio
::io
::copy(&mut *contents
, &mut file
)
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
);