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}
;
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 pub fn extract_archive
<T
, F
>(
27 mut decoder
: pxar
::decoder
::Decoder
<T
>,
29 match_list
: &[MatchEntry
],
30 extract_match_default
: bool
,
32 allow_existing_dirs
: bool
,
34 ) -> Result
<(), Error
>
36 T
: pxar
::decoder
::SeqRead
,
39 // we use this to keep track of our directory-traversal
40 decoder
.enable_goodbye_entries(true);
44 .ok_or_else(|| format_err
!("found empty pxar archive"))?
45 .map_err(|err
| format_err
!("error reading pxar archive: {}", err
))?
;
48 bail
!("pxar archive does not start with a directory entry!");
54 Some(CreateOptions
::new().perm(Mode
::from_bits_truncate(0o700))),
56 .map_err(|err
| format_err
!("error creating directory {:?}: {}", destination
, err
))?
;
60 OFlag
::O_DIRECTORY
| OFlag
::O_CLOEXEC
,
63 .map_err(|err
| format_err
!("unable to open target directory {:?}: {}", destination
, err
,))?
;
65 let mut extractor
= Extractor
::new(
67 root
.metadata().clone(),
72 let mut match_stack
= Vec
::new();
73 let mut current_match
= extract_match_default
;
74 while let Some(entry
) = decoder
.next() {
77 let entry
= entry
.map_err(|err
| format_err
!("error reading pxar archive: {}", err
))?
;
79 let file_name_os
= entry
.file_name();
81 // safety check: a file entry in an archive must never contain slashes:
82 if file_name_os
.as_bytes().contains(&b'
/'
) {
83 bail
!("archive file entry contains slashes, which is invalid and a security concern");
86 let file_name
= CString
::new(file_name_os
.as_bytes())
87 .map_err(|_
| format_err
!("encountered file name with null-bytes"))?
;
89 let metadata
= entry
.metadata();
91 let match_result
= match_list
.matches(
92 entry
.path().as_os_str().as_bytes(),
93 Some(metadata
.file_type() as u32),
96 let did_match
= match match_result
{
97 Some(MatchType
::Include
) => true,
98 Some(MatchType
::Exclude
) => false,
99 None
=> current_match
,
101 match (did_match
, entry
.kind()) {
102 (_
, EntryKind
::Directory
) => {
103 callback(entry
.path());
105 let create
= current_match
&& match_result
!= Some(MatchType
::Exclude
);
106 extractor
.enter_directory(file_name_os
.to_owned(), metadata
.clone(), create
)?
;
108 // We're starting a new directory, push our old matching state and replace it with
110 match_stack
.push(current_match
);
111 current_match
= did_match
;
115 (_
, EntryKind
::GoodbyeTable
) => {
119 .map_err(|err
| format_err
!("error at entry {:?}: {}", file_name_os
, err
))?
;
121 // We left a directory, also get back our previous matching state. This is in sync
122 // with `dir_stack` so this should never be empty except for the final goodbye
123 // table, in which case we get back to the default of `true`.
124 current_match
= match_stack
.pop().unwrap_or(true);
128 (true, EntryKind
::Symlink(link
)) => {
129 callback(entry
.path());
130 extractor
.extract_symlink(&file_name
, metadata
, link
.as_ref())
132 (true, EntryKind
::Hardlink(link
)) => {
133 callback(entry
.path());
134 extractor
.extract_hardlink(&file_name
, link
.as_os_str())
136 (true, EntryKind
::Device(dev
)) => {
137 if extractor
.contains_flags(Flags
::WITH_DEVICE_NODES
) {
138 callback(entry
.path());
139 extractor
.extract_device(&file_name
, metadata
, dev
)
144 (true, EntryKind
::Fifo
) => {
145 if extractor
.contains_flags(Flags
::WITH_FIFOS
) {
146 callback(entry
.path());
147 extractor
.extract_special(&file_name
, metadata
, 0)
152 (true, EntryKind
::Socket
) => {
153 if extractor
.contains_flags(Flags
::WITH_SOCKETS
) {
154 callback(entry
.path());
155 extractor
.extract_special(&file_name
, metadata
, 0)
160 (true, EntryKind
::File { size, .. }
) => extractor
.extract_file(
164 &mut decoder
.contents().ok_or_else(|| {
165 format_err
!("found regular file entry without contents in archive")
168 (false, _
) => Ok(()), // skip this
170 .map_err(|err
| format_err
!("error at entry {:?}: {}", file_name_os
, err
))?
;
173 if !extractor
.dir_stack
.is_empty() {
174 bail
!("unexpected eof while decoding pxar archive");
180 /// Common state for file extraction.
181 pub(crate) struct Extractor
{
182 feature_flags
: Flags
,
183 allow_existing_dirs
: bool
,
184 dir_stack
: PxarDirStack
,
188 /// Create a new extractor state for a target directory.
192 allow_existing_dirs
: bool
,
193 feature_flags
: Flags
,
196 dir_stack
: PxarDirStack
::new(root_dir
, metadata
),
202 /// When encountering a directory during extraction, this is used to keep track of it. If
203 /// `create` is true it is immediately created and its metadata will be updated once we leave
204 /// it. If `create` is false it will only be created if it is going to have any actual content.
205 pub fn enter_directory(
210 ) -> Result
<(), Error
> {
211 self.dir_stack
.push(file_name
, metadata
)?
;
214 self.dir_stack
.create_last_dir(self.allow_existing_dirs
)?
;
220 /// When done with a directory we need to make sure we're
221 pub fn leave_directory(&mut self) -> Result
<(), Error
> {
225 .map_err(|err
| format_err
!("unexpected end of directory entry: {}", err
))?
226 .ok_or_else(|| format_err
!("broken pxar archive (directory stack underrun)"))?
;
228 if let Some(fd
) = dir
.try_as_raw_fd() {
233 &CString
::new(dir
.file_name().as_bytes())?
,
235 .map_err(|err
| format_err
!("failed to apply directory metadata: {}", err
))?
;
241 fn contains_flags(&self, flag
: Flags
) -> bool
{
242 self.feature_flags
.contains(flag
)
245 fn parent_fd(&mut self) -> Result
<RawFd
, Error
> {
247 .last_dir_fd(self.allow_existing_dirs
)
248 .map_err(|err
| format_err
!("failed to get parent directory file descriptor: {}", err
))
251 pub fn extract_symlink(
256 ) -> Result
<(), Error
> {
257 let parent
= self.parent_fd()?
;
258 nix
::unistd
::symlinkat(link
, Some(parent
), file_name
)?
;
259 metadata
::apply_at(self.feature_flags
, metadata
, parent
, file_name
)
262 pub fn extract_hardlink(
266 ) -> Result
<(), Error
> {
267 crate::pxar
::tools
::assert_relative_path(link
)?
;
269 let parent
= self.parent_fd()?
;
270 let root
= self.dir_stack
.root_dir_fd()?
;
271 let target
= CString
::new(link
.as_bytes())?
;
277 nix
::unistd
::LinkatFlags
::NoSymlinkFollow
,
283 pub fn extract_device(
288 ) -> Result
<(), Error
> {
289 self.extract_special(file_name
, metadata
, device
.to_dev_t())
292 pub fn extract_special(
297 ) -> Result
<(), Error
> {
298 let mode
= metadata
.stat
.mode
;
299 let mode
= u32::try_from(mode
).map_err(|_
| {
301 "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
306 let parent
= self.parent_fd()?
;
307 unsafe { c_result!(libc::mknodat(parent, file_name.as_ptr(), mode, device)) }
308 .map_err(|err
| format_err
!("failed to create device node: {}", err
))?
;
310 metadata
::apply_at(self.feature_flags
, metadata
, parent
, file_name
)
318 contents
: &mut dyn io
::Read
,
319 ) -> Result
<(), Error
> {
320 let parent
= self.parent_fd()?
;
321 let mut file
= unsafe {
322 std
::fs
::File
::from_raw_fd(nix
::fcntl
::openat(
325 OFlag
::O_CREAT
| OFlag
::O_EXCL
| OFlag
::O_WRONLY
| OFlag
::O_CLOEXEC
,
326 Mode
::from_bits(0o600).unwrap(),
328 .map_err(|err
| format_err
!("failed to create file {:?}: {}", file_name
, err
))?
)
331 metadata
::apply_initial_flags(self.feature_flags
, metadata
, file
.as_raw_fd())?
;
333 let extracted
= io
::copy(&mut *contents
, &mut file
)
334 .map_err(|err
| format_err
!("failed to copy file contents: {}", err
))?
;
335 if size
!= extracted
{
336 bail
!("extracted {} bytes of a file of {} bytes", extracted
, size
);
339 metadata
::apply(self.feature_flags
, metadata
, file
.as_raw_fd(), file_name
)
342 pub async
fn async_extract_file
<T
: tokio
::io
::AsyncRead
+ Unpin
>(
348 ) -> Result
<(), Error
> {
349 let parent
= self.parent_fd()?
;
350 let mut file
= tokio
::fs
::File
::from_std(unsafe {
351 std
::fs
::File
::from_raw_fd(nix
::fcntl
::openat(
354 OFlag
::O_CREAT
| OFlag
::O_EXCL
| OFlag
::O_WRONLY
| OFlag
::O_CLOEXEC
,
355 Mode
::from_bits(0o600).unwrap(),
357 .map_err(|err
| format_err
!("failed to create file {:?}: {}", file_name
, err
))?
)
360 metadata
::apply_initial_flags(self.feature_flags
, metadata
, file
.as_raw_fd())?
;
362 let extracted
= tokio
::io
::copy(&mut *contents
, &mut file
)
364 .map_err(|err
| format_err
!("failed to copy file contents: {}", err
))?
;
365 if size
!= extracted
{
366 bail
!("extracted {} bytes of a file of {} bytes", extracted
, size
);
369 metadata
::apply(self.feature_flags
, metadata
, file
.as_raw_fd(), file_name
)