1 //! Code for extraction of pxar contents onto the file system.
3 use std
::convert
::TryFrom
;
4 use std
::ffi
::{CStr, CString, OsStr}
;
6 use std
::os
::unix
::ffi
::OsStrExt
;
7 use std
::os
::unix
::io
::{AsRawFd, FromRawFd, RawFd}
;
10 use anyhow
::{bail, format_err, Error}
;
12 use nix
::fcntl
::OFlag
;
13 use nix
::sys
::stat
::Mode
;
15 use pathpatterns
::{MatchEntry, MatchList, MatchType}
;
16 use pxar
::format
::Device
;
19 use proxmox
::c_result
;
20 use proxmox
::tools
::fs
::{create_path, CreateOptions}
;
22 use crate::pxar
::dir_stack
::PxarDirStack
;
23 use crate::pxar
::flags
;
24 use crate::pxar
::metadata
;
26 struct Extractor
<'a
> {
27 /// FIXME: use bitflags!() for feature_flags
29 allow_existing_dirs
: bool
,
30 callback
: &'a
mut dyn FnMut(&Path
),
31 dir_stack
: PxarDirStack
,
34 impl<'a
> Extractor
<'a
> {
35 fn with_flag(&self, flag
: u64) -> bool
{
36 flag
== (self.feature_flags
& flag
)
40 pub fn extract_archive
<T
, F
>(
41 mut decoder
: pxar
::decoder
::Decoder
<T
>,
43 match_list
: &[MatchEntry
],
45 allow_existing_dirs
: bool
,
47 ) -> Result
<(), Error
>
49 T
: pxar
::decoder
::SeqRead
,
52 // we use this to keep track of our directory-traversal
53 decoder
.enable_goodbye_entries(true);
57 .ok_or_else(|| format_err
!("found empty pxar archive"))?
58 .map_err(|err
| format_err
!("error reading pxar archive: {}", err
))?
;
61 bail
!("pxar archive does not start with a directory entry!");
67 Some(CreateOptions
::new().perm(Mode
::from_bits_truncate(0o700))),
69 .map_err(|err
| format_err
!("error creating directory {:?}: {}", destination
, err
))?
;
73 OFlag
::O_DIRECTORY
| OFlag
::O_CLOEXEC
,
76 .map_err(|err
| format_err
!("unable to open target directory {:?}: {}", destination
, err
,))?
;
78 let mut extractor
= Extractor
{
81 callback
: &mut callback
,
82 dir_stack
: PxarDirStack
::new(dir
, root
.metadata().clone()),
85 let mut match_stack
= Vec
::new();
86 let mut current_match
= true;
87 while let Some(entry
) = decoder
.next() {
90 let entry
= entry
.map_err(|err
| format_err
!("error reading pxar archive: {}", err
))?
;
92 let file_name_os
= entry
.file_name();
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");
99 let file_name
= CString
::new(file_name_os
.as_bytes())
100 .map_err(|_
| format_err
!("encountered file name with null-bytes"))?
;
102 let metadata
= entry
.metadata();
104 let match_result
= match_list
.matches(
105 entry
.path().as_os_str().as_bytes(),
106 Some(metadata
.file_type() as u32),
109 let did_match
= match match_result
{
110 Some(MatchType
::Include
) => true,
111 Some(MatchType
::Exclude
) => false,
112 None
=> current_match
,
114 match (did_match
, entry
.kind()) {
115 (_
, EntryKind
::Directory
) => {
116 extractor
.callback(entry
.path());
120 .push(file_name_os
.to_owned(), metadata
.clone())?
;
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:
127 .last_dir_fd(extractor
.allow_existing_dirs
)
129 format_err
!("error creating entry {:?}: {}", file_name_os
, err
)
133 // We're starting a new directory, push our old matching state and replace it with
135 match_stack
.push(current_match
);
136 current_match
= did_match
;
140 (_
, EntryKind
::GoodbyeTable
) => {
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);
152 if let Some(fd
) = dir
.try_as_raw_fd() {
153 metadata
::apply(extractor
.feature_flags
, dir
.metadata(), fd
, &file_name
)
158 (true, EntryKind
::Symlink(link
)) => {
159 extractor
.callback(entry
.path());
160 extractor
.extract_symlink(&file_name
, metadata
, link
.as_ref())
162 (true, EntryKind
::Hardlink(link
)) => {
163 extractor
.callback(entry
.path());
164 extractor
.extract_hardlink(&file_name
, metadata
, link
.as_os_str())
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
)
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)
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)
190 (true, EntryKind
::File { size, .. }
) => extractor
.extract_file(
194 &mut decoder
.contents().ok_or_else(|| {
195 format_err
!("found regular file entry without contents in archive")
198 (false, _
) => Ok(()), // skip this
200 .map_err(|err
| format_err
!("error at entry {:?}: {}", file_name_os
, err
))?
;
203 if !extractor
.dir_stack
.is_empty() {
204 bail
!("unexpected eof while decoding pxar archive");
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
)
215 fn callback(&mut self, path
: &Path
) {
216 (self.callback
)(path
)
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
)
233 _metadata
: &Metadata
, // for now we don't use this because hardlinks don't need it...
235 ) -> Result
<(), Error
> {
236 crate::pxar
::tools
::assert_relative_path(link
)?
;
238 let parent
= self.parent_fd()?
;
239 let root
= self.dir_stack
.root_dir_fd()?
;
240 let target
= CString
::new(link
.as_bytes())?
;
246 nix
::unistd
::LinkatFlags
::NoSymlinkFollow
,
257 ) -> Result
<(), Error
> {
258 self.extract_special(file_name
, metadata
, device
.to_dev_t())
266 ) -> Result
<(), Error
> {
267 let mode
= metadata
.stat
.mode
;
268 let mode
= u32::try_from(mode
).map_err(|_
| {
270 "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
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
))?
;
279 metadata
::apply_at(self.feature_flags
, metadata
, parent
, file_name
)
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(
294 OFlag
::O_CREAT
| OFlag
::O_WRONLY
| OFlag
::O_CLOEXEC
,
295 Mode
::from_bits(0o600).unwrap(),
299 let extracted
= io
::copy(&mut *contents
, &mut file
)?
;
300 if size
!= extracted
{
301 bail
!("extracted {} bytes of a file of {} bytes", extracted
, size
);
304 metadata
::apply(self.feature_flags
, metadata
, file
.as_raw_fd(), file_name
)