]> git.proxmox.com Git - proxmox-backup.git/blame - src/pxar/create.rs
remove DummyCatalogWriter
[proxmox-backup.git] / src / pxar / create.rs
CommitLineData
c443f58b 1use std::collections::{HashSet, HashMap};
c443f58b
WB
2use std::ffi::{CStr, CString, OsStr};
3use std::fmt;
a8e2940f 4use std::io::{self, Read, Write};
c443f58b
WB
5use std::os::unix::ffi::OsStrExt;
6use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
7use std::path::{Path, PathBuf};
8
9use anyhow::{bail, format_err, Error};
10use nix::dir::Dir;
11use nix::errno::Errno;
12use nix::fcntl::OFlag;
13use nix::sys::stat::{FileStat, Mode};
14
15use pathpatterns::{MatchEntry, MatchList, MatchType, PatternFlag};
16use pxar::Metadata;
17use pxar::encoder::LinkOffset;
18
3d68536f 19use proxmox::c_str;
c443f58b
WB
20use proxmox::sys::error::SysError;
21use proxmox::tools::fd::RawFdNum;
a8e2940f 22use proxmox::tools::vec;
c443f58b
WB
23
24use crate::pxar::catalog::BackupCatalogWriter;
032cd1b8 25use crate::pxar::metadata::errno_is_unsupported;
5444fa94 26use crate::pxar::Flags;
7eacdc76 27use crate::pxar::tools::assert_single_path_component;
c443f58b
WB
28use crate::tools::{acl, fs, xattr, Fd};
29
30fn detect_fs_type(fd: RawFd) -> Result<i64, Error> {
31 let mut fs_stat = std::mem::MaybeUninit::uninit();
32 let res = unsafe { libc::fstatfs(fd, fs_stat.as_mut_ptr()) };
33 Errno::result(res)?;
34 let fs_stat = unsafe { fs_stat.assume_init() };
35
36 Ok(fs_stat.f_type)
37}
38
a8e2940f 39#[rustfmt::skip]
c443f58b
WB
40pub fn is_virtual_file_system(magic: i64) -> bool {
41 use proxmox::sys::linux::magic::*;
42
43 match magic {
44 BINFMTFS_MAGIC |
45 CGROUP2_SUPER_MAGIC |
46 CGROUP_SUPER_MAGIC |
47 CONFIGFS_MAGIC |
48 DEBUGFS_MAGIC |
49 DEVPTS_SUPER_MAGIC |
50 EFIVARFS_MAGIC |
51 FUSE_CTL_SUPER_MAGIC |
52 HUGETLBFS_MAGIC |
53 MQUEUE_MAGIC |
54 NFSD_MAGIC |
55 PROC_SUPER_MAGIC |
56 PSTOREFS_MAGIC |
57 RPCAUTH_GSSMAGIC |
58 SECURITYFS_MAGIC |
59 SELINUX_MAGIC |
60 SMACK_MAGIC |
61 SYSFS_MAGIC => true,
62 _ => false
63 }
64}
65
66#[derive(Debug)]
67struct ArchiveError {
68 path: PathBuf,
69 error: Error,
70}
71
72impl ArchiveError {
73 fn new(path: PathBuf, error: Error) -> Self {
74 Self { path, error }
75 }
76}
77
78impl std::error::Error for ArchiveError {}
79
80impl fmt::Display for ArchiveError {
81 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82 write!(f, "error at {:?}: {}", self.path, self.error)
83 }
84}
85
86#[derive(Eq, PartialEq, Hash)]
87struct HardLinkInfo {
88 st_dev: u64,
89 st_ino: u64,
90}
91
3d68536f
WB
92/// In case we want to collect them or redirect them we can just add this here:
93struct ErrorReporter;
94
95impl std::io::Write for ErrorReporter {
96 fn write(&mut self, data: &[u8]) -> io::Result<usize> {
97 std::io::stderr().write(data)
98 }
99
100 fn flush(&mut self) -> io::Result<()> {
101 std::io::stderr().flush()
102 }
103}
104
c443f58b 105struct Archiver<'a, 'b> {
5444fa94
WB
106 feature_flags: Flags,
107 fs_feature_flags: Flags,
c443f58b 108 fs_magic: i64,
3d68536f 109 patterns: Vec<MatchEntry>,
c443f58b
WB
110 callback: &'a mut dyn FnMut(&Path) -> Result<(), Error>,
111 catalog: Option<&'b mut dyn BackupCatalogWriter>,
112 path: PathBuf,
113 entry_counter: usize,
114 entry_limit: usize,
115 current_st_dev: libc::dev_t,
116 device_set: Option<HashSet<u64>>,
117 hardlinks: HashMap<HardLinkInfo, (PathBuf, LinkOffset)>,
3d68536f 118 errors: ErrorReporter,
a8e2940f 119 file_copy_buffer: Vec<u8>,
c443f58b
WB
120}
121
122type Encoder<'a, 'b> = pxar::encoder::Encoder<'a, &'b mut dyn pxar::encoder::SeqWrite>;
123
124pub fn create_archive<T, F>(
125 source_dir: Dir,
126 mut writer: T,
239e49f9 127 mut patterns: Vec<MatchEntry>,
5444fa94 128 feature_flags: Flags,
c443f58b
WB
129 mut device_set: Option<HashSet<u64>>,
130 skip_lost_and_found: bool,
131 mut callback: F,
132 entry_limit: usize,
133 catalog: Option<&mut dyn BackupCatalogWriter>,
134) -> Result<(), Error>
135where
136 T: pxar::encoder::SeqWrite,
137 F: FnMut(&Path) -> Result<(), Error>,
138{
139 let fs_magic = detect_fs_type(source_dir.as_raw_fd())?;
140 if is_virtual_file_system(fs_magic) {
141 bail!("refusing to backup a virtual file system");
142 }
143
5444fa94 144 let fs_feature_flags = Flags::from_magic(fs_magic);
c443f58b
WB
145
146 let stat = nix::sys::stat::fstat(source_dir.as_raw_fd())?;
147 let metadata = get_metadata(
148 source_dir.as_raw_fd(),
149 &stat,
150 feature_flags & fs_feature_flags,
151 fs_magic,
152 )
153 .map_err(|err| format_err!("failed to get metadata for source directory: {}", err))?;
154
155 if let Some(ref mut set) = device_set {
156 set.insert(stat.st_dev);
157 }
158
159 let writer = &mut writer as &mut dyn pxar::encoder::SeqWrite;
160 let mut encoder = Encoder::new(writer, &metadata)?;
161
162 if skip_lost_and_found {
239e49f9 163 patterns.push(MatchEntry::parse_pattern(
bf7e2a46 164 "lost+found",
c443f58b
WB
165 PatternFlag::PATH_NAME,
166 MatchType::Exclude,
167 )?);
168 }
169
170 let mut archiver = Archiver {
171 feature_flags,
172 fs_feature_flags,
173 fs_magic,
174 callback: &mut callback,
3d68536f 175 patterns,
c443f58b
WB
176 catalog,
177 path: PathBuf::new(),
178 entry_counter: 0,
179 entry_limit,
180 current_st_dev: stat.st_dev,
181 device_set,
182 hardlinks: HashMap::new(),
3d68536f 183 errors: ErrorReporter,
a8e2940f 184 file_copy_buffer: vec::undefined(4 * 1024 * 1024),
c443f58b
WB
185 };
186
30140886 187 archiver.archive_dir_contents(&mut encoder, source_dir, true)?;
c443f58b
WB
188 encoder.finish()?;
189 Ok(())
190}
191
192struct FileListEntry {
193 name: CString,
194 path: PathBuf,
195 stat: FileStat,
196}
197
198impl<'a, 'b> Archiver<'a, 'b> {
5444fa94
WB
199 /// Get the currently effective feature flags. (Requested flags masked by the file system
200 /// feature flags).
201 fn flags(&self) -> Flags {
c443f58b
WB
202 self.feature_flags & self.fs_feature_flags
203 }
204
205 fn wrap_err(&self, err: Error) -> Error {
206 if err.downcast_ref::<ArchiveError>().is_some() {
207 err
208 } else {
209 ArchiveError::new(self.path.clone(), err).into()
210 }
211 }
212
30140886
WB
213 fn archive_dir_contents(
214 &mut self,
215 encoder: &mut Encoder,
216 mut dir: Dir,
217 is_root: bool,
218 ) -> Result<(), Error> {
c443f58b
WB
219 let entry_counter = self.entry_counter;
220
3d68536f
WB
221 let old_patterns_count = self.patterns.len();
222 self.read_pxar_excludes(dir.as_raw_fd())?;
223
30140886 224 let file_list = self.generate_directory_file_list(&mut dir, is_root)?;
c443f58b
WB
225
226 let dir_fd = dir.as_raw_fd();
227
228 let old_path = std::mem::take(&mut self.path);
3d68536f 229
c443f58b 230 for file_entry in file_list {
30140886 231 let file_name = file_entry.name.to_bytes();
3d68536f 232
30140886
WB
233 if is_root && file_name == b".pxarexclude-cli" {
234 self.encode_pxarexclude_cli(encoder, &file_entry.name)?;
235 continue;
236 }
3d68536f 237
30140886 238 (self.callback)(Path::new(OsStr::from_bytes(file_name)))?;
c443f58b
WB
239 self.path = file_entry.path;
240 self.add_entry(encoder, dir_fd, &file_entry.name, &file_entry.stat)
241 .map_err(|err| self.wrap_err(err))?;
242 }
243 self.path = old_path;
244 self.entry_counter = entry_counter;
3d68536f
WB
245 self.patterns.truncate(old_patterns_count);
246
247 Ok(())
248 }
249
250 /// openat() wrapper which allows but logs `EACCES` and turns `ENOENT` into `None`.
7d0754a6
WB
251 ///
252 /// The `existed` flag is set when iterating through a directory to note that we know the file
253 /// is supposed to exist and we should warn if it doesnt'.
3d68536f
WB
254 fn open_file(
255 &mut self,
256 parent: RawFd,
257 file_name: &CStr,
258 oflags: OFlag,
7d0754a6 259 existed: bool,
3d68536f 260 ) -> Result<Option<Fd>, Error> {
e51be338
WB
261 // common flags we always want to use:
262 let oflags = oflags | OFlag::O_CLOEXEC | OFlag::O_NOCTTY;
263
30c3c5d6
WB
264 let mut noatime = OFlag::O_NOATIME;
265 loop {
266 return match Fd::openat(
267 &unsafe { RawFdNum::from_raw_fd(parent) },
268 file_name,
269 oflags | noatime,
270 Mode::empty(),
271 ) {
272 Ok(fd) => Ok(Some(fd)),
273 Err(nix::Error::Sys(Errno::ENOENT)) => {
274 if existed {
275 self.report_vanished_file()?;
276 }
277 Ok(None)
7d0754a6 278 }
30c3c5d6
WB
279 Err(nix::Error::Sys(Errno::EACCES)) => {
280 writeln!(self.errors, "failed to open file: {:?}: access denied", file_name)?;
281 Ok(None)
282 }
283 Err(nix::Error::Sys(Errno::EPERM)) if !noatime.is_empty() => {
284 // Retry without O_NOATIME:
285 noatime = OFlag::empty();
286 continue;
287 }
288 Err(other) => Err(Error::from(other)),
3d68536f 289 }
3d68536f
WB
290 }
291 }
292
293 fn read_pxar_excludes(&mut self, parent: RawFd) -> Result<(), Error> {
e51be338 294 let fd = self.open_file(parent, c_str!(".pxarexclude"), OFlag::O_RDONLY, false)?;
3d68536f
WB
295
296 let old_pattern_count = self.patterns.len();
297
b25deec0
WB
298 let path_bytes = self.path.as_os_str().as_bytes();
299
3d68536f
WB
300 if let Some(fd) = fd {
301 let file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
302
303 use io::BufRead;
b25deec0 304 for line in io::BufReader::new(file).split(b'\n') {
3d68536f
WB
305 let line = match line {
306 Ok(line) => line,
307 Err(err) => {
5afa0755 308 let _ = writeln!(
3d68536f
WB
309 self.errors,
310 "ignoring .pxarexclude after read error in {:?}: {}",
311 self.path,
312 err,
313 );
314 self.patterns.truncate(old_pattern_count);
315 return Ok(());
316 }
317 };
318
b25deec0 319 let line = crate::tools::strip_ascii_whitespace(&line);
3d68536f 320
b25deec0 321 if line.is_empty() || line[0] == b'#' {
3d68536f
WB
322 continue;
323 }
324
b25deec0
WB
325 let mut buf;
326 let (line, mode) = if line[0] == b'/' {
327 buf = Vec::with_capacity(path_bytes.len() + 1 + line.len());
328 buf.extend(path_bytes);
329 buf.extend(line);
330 (&buf[..], MatchType::Exclude)
331 } else if line.starts_with(b"!/") {
332 // inverted case with absolute path
333 buf = Vec::with_capacity(path_bytes.len() + line.len());
334 buf.extend(path_bytes);
335 buf.extend(&line[1..]); // without the '!'
336 (&buf[..], MatchType::Include)
337 } else {
338 (line, MatchType::Exclude)
339 };
340
341 match MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, mode) {
3d68536f
WB
342 Ok(pattern) => self.patterns.push(pattern),
343 Err(err) => {
5afa0755 344 let _ = writeln!(self.errors, "bad pattern in {:?}: {}", self.path, err);
3d68536f
WB
345 }
346 }
347 }
348 }
c443f58b
WB
349
350 Ok(())
351 }
352
30140886
WB
353 fn encode_pxarexclude_cli(
354 &mut self,
355 encoder: &mut Encoder,
356 file_name: &CStr,
357 ) -> Result<(), Error> {
358 let content = generate_pxar_excludes_cli(&self.patterns);
359
360 if let Some(ref mut catalog) = self.catalog {
361 catalog.add_file(file_name, content.len() as u64, 0)?;
362 }
363
364 let mut metadata = Metadata::default();
365 metadata.stat.mode = pxar::format::mode::IFREG | 0o600;
366
367 let mut file = encoder.create_file(&metadata, ".pxarexclude-cli", content.len() as u64)?;
30140886
WB
368 file.write_all(&content)?;
369
370 Ok(())
371 }
372
373 fn generate_directory_file_list(
374 &mut self,
375 dir: &mut Dir,
376 is_root: bool,
377 ) -> Result<Vec<FileListEntry>, Error> {
c443f58b
WB
378 let dir_fd = dir.as_raw_fd();
379
380 let mut file_list = Vec::new();
381
30140886
WB
382 if is_root && !self.patterns.is_empty() {
383 file_list.push(FileListEntry {
384 name: CString::new(".pxarexclude-cli").unwrap(),
385 path: PathBuf::new(),
386 stat: unsafe { std::mem::zeroed() },
387 });
388 }
389
c443f58b
WB
390 for file in dir.iter() {
391 let file = file?;
392
393 let file_name = file.file_name().to_owned();
394 let file_name_bytes = file_name.to_bytes();
395 if file_name_bytes == b"." || file_name_bytes == b".." {
396 continue;
397 }
398
30140886
WB
399 if is_root && file_name_bytes == b".pxarexclude-cli" {
400 continue;
401 }
402
c443f58b 403 if file_name_bytes == b".pxarexclude" {
c443f58b
WB
404 continue;
405 }
406
407 let os_file_name = OsStr::from_bytes(file_name_bytes);
7eacdc76 408 assert_single_path_component(os_file_name)?;
c443f58b
WB
409 let full_path = self.path.join(os_file_name);
410
411 let stat = match nix::sys::stat::fstatat(
412 dir_fd,
413 file_name.as_c_str(),
414 nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
415 ) {
416 Ok(stat) => stat,
417 Err(ref err) if err.not_found() => continue,
418 Err(err) => bail!("stat failed on {:?}: {}", full_path, err),
419 };
420
421 if self
239e49f9 422 .patterns
c443f58b
WB
423 .matches(full_path.as_os_str().as_bytes(), Some(stat.st_mode as u32))
424 == Some(MatchType::Exclude)
425 {
426 continue;
427 }
428
429 self.entry_counter += 1;
430 if self.entry_counter > self.entry_limit {
431 bail!("exceeded allowed number of file entries (> {})",self.entry_limit);
432 }
433
434 file_list.push(FileListEntry {
435 name: file_name,
436 path: full_path,
437 stat
438 });
439 }
440
441 file_list.sort_unstable_by(|a, b| a.name.cmp(&b.name));
442
443 Ok(file_list)
444 }
445
3d68536f 446 fn report_vanished_file(&mut self) -> Result<(), Error> {
5afa0755 447 writeln!(self.errors, "warning: file vanished while reading: {:?}", self.path)?;
3d68536f
WB
448 Ok(())
449 }
450
a8e2940f 451 fn report_file_shrunk_while_reading(&mut self) -> Result<(), Error> {
5afa0755 452 writeln!(
a8e2940f
WB
453 self.errors,
454 "warning: file size shrunk while reading: {:?}, file will be padded with zeros!",
455 self.path,
456 )?;
457 Ok(())
458 }
459
460 fn report_file_grew_while_reading(&mut self) -> Result<(), Error> {
5afa0755 461 writeln!(
a8e2940f
WB
462 self.errors,
463 "warning: file size increased while reading: {:?}, file will be truncated!",
464 self.path,
465 )?;
466 Ok(())
467 }
468
c443f58b
WB
469 fn add_entry(
470 &mut self,
471 encoder: &mut Encoder,
472 parent: RawFd,
473 c_file_name: &CStr,
474 stat: &FileStat,
475 ) -> Result<(), Error> {
476 use pxar::format::mode;
477
478 let file_mode = stat.st_mode & libc::S_IFMT;
1008a69a 479 let open_mode = if file_mode == libc::S_IFREG || file_mode == libc::S_IFDIR {
c443f58b 480 OFlag::empty()
1008a69a
WB
481 } else {
482 OFlag::O_PATH
c443f58b
WB
483 };
484
3d68536f
WB
485 let fd = self.open_file(
486 parent,
c443f58b 487 c_file_name,
e51be338 488 open_mode | OFlag::O_RDONLY | OFlag::O_NOFOLLOW,
7d0754a6 489 true,
c443f58b
WB
490 )?;
491
3d68536f
WB
492 let fd = match fd {
493 Some(fd) => fd,
7d0754a6 494 None => return Ok(()),
3d68536f
WB
495 };
496
c443f58b
WB
497 let metadata = get_metadata(fd.as_raw_fd(), &stat, self.flags(), self.fs_magic)?;
498
499 if self
239e49f9 500 .patterns
c443f58b
WB
501 .matches(self.path.as_os_str().as_bytes(), Some(stat.st_mode as u32))
502 == Some(MatchType::Exclude)
503 {
504 return Ok(());
505 }
506
507 let file_name: &Path = OsStr::from_bytes(c_file_name.to_bytes()).as_ref();
508 match metadata.file_type() {
509 mode::IFREG => {
510 let link_info = HardLinkInfo {
511 st_dev: stat.st_dev,
512 st_ino: stat.st_ino,
513 };
514
515 if stat.st_nlink > 1 {
516 if let Some((path, offset)) = self.hardlinks.get(&link_info) {
517 if let Some(ref mut catalog) = self.catalog {
518 catalog.add_hardlink(c_file_name)?;
519 }
520
521 encoder.add_hardlink(file_name, path, *offset)?;
522
523 return Ok(());
524 }
525 }
526
527 let file_size = stat.st_size as u64;
528 if let Some(ref mut catalog) = self.catalog {
2564b083 529 catalog.add_file(c_file_name, file_size, stat.st_mtime as u64)?;
c443f58b
WB
530 }
531
532 let offset: LinkOffset =
533 self.add_regular_file(encoder, fd, file_name, &metadata, file_size)?;
534
535 if stat.st_nlink > 1 {
536 self.hardlinks.insert(link_info, (self.path.clone(), offset));
537 }
538
539 Ok(())
540 }
541 mode::IFDIR => {
542 let dir = Dir::from_fd(fd.into_raw_fd())?;
9321bbd1
WB
543
544 if let Some(ref mut catalog) = self.catalog {
545 catalog.start_directory(c_file_name)?;
546 }
547 let result = self.add_directory(encoder, dir, c_file_name, &metadata, stat);
548 if let Some(ref mut catalog) = self.catalog {
549 catalog.end_directory()?;
550 }
551 result
c443f58b
WB
552 }
553 mode::IFSOCK => {
554 if let Some(ref mut catalog) = self.catalog {
555 catalog.add_socket(c_file_name)?;
556 }
557
558 Ok(encoder.add_socket(&metadata, file_name)?)
559 }
560 mode::IFIFO => {
561 if let Some(ref mut catalog) = self.catalog {
562 catalog.add_fifo(c_file_name)?;
563 }
564
565 Ok(encoder.add_fifo(&metadata, file_name)?)
566 }
567 mode::IFLNK => {
568 if let Some(ref mut catalog) = self.catalog {
569 catalog.add_symlink(c_file_name)?;
570 }
571
572 self.add_symlink(encoder, fd, file_name, &metadata)
573 }
574 mode::IFBLK => {
575 if let Some(ref mut catalog) = self.catalog {
576 catalog.add_block_device(c_file_name)?;
577 }
578
579 self.add_device(encoder, file_name, &metadata, &stat)
580 }
581 mode::IFCHR => {
582 if let Some(ref mut catalog) = self.catalog {
583 catalog.add_char_device(c_file_name)?;
584 }
585
586 self.add_device(encoder, file_name, &metadata, &stat)
587 }
588 other => bail!(
589 "encountered unknown file type: 0x{:x} (0o{:o})",
590 other,
591 other
592 ),
593 }
594 }
595
596 fn add_directory(
597 &mut self,
598 encoder: &mut Encoder,
599 dir: Dir,
600 dir_name: &CStr,
601 metadata: &Metadata,
602 stat: &FileStat,
603 ) -> Result<(), Error> {
604 let dir_name = OsStr::from_bytes(dir_name.to_bytes());
605
606 let mut encoder = encoder.create_directory(dir_name, &metadata)?;
607
608 let old_fs_magic = self.fs_magic;
609 let old_fs_feature_flags = self.fs_feature_flags;
610 let old_st_dev = self.current_st_dev;
611
612 let mut skip_contents = false;
613 if old_st_dev != stat.st_dev {
614 self.fs_magic = detect_fs_type(dir.as_raw_fd())?;
5444fa94 615 self.fs_feature_flags = Flags::from_magic(self.fs_magic);
c443f58b
WB
616 self.current_st_dev = stat.st_dev;
617
618 if is_virtual_file_system(self.fs_magic) {
619 skip_contents = true;
620 } else if let Some(set) = &self.device_set {
621 skip_contents = !set.contains(&stat.st_dev);
622 }
623 }
624
625 let result = if skip_contents {
626 Ok(())
627 } else {
30140886 628 self.archive_dir_contents(&mut encoder, dir, false)
c443f58b
WB
629 };
630
631 self.fs_magic = old_fs_magic;
632 self.fs_feature_flags = old_fs_feature_flags;
633 self.current_st_dev = old_st_dev;
634
635 encoder.finish()?;
636 result
637 }
638
639 fn add_regular_file(
640 &mut self,
641 encoder: &mut Encoder,
642 fd: Fd,
643 file_name: &Path,
644 metadata: &Metadata,
645 file_size: u64,
646 ) -> Result<LinkOffset, Error> {
647 let mut file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
a8e2940f
WB
648 let mut remaining = file_size;
649 let mut out = encoder.create_file(metadata, file_name, file_size)?;
650 while remaining != 0 {
651 let mut got = file.read(&mut self.file_copy_buffer[..])?;
652 if got as u64 > remaining {
653 self.report_file_grew_while_reading()?;
654 got = remaining as usize;
655 }
656 out.write_all(&self.file_copy_buffer[..got])?;
657 remaining -= got as u64;
658 }
659 if remaining > 0 {
660 self.report_file_shrunk_while_reading()?;
661 let to_zero = remaining.min(self.file_copy_buffer.len() as u64) as usize;
662 vec::clear(&mut self.file_copy_buffer[..to_zero]);
663 while remaining != 0 {
664 let fill = remaining.min(self.file_copy_buffer.len() as u64) as usize;
665 out.write_all(&self.file_copy_buffer[..fill])?;
666 remaining -= fill as u64;
667 }
668 }
669
670 Ok(out.file_offset())
c443f58b
WB
671 }
672
673 fn add_symlink(
674 &mut self,
675 encoder: &mut Encoder,
676 fd: Fd,
677 file_name: &Path,
678 metadata: &Metadata,
679 ) -> Result<(), Error> {
680 let dest = nix::fcntl::readlinkat(fd.as_raw_fd(), &b""[..])?;
681 encoder.add_symlink(metadata, file_name, dest)?;
682 Ok(())
683 }
684
685 fn add_device(
686 &mut self,
687 encoder: &mut Encoder,
688 file_name: &Path,
689 metadata: &Metadata,
690 stat: &FileStat,
691 ) -> Result<(), Error> {
692 Ok(encoder.add_device(
693 metadata,
694 file_name,
695 pxar::format::Device::from_dev_t(stat.st_rdev),
696 )?)
697 }
698}
699
5444fa94 700fn get_metadata(fd: RawFd, stat: &FileStat, flags: Flags, fs_magic: i64) -> Result<Metadata, Error> {
c443f58b
WB
701 // required for some of these
702 let proc_path = Path::new("/proc/self/fd/").join(fd.to_string());
703
c443f58b
WB
704 let mut meta = Metadata {
705 stat: pxar::Stat {
706 mode: u64::from(stat.st_mode),
707 flags: 0,
708 uid: stat.st_uid,
709 gid: stat.st_gid,
f6c6e09a
WB
710 mtime: pxar::format::StatxTimestamp {
711 secs: stat.st_mtime,
712 nanos: stat.st_mtime_nsec as u32,
713 },
c443f58b
WB
714 },
715 ..Default::default()
716 };
717
718 get_xattr_fcaps_acl(&mut meta, fd, &proc_path, flags)?;
719 get_chattr(&mut meta, fd)?;
720 get_fat_attr(&mut meta, fd, fs_magic)?;
721 get_quota_project_id(&mut meta, fd, flags, fs_magic)?;
722 Ok(meta)
723}
724
5444fa94
WB
725fn get_fcaps(meta: &mut Metadata, fd: RawFd, flags: Flags) -> Result<(), Error> {
726 if flags.contains(Flags::WITH_FCAPS) {
c443f58b
WB
727 return Ok(());
728 }
729
730 match xattr::fgetxattr(fd, xattr::xattr_name_fcaps()) {
731 Ok(data) => {
732 meta.fcaps = Some(pxar::format::FCaps { data });
733 Ok(())
734 }
735 Err(Errno::ENODATA) => Ok(()),
736 Err(Errno::EOPNOTSUPP) => Ok(()),
737 Err(Errno::EBADF) => Ok(()), // symlinks
738 Err(err) => bail!("failed to read file capabilities: {}", err),
739 }
740}
741
742fn get_xattr_fcaps_acl(
743 meta: &mut Metadata,
744 fd: RawFd,
745 proc_path: &Path,
5444fa94 746 flags: Flags,
c443f58b 747) -> Result<(), Error> {
5444fa94 748 if flags.contains(Flags::WITH_XATTRS) {
c443f58b
WB
749 return Ok(());
750 }
751
752 let xattrs = match xattr::flistxattr(fd) {
753 Ok(names) => names,
754 Err(Errno::EOPNOTSUPP) => return Ok(()),
755 Err(Errno::EBADF) => return Ok(()), // symlinks
756 Err(err) => bail!("failed to read xattrs: {}", err),
757 };
758
759 for attr in &xattrs {
760 if xattr::is_security_capability(&attr) {
761 get_fcaps(meta, fd, flags)?;
762 continue;
763 }
764
765 if xattr::is_acl(&attr) {
766 get_acl(meta, proc_path, flags)?;
767 continue;
768 }
769
770 if !xattr::is_valid_xattr_name(&attr) {
771 continue;
772 }
773
774 match xattr::fgetxattr(fd, attr) {
775 Ok(data) => meta
776 .xattrs
777 .push(pxar::format::XAttr::new(attr.to_bytes(), data)),
778 Err(Errno::ENODATA) => (), // it got removed while we were iterating...
779 Err(Errno::EOPNOTSUPP) => (), // shouldn't be possible so just ignore this
780 Err(Errno::EBADF) => (), // symlinks, shouldn't be able to reach this either
781 Err(err) => bail!("error reading extended attribute {:?}: {}", attr, err),
782 }
783 }
784
785 Ok(())
786}
787
788fn get_chattr(metadata: &mut Metadata, fd: RawFd) -> Result<(), Error> {
032cd1b8 789 let mut attr: libc::c_long = 0;
c443f58b
WB
790
791 match unsafe { fs::read_attr_fd(fd, &mut attr) } {
792 Ok(_) => (),
793 Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
794 return Ok(());
795 }
796 Err(err) => bail!("failed to read file attributes: {}", err),
797 }
798
032cd1b8 799 metadata.stat.flags |= Flags::from_chattr(attr).bits();
c443f58b
WB
800
801 Ok(())
802}
803
804fn get_fat_attr(metadata: &mut Metadata, fd: RawFd, fs_magic: i64) -> Result<(), Error> {
805 use proxmox::sys::linux::magic::*;
806
807 if fs_magic != MSDOS_SUPER_MAGIC && fs_magic != FUSE_SUPER_MAGIC {
808 return Ok(());
809 }
810
811 let mut attr: u32 = 0;
812
813 match unsafe { fs::read_fat_attr_fd(fd, &mut attr) } {
814 Ok(_) => (),
815 Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
816 return Ok(());
817 }
818 Err(err) => bail!("failed to read fat attributes: {}", err),
819 }
820
5444fa94 821 metadata.stat.flags |= Flags::from_fat_attr(attr).bits();
c443f58b
WB
822
823 Ok(())
824}
825
826/// Read the quota project id for an inode, supported on ext4/XFS/FUSE/ZFS filesystems
827fn get_quota_project_id(
828 metadata: &mut Metadata,
829 fd: RawFd,
5444fa94 830 flags: Flags,
c443f58b
WB
831 magic: i64,
832) -> Result<(), Error> {
833 if !(metadata.is_dir() || metadata.is_regular_file()) {
834 return Ok(());
835 }
836
5444fa94 837 if flags.contains(Flags::WITH_QUOTA_PROJID) {
c443f58b
WB
838 return Ok(());
839 }
840
841 use proxmox::sys::linux::magic::*;
842
843 match magic {
844 EXT4_SUPER_MAGIC | XFS_SUPER_MAGIC | FUSE_SUPER_MAGIC | ZFS_SUPER_MAGIC => (),
845 _ => return Ok(()),
846 }
847
848 let mut fsxattr = fs::FSXAttr::default();
849 let res = unsafe { fs::fs_ioc_fsgetxattr(fd, &mut fsxattr) };
850
851 // On some FUSE filesystems it can happen that ioctl is not supported.
852 // For these cases projid is set to 0 while the error is ignored.
853 if let Err(err) = res {
854 let errno = err
855 .as_errno()
856 .ok_or_else(|| format_err!("error while reading quota project id"))?;
857 if errno_is_unsupported(errno) {
858 return Ok(());
859 } else {
860 bail!("error while reading quota project id ({})", errno);
861 }
862 }
863
864 let projid = fsxattr.fsx_projid as u64;
865 if projid != 0 {
866 metadata.quota_project_id = Some(pxar::format::QuotaProjectId { projid });
867 }
868 Ok(())
869}
870
5444fa94
WB
871fn get_acl(metadata: &mut Metadata, proc_path: &Path, flags: Flags) -> Result<(), Error> {
872 if flags.contains(Flags::WITH_ACL) {
c443f58b
WB
873 return Ok(());
874 }
875
876 if metadata.is_symlink() {
877 return Ok(());
878 }
879
880 get_acl_do(metadata, proc_path, acl::ACL_TYPE_ACCESS)?;
881
882 if metadata.is_dir() {
883 get_acl_do(metadata, proc_path, acl::ACL_TYPE_DEFAULT)?;
884 }
885
886 Ok(())
887}
888
889fn get_acl_do(
890 metadata: &mut Metadata,
891 proc_path: &Path,
892 acl_type: acl::ACLType,
893) -> Result<(), Error> {
894 // In order to be able to get ACLs with type ACL_TYPE_DEFAULT, we have
895 // to create a path for acl_get_file(). acl_get_fd() only allows to get
896 // ACL_TYPE_ACCESS attributes.
897 let acl = match acl::ACL::get_file(&proc_path, acl_type) {
898 Ok(acl) => acl,
899 // Don't bail if underlying endpoint does not support acls
900 Err(Errno::EOPNOTSUPP) => return Ok(()),
901 // Don't bail if the endpoint cannot carry acls
902 Err(Errno::EBADF) => return Ok(()),
903 // Don't bail if there is no data
904 Err(Errno::ENODATA) => return Ok(()),
905 Err(err) => bail!("error while reading ACL - {}", err),
906 };
907
908 process_acl(metadata, acl, acl_type)
909}
910
911fn process_acl(
912 metadata: &mut Metadata,
913 acl: acl::ACL,
914 acl_type: acl::ACLType,
915) -> Result<(), Error> {
916 use pxar::format::acl as pxar_acl;
917 use pxar::format::acl::{Group, GroupObject, Permissions, User};
918
919 let mut acl_user = Vec::new();
920 let mut acl_group = Vec::new();
921 let mut acl_group_obj = None;
922 let mut acl_default = None;
923 let mut user_obj_permissions = None;
924 let mut group_obj_permissions = None;
925 let mut other_permissions = None;
926 let mut mask_permissions = None;
927
928 for entry in &mut acl.entries() {
929 let tag = entry.get_tag_type()?;
930 let permissions = entry.get_permissions()?;
931 match tag {
932 acl::ACL_USER_OBJ => user_obj_permissions = Some(Permissions(permissions)),
933 acl::ACL_GROUP_OBJ => group_obj_permissions = Some(Permissions(permissions)),
934 acl::ACL_OTHER => other_permissions = Some(Permissions(permissions)),
935 acl::ACL_MASK => mask_permissions = Some(Permissions(permissions)),
936 acl::ACL_USER => {
937 acl_user.push(User {
938 uid: entry.get_qualifier()?,
939 permissions: Permissions(permissions),
940 });
941 }
942 acl::ACL_GROUP => {
943 acl_group.push(Group {
944 gid: entry.get_qualifier()?,
945 permissions: Permissions(permissions),
946 });
947 }
948 _ => bail!("Unexpected ACL tag encountered!"),
949 }
950 }
951
952 acl_user.sort();
953 acl_group.sort();
954
955 match acl_type {
956 acl::ACL_TYPE_ACCESS => {
957 // The mask permissions are mapped to the stat group permissions
958 // in case that the ACL group permissions were set.
959 // Only in that case we need to store the group permissions,
960 // in the other cases they are identical to the stat group permissions.
961 if let (Some(gop), true) = (group_obj_permissions, mask_permissions.is_some()) {
962 acl_group_obj = Some(GroupObject { permissions: gop });
963 }
964
965 metadata.acl.users = acl_user;
966 metadata.acl.groups = acl_group;
967 }
968 acl::ACL_TYPE_DEFAULT => {
969 if user_obj_permissions != None
970 || group_obj_permissions != None
971 || other_permissions != None
972 || mask_permissions != None
973 {
974 acl_default = Some(pxar_acl::Default {
975 // The value is set to UINT64_MAX as placeholder if one
976 // of the permissions is not set
977 user_obj_permissions: user_obj_permissions.unwrap_or(Permissions::NO_MASK),
978 group_obj_permissions: group_obj_permissions.unwrap_or(Permissions::NO_MASK),
979 other_permissions: other_permissions.unwrap_or(Permissions::NO_MASK),
980 mask_permissions: mask_permissions.unwrap_or(Permissions::NO_MASK),
981 });
982 }
983
984 metadata.acl.default_users = acl_user;
985 metadata.acl.default_groups = acl_group;
986 }
987 _ => bail!("Unexpected ACL type encountered"),
988 }
989
990 metadata.acl.group_obj = acl_group_obj;
991 metadata.acl.default = acl_default;
992
993 Ok(())
994}
239e49f9
WB
995
996/// Note that our pattern lists are "positive". `MatchType::Include` means the file is included.
997/// Since we are generating an *exclude* list, we need to invert this, so includes get a `'!'`
998/// prefix.
999fn generate_pxar_excludes_cli(patterns: &[MatchEntry]) -> Vec<u8> {
1000 use pathpatterns::{MatchFlag, MatchPattern};
1001
1002 let mut content = Vec::new();
1003
1004 for pattern in patterns {
1005 match pattern.match_type() {
1006 MatchType::Include => content.push(b'!'),
1007 MatchType::Exclude => (),
1008 }
1009
1010 match pattern.pattern() {
1011 MatchPattern::Literal(lit) => content.extend(lit),
1012 MatchPattern::Pattern(pat) => content.extend(pat.pattern().to_bytes()),
1013 }
1014
1015 if pattern.match_flags() == MatchFlag::MATCH_DIRECTORIES && content.last() != Some(&b'/') {
1016 content.push(b'/');
1017 }
1018
1019 content.push(b'\n');
1020 }
1021
1022 content
1023}