]> git.proxmox.com Git - proxmox-backup.git/blob - src/pxar/extract.rs
fix #2873: if --pattern is used, default to not extracting
[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
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 pub fn extract_archive<T, F>(
27 mut decoder: pxar::decoder::Decoder<T>,
28 destination: &Path,
29 match_list: &[MatchEntry],
30 extract_match_default: bool,
31 feature_flags: Flags,
32 allow_existing_dirs: bool,
33 mut callback: F,
34 ) -> Result<(), Error>
35 where
36 T: pxar::decoder::SeqRead,
37 F: FnMut(&Path),
38 {
39 // we use this to keep track of our directory-traversal
40 decoder.enable_goodbye_entries(true);
41
42 let root = decoder
43 .next()
44 .ok_or_else(|| format_err!("found empty pxar archive"))?
45 .map_err(|err| format_err!("error reading pxar archive: {}", err))?;
46
47 if !root.is_dir() {
48 bail!("pxar archive does not start with a directory entry!");
49 }
50
51 create_path(
52 &destination,
53 None,
54 Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
55 )
56 .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
57
58 let dir = Dir::open(
59 destination,
60 OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
61 Mode::empty(),
62 )
63 .map_err(|err| format_err!("unable to open target directory {:?}: {}", destination, err,))?;
64
65 let mut extractor = Extractor::new(
66 dir,
67 root.metadata().clone(),
68 allow_existing_dirs,
69 feature_flags,
70 );
71
72 let mut match_stack = Vec::new();
73 let mut current_match = extract_match_default;
74 while let Some(entry) = decoder.next() {
75 use pxar::EntryKind;
76
77 let entry = entry.map_err(|err| format_err!("error reading pxar archive: {}", err))?;
78
79 let file_name_os = entry.file_name();
80
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");
84 }
85
86 let file_name = CString::new(file_name_os.as_bytes())
87 .map_err(|_| format_err!("encountered file name with null-bytes"))?;
88
89 let metadata = entry.metadata();
90
91 let match_result = match_list.matches(
92 entry.path().as_os_str().as_bytes(),
93 Some(metadata.file_type() as u32),
94 );
95
96 let did_match = match match_result {
97 Some(MatchType::Include) => true,
98 Some(MatchType::Exclude) => false,
99 None => current_match,
100 };
101 match (did_match, entry.kind()) {
102 (_, EntryKind::Directory) => {
103 callback(entry.path());
104
105 let create = current_match && match_result != Some(MatchType::Exclude);
106 extractor.enter_directory(file_name_os.to_owned(), metadata.clone(), create)?;
107
108 // We're starting a new directory, push our old matching state and replace it with
109 // our new one:
110 match_stack.push(current_match);
111 current_match = did_match;
112
113 Ok(())
114 }
115 (_, EntryKind::GoodbyeTable) => {
116 // go up a directory
117 extractor
118 .leave_directory()
119 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
120
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);
125
126 Ok(())
127 }
128 (true, EntryKind::Symlink(link)) => {
129 callback(entry.path());
130 extractor.extract_symlink(&file_name, metadata, link.as_ref())
131 }
132 (true, EntryKind::Hardlink(link)) => {
133 callback(entry.path());
134 extractor.extract_hardlink(&file_name, link.as_os_str())
135 }
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)
140 } else {
141 Ok(())
142 }
143 }
144 (true, EntryKind::Fifo) => {
145 if extractor.contains_flags(Flags::WITH_FIFOS) {
146 callback(entry.path());
147 extractor.extract_special(&file_name, metadata, 0)
148 } else {
149 Ok(())
150 }
151 }
152 (true, EntryKind::Socket) => {
153 if extractor.contains_flags(Flags::WITH_SOCKETS) {
154 callback(entry.path());
155 extractor.extract_special(&file_name, metadata, 0)
156 } else {
157 Ok(())
158 }
159 }
160 (true, EntryKind::File { size, .. }) => extractor.extract_file(
161 &file_name,
162 metadata,
163 *size,
164 &mut decoder.contents().ok_or_else(|| {
165 format_err!("found regular file entry without contents in archive")
166 })?,
167 ),
168 (false, _) => Ok(()), // skip this
169 }
170 .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
171 }
172
173 if !extractor.dir_stack.is_empty() {
174 bail!("unexpected eof while decoding pxar archive");
175 }
176
177 Ok(())
178 }
179
180 /// Common state for file extraction.
181 pub(crate) struct Extractor {
182 feature_flags: Flags,
183 allow_existing_dirs: bool,
184 dir_stack: PxarDirStack,
185 }
186
187 impl Extractor {
188 /// Create a new extractor state for a target directory.
189 pub fn new(
190 root_dir: Dir,
191 metadata: Metadata,
192 allow_existing_dirs: bool,
193 feature_flags: Flags,
194 ) -> Self {
195 Self {
196 dir_stack: PxarDirStack::new(root_dir, metadata),
197 allow_existing_dirs,
198 feature_flags,
199 }
200 }
201
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(
206 &mut self,
207 file_name: OsString,
208 metadata: Metadata,
209 create: bool,
210 ) -> Result<(), Error> {
211 self.dir_stack.push(file_name, metadata)?;
212
213 if create {
214 self.dir_stack.create_last_dir(self.allow_existing_dirs)?;
215 }
216
217 Ok(())
218 }
219
220 /// When done with a directory we need to make sure we're
221 pub fn leave_directory(&mut self) -> Result<(), Error> {
222 let dir = self
223 .dir_stack
224 .pop()
225 .map_err(|err| format_err!("unexpected end of directory entry: {}", err))?
226 .ok_or_else(|| format_err!("broken pxar archive (directory stack underrun)"))?;
227
228 if let Some(fd) = dir.try_as_raw_fd() {
229 metadata::apply(
230 self.feature_flags,
231 dir.metadata(),
232 fd,
233 &CString::new(dir.file_name().as_bytes())?,
234 )
235 .map_err(|err| format_err!("failed to apply directory metadata: {}", err))?;
236 }
237
238 Ok(())
239 }
240
241 fn contains_flags(&self, flag: Flags) -> bool {
242 self.feature_flags.contains(flag)
243 }
244
245 fn parent_fd(&mut self) -> Result<RawFd, Error> {
246 self.dir_stack
247 .last_dir_fd(self.allow_existing_dirs)
248 .map_err(|err| format_err!("failed to get parent directory file descriptor: {}", err))
249 }
250
251 pub fn extract_symlink(
252 &mut self,
253 file_name: &CStr,
254 metadata: &Metadata,
255 link: &OsStr,
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)
260 }
261
262 pub fn extract_hardlink(
263 &mut self,
264 file_name: &CStr,
265 link: &OsStr,
266 ) -> Result<(), Error> {
267 crate::pxar::tools::assert_relative_path(link)?;
268
269 let parent = self.parent_fd()?;
270 let root = self.dir_stack.root_dir_fd()?;
271 let target = CString::new(link.as_bytes())?;
272 nix::unistd::linkat(
273 Some(root),
274 target.as_c_str(),
275 Some(parent),
276 file_name,
277 nix::unistd::LinkatFlags::NoSymlinkFollow,
278 )?;
279
280 Ok(())
281 }
282
283 pub fn extract_device(
284 &mut self,
285 file_name: &CStr,
286 metadata: &Metadata,
287 device: &Device,
288 ) -> Result<(), Error> {
289 self.extract_special(file_name, metadata, device.to_dev_t())
290 }
291
292 pub fn extract_special(
293 &mut self,
294 file_name: &CStr,
295 metadata: &Metadata,
296 device: libc::dev_t,
297 ) -> Result<(), Error> {
298 let mode = metadata.stat.mode;
299 let mode = u32::try_from(mode).map_err(|_| {
300 format_err!(
301 "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
302 mode,
303 mode,
304 )
305 })?;
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))?;
309
310 metadata::apply_at(self.feature_flags, metadata, parent, file_name)
311 }
312
313 pub fn extract_file(
314 &mut self,
315 file_name: &CStr,
316 metadata: &Metadata,
317 size: u64,
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(
323 parent,
324 file_name,
325 OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_WRONLY | OFlag::O_CLOEXEC,
326 Mode::from_bits(0o600).unwrap(),
327 )
328 .map_err(|err| format_err!("failed to create file {:?}: {}", file_name, err))?)
329 };
330
331 metadata::apply_initial_flags(self.feature_flags, metadata, file.as_raw_fd())?;
332
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);
337 }
338
339 metadata::apply(self.feature_flags, metadata, file.as_raw_fd(), file_name)
340 }
341
342 pub async fn async_extract_file<T: tokio::io::AsyncRead + Unpin>(
343 &mut self,
344 file_name: &CStr,
345 metadata: &Metadata,
346 size: u64,
347 contents: &mut T,
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(
352 parent,
353 file_name,
354 OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_WRONLY | OFlag::O_CLOEXEC,
355 Mode::from_bits(0o600).unwrap(),
356 )
357 .map_err(|err| format_err!("failed to create file {:?}: {}", file_name, err))?)
358 });
359
360 metadata::apply_initial_flags(self.feature_flags, metadata, file.as_raw_fd())?;
361
362 let extracted = tokio::io::copy(&mut *contents, &mut file)
363 .await
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);
367 }
368
369 metadata::apply(self.feature_flags, metadata, file.as_raw_fd(), file_name)
370 }
371 }