switch to external pxar and fuse crates
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
		@ -628,9 +628,9 @@ fn dynamic_chunk_index(
 | 
			
		||||
 | 
			
		||||
        let count = index.index_count();
 | 
			
		||||
        for pos in 0..count {
 | 
			
		||||
            let (start, end, digest) = index.chunk_info(pos)?;
 | 
			
		||||
            let size = (end - start) as u32;
 | 
			
		||||
            env.register_chunk(digest, size)?;
 | 
			
		||||
            let info = index.chunk_info(pos)?;
 | 
			
		||||
            let size = info.size() as u32;
 | 
			
		||||
            env.register_chunk(info.digest, size)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let reader = DigestListEncoder::new(Box::new(index));
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,18 @@
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::ffi::{CStr, CString, OsStr};
 | 
			
		||||
use std::os::unix::ffi::OsStrExt;
 | 
			
		||||
use std::io::{Read, Write, Seek, SeekFrom};
 | 
			
		||||
use std::convert::TryFrom;
 | 
			
		||||
use std::ffi::{CStr, CString, OsStr};
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::io::{Read, Write, Seek, SeekFrom};
 | 
			
		||||
use std::os::unix::ffi::OsStrExt;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use chrono::offset::{TimeZone, Local};
 | 
			
		||||
 | 
			
		||||
use proxmox::tools::io::ReadExt;
 | 
			
		||||
use pathpatterns::{MatchList, MatchType};
 | 
			
		||||
use proxmox::sys::error::io_err_other;
 | 
			
		||||
use proxmox::tools::io::ReadExt;
 | 
			
		||||
 | 
			
		||||
use crate::pxar::catalog::BackupCatalogWriter;
 | 
			
		||||
use crate::pxar::{MatchPattern, MatchPatternSlice, MatchType};
 | 
			
		||||
use crate::backup::file_formats::PROXMOX_CATALOG_FILE_MAGIC_1_0;
 | 
			
		||||
use crate::pxar::catalog::BackupCatalogWriter;
 | 
			
		||||
use crate::tools::runtime::block_on;
 | 
			
		||||
 | 
			
		||||
#[repr(u8)]
 | 
			
		||||
@ -63,7 +63,7 @@ pub struct DirEntry {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Used to specific additional attributes inside DirEntry
 | 
			
		||||
#[derive(Clone, PartialEq)]
 | 
			
		||||
#[derive(Clone, Debug, PartialEq)]
 | 
			
		||||
pub enum DirEntryAttribute {
 | 
			
		||||
    Directory { start: u64 },
 | 
			
		||||
    File { size: u64, mtime: u64 },
 | 
			
		||||
@ -106,6 +106,23 @@ impl DirEntry {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get file mode bits for this entry to be used with the `MatchList` api.
 | 
			
		||||
    pub fn get_file_mode(&self) -> Option<u32> {
 | 
			
		||||
        Some(
 | 
			
		||||
            match self.attr {
 | 
			
		||||
                DirEntryAttribute::Directory { .. } => pxar::mode::IFDIR,
 | 
			
		||||
                DirEntryAttribute::File { .. } => pxar::mode::IFREG,
 | 
			
		||||
                DirEntryAttribute::Symlink => pxar::mode::IFLNK,
 | 
			
		||||
                DirEntryAttribute::Hardlink => return None,
 | 
			
		||||
                DirEntryAttribute::BlockDevice => pxar::mode::IFBLK,
 | 
			
		||||
                DirEntryAttribute::CharDevice => pxar::mode::IFCHR,
 | 
			
		||||
                DirEntryAttribute::Fifo => pxar::mode::IFIFO,
 | 
			
		||||
                DirEntryAttribute::Socket => pxar::mode::IFSOCK,
 | 
			
		||||
            }
 | 
			
		||||
            as u32
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Check if DirEntry is a directory
 | 
			
		||||
    pub fn is_directory(&self) -> bool {
 | 
			
		||||
        match self.attr {
 | 
			
		||||
@ -476,7 +493,7 @@ impl <R: Read + Seek> CatalogReader<R> {
 | 
			
		||||
        &mut self,
 | 
			
		||||
        parent: &DirEntry,
 | 
			
		||||
        filename: &[u8],
 | 
			
		||||
    ) -> Result<DirEntry, Error>  {
 | 
			
		||||
    ) -> Result<Option<DirEntry>, Error>  {
 | 
			
		||||
 | 
			
		||||
        let start = match parent.attr {
 | 
			
		||||
            DirEntryAttribute::Directory { start } => start,
 | 
			
		||||
@ -496,10 +513,7 @@ impl <R: Read + Seek> CatalogReader<R> {
 | 
			
		||||
            Ok(false) // stop parsing
 | 
			
		||||
        })?;
 | 
			
		||||
 | 
			
		||||
        match item {
 | 
			
		||||
            None => bail!("no such file"),
 | 
			
		||||
            Some(entry) => Ok(entry),
 | 
			
		||||
        }
 | 
			
		||||
        Ok(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Read the raw directory info block from current reader position.
 | 
			
		||||
@ -555,38 +569,30 @@ impl <R: Read + Seek> CatalogReader<R> {
 | 
			
		||||
    /// provided callback on them.
 | 
			
		||||
    pub fn find(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        mut entry: &mut Vec<DirEntry>,
 | 
			
		||||
        pattern: &[MatchPatternSlice],
 | 
			
		||||
        callback: &Box<fn(&[DirEntry])>,
 | 
			
		||||
        parent: &DirEntry,
 | 
			
		||||
        file_path: &mut Vec<u8>,
 | 
			
		||||
        match_list: &impl MatchList, //&[MatchEntry],
 | 
			
		||||
        callback: &mut dyn FnMut(&[u8]) -> Result<(), Error>,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        let parent = entry.last().unwrap();
 | 
			
		||||
        if !parent.is_directory() {
 | 
			
		||||
            return Ok(())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let file_len = file_path.len();
 | 
			
		||||
        for e in self.read_dir(parent)?  {
 | 
			
		||||
            match MatchPatternSlice::match_filename_include(
 | 
			
		||||
                &CString::new(e.name.clone())?,
 | 
			
		||||
                e.is_directory(),
 | 
			
		||||
                pattern,
 | 
			
		||||
            )? {
 | 
			
		||||
                (MatchType::Positive, _) => {
 | 
			
		||||
                    entry.push(e);
 | 
			
		||||
                    callback(&entry);
 | 
			
		||||
                    let pattern = MatchPattern::from_line(b"**/*").unwrap().unwrap();
 | 
			
		||||
                    let child_pattern = vec![pattern.as_slice()];
 | 
			
		||||
                    self.find(&mut entry, &child_pattern, callback)?;
 | 
			
		||||
                    entry.pop();
 | 
			
		||||
                }
 | 
			
		||||
                (MatchType::PartialPositive, child_pattern)
 | 
			
		||||
                | (MatchType::PartialNegative, child_pattern) => {
 | 
			
		||||
                    entry.push(e);
 | 
			
		||||
                    self.find(&mut entry, &child_pattern, callback)?;
 | 
			
		||||
                    entry.pop();
 | 
			
		||||
                }
 | 
			
		||||
                _ => {}
 | 
			
		||||
            let is_dir = e.is_directory();
 | 
			
		||||
            file_path.truncate(file_len);
 | 
			
		||||
            if !e.name.starts_with(b"/") {
 | 
			
		||||
                file_path.reserve(e.name.len() + 1);
 | 
			
		||||
                file_path.push(b'/');
 | 
			
		||||
            }
 | 
			
		||||
            file_path.extend(&e.name);
 | 
			
		||||
            match match_list.matches(&file_path, e.get_file_mode()) {
 | 
			
		||||
                Some(MatchType::Exclude) => continue,
 | 
			
		||||
                Some(MatchType::Include) => callback(&file_path)?,
 | 
			
		||||
                None => (),
 | 
			
		||||
            }
 | 
			
		||||
            if is_dir {
 | 
			
		||||
                self.find(&e, file_path, match_list, callback)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        file_path.truncate(file_len);
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,6 +1,7 @@
 | 
			
		||||
use std::convert::TryInto;
 | 
			
		||||
use std::fs::File;
 | 
			
		||||
use std::io::{BufWriter, Seek, SeekFrom, Write};
 | 
			
		||||
use std::ops::Range;
 | 
			
		||||
use std::os::unix::io::AsRawFd;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
@ -13,6 +14,7 @@ use proxmox::tools::vec;
 | 
			
		||||
 | 
			
		||||
use super::chunk_stat::ChunkStat;
 | 
			
		||||
use super::chunk_store::ChunkStore;
 | 
			
		||||
use super::index::ChunkReadInfo;
 | 
			
		||||
use super::read_chunk::ReadChunk;
 | 
			
		||||
use super::Chunker;
 | 
			
		||||
use super::IndexFile;
 | 
			
		||||
@ -136,7 +138,7 @@ impl DynamicIndexReader {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[allow(clippy::cast_ptr_alignment)]
 | 
			
		||||
    pub fn chunk_info(&self, pos: usize) -> Result<(u64, u64, [u8; 32]), Error> {
 | 
			
		||||
    pub fn chunk_info(&self, pos: usize) -> Result<ChunkReadInfo, Error> {
 | 
			
		||||
        if pos >= self.index_entries {
 | 
			
		||||
            bail!("chunk index out of range");
 | 
			
		||||
        }
 | 
			
		||||
@ -157,7 +159,10 @@ impl DynamicIndexReader {
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok((start, end, unsafe { digest.assume_init() }))
 | 
			
		||||
        Ok(ChunkReadInfo {
 | 
			
		||||
            range: start..end,
 | 
			
		||||
            digest: unsafe { digest.assume_init() },
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[inline]
 | 
			
		||||
@ -258,6 +263,25 @@ impl IndexFile for DynamicIndexReader {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct CachedChunk {
 | 
			
		||||
    range: Range<u64>,
 | 
			
		||||
    data: Vec<u8>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CachedChunk {
 | 
			
		||||
    /// Perform sanity checks on the range and data size:
 | 
			
		||||
    pub fn new(range: Range<u64>, data: Vec<u8>) -> Result<Self, Error> {
 | 
			
		||||
        if data.len() as u64 != range.end - range.start {
 | 
			
		||||
            bail!(
 | 
			
		||||
                "read chunk with wrong size ({} != {})",
 | 
			
		||||
                data.len(),
 | 
			
		||||
                range.end - range.start,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        Ok(Self { range, data })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct BufferedDynamicReader<S> {
 | 
			
		||||
    store: S,
 | 
			
		||||
    index: DynamicIndexReader,
 | 
			
		||||
@ -266,7 +290,7 @@ pub struct BufferedDynamicReader<S> {
 | 
			
		||||
    buffered_chunk_idx: usize,
 | 
			
		||||
    buffered_chunk_start: u64,
 | 
			
		||||
    read_offset: u64,
 | 
			
		||||
    lru_cache: crate::tools::lru_cache::LruCache<usize, (u64, u64, Vec<u8>)>,
 | 
			
		||||
    lru_cache: crate::tools::lru_cache::LruCache<usize, CachedChunk>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct ChunkCacher<'a, S> {
 | 
			
		||||
@ -274,10 +298,12 @@ struct ChunkCacher<'a, S> {
 | 
			
		||||
    index: &'a DynamicIndexReader,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a, S: ReadChunk> crate::tools::lru_cache::Cacher<usize, (u64, u64, Vec<u8>)> for ChunkCacher<'a, S> {
 | 
			
		||||
    fn fetch(&mut self, index: usize) -> Result<Option<(u64, u64, Vec<u8>)>, anyhow::Error> {
 | 
			
		||||
        let (start, end, digest) = self.index.chunk_info(index)?;
 | 
			
		||||
        self.store.read_chunk(&digest).and_then(|data| Ok(Some((start, end, data))))
 | 
			
		||||
impl<'a, S: ReadChunk> crate::tools::lru_cache::Cacher<usize, CachedChunk> for ChunkCacher<'a, S> {
 | 
			
		||||
    fn fetch(&mut self, index: usize) -> Result<Option<CachedChunk>, Error> {
 | 
			
		||||
        let info = self.index.chunk_info(index)?;
 | 
			
		||||
        let range = info.range;
 | 
			
		||||
        let data = self.store.read_chunk(&info.digest)?;
 | 
			
		||||
        CachedChunk::new(range, data).map(Some)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -301,7 +327,8 @@ impl<S: ReadChunk> BufferedDynamicReader<S> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn buffer_chunk(&mut self, idx: usize) -> Result<(), Error> {
 | 
			
		||||
        let (start, end, data) = self.lru_cache.access(
 | 
			
		||||
        //let (start, end, data) = self.lru_cache.access(
 | 
			
		||||
        let cached_chunk = self.lru_cache.access(
 | 
			
		||||
            idx,
 | 
			
		||||
            &mut ChunkCacher {
 | 
			
		||||
                store: &mut self.store,
 | 
			
		||||
@ -309,21 +336,13 @@ impl<S: ReadChunk> BufferedDynamicReader<S> {
 | 
			
		||||
            },
 | 
			
		||||
        )?.ok_or_else(|| format_err!("chunk not found by cacher"))?;
 | 
			
		||||
 | 
			
		||||
        if (*end - *start) != data.len() as u64 {
 | 
			
		||||
            bail!(
 | 
			
		||||
                "read chunk with wrong size ({} != {}",
 | 
			
		||||
                (*end - *start),
 | 
			
		||||
                data.len()
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // fixme: avoid copy
 | 
			
		||||
        self.read_buffer.clear();
 | 
			
		||||
        self.read_buffer.extend_from_slice(&data);
 | 
			
		||||
        self.read_buffer.extend_from_slice(&cached_chunk.data);
 | 
			
		||||
 | 
			
		||||
        self.buffered_chunk_idx = idx;
 | 
			
		||||
 | 
			
		||||
        self.buffered_chunk_start = *start;
 | 
			
		||||
        self.buffered_chunk_start = cached_chunk.range.start;
 | 
			
		||||
        //println!("BUFFER {} {}",  self.buffered_chunk_start, end);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::ops::Range;
 | 
			
		||||
use std::pin::Pin;
 | 
			
		||||
use std::task::{Context, Poll};
 | 
			
		||||
 | 
			
		||||
@ -6,6 +7,18 @@ use bytes::{Bytes, BytesMut};
 | 
			
		||||
use anyhow::{format_err, Error};
 | 
			
		||||
use futures::*;
 | 
			
		||||
 | 
			
		||||
pub struct ChunkReadInfo {
 | 
			
		||||
    pub range: Range<u64>,
 | 
			
		||||
    pub digest: [u8; 32],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ChunkReadInfo {
 | 
			
		||||
    #[inline]
 | 
			
		||||
    pub fn size(&self) -> u64 {
 | 
			
		||||
        self.range.end - self.range.start
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Trait to get digest list from index files
 | 
			
		||||
///
 | 
			
		||||
/// To allow easy iteration over all used chunks.
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,25 @@
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use nix::unistd::{fork, ForkResult, pipe};
 | 
			
		||||
use std::os::unix::io::RawFd;
 | 
			
		||||
use chrono::{Local, DateTime, Utc, TimeZone};
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::collections::{HashSet, HashMap};
 | 
			
		||||
use std::ffi::OsStr;
 | 
			
		||||
use std::io::{Write, Seek, SeekFrom};
 | 
			
		||||
use std::io::{self, Write, Seek, SeekFrom};
 | 
			
		||||
use std::os::unix::fs::OpenOptionsExt;
 | 
			
		||||
use std::os::unix::io::RawFd;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::pin::Pin;
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
use std::task::{Context, Poll};
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use chrono::{Local, DateTime, Utc, TimeZone};
 | 
			
		||||
use futures::future::FutureExt;
 | 
			
		||||
use futures::select;
 | 
			
		||||
use futures::stream::{StreamExt, TryStreamExt};
 | 
			
		||||
use nix::unistd::{fork, ForkResult, pipe};
 | 
			
		||||
use serde_json::{json, Value};
 | 
			
		||||
use tokio::signal::unix::{signal, SignalKind};
 | 
			
		||||
use tokio::sync::mpsc;
 | 
			
		||||
use xdg::BaseDirectories;
 | 
			
		||||
 | 
			
		||||
use pathpatterns::{MatchEntry, MatchType, PatternFlag};
 | 
			
		||||
use proxmox::{sortable, identity};
 | 
			
		||||
use proxmox::tools::fs::{file_get_contents, file_get_json, replace_file, CreateOptions, image_size};
 | 
			
		||||
use proxmox::sys::linux::tty;
 | 
			
		||||
@ -20,16 +32,7 @@ use proxmox_backup::tools;
 | 
			
		||||
use proxmox_backup::api2::types::*;
 | 
			
		||||
use proxmox_backup::client::*;
 | 
			
		||||
use proxmox_backup::backup::*;
 | 
			
		||||
use proxmox_backup::pxar::{ self, catalog::* };
 | 
			
		||||
 | 
			
		||||
use serde_json::{json, Value};
 | 
			
		||||
//use hyper::Body;
 | 
			
		||||
use std::sync::{Arc, Mutex};
 | 
			
		||||
//use regex::Regex;
 | 
			
		||||
use xdg::BaseDirectories;
 | 
			
		||||
 | 
			
		||||
use futures::*;
 | 
			
		||||
use tokio::sync::mpsc;
 | 
			
		||||
use proxmox_backup::pxar::catalog::*;
 | 
			
		||||
 | 
			
		||||
const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT";
 | 
			
		||||
const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD";
 | 
			
		||||
@ -243,7 +246,7 @@ async fn backup_directory<P: AsRef<Path>>(
 | 
			
		||||
    skip_lost_and_found: bool,
 | 
			
		||||
    crypt_config: Option<Arc<CryptConfig>>,
 | 
			
		||||
    catalog: Arc<Mutex<CatalogWriter<crate::tools::StdChannelWriter>>>,
 | 
			
		||||
    exclude_pattern: Vec<pxar::MatchPattern>,
 | 
			
		||||
    exclude_pattern: Vec<MatchEntry>,
 | 
			
		||||
    entries_max: usize,
 | 
			
		||||
) -> Result<BackupStats, Error> {
 | 
			
		||||
 | 
			
		||||
@ -769,7 +772,7 @@ fn spawn_catalog_upload(
 | 
			
		||||
               type: Integer,
 | 
			
		||||
               description: "Max number of entries to hold in memory.",
 | 
			
		||||
               optional: true,
 | 
			
		||||
               default: pxar::ENCODER_MAX_ENTRIES as isize,
 | 
			
		||||
               default: proxmox_backup::pxar::ENCODER_MAX_ENTRIES as isize,
 | 
			
		||||
           },
 | 
			
		||||
           "verbose": {
 | 
			
		||||
               type: Boolean,
 | 
			
		||||
@ -812,17 +815,19 @@ async fn create_backup(
 | 
			
		||||
 | 
			
		||||
    let include_dev = param["include-dev"].as_array();
 | 
			
		||||
 | 
			
		||||
    let entries_max = param["entries-max"].as_u64().unwrap_or(pxar::ENCODER_MAX_ENTRIES as u64);
 | 
			
		||||
    let entries_max = param["entries-max"].as_u64()
 | 
			
		||||
        .unwrap_or(proxmox_backup::pxar::ENCODER_MAX_ENTRIES as u64);
 | 
			
		||||
 | 
			
		||||
    let empty = Vec::new();
 | 
			
		||||
    let arg_pattern = param["exclude"].as_array().unwrap_or(&empty);
 | 
			
		||||
    let exclude_args = param["exclude"].as_array().unwrap_or(&empty);
 | 
			
		||||
 | 
			
		||||
    let mut pattern_list = Vec::with_capacity(arg_pattern.len());
 | 
			
		||||
    for s in arg_pattern {
 | 
			
		||||
        let l = s.as_str().ok_or_else(|| format_err!("Invalid pattern string slice"))?;
 | 
			
		||||
        let p = pxar::MatchPattern::from_line(l.as_bytes())?
 | 
			
		||||
            .ok_or_else(|| format_err!("Invalid match pattern in arguments"))?;
 | 
			
		||||
        pattern_list.push(p);
 | 
			
		||||
    let mut exclude_list = Vec::with_capacity(exclude_args.len());
 | 
			
		||||
    for entry in exclude_args {
 | 
			
		||||
        let entry = entry.as_str().ok_or_else(|| format_err!("Invalid pattern string slice"))?;
 | 
			
		||||
        exclude_list.push(
 | 
			
		||||
            MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Exclude)
 | 
			
		||||
                .map_err(|err| format_err!("invalid exclude pattern entry: {}", err))?
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut devices = if all_file_systems { None } else { Some(HashSet::new()) };
 | 
			
		||||
@ -966,7 +971,7 @@ async fn create_backup(
 | 
			
		||||
                    skip_lost_and_found,
 | 
			
		||||
                    crypt_config.clone(),
 | 
			
		||||
                    catalog.clone(),
 | 
			
		||||
                    pattern_list.clone(),
 | 
			
		||||
                    exclude_list.clone(),
 | 
			
		||||
                    entries_max as usize,
 | 
			
		||||
                ).await?;
 | 
			
		||||
                manifest.add_file(target, stats.size, stats.csum)?;
 | 
			
		||||
@ -1246,18 +1251,19 @@ async fn restore(param: Value) -> Result<Value, Error> {
 | 
			
		||||
        let mut reader = BufferedDynamicReader::new(index, chunk_reader);
 | 
			
		||||
 | 
			
		||||
        if let Some(target) = target {
 | 
			
		||||
 | 
			
		||||
            let feature_flags = pxar::flags::DEFAULT;
 | 
			
		||||
            let mut decoder = pxar::SequentialDecoder::new(&mut reader, feature_flags);
 | 
			
		||||
            decoder.set_callback(move |path| {
 | 
			
		||||
                if verbose {
 | 
			
		||||
                    eprintln!("{:?}", path);
 | 
			
		||||
                }
 | 
			
		||||
                Ok(())
 | 
			
		||||
            });
 | 
			
		||||
            decoder.set_allow_existing_dirs(allow_existing_dirs);
 | 
			
		||||
 | 
			
		||||
            decoder.restore(Path::new(target), &Vec::new())?;
 | 
			
		||||
            proxmox_backup::pxar::extract_archive(
 | 
			
		||||
                pxar::decoder::Decoder::from_std(reader)?,
 | 
			
		||||
                Path::new(target),
 | 
			
		||||
                &[],
 | 
			
		||||
                proxmox_backup::pxar::flags::DEFAULT,
 | 
			
		||||
                allow_existing_dirs,
 | 
			
		||||
                |path| {
 | 
			
		||||
                    if verbose {
 | 
			
		||||
                        println!("{:?}", path);
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            .map_err(|err| format_err!("error extracting archive - {}", err))?;
 | 
			
		||||
        } else {
 | 
			
		||||
            let mut writer = std::fs::OpenOptions::new()
 | 
			
		||||
                .write(true)
 | 
			
		||||
@ -1966,6 +1972,41 @@ fn mount(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
use proxmox_backup::client::RemoteChunkReader;
 | 
			
		||||
/// This is a workaround until we have cleaned up the chunk/reader/... infrastructure for better
 | 
			
		||||
/// async use!
 | 
			
		||||
///
 | 
			
		||||
/// Ideally BufferedDynamicReader gets replaced so the LruCache maps to `BroadcastFuture<Chunk>`,
 | 
			
		||||
/// so that we can properly access it from multiple threads simultaneously while not issuing
 | 
			
		||||
/// duplicate simultaneous reads over http.
 | 
			
		||||
struct BufferedDynamicReadAt {
 | 
			
		||||
    inner: Mutex<BufferedDynamicReader<RemoteChunkReader>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl BufferedDynamicReadAt {
 | 
			
		||||
    fn new(inner: BufferedDynamicReader<RemoteChunkReader>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            inner: Mutex::new(inner),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl pxar::accessor::ReadAt for BufferedDynamicReadAt {
 | 
			
		||||
    fn poll_read_at(
 | 
			
		||||
        self: Pin<&Self>,
 | 
			
		||||
        _cx: &mut Context,
 | 
			
		||||
        buf: &mut [u8],
 | 
			
		||||
        offset: u64,
 | 
			
		||||
    ) -> Poll<io::Result<usize>> {
 | 
			
		||||
        use std::io::Read;
 | 
			
		||||
        tokio::task::block_in_place(move || {
 | 
			
		||||
            let mut reader = self.inner.lock().unwrap();
 | 
			
		||||
            reader.seek(SeekFrom::Start(offset))?;
 | 
			
		||||
            Poll::Ready(Ok(reader.read(buf)?))
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
 | 
			
		||||
    let repo = extract_repository_from_value(¶m)?;
 | 
			
		||||
    let archive_name = tools::required_string_param(¶m, "archive-name")?;
 | 
			
		||||
@ -2015,15 +2056,19 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
 | 
			
		||||
        let most_used = index.find_most_used_chunks(8);
 | 
			
		||||
        let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, most_used);
 | 
			
		||||
        let reader = BufferedDynamicReader::new(index, chunk_reader);
 | 
			
		||||
        let decoder = pxar::Decoder::new(reader)?;
 | 
			
		||||
        let archive_size = reader.archive_size();
 | 
			
		||||
        let reader: proxmox_backup::pxar::fuse::Reader =
 | 
			
		||||
            Arc::new(BufferedDynamicReadAt::new(reader));
 | 
			
		||||
        let decoder = proxmox_backup::pxar::fuse::Accessor::new(reader, archive_size).await?;
 | 
			
		||||
        let options = OsStr::new("ro,default_permissions");
 | 
			
		||||
        let mut session = pxar::fuse::Session::new(decoder, &options, pipe.is_none())
 | 
			
		||||
            .map_err(|err| format_err!("pxar mount failed: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        // Mount the session but not call fuse deamonize as this will cause
 | 
			
		||||
        // issues with the runtime after the fork
 | 
			
		||||
        let deamonize = false;
 | 
			
		||||
        session.mount(&Path::new(target), deamonize)?;
 | 
			
		||||
        let session = proxmox_backup::pxar::fuse::Session::mount(
 | 
			
		||||
            decoder,
 | 
			
		||||
            &options,
 | 
			
		||||
            false,
 | 
			
		||||
            Path::new(target),
 | 
			
		||||
        )
 | 
			
		||||
        .map_err(|err| format_err!("pxar mount failed: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        if let Some(pipe) = pipe {
 | 
			
		||||
            nix::unistd::chdir(Path::new("/")).unwrap();
 | 
			
		||||
@ -2045,8 +2090,13 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
 | 
			
		||||
            nix::unistd::close(pipe).unwrap();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let multithreaded = true;
 | 
			
		||||
        session.run_loop(multithreaded)?;
 | 
			
		||||
        let mut interrupt = signal(SignalKind::interrupt())?;
 | 
			
		||||
        select! {
 | 
			
		||||
            res = session.fuse() => res?,
 | 
			
		||||
            _ = interrupt.recv().fuse() => {
 | 
			
		||||
                // exit on interrupted
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        bail!("unknown archive file extension (expected .pxar)");
 | 
			
		||||
    }
 | 
			
		||||
@ -2129,11 +2179,10 @@ async fn catalog_shell(param: Value) -> Result<(), Error> {
 | 
			
		||||
    let most_used = index.find_most_used_chunks(8);
 | 
			
		||||
    let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config.clone(), most_used);
 | 
			
		||||
    let reader = BufferedDynamicReader::new(index, chunk_reader);
 | 
			
		||||
    let mut decoder = pxar::Decoder::new(reader)?;
 | 
			
		||||
    decoder.set_callback(|path| {
 | 
			
		||||
        println!("{:?}", path);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    });
 | 
			
		||||
    let archive_size = reader.archive_size();
 | 
			
		||||
    let reader: proxmox_backup::pxar::fuse::Reader =
 | 
			
		||||
        Arc::new(BufferedDynamicReadAt::new(reader));
 | 
			
		||||
    let decoder = proxmox_backup::pxar::fuse::Accessor::new(reader, archive_size).await?;
 | 
			
		||||
 | 
			
		||||
    let tmpfile = client.download(CATALOG_NAME, tmpfile).await?;
 | 
			
		||||
    let index = DynamicIndexReader::new(tmpfile)
 | 
			
		||||
@ -2161,10 +2210,10 @@ async fn catalog_shell(param: Value) -> Result<(), Error> {
 | 
			
		||||
        catalog_reader,
 | 
			
		||||
        &server_archive_name,
 | 
			
		||||
        decoder,
 | 
			
		||||
    )?;
 | 
			
		||||
    ).await?;
 | 
			
		||||
 | 
			
		||||
    println!("Starting interactive shell");
 | 
			
		||||
    state.shell()?;
 | 
			
		||||
    state.shell().await?;
 | 
			
		||||
 | 
			
		||||
    record_repository(&repo);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										798
									
								
								src/bin/pxar.rs
									
									
									
									
									
								
							
							
						
						
									
										798
									
								
								src/bin/pxar.rs
									
									
									
									
									
								
							@ -1,66 +1,21 @@
 | 
			
		||||
extern crate proxmox_backup;
 | 
			
		||||
use std::collections::HashSet;
 | 
			
		||||
use std::ffi::OsStr;
 | 
			
		||||
use std::fs::OpenOptions;
 | 
			
		||||
use std::os::unix::fs::OpenOptionsExt;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
 | 
			
		||||
use anyhow::{format_err, Error};
 | 
			
		||||
use futures::future::FutureExt;
 | 
			
		||||
use futures::select;
 | 
			
		||||
use tokio::signal::unix::{signal, SignalKind};
 | 
			
		||||
 | 
			
		||||
use pathpatterns::{MatchEntry, MatchType, PatternFlag};
 | 
			
		||||
 | 
			
		||||
use proxmox::{sortable, identity};
 | 
			
		||||
use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment};
 | 
			
		||||
use proxmox::api::schema::*;
 | 
			
		||||
use proxmox::api::cli::*;
 | 
			
		||||
use proxmox::api::api;
 | 
			
		||||
 | 
			
		||||
use proxmox_backup::tools;
 | 
			
		||||
 | 
			
		||||
use serde_json::{Value};
 | 
			
		||||
 | 
			
		||||
use std::io::Write;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::fs::OpenOptions;
 | 
			
		||||
use std::ffi::OsStr;
 | 
			
		||||
use std::os::unix::fs::OpenOptionsExt;
 | 
			
		||||
use std::os::unix::io::AsRawFd;
 | 
			
		||||
use std::collections::HashSet;
 | 
			
		||||
 | 
			
		||||
use proxmox_backup::pxar;
 | 
			
		||||
 | 
			
		||||
fn dump_archive_from_reader<R: std::io::Read>(
 | 
			
		||||
    reader: &mut R,
 | 
			
		||||
    feature_flags: u64,
 | 
			
		||||
    verbose: bool,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    let mut decoder = pxar::SequentialDecoder::new(reader, feature_flags);
 | 
			
		||||
 | 
			
		||||
    let stdout = std::io::stdout();
 | 
			
		||||
    let mut out = stdout.lock();
 | 
			
		||||
 | 
			
		||||
    let mut path = PathBuf::new();
 | 
			
		||||
    decoder.dump_entry(&mut path, verbose, &mut out)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn dump_archive(
 | 
			
		||||
    param: Value,
 | 
			
		||||
    _info: &ApiMethod,
 | 
			
		||||
    _rpcenv: &mut dyn RpcEnvironment,
 | 
			
		||||
) -> Result<Value, Error> {
 | 
			
		||||
 | 
			
		||||
    let archive = tools::required_string_param(¶m, "archive")?;
 | 
			
		||||
    let verbose = param["verbose"].as_bool().unwrap_or(false);
 | 
			
		||||
 | 
			
		||||
    let feature_flags = pxar::flags::DEFAULT;
 | 
			
		||||
 | 
			
		||||
    if archive == "-" {
 | 
			
		||||
        let stdin = std::io::stdin();
 | 
			
		||||
        let mut reader = stdin.lock();
 | 
			
		||||
        dump_archive_from_reader(&mut reader, feature_flags, verbose)?;
 | 
			
		||||
    } else {
 | 
			
		||||
        if verbose { println!("PXAR dump: {}", archive); }
 | 
			
		||||
        let file = std::fs::File::open(archive)?;
 | 
			
		||||
        let mut reader = std::io::BufReader::new(file);
 | 
			
		||||
        dump_archive_from_reader(&mut reader, feature_flags, verbose)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(Value::Null)
 | 
			
		||||
}
 | 
			
		||||
use proxmox_backup::pxar::{flags, fuse, format_single_line_entry, ENCODER_MAX_ENTRIES};
 | 
			
		||||
 | 
			
		||||
fn extract_archive_from_reader<R: std::io::Read>(
 | 
			
		||||
    reader: &mut R,
 | 
			
		||||
@ -68,124 +23,283 @@ fn extract_archive_from_reader<R: std::io::Read>(
 | 
			
		||||
    feature_flags: u64,
 | 
			
		||||
    allow_existing_dirs: bool,
 | 
			
		||||
    verbose: bool,
 | 
			
		||||
    pattern: Option<Vec<pxar::MatchPattern>>
 | 
			
		||||
    match_list: &[MatchEntry],
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    let mut decoder = pxar::SequentialDecoder::new(reader, feature_flags);
 | 
			
		||||
    decoder.set_callback(move |path| {
 | 
			
		||||
        if verbose {
 | 
			
		||||
            println!("{:?}", path);
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    });
 | 
			
		||||
    decoder.set_allow_existing_dirs(allow_existing_dirs);
 | 
			
		||||
 | 
			
		||||
    let pattern = pattern.unwrap_or_else(Vec::new);
 | 
			
		||||
    decoder.restore(Path::new(target), &pattern)?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
    proxmox_backup::pxar::extract_archive(
 | 
			
		||||
        pxar::decoder::Decoder::from_std(reader)?,
 | 
			
		||||
        Path::new(target),
 | 
			
		||||
        &match_list,
 | 
			
		||||
        feature_flags,
 | 
			
		||||
        allow_existing_dirs,
 | 
			
		||||
        |path| {
 | 
			
		||||
            if verbose {
 | 
			
		||||
                println!("{:?}", path);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            archive: {
 | 
			
		||||
                description: "Archive name.",
 | 
			
		||||
            },
 | 
			
		||||
            pattern: {
 | 
			
		||||
                description: "List of paths or pattern matching files to restore",
 | 
			
		||||
                type: Array,
 | 
			
		||||
                items: {
 | 
			
		||||
                    type: String,
 | 
			
		||||
                    description: "Path or pattern matching files to restore.",
 | 
			
		||||
                },
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
            target: {
 | 
			
		||||
                description: "Target directory",
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
            verbose: {
 | 
			
		||||
                description: "Verbose output.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-xattrs": {
 | 
			
		||||
                description: "Ignore extended file attributes.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-fcaps": {
 | 
			
		||||
                description: "Ignore file capabilities.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-acls": {
 | 
			
		||||
                description: "Ignore access control list entries.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "allow-existing-dirs": {
 | 
			
		||||
                description: "Allows directories to already exist on restore.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "files-from": {
 | 
			
		||||
                description: "File containing match pattern for files to restore.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
            },
 | 
			
		||||
            "no-device-nodes": {
 | 
			
		||||
                description: "Ignore device nodes.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-fifos": {
 | 
			
		||||
                description: "Ignore fifos.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-sockets": {
 | 
			
		||||
                description: "Ignore sockets.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// Extract an archive.
 | 
			
		||||
fn extract_archive(
 | 
			
		||||
    param: Value,
 | 
			
		||||
    _info: &ApiMethod,
 | 
			
		||||
    _rpcenv: &mut dyn RpcEnvironment,
 | 
			
		||||
) -> Result<Value, Error> {
 | 
			
		||||
 | 
			
		||||
    let archive = tools::required_string_param(¶m, "archive")?;
 | 
			
		||||
    let target = param["target"].as_str().unwrap_or(".");
 | 
			
		||||
    let verbose = param["verbose"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_xattrs = param["no-xattrs"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_fcaps = param["no-fcaps"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_acls = param["no-acls"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_device_nodes = param["no-device-nodes"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_fifos = param["no-fifos"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_sockets = param["no-sockets"].as_bool().unwrap_or(false);
 | 
			
		||||
    let allow_existing_dirs = param["allow-existing-dirs"].as_bool().unwrap_or(false);
 | 
			
		||||
    let files_from = param["files-from"].as_str();
 | 
			
		||||
    let empty = Vec::new();
 | 
			
		||||
    let arg_pattern = param["pattern"].as_array().unwrap_or(&empty);
 | 
			
		||||
 | 
			
		||||
    let mut feature_flags = pxar::flags::DEFAULT;
 | 
			
		||||
    archive: String,
 | 
			
		||||
    pattern: Option<Vec<String>>,
 | 
			
		||||
    target: Option<String>,
 | 
			
		||||
    verbose: bool,
 | 
			
		||||
    no_xattrs: bool,
 | 
			
		||||
    no_fcaps: bool,
 | 
			
		||||
    no_acls: bool,
 | 
			
		||||
    allow_existing_dirs: bool,
 | 
			
		||||
    files_from: Option<String>,
 | 
			
		||||
    no_device_nodes: bool,
 | 
			
		||||
    no_fifos: bool,
 | 
			
		||||
    no_sockets: bool,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    let mut feature_flags = flags::DEFAULT;
 | 
			
		||||
    if no_xattrs {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_XATTRS;
 | 
			
		||||
        feature_flags ^= flags::WITH_XATTRS;
 | 
			
		||||
    }
 | 
			
		||||
    if no_fcaps {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_FCAPS;
 | 
			
		||||
        feature_flags ^= flags::WITH_FCAPS;
 | 
			
		||||
    }
 | 
			
		||||
    if no_acls {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_ACL;
 | 
			
		||||
        feature_flags ^= flags::WITH_ACL;
 | 
			
		||||
    }
 | 
			
		||||
    if no_device_nodes {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_DEVICE_NODES;
 | 
			
		||||
        feature_flags ^= flags::WITH_DEVICE_NODES;
 | 
			
		||||
    }
 | 
			
		||||
    if no_fifos {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_FIFOS;
 | 
			
		||||
        feature_flags ^= flags::WITH_FIFOS;
 | 
			
		||||
    }
 | 
			
		||||
    if no_sockets {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_SOCKETS;
 | 
			
		||||
        feature_flags ^= flags::WITH_SOCKETS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut pattern_list = Vec::new();
 | 
			
		||||
    if let Some(filename) = files_from {
 | 
			
		||||
        let dir = nix::dir::Dir::open("./", nix::fcntl::OFlag::O_RDONLY, nix::sys::stat::Mode::empty())?;
 | 
			
		||||
        if let Some((mut pattern, _, _)) = pxar::MatchPattern::from_file(dir.as_raw_fd(), filename)? {
 | 
			
		||||
            pattern_list.append(&mut pattern);
 | 
			
		||||
    let pattern = pattern.unwrap_or_else(Vec::new);
 | 
			
		||||
    let target = target.as_ref().map_or_else(|| ".", String::as_str);
 | 
			
		||||
 | 
			
		||||
    let mut match_list = Vec::new();
 | 
			
		||||
    if let Some(filename) = &files_from {
 | 
			
		||||
        for line in proxmox_backup::tools::file_get_non_comment_lines(filename)? {
 | 
			
		||||
            let line = line
 | 
			
		||||
                .map_err(|err| format_err!("error reading {}: {}", filename, err))?;
 | 
			
		||||
            match_list.push(
 | 
			
		||||
                MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, MatchType::Include)
 | 
			
		||||
                    .map_err(|err| format_err!("bad pattern in file '{}': {}", filename, err))?,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for s in arg_pattern {
 | 
			
		||||
        let l = s.as_str().ok_or_else(|| format_err!("Invalid pattern string slice"))?;
 | 
			
		||||
        let p = pxar::MatchPattern::from_line(l.as_bytes())?
 | 
			
		||||
            .ok_or_else(|| format_err!("Invalid match pattern in arguments"))?;
 | 
			
		||||
        pattern_list.push(p);
 | 
			
		||||
    for entry in pattern {
 | 
			
		||||
        match_list.push(
 | 
			
		||||
            MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Include)
 | 
			
		||||
                .map_err(|err| format_err!("error in pattern: {}", err))?,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let pattern = if pattern_list.is_empty() {
 | 
			
		||||
        None
 | 
			
		||||
    } else {
 | 
			
		||||
        Some(pattern_list)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if archive == "-" {
 | 
			
		||||
        let stdin = std::io::stdin();
 | 
			
		||||
        let mut reader = stdin.lock();
 | 
			
		||||
        extract_archive_from_reader(&mut reader, target, feature_flags, allow_existing_dirs, verbose, pattern)?;
 | 
			
		||||
        extract_archive_from_reader(
 | 
			
		||||
            &mut reader,
 | 
			
		||||
            &target,
 | 
			
		||||
            feature_flags,
 | 
			
		||||
            allow_existing_dirs,
 | 
			
		||||
            verbose,
 | 
			
		||||
            &match_list,
 | 
			
		||||
        )?;
 | 
			
		||||
    } else {
 | 
			
		||||
        if verbose { println!("PXAR extract: {}", archive); }
 | 
			
		||||
        if verbose {
 | 
			
		||||
            println!("PXAR extract: {}", archive);
 | 
			
		||||
        }
 | 
			
		||||
        let file = std::fs::File::open(archive)?;
 | 
			
		||||
        let mut reader = std::io::BufReader::new(file);
 | 
			
		||||
        extract_archive_from_reader(&mut reader, target, feature_flags, allow_existing_dirs, verbose, pattern)?;
 | 
			
		||||
        extract_archive_from_reader(
 | 
			
		||||
            &mut reader,
 | 
			
		||||
            &target,
 | 
			
		||||
            feature_flags,
 | 
			
		||||
            allow_existing_dirs,
 | 
			
		||||
            verbose,
 | 
			
		||||
            &match_list,
 | 
			
		||||
        )?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(Value::Null)
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            archive: {
 | 
			
		||||
                description: "Archive name.",
 | 
			
		||||
            },
 | 
			
		||||
            source: {
 | 
			
		||||
                description: "Source directory.",
 | 
			
		||||
            },
 | 
			
		||||
            verbose: {
 | 
			
		||||
                description: "Verbose output.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-xattrs": {
 | 
			
		||||
                description: "Ignore extended file attributes.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-fcaps": {
 | 
			
		||||
                description: "Ignore file capabilities.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-acls": {
 | 
			
		||||
                description: "Ignore access control list entries.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "all-file-systems": {
 | 
			
		||||
                description: "Include mounted sudirs.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-device-nodes": {
 | 
			
		||||
                description: "Ignore device nodes.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-fifos": {
 | 
			
		||||
                description: "Ignore fifos.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            "no-sockets": {
 | 
			
		||||
                description: "Ignore sockets.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
            exclude: {
 | 
			
		||||
                description: "List of paths or pattern matching files to exclude.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                type: Array,
 | 
			
		||||
                items: {
 | 
			
		||||
                    description: "Path or pattern matching files to restore",
 | 
			
		||||
                    type: String,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            "entries-max": {
 | 
			
		||||
                description: "Max number of entries loaded at once into memory",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: ENCODER_MAX_ENTRIES as isize,
 | 
			
		||||
                minimum: 0,
 | 
			
		||||
                maximum: std::isize::MAX,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// Create a new .pxar archive.
 | 
			
		||||
fn create_archive(
 | 
			
		||||
    param: Value,
 | 
			
		||||
    _info: &ApiMethod,
 | 
			
		||||
    _rpcenv: &mut dyn RpcEnvironment,
 | 
			
		||||
) -> Result<Value, Error> {
 | 
			
		||||
    archive: String,
 | 
			
		||||
    source: String,
 | 
			
		||||
    verbose: bool,
 | 
			
		||||
    no_xattrs: bool,
 | 
			
		||||
    no_fcaps: bool,
 | 
			
		||||
    no_acls: bool,
 | 
			
		||||
    all_file_systems: bool,
 | 
			
		||||
    no_device_nodes: bool,
 | 
			
		||||
    no_fifos: bool,
 | 
			
		||||
    no_sockets: bool,
 | 
			
		||||
    exclude: Option<Vec<String>>,
 | 
			
		||||
    entries_max: isize,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    let exclude_list = {
 | 
			
		||||
        let input = exclude.unwrap_or_else(Vec::new);
 | 
			
		||||
        let mut exclude = Vec::with_capacity(input.len());
 | 
			
		||||
        for entry in input {
 | 
			
		||||
            exclude.push(
 | 
			
		||||
                MatchEntry::parse_pattern(entry, PatternFlag::PATH_NAME, MatchType::Exclude)
 | 
			
		||||
                    .map_err(|err| format_err!("error in exclude pattern: {}", err))?,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        exclude
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let archive = tools::required_string_param(¶m, "archive")?;
 | 
			
		||||
    let source = tools::required_string_param(¶m, "source")?;
 | 
			
		||||
    let verbose = param["verbose"].as_bool().unwrap_or(false);
 | 
			
		||||
    let all_file_systems = param["all-file-systems"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_xattrs = param["no-xattrs"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_fcaps = param["no-fcaps"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_acls = param["no-acls"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_device_nodes = param["no-device-nodes"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_fifos = param["no-fifos"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_sockets = param["no-sockets"].as_bool().unwrap_or(false);
 | 
			
		||||
    let empty = Vec::new();
 | 
			
		||||
    let exclude_pattern = param["exclude"].as_array().unwrap_or(&empty);
 | 
			
		||||
    let entries_max = param["entries-max"].as_u64().unwrap_or(pxar::ENCODER_MAX_ENTRIES as u64);
 | 
			
		||||
 | 
			
		||||
    let devices = if all_file_systems { None } else { Some(HashSet::new()) };
 | 
			
		||||
    let device_set = if all_file_systems {
 | 
			
		||||
        None
 | 
			
		||||
    } else {
 | 
			
		||||
        Some(HashSet::new())
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let source = PathBuf::from(source);
 | 
			
		||||
 | 
			
		||||
    let mut dir = nix::dir::Dir::open(
 | 
			
		||||
        &source, nix::fcntl::OFlag::O_NOFOLLOW, nix::sys::stat::Mode::empty())?;
 | 
			
		||||
    let dir = nix::dir::Dir::open(
 | 
			
		||||
        &source,
 | 
			
		||||
        nix::fcntl::OFlag::O_NOFOLLOW,
 | 
			
		||||
        nix::sys::stat::Mode::empty(),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    let file = OpenOptions::new()
 | 
			
		||||
        .create_new(true)
 | 
			
		||||
@ -193,332 +307,150 @@ fn create_archive(
 | 
			
		||||
        .mode(0o640)
 | 
			
		||||
        .open(archive)?;
 | 
			
		||||
 | 
			
		||||
    let mut writer = std::io::BufWriter::with_capacity(1024*1024, file);
 | 
			
		||||
    let mut feature_flags = pxar::flags::DEFAULT;
 | 
			
		||||
    let writer = std::io::BufWriter::with_capacity(1024 * 1024, file);
 | 
			
		||||
    let mut feature_flags = flags::DEFAULT;
 | 
			
		||||
    if no_xattrs {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_XATTRS;
 | 
			
		||||
        feature_flags ^= flags::WITH_XATTRS;
 | 
			
		||||
    }
 | 
			
		||||
    if no_fcaps {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_FCAPS;
 | 
			
		||||
        feature_flags ^= flags::WITH_FCAPS;
 | 
			
		||||
    }
 | 
			
		||||
    if no_acls {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_ACL;
 | 
			
		||||
        feature_flags ^= flags::WITH_ACL;
 | 
			
		||||
    }
 | 
			
		||||
    if no_device_nodes {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_DEVICE_NODES;
 | 
			
		||||
        feature_flags ^= flags::WITH_DEVICE_NODES;
 | 
			
		||||
    }
 | 
			
		||||
    if no_fifos {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_FIFOS;
 | 
			
		||||
        feature_flags ^= flags::WITH_FIFOS;
 | 
			
		||||
    }
 | 
			
		||||
    if no_sockets {
 | 
			
		||||
        feature_flags ^= pxar::flags::WITH_SOCKETS;
 | 
			
		||||
        feature_flags ^= flags::WITH_SOCKETS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut pattern_list = Vec::new();
 | 
			
		||||
    for s in exclude_pattern {
 | 
			
		||||
        let l = s.as_str().ok_or_else(|| format_err!("Invalid pattern string slice"))?;
 | 
			
		||||
        let p = pxar::MatchPattern::from_line(l.as_bytes())?
 | 
			
		||||
            .ok_or_else(|| format_err!("Invalid match pattern in arguments"))?;
 | 
			
		||||
        pattern_list.push(p);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let catalog = None::<&mut pxar::catalog::DummyCatalogWriter>;
 | 
			
		||||
    pxar::Encoder::encode(
 | 
			
		||||
        source,
 | 
			
		||||
        &mut dir,
 | 
			
		||||
        &mut writer,
 | 
			
		||||
        catalog,
 | 
			
		||||
        devices,
 | 
			
		||||
        verbose,
 | 
			
		||||
        false,
 | 
			
		||||
    let writer = pxar::encoder::sync::StandardWriter::new(writer);
 | 
			
		||||
    proxmox_backup::pxar::create_archive(
 | 
			
		||||
        dir,
 | 
			
		||||
        writer,
 | 
			
		||||
        exclude_list,
 | 
			
		||||
        feature_flags,
 | 
			
		||||
        pattern_list,
 | 
			
		||||
        device_set,
 | 
			
		||||
        true,
 | 
			
		||||
        |path| {
 | 
			
		||||
            if verbose {
 | 
			
		||||
                println!("{:?}", path);
 | 
			
		||||
            }
 | 
			
		||||
            Ok(())
 | 
			
		||||
        },
 | 
			
		||||
        entries_max as usize,
 | 
			
		||||
        None,
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    writer.flush()?;
 | 
			
		||||
 | 
			
		||||
    Ok(Value::Null)
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            archive: { description: "Archive name." },
 | 
			
		||||
            mountpoint: { description: "Mountpoint for the file system." },
 | 
			
		||||
            verbose: {
 | 
			
		||||
                description: "Verbose output, running in the foreground (for debugging).",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// Mount the archive to the provided mountpoint via FUSE.
 | 
			
		||||
fn mount_archive(
 | 
			
		||||
    param: Value,
 | 
			
		||||
    _info: &ApiMethod,
 | 
			
		||||
    _rpcenv: &mut dyn RpcEnvironment,
 | 
			
		||||
) -> Result<Value, Error> {
 | 
			
		||||
    let archive = tools::required_string_param(¶m, "archive")?;
 | 
			
		||||
    let mountpoint = tools::required_string_param(¶m, "mountpoint")?;
 | 
			
		||||
    let verbose = param["verbose"].as_bool().unwrap_or(false);
 | 
			
		||||
    let no_mt = param["no-mt"].as_bool().unwrap_or(false);
 | 
			
		||||
 | 
			
		||||
    let archive = Path::new(archive);
 | 
			
		||||
    let mountpoint = Path::new(mountpoint);
 | 
			
		||||
async fn mount_archive(
 | 
			
		||||
    archive: String,
 | 
			
		||||
    mountpoint: String,
 | 
			
		||||
    verbose: bool,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    let archive = Path::new(&archive);
 | 
			
		||||
    let mountpoint = Path::new(&mountpoint);
 | 
			
		||||
    let options = OsStr::new("ro,default_permissions");
 | 
			
		||||
    let mut session = pxar::fuse::Session::from_path(&archive, &options, verbose)
 | 
			
		||||
        .map_err(|err| format_err!("pxar mount failed: {}", err))?;
 | 
			
		||||
    // Mount the session and deamonize if verbose is not set
 | 
			
		||||
    session.mount(&mountpoint, !verbose)?;
 | 
			
		||||
    session.run_loop(!no_mt)?;
 | 
			
		||||
 | 
			
		||||
    Ok(Value::Null)
 | 
			
		||||
    let session = fuse::Session::mount_path(&archive, &options, verbose, mountpoint)
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|err| format_err!("pxar mount failed: {}", err))?;
 | 
			
		||||
 | 
			
		||||
    let mut interrupt = signal(SignalKind::interrupt())?;
 | 
			
		||||
 | 
			
		||||
    select! {
 | 
			
		||||
        res = session.fuse() => res?,
 | 
			
		||||
        _ = interrupt.recv().fuse() => {
 | 
			
		||||
            if verbose {
 | 
			
		||||
                eprintln!("interrupted");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[sortable]
 | 
			
		||||
const API_METHOD_CREATE_ARCHIVE: ApiMethod = ApiMethod::new(
 | 
			
		||||
    &ApiHandler::Sync(&create_archive),
 | 
			
		||||
    &ObjectSchema::new(
 | 
			
		||||
        "Create new .pxar archive.",
 | 
			
		||||
        &sorted!([
 | 
			
		||||
            (
 | 
			
		||||
                "archive",
 | 
			
		||||
                false,
 | 
			
		||||
                &StringSchema::new("Archive name").schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "source",
 | 
			
		||||
                false,
 | 
			
		||||
                &StringSchema::new("Source directory.").schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "verbose",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Verbose output.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-xattrs",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore extended file attributes.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-fcaps",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore file capabilities.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-acls",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore access control list entries.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "all-file-systems",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Include mounted sudirs.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-device-nodes",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore device nodes.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-fifos",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore fifos.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-sockets",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore sockets.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "exclude",
 | 
			
		||||
                true,
 | 
			
		||||
                &ArraySchema::new(
 | 
			
		||||
                    "List of paths or pattern matching files to exclude.",
 | 
			
		||||
                    &StringSchema::new("Path or pattern matching files to restore.").schema()
 | 
			
		||||
                ).schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "entries-max",
 | 
			
		||||
                true,
 | 
			
		||||
                &IntegerSchema::new("Max number of entries loaded at once into memory")
 | 
			
		||||
                    .default(pxar::ENCODER_MAX_ENTRIES as isize)
 | 
			
		||||
                    .minimum(0)
 | 
			
		||||
                    .maximum(std::isize::MAX)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
        ]),
 | 
			
		||||
    )
 | 
			
		||||
);
 | 
			
		||||
#[api(
 | 
			
		||||
    input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
            archive: {
 | 
			
		||||
                description: "Archive name.",
 | 
			
		||||
            },
 | 
			
		||||
            verbose: {
 | 
			
		||||
                description: "Verbose output.",
 | 
			
		||||
                optional: true,
 | 
			
		||||
                default: false,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
)]
 | 
			
		||||
/// List the contents of an archive.
 | 
			
		||||
fn dump_archive(archive: String, verbose: bool) -> Result<(), Error> {
 | 
			
		||||
    for entry in pxar::decoder::Decoder::open(archive)? {
 | 
			
		||||
        let entry = entry?;
 | 
			
		||||
 | 
			
		||||
#[sortable]
 | 
			
		||||
const API_METHOD_EXTRACT_ARCHIVE: ApiMethod = ApiMethod::new(
 | 
			
		||||
    &ApiHandler::Sync(&extract_archive),
 | 
			
		||||
    &ObjectSchema::new(
 | 
			
		||||
        "Extract an archive.",
 | 
			
		||||
        &sorted!([
 | 
			
		||||
            (
 | 
			
		||||
                "archive",
 | 
			
		||||
                false,
 | 
			
		||||
                &StringSchema::new("Archive name.").schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "pattern",
 | 
			
		||||
                true,
 | 
			
		||||
                &ArraySchema::new(
 | 
			
		||||
                    "List of paths or pattern matching files to restore",
 | 
			
		||||
                    &StringSchema::new("Path or pattern matching files to restore.").schema()
 | 
			
		||||
                ).schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "target",
 | 
			
		||||
                true,
 | 
			
		||||
                &StringSchema::new("Target directory.").schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "verbose",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Verbose output.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-xattrs",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore extended file attributes.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-fcaps",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore file capabilities.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-acls",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore access control list entries.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "allow-existing-dirs",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Allows directories to already exist on restore.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "files-from",
 | 
			
		||||
                true,
 | 
			
		||||
                &StringSchema::new("Match pattern for files to restore.").schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-device-nodes",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore device nodes.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-fifos",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore fifos.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-sockets",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Ignore sockets.")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
        ]),
 | 
			
		||||
    )
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
#[sortable]
 | 
			
		||||
const API_METHOD_MOUNT_ARCHIVE: ApiMethod = ApiMethod::new(
 | 
			
		||||
    &ApiHandler::Sync(&mount_archive),
 | 
			
		||||
    &ObjectSchema::new(
 | 
			
		||||
        "Mount the archive as filesystem via FUSE.",
 | 
			
		||||
        &sorted!([
 | 
			
		||||
            (
 | 
			
		||||
                "archive",
 | 
			
		||||
                false,
 | 
			
		||||
                &StringSchema::new("Archive name.").schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "mountpoint",
 | 
			
		||||
                false,
 | 
			
		||||
                &StringSchema::new("Mountpoint for the filesystem root.").schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "verbose",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Verbose output, keeps process running in foreground (for debugging).")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "no-mt",
 | 
			
		||||
                true,
 | 
			
		||||
                &BooleanSchema::new("Run in single threaded mode (for debugging).")
 | 
			
		||||
                    .default(false)
 | 
			
		||||
                    .schema()
 | 
			
		||||
            ),
 | 
			
		||||
        ]),
 | 
			
		||||
    )
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
#[sortable]
 | 
			
		||||
const API_METHOD_DUMP_ARCHIVE: ApiMethod = ApiMethod::new(
 | 
			
		||||
    &ApiHandler::Sync(&dump_archive),
 | 
			
		||||
    &ObjectSchema::new(
 | 
			
		||||
        "List the contents of an archive.",
 | 
			
		||||
        &sorted!([
 | 
			
		||||
            ( "archive", false, &StringSchema::new("Archive name.").schema()),
 | 
			
		||||
            ( "verbose", true, &BooleanSchema::new("Verbose output.")
 | 
			
		||||
               .default(false)
 | 
			
		||||
               .schema()
 | 
			
		||||
            ),
 | 
			
		||||
        ])
 | 
			
		||||
    )
 | 
			
		||||
);
 | 
			
		||||
        if verbose {
 | 
			
		||||
            println!("{}", format_single_line_entry(&entry));
 | 
			
		||||
        } else {
 | 
			
		||||
            println!("{:?}", entry.path());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
 | 
			
		||||
    let cmd_def = CliCommandMap::new()
 | 
			
		||||
        .insert("create", CliCommand::new(&API_METHOD_CREATE_ARCHIVE)
 | 
			
		||||
            .arg_param(&["archive", "source"])
 | 
			
		||||
            .completion_cb("archive", tools::complete_file_name)
 | 
			
		||||
            .completion_cb("source", tools::complete_file_name)
 | 
			
		||||
        .insert(
 | 
			
		||||
            "create",
 | 
			
		||||
            CliCommand::new(&API_METHOD_CREATE_ARCHIVE)
 | 
			
		||||
                .arg_param(&["archive", "source"])
 | 
			
		||||
                .completion_cb("archive", tools::complete_file_name)
 | 
			
		||||
                .completion_cb("source", tools::complete_file_name),
 | 
			
		||||
        )
 | 
			
		||||
        .insert("extract", CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE)
 | 
			
		||||
            .arg_param(&["archive", "target"])
 | 
			
		||||
            .completion_cb("archive", tools::complete_file_name)
 | 
			
		||||
            .completion_cb("target", tools::complete_file_name)
 | 
			
		||||
            .completion_cb("files-from", tools::complete_file_name)
 | 
			
		||||
         )
 | 
			
		||||
        .insert("mount", CliCommand::new(&API_METHOD_MOUNT_ARCHIVE)
 | 
			
		||||
            .arg_param(&["archive", "mountpoint"])
 | 
			
		||||
            .completion_cb("archive", tools::complete_file_name)
 | 
			
		||||
            .completion_cb("mountpoint", tools::complete_file_name)
 | 
			
		||||
        .insert(
 | 
			
		||||
            "extract",
 | 
			
		||||
            CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE)
 | 
			
		||||
                .arg_param(&["archive", "target"])
 | 
			
		||||
                .completion_cb("archive", tools::complete_file_name)
 | 
			
		||||
                .completion_cb("target", tools::complete_file_name)
 | 
			
		||||
                .completion_cb("files-from", tools::complete_file_name),
 | 
			
		||||
        )
 | 
			
		||||
        .insert("list", CliCommand::new(&API_METHOD_DUMP_ARCHIVE)
 | 
			
		||||
            .arg_param(&["archive"])
 | 
			
		||||
            .completion_cb("archive", tools::complete_file_name)
 | 
			
		||||
        .insert(
 | 
			
		||||
            "mount",
 | 
			
		||||
            CliCommand::new(&API_METHOD_MOUNT_ARCHIVE)
 | 
			
		||||
                .arg_param(&["archive", "mountpoint"])
 | 
			
		||||
                .completion_cb("archive", tools::complete_file_name)
 | 
			
		||||
                .completion_cb("mountpoint", tools::complete_file_name),
 | 
			
		||||
        )
 | 
			
		||||
        .insert(
 | 
			
		||||
            "list",
 | 
			
		||||
            CliCommand::new(&API_METHOD_DUMP_ARCHIVE)
 | 
			
		||||
                .arg_param(&["archive"])
 | 
			
		||||
                .completion_cb("archive", tools::complete_file_name),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    let rpcenv = CliEnvironment::new();
 | 
			
		||||
    run_cli_command(cmd_def, rpcenv, None);
 | 
			
		||||
    run_cli_command(cmd_def, rpcenv, Some(|future| {
 | 
			
		||||
        proxmox_backup::tools::runtime::main(future)
 | 
			
		||||
    }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,11 @@
 | 
			
		||||
//! This library implements the client side to access the backups
 | 
			
		||||
//! server using https.
 | 
			
		||||
 | 
			
		||||
pub mod pipe_to_stream;
 | 
			
		||||
mod merge_known_chunks;
 | 
			
		||||
pub mod pipe_to_stream;
 | 
			
		||||
 | 
			
		||||
mod http_client;
 | 
			
		||||
pub use  http_client::*;
 | 
			
		||||
pub use http_client::*;
 | 
			
		||||
 | 
			
		||||
mod task_log;
 | 
			
		||||
pub use task_log::*;
 | 
			
		||||
@ -24,9 +24,6 @@ pub use remote_chunk_reader::*;
 | 
			
		||||
mod pxar_backup_stream;
 | 
			
		||||
pub use pxar_backup_stream::*;
 | 
			
		||||
 | 
			
		||||
mod pxar_decode_writer;
 | 
			
		||||
pub use pxar_decode_writer::*;
 | 
			
		||||
 | 
			
		||||
mod backup_repo;
 | 
			
		||||
pub use backup_repo::*;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,12 +9,12 @@ use std::thread;
 | 
			
		||||
 | 
			
		||||
use anyhow::{format_err, Error};
 | 
			
		||||
use futures::stream::Stream;
 | 
			
		||||
 | 
			
		||||
use nix::dir::Dir;
 | 
			
		||||
use nix::fcntl::OFlag;
 | 
			
		||||
use nix::sys::stat::Mode;
 | 
			
		||||
use nix::dir::Dir;
 | 
			
		||||
 | 
			
		||||
use crate::pxar;
 | 
			
		||||
use pathpatterns::MatchEntry;
 | 
			
		||||
 | 
			
		||||
use crate::backup::CatalogWriter;
 | 
			
		||||
 | 
			
		||||
/// Stream implementation to encode and upload .pxar archives.
 | 
			
		||||
@ -29,7 +29,6 @@ pub struct PxarBackupStream {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for PxarBackupStream {
 | 
			
		||||
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        self.rx = None;
 | 
			
		||||
        self.child.take().unwrap().join().unwrap();
 | 
			
		||||
@ -37,46 +36,49 @@ impl Drop for PxarBackupStream {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PxarBackupStream {
 | 
			
		||||
 | 
			
		||||
    pub fn new<W: Write + Send + 'static>(
 | 
			
		||||
        mut dir: Dir,
 | 
			
		||||
        path: PathBuf,
 | 
			
		||||
        dir: Dir,
 | 
			
		||||
        _path: PathBuf,
 | 
			
		||||
        device_set: Option<HashSet<u64>>,
 | 
			
		||||
        verbose: bool,
 | 
			
		||||
        _verbose: bool,
 | 
			
		||||
        skip_lost_and_found: bool,
 | 
			
		||||
        catalog: Arc<Mutex<CatalogWriter<W>>>,
 | 
			
		||||
        exclude_pattern: Vec<pxar::MatchPattern>,
 | 
			
		||||
        exclude_pattern: Vec<MatchEntry>,
 | 
			
		||||
        entries_max: usize,
 | 
			
		||||
    ) -> Result<Self, Error> {
 | 
			
		||||
 | 
			
		||||
        let (tx, rx) = std::sync::mpsc::sync_channel(10);
 | 
			
		||||
 | 
			
		||||
        let buffer_size = 256*1024;
 | 
			
		||||
        let buffer_size = 256 * 1024;
 | 
			
		||||
 | 
			
		||||
        let error = Arc::new(Mutex::new(None));
 | 
			
		||||
        let error2 = error.clone();
 | 
			
		||||
        let child = std::thread::Builder::new()
 | 
			
		||||
            .name("PxarBackupStream".to_string())
 | 
			
		||||
            .spawn({
 | 
			
		||||
                let error = Arc::clone(&error);
 | 
			
		||||
                move || {
 | 
			
		||||
                    let mut catalog_guard = catalog.lock().unwrap();
 | 
			
		||||
                    let writer = std::io::BufWriter::with_capacity(
 | 
			
		||||
                        buffer_size,
 | 
			
		||||
                        crate::tools::StdChannelWriter::new(tx),
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
        let catalog = catalog.clone();
 | 
			
		||||
        let child = std::thread::Builder::new().name("PxarBackupStream".to_string()).spawn(move || {
 | 
			
		||||
            let mut guard = catalog.lock().unwrap();
 | 
			
		||||
            let mut writer = std::io::BufWriter::with_capacity(buffer_size, crate::tools::StdChannelWriter::new(tx));
 | 
			
		||||
 | 
			
		||||
            if let Err(err) = pxar::Encoder::encode(
 | 
			
		||||
                path,
 | 
			
		||||
                &mut dir,
 | 
			
		||||
                &mut writer,
 | 
			
		||||
                Some(&mut *guard),
 | 
			
		||||
                device_set,
 | 
			
		||||
                verbose,
 | 
			
		||||
                skip_lost_and_found,
 | 
			
		||||
                pxar::flags::DEFAULT,
 | 
			
		||||
                exclude_pattern,
 | 
			
		||||
                entries_max,
 | 
			
		||||
            ) {
 | 
			
		||||
                let mut error = error2.lock().unwrap();
 | 
			
		||||
                *error = Some(err.to_string());
 | 
			
		||||
            }
 | 
			
		||||
        })?;
 | 
			
		||||
                    let writer = pxar::encoder::sync::StandardWriter::new(writer);
 | 
			
		||||
                    if let Err(err) = crate::pxar::create_archive(
 | 
			
		||||
                        dir,
 | 
			
		||||
                        writer,
 | 
			
		||||
                        exclude_pattern,
 | 
			
		||||
                        crate::pxar::flags::DEFAULT,
 | 
			
		||||
                        device_set,
 | 
			
		||||
                        skip_lost_and_found,
 | 
			
		||||
                        |_| Ok(()),
 | 
			
		||||
                        entries_max,
 | 
			
		||||
                        Some(&mut *catalog_guard),
 | 
			
		||||
                    ) {
 | 
			
		||||
                        let mut error = error.lock().unwrap();
 | 
			
		||||
                        *error = Some(err.to_string());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })?;
 | 
			
		||||
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            rx: Some(rx),
 | 
			
		||||
@ -91,23 +93,31 @@ impl PxarBackupStream {
 | 
			
		||||
        verbose: bool,
 | 
			
		||||
        skip_lost_and_found: bool,
 | 
			
		||||
        catalog: Arc<Mutex<CatalogWriter<W>>>,
 | 
			
		||||
        exclude_pattern: Vec<pxar::MatchPattern>,
 | 
			
		||||
        exclude_pattern: Vec<MatchEntry>,
 | 
			
		||||
        entries_max: usize,
 | 
			
		||||
    ) -> Result<Self, Error> {
 | 
			
		||||
 | 
			
		||||
        let dir = nix::dir::Dir::open(dirname, OFlag::O_DIRECTORY, Mode::empty())?;
 | 
			
		||||
        let path = std::path::PathBuf::from(dirname);
 | 
			
		||||
 | 
			
		||||
        Self::new(dir, path, device_set, verbose, skip_lost_and_found, catalog, exclude_pattern, entries_max)
 | 
			
		||||
        Self::new(
 | 
			
		||||
            dir,
 | 
			
		||||
            path,
 | 
			
		||||
            device_set,
 | 
			
		||||
            verbose,
 | 
			
		||||
            skip_lost_and_found,
 | 
			
		||||
            catalog,
 | 
			
		||||
            exclude_pattern,
 | 
			
		||||
            entries_max,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Stream for PxarBackupStream {
 | 
			
		||||
 | 
			
		||||
    type Item = Result<Vec<u8>, Error>;
 | 
			
		||||
 | 
			
		||||
    fn poll_next(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
 | 
			
		||||
        { // limit lock scope
 | 
			
		||||
        {
 | 
			
		||||
            // limit lock scope
 | 
			
		||||
            let error = self.error.lock().unwrap();
 | 
			
		||||
            if let Some(ref msg) = *error {
 | 
			
		||||
                return Poll::Ready(Some(Err(format_err!("{}", msg))));
 | 
			
		||||
 | 
			
		||||
@ -1,70 +0,0 @@
 | 
			
		||||
use anyhow::{Error};
 | 
			
		||||
 | 
			
		||||
use std::thread;
 | 
			
		||||
use std::os::unix::io::FromRawFd;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::io::Write;
 | 
			
		||||
 | 
			
		||||
use crate::pxar;
 | 
			
		||||
 | 
			
		||||
/// Writer implementation to deccode a .pxar archive (download).
 | 
			
		||||
 | 
			
		||||
pub struct PxarDecodeWriter {
 | 
			
		||||
    pipe: Option<std::fs::File>,
 | 
			
		||||
    child: Option<thread::JoinHandle<()>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for PxarDecodeWriter {
 | 
			
		||||
 | 
			
		||||
    fn drop(&mut self) {
 | 
			
		||||
        drop(self.pipe.take());
 | 
			
		||||
        self.child.take().unwrap().join().unwrap();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PxarDecodeWriter {
 | 
			
		||||
 | 
			
		||||
    pub fn new(base: &Path, verbose: bool) -> Result<Self, Error> {
 | 
			
		||||
        let (rx, tx) = nix::unistd::pipe()?;
 | 
			
		||||
 | 
			
		||||
        let base = PathBuf::from(base);
 | 
			
		||||
        
 | 
			
		||||
        let child = thread::spawn(move|| {
 | 
			
		||||
            let mut reader = unsafe { std::fs::File::from_raw_fd(rx) };
 | 
			
		||||
            let mut decoder = pxar::SequentialDecoder::new(&mut reader, pxar::flags::DEFAULT);
 | 
			
		||||
            decoder.set_callback(move |path| {
 | 
			
		||||
                if verbose {
 | 
			
		||||
                    println!("{:?}", path);
 | 
			
		||||
                }
 | 
			
		||||
                Ok(())
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if let Err(err) = decoder.restore(&base, &Vec::new()) {
 | 
			
		||||
                eprintln!("pxar decode failed - {}", err);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let pipe = unsafe { std::fs::File::from_raw_fd(tx) };
 | 
			
		||||
 | 
			
		||||
        Ok(Self { pipe: Some(pipe), child: Some(child) })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Write for PxarDecodeWriter {
 | 
			
		||||
 | 
			
		||||
    fn write(&mut self, buffer: &[u8]) -> Result<usize, std::io::Error> {
 | 
			
		||||
        let pipe = match self.pipe {
 | 
			
		||||
            Some(ref mut pipe) => pipe,
 | 
			
		||||
            None => unreachable!(),
 | 
			
		||||
        };
 | 
			
		||||
        pipe.write(buffer)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn flush(&mut self) -> Result<(), std::io::Error> {
 | 
			
		||||
        let pipe = match self.pipe {
 | 
			
		||||
            Some(ref mut pipe) => pipe,
 | 
			
		||||
            None => unreachable!(),
 | 
			
		||||
        };
 | 
			
		||||
        pipe.flush()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								src/pxar.rs
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								src/pxar.rs
									
									
									
									
									
								
							@ -47,33 +47,21 @@
 | 
			
		||||
//! (user, group, acl, ...) because this is already defined by the
 | 
			
		||||
//! linked `ENTRY`.
 | 
			
		||||
 | 
			
		||||
mod binary_search_tree;
 | 
			
		||||
pub use binary_search_tree::*;
 | 
			
		||||
 | 
			
		||||
pub mod flags;
 | 
			
		||||
pub use flags::*;
 | 
			
		||||
 | 
			
		||||
mod format_definition;
 | 
			
		||||
pub use format_definition::*;
 | 
			
		||||
 | 
			
		||||
mod encoder;
 | 
			
		||||
pub use encoder::*;
 | 
			
		||||
 | 
			
		||||
mod sequential_decoder;
 | 
			
		||||
pub use sequential_decoder::*;
 | 
			
		||||
 | 
			
		||||
mod decoder;
 | 
			
		||||
pub use decoder::*;
 | 
			
		||||
 | 
			
		||||
mod match_pattern;
 | 
			
		||||
pub use match_pattern::*;
 | 
			
		||||
 | 
			
		||||
mod dir_stack;
 | 
			
		||||
pub use dir_stack::*;
 | 
			
		||||
 | 
			
		||||
pub mod fuse;
 | 
			
		||||
pub use fuse::*;
 | 
			
		||||
 | 
			
		||||
pub mod catalog;
 | 
			
		||||
pub(crate) mod create;
 | 
			
		||||
pub(crate) mod dir_stack;
 | 
			
		||||
pub(crate) mod extract;
 | 
			
		||||
pub(crate) mod metadata;
 | 
			
		||||
pub mod flags;
 | 
			
		||||
pub mod fuse;
 | 
			
		||||
pub(crate) mod tools;
 | 
			
		||||
 | 
			
		||||
mod helper;
 | 
			
		||||
pub use create::create_archive;
 | 
			
		||||
pub use extract::extract_archive;
 | 
			
		||||
 | 
			
		||||
/// The format requires to build sorted directory lookup tables in
 | 
			
		||||
/// memory, so we restrict the number of allowed entries to limit
 | 
			
		||||
/// maximum memory usage.
 | 
			
		||||
pub const ENCODER_MAX_ENTRIES: usize = 1024 * 1024;
 | 
			
		||||
 | 
			
		||||
pub use tools::{format_multi_line_entry, format_single_line_entry};
 | 
			
		||||
 | 
			
		||||
@ -1,229 +0,0 @@
 | 
			
		||||
//! Helpers to generate a binary search tree stored in an array from a
 | 
			
		||||
//! sorted array.
 | 
			
		||||
//!
 | 
			
		||||
//! Specifically, for any given sorted array 'input' permute the
 | 
			
		||||
//! array so that the following rule holds:
 | 
			
		||||
//!
 | 
			
		||||
//! For each array item with index i, the item at 2i+1 is smaller and
 | 
			
		||||
//! the item 2i+2 is larger.
 | 
			
		||||
//!
 | 
			
		||||
//! This structure permits efficient (meaning: O(log(n)) binary
 | 
			
		||||
//! searches: start with item i=0 (i.e. the root of the BST), compare
 | 
			
		||||
//! the value with the searched item, if smaller proceed at item
 | 
			
		||||
//! 2i+1, if larger proceed at item 2i+2, and repeat, until either
 | 
			
		||||
//! the item is found, or the indexes grow beyond the array size,
 | 
			
		||||
//! which means the entry does not exist.
 | 
			
		||||
//!
 | 
			
		||||
//! Effectively this implements bisection, but instead of jumping
 | 
			
		||||
//! around wildly in the array during a single search we only search
 | 
			
		||||
//! with strictly monotonically increasing indexes.
 | 
			
		||||
//!
 | 
			
		||||
//! Algorithm is from casync (camakebst.c), simplified and optimized
 | 
			
		||||
//! for rust. Permutation function originally by L. Bressel, 2017. We
 | 
			
		||||
//! pass permutation info to user provided callback, which actually
 | 
			
		||||
//! implements the data copy.
 | 
			
		||||
//!
 | 
			
		||||
//! The Wikipedia Artikel for [Binary
 | 
			
		||||
//! Heap](https://en.wikipedia.org/wiki/Binary_heap) gives a short
 | 
			
		||||
//! intro howto store binary trees using an array.
 | 
			
		||||
 | 
			
		||||
use std::cmp::Ordering;
 | 
			
		||||
 | 
			
		||||
#[allow(clippy::many_single_char_names)]
 | 
			
		||||
fn copy_binary_search_tree_inner<F:  FnMut(usize, usize)>(
 | 
			
		||||
    copy_func: &mut F,
 | 
			
		||||
    // we work on input array input[o..o+n]
 | 
			
		||||
    n: usize,
 | 
			
		||||
    o: usize,
 | 
			
		||||
    e: usize,
 | 
			
		||||
    i: usize,
 | 
			
		||||
) {
 | 
			
		||||
    let p = 1 << e;
 | 
			
		||||
 | 
			
		||||
    let t = p + (p>>1) - 1;
 | 
			
		||||
 | 
			
		||||
    let m = if n > t {
 | 
			
		||||
        // |...........p.............t....n........(2p)|
 | 
			
		||||
        p - 1
 | 
			
		||||
    } else {
 | 
			
		||||
        // |...........p.....n.......t.............(2p)|
 | 
			
		||||
        p - 1 - (t-n)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    (copy_func)(o+m, i);
 | 
			
		||||
 | 
			
		||||
    if m > 0 {
 | 
			
		||||
        copy_binary_search_tree_inner(copy_func, m, o, e-1, i*2+1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (m + 1) < n {
 | 
			
		||||
        copy_binary_search_tree_inner(copy_func, n-m-1, o+m+1, e-1, i*2+2);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// This function calls the provided `copy_func()` with the permutation
 | 
			
		||||
/// info.
 | 
			
		||||
///
 | 
			
		||||
/// ```
 | 
			
		||||
/// # use proxmox_backup::pxar::copy_binary_search_tree;
 | 
			
		||||
/// copy_binary_search_tree(5, |src, dest| {
 | 
			
		||||
///    println!("Copy {} to {}", src, dest);
 | 
			
		||||
/// });
 | 
			
		||||
/// ```
 | 
			
		||||
///
 | 
			
		||||
/// This will produce the following output:
 | 
			
		||||
///
 | 
			
		||||
/// ```no-compile
 | 
			
		||||
/// Copy 3 to 0
 | 
			
		||||
/// Copy 1 to 1
 | 
			
		||||
/// Copy 0 to 3
 | 
			
		||||
/// Copy 2 to 4
 | 
			
		||||
/// Copy 4 to 2
 | 
			
		||||
/// ```
 | 
			
		||||
///
 | 
			
		||||
/// So this generates the following permutation: `[3,1,4,0,2]`.
 | 
			
		||||
 | 
			
		||||
pub fn copy_binary_search_tree<F:  FnMut(usize, usize)>(
 | 
			
		||||
    n: usize,
 | 
			
		||||
    mut copy_func: F,
 | 
			
		||||
) {
 | 
			
		||||
    if n == 0 { return };
 | 
			
		||||
    let e = (64 - n.leading_zeros() - 1) as usize; // fast log2(n)
 | 
			
		||||
 | 
			
		||||
    copy_binary_search_tree_inner(&mut copy_func, n, 0, e, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// This function searches for the index where the comparison by the provided
 | 
			
		||||
/// `compare()` function returns `Ordering::Equal`.
 | 
			
		||||
/// The order of the comparison matters (noncommutative) and should be search
 | 
			
		||||
/// value compared to value at given index as shown in the examples.
 | 
			
		||||
/// The parameter `skip_multiples` defines the number of matches to ignore while
 | 
			
		||||
/// searching before returning the index in order to lookup duplicate entries in
 | 
			
		||||
/// the tree.
 | 
			
		||||
///
 | 
			
		||||
/// ```
 | 
			
		||||
/// # use proxmox_backup::pxar::{copy_binary_search_tree, search_binary_tree_by};
 | 
			
		||||
/// let mut vals = vec![0,1,2,2,2,3,4,5,6,6,7,8,8,8];
 | 
			
		||||
///
 | 
			
		||||
/// let clone = vals.clone();
 | 
			
		||||
/// copy_binary_search_tree(vals.len(), |s, d| {
 | 
			
		||||
///     vals[d] = clone[s];
 | 
			
		||||
/// });
 | 
			
		||||
/// let should_be = vec![5,2,8,1,3,6,8,0,2,2,4,6,7,8];
 | 
			
		||||
/// assert_eq!(vals, should_be);
 | 
			
		||||
///
 | 
			
		||||
/// let find = 8;
 | 
			
		||||
/// let skip_multiples = 0;
 | 
			
		||||
/// let idx = search_binary_tree_by(0, vals.len(), skip_multiples, |idx| find.cmp(&vals[idx]));
 | 
			
		||||
/// assert_eq!(idx, Some(2));
 | 
			
		||||
///
 | 
			
		||||
/// let find = 8;
 | 
			
		||||
/// let skip_multiples = 1;
 | 
			
		||||
/// let idx = search_binary_tree_by(2, vals.len(), skip_multiples, |idx| find.cmp(&vals[idx]));
 | 
			
		||||
/// assert_eq!(idx, Some(6));
 | 
			
		||||
///
 | 
			
		||||
/// let find = 8;
 | 
			
		||||
/// let skip_multiples = 1;
 | 
			
		||||
/// let idx = search_binary_tree_by(6, vals.len(), skip_multiples, |idx| find.cmp(&vals[idx]));
 | 
			
		||||
/// assert_eq!(idx, Some(13));
 | 
			
		||||
///
 | 
			
		||||
/// let find = 5;
 | 
			
		||||
/// let skip_multiples = 1;
 | 
			
		||||
/// let idx = search_binary_tree_by(0, vals.len(), skip_multiples, |idx| find.cmp(&vals[idx]));
 | 
			
		||||
/// assert!(idx.is_none());
 | 
			
		||||
///
 | 
			
		||||
/// let find = 5;
 | 
			
		||||
/// let skip_multiples = 0;
 | 
			
		||||
/// // if start index is equal to the array length, `None` is returned.
 | 
			
		||||
/// let idx = search_binary_tree_by(vals.len(), vals.len(), skip_multiples, |idx| find.cmp(&vals[idx]));
 | 
			
		||||
/// assert!(idx.is_none());
 | 
			
		||||
///
 | 
			
		||||
/// let find = 5;
 | 
			
		||||
/// let skip_multiples = 0;
 | 
			
		||||
/// // if start index is larger than length, `None` is returned.
 | 
			
		||||
/// let idx = search_binary_tree_by(vals.len() + 1, vals.len(), skip_multiples, |idx| find.cmp(&vals[idx]));
 | 
			
		||||
/// assert!(idx.is_none());
 | 
			
		||||
/// ```
 | 
			
		||||
 | 
			
		||||
pub fn search_binary_tree_by<F: Copy + Fn(usize) -> Ordering>(
 | 
			
		||||
    start: usize,
 | 
			
		||||
    size: usize,
 | 
			
		||||
    skip_multiples: usize,
 | 
			
		||||
    compare: F
 | 
			
		||||
) -> Option<usize> {
 | 
			
		||||
    if start >= size {
 | 
			
		||||
        return None;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut skip = skip_multiples;
 | 
			
		||||
    let cmp = compare(start);
 | 
			
		||||
    if cmp == Ordering::Equal {
 | 
			
		||||
        if skip == 0 {
 | 
			
		||||
            // Found matching hash and want this one
 | 
			
		||||
            return Some(start);
 | 
			
		||||
        }
 | 
			
		||||
        // Found matching hash, but we should skip the first `skip_multiple`,
 | 
			
		||||
        // so continue search with reduced skip count.
 | 
			
		||||
        skip -= 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if cmp == Ordering::Less || cmp == Ordering::Equal {
 | 
			
		||||
        let res = search_binary_tree_by(2 * start + 1, size, skip, compare);
 | 
			
		||||
        if res.is_some() {
 | 
			
		||||
            return res;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if cmp == Ordering::Greater || cmp == Ordering::Equal {
 | 
			
		||||
        let res = search_binary_tree_by(2 * start + 2, size, skip, compare);
 | 
			
		||||
        if res.is_some() {
 | 
			
		||||
            return res;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn test_binary_search_tree() {
 | 
			
		||||
 | 
			
		||||
    fn run_test(len: usize) -> Vec<usize> {
 | 
			
		||||
 | 
			
		||||
        const MARKER: usize = 0xfffffff;
 | 
			
		||||
        let mut output = vec![];
 | 
			
		||||
        for _i in 0..len { output.push(MARKER); }
 | 
			
		||||
        copy_binary_search_tree(len, |s, d| {
 | 
			
		||||
            assert!(output[d] == MARKER);
 | 
			
		||||
            output[d] = s;
 | 
			
		||||
        });
 | 
			
		||||
        if len < 32 { println!("GOT:{}:{:?}", len, output); }
 | 
			
		||||
        for i in 0..len {
 | 
			
		||||
            assert!(output[i] != MARKER);
 | 
			
		||||
        }
 | 
			
		||||
        output
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    assert!(run_test(0).len() == 0);
 | 
			
		||||
    assert!(run_test(1) == [0]);
 | 
			
		||||
    assert!(run_test(2) == [1,0]);
 | 
			
		||||
    assert!(run_test(3) == [1,0,2]);
 | 
			
		||||
    assert!(run_test(4) == [2,1,3,0]);
 | 
			
		||||
    assert!(run_test(5) == [3,1,4,0,2]);
 | 
			
		||||
    assert!(run_test(6) == [3,1,5,0,2,4]);
 | 
			
		||||
    assert!(run_test(7) == [3,1,5,0,2,4,6]);
 | 
			
		||||
    assert!(run_test(8) == [4,2,6,1,3,5,7,0]);
 | 
			
		||||
    assert!(run_test(9) == [5,3,7,1,4,6,8,0,2]);
 | 
			
		||||
    assert!(run_test(10) == [6,3,8,1,5,7,9,0,2,4]);
 | 
			
		||||
    assert!(run_test(11) == [7,3,9,1,5,8,10,0,2,4,6]);
 | 
			
		||||
    assert!(run_test(12) == [7,3,10,1,5,9,11,0,2,4,6,8]);
 | 
			
		||||
    assert!(run_test(13) == [7,3,11,1,5,9,12,0,2,4,6,8,10]);
 | 
			
		||||
    assert!(run_test(14) == [7,3,11,1,5,9,13,0,2,4,6,8,10,12]);
 | 
			
		||||
    assert!(run_test(15) == [7,3,11,1,5,9,13,0,2,4,6,8,10,12,14]);
 | 
			
		||||
    assert!(run_test(16) == [8,4,12,2,6,10,14,1,3,5,7,9,11,13,15,0]);
 | 
			
		||||
    assert!(run_test(17) == [9,5,13,3,7,11,15,1,4,6,8,10,12,14,16,0,2]);
 | 
			
		||||
 | 
			
		||||
    for len in 18..1000 {
 | 
			
		||||
        run_test(len);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										769
									
								
								src/pxar/create.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										769
									
								
								src/pxar/create.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,769 @@
 | 
			
		||||
use std::collections::{HashSet, HashMap};
 | 
			
		||||
use std::convert::TryFrom;
 | 
			
		||||
use std::ffi::{CStr, CString, OsStr};
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::os::unix::ffi::OsStrExt;
 | 
			
		||||
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use nix::dir::Dir;
 | 
			
		||||
use nix::errno::Errno;
 | 
			
		||||
use nix::fcntl::OFlag;
 | 
			
		||||
use nix::sys::stat::{FileStat, Mode};
 | 
			
		||||
 | 
			
		||||
use pathpatterns::{MatchEntry, MatchList, MatchType, PatternFlag};
 | 
			
		||||
use pxar::Metadata;
 | 
			
		||||
use pxar::encoder::LinkOffset;
 | 
			
		||||
 | 
			
		||||
use proxmox::sys::error::SysError;
 | 
			
		||||
use proxmox::tools::fd::RawFdNum;
 | 
			
		||||
 | 
			
		||||
use crate::pxar::catalog::BackupCatalogWriter;
 | 
			
		||||
use crate::pxar::flags;
 | 
			
		||||
use crate::pxar::tools::assert_relative_path;
 | 
			
		||||
use crate::tools::{acl, fs, xattr, Fd};
 | 
			
		||||
 | 
			
		||||
fn detect_fs_type(fd: RawFd) -> Result<i64, Error> {
 | 
			
		||||
    let mut fs_stat = std::mem::MaybeUninit::uninit();
 | 
			
		||||
    let res = unsafe { libc::fstatfs(fd, fs_stat.as_mut_ptr()) };
 | 
			
		||||
    Errno::result(res)?;
 | 
			
		||||
    let fs_stat = unsafe { fs_stat.assume_init() };
 | 
			
		||||
 | 
			
		||||
    Ok(fs_stat.f_type)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn is_virtual_file_system(magic: i64) -> bool {
 | 
			
		||||
    use proxmox::sys::linux::magic::*;
 | 
			
		||||
 | 
			
		||||
    match magic {
 | 
			
		||||
        BINFMTFS_MAGIC |
 | 
			
		||||
        CGROUP2_SUPER_MAGIC |
 | 
			
		||||
        CGROUP_SUPER_MAGIC |
 | 
			
		||||
        CONFIGFS_MAGIC |
 | 
			
		||||
        DEBUGFS_MAGIC |
 | 
			
		||||
        DEVPTS_SUPER_MAGIC |
 | 
			
		||||
        EFIVARFS_MAGIC |
 | 
			
		||||
        FUSE_CTL_SUPER_MAGIC |
 | 
			
		||||
        HUGETLBFS_MAGIC |
 | 
			
		||||
        MQUEUE_MAGIC |
 | 
			
		||||
        NFSD_MAGIC |
 | 
			
		||||
        PROC_SUPER_MAGIC |
 | 
			
		||||
        PSTOREFS_MAGIC |
 | 
			
		||||
        RPCAUTH_GSSMAGIC |
 | 
			
		||||
        SECURITYFS_MAGIC |
 | 
			
		||||
        SELINUX_MAGIC |
 | 
			
		||||
        SMACK_MAGIC |
 | 
			
		||||
        SYSFS_MAGIC => true,
 | 
			
		||||
        _ => false
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
struct ArchiveError {
 | 
			
		||||
    path: PathBuf,
 | 
			
		||||
    error: Error,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ArchiveError {
 | 
			
		||||
    fn new(path: PathBuf, error: Error) -> Self {
 | 
			
		||||
        Self { path, error }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::error::Error for ArchiveError {}
 | 
			
		||||
 | 
			
		||||
impl fmt::Display for ArchiveError {
 | 
			
		||||
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
			
		||||
        write!(f, "error at {:?}: {}", self.path, self.error)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Eq, PartialEq, Hash)]
 | 
			
		||||
struct HardLinkInfo {
 | 
			
		||||
    st_dev: u64,
 | 
			
		||||
    st_ino: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Archiver<'a, 'b> {
 | 
			
		||||
    /// FIXME: use bitflags!() for feature_flags
 | 
			
		||||
    feature_flags: u64,
 | 
			
		||||
    fs_feature_flags: u64,
 | 
			
		||||
    fs_magic: i64,
 | 
			
		||||
    excludes: &'a [MatchEntry],
 | 
			
		||||
    callback: &'a mut dyn FnMut(&Path) -> Result<(), Error>,
 | 
			
		||||
    catalog: Option<&'b mut dyn BackupCatalogWriter>,
 | 
			
		||||
    path: PathBuf,
 | 
			
		||||
    entry_counter: usize,
 | 
			
		||||
    entry_limit: usize,
 | 
			
		||||
    current_st_dev: libc::dev_t,
 | 
			
		||||
    device_set: Option<HashSet<u64>>,
 | 
			
		||||
    hardlinks: HashMap<HardLinkInfo, (PathBuf, LinkOffset)>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Encoder<'a, 'b> = pxar::encoder::Encoder<'a, &'b mut dyn pxar::encoder::SeqWrite>;
 | 
			
		||||
 | 
			
		||||
pub fn create_archive<T, F>(
 | 
			
		||||
    source_dir: Dir,
 | 
			
		||||
    mut writer: T,
 | 
			
		||||
    mut excludes: Vec<MatchEntry>,
 | 
			
		||||
    feature_flags: u64,
 | 
			
		||||
    mut device_set: Option<HashSet<u64>>,
 | 
			
		||||
    skip_lost_and_found: bool,
 | 
			
		||||
    mut callback: F,
 | 
			
		||||
    entry_limit: usize,
 | 
			
		||||
    catalog: Option<&mut dyn BackupCatalogWriter>,
 | 
			
		||||
) -> Result<(), Error>
 | 
			
		||||
where
 | 
			
		||||
    T: pxar::encoder::SeqWrite,
 | 
			
		||||
    F: FnMut(&Path) -> Result<(), Error>,
 | 
			
		||||
{
 | 
			
		||||
    let fs_magic = detect_fs_type(source_dir.as_raw_fd())?;
 | 
			
		||||
    if is_virtual_file_system(fs_magic) {
 | 
			
		||||
        bail!("refusing to backup a virtual file system");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let fs_feature_flags = flags::feature_flags_from_magic(fs_magic);
 | 
			
		||||
 | 
			
		||||
    let stat = nix::sys::stat::fstat(source_dir.as_raw_fd())?;
 | 
			
		||||
    let metadata = get_metadata(
 | 
			
		||||
        source_dir.as_raw_fd(),
 | 
			
		||||
        &stat,
 | 
			
		||||
        feature_flags & fs_feature_flags,
 | 
			
		||||
        fs_magic,
 | 
			
		||||
    )
 | 
			
		||||
    .map_err(|err| format_err!("failed to get metadata for source directory: {}", err))?;
 | 
			
		||||
 | 
			
		||||
    if let Some(ref mut set) = device_set {
 | 
			
		||||
        set.insert(stat.st_dev);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let writer = &mut writer as &mut dyn pxar::encoder::SeqWrite;
 | 
			
		||||
    let mut encoder = Encoder::new(writer, &metadata)?;
 | 
			
		||||
 | 
			
		||||
    if skip_lost_and_found {
 | 
			
		||||
        excludes.push(MatchEntry::parse_pattern(
 | 
			
		||||
            "**/lost+found",
 | 
			
		||||
            PatternFlag::PATH_NAME,
 | 
			
		||||
            MatchType::Exclude,
 | 
			
		||||
        )?);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut archiver = Archiver {
 | 
			
		||||
        feature_flags,
 | 
			
		||||
        fs_feature_flags,
 | 
			
		||||
        fs_magic,
 | 
			
		||||
        callback: &mut callback,
 | 
			
		||||
        excludes: &excludes,
 | 
			
		||||
        catalog,
 | 
			
		||||
        path: PathBuf::new(),
 | 
			
		||||
        entry_counter: 0,
 | 
			
		||||
        entry_limit,
 | 
			
		||||
        current_st_dev: stat.st_dev,
 | 
			
		||||
        device_set,
 | 
			
		||||
        hardlinks: HashMap::new(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    archiver.archive_dir_contents(&mut encoder, source_dir)?;
 | 
			
		||||
    encoder.finish()?;
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct FileListEntry {
 | 
			
		||||
    name: CString,
 | 
			
		||||
    path: PathBuf,
 | 
			
		||||
    stat: FileStat,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a, 'b> Archiver<'a, 'b> {
 | 
			
		||||
    fn flags(&self) -> u64 {
 | 
			
		||||
        self.feature_flags & self.fs_feature_flags
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn wrap_err(&self, err: Error) -> Error {
 | 
			
		||||
        if err.downcast_ref::<ArchiveError>().is_some() {
 | 
			
		||||
            err
 | 
			
		||||
        } else {
 | 
			
		||||
            ArchiveError::new(self.path.clone(), err).into()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn archive_dir_contents(&mut self, encoder: &mut Encoder, mut dir: Dir) -> Result<(), Error> {
 | 
			
		||||
        let entry_counter = self.entry_counter;
 | 
			
		||||
 | 
			
		||||
        let file_list = self.generate_directory_file_list(&mut dir)?;
 | 
			
		||||
 | 
			
		||||
        let dir_fd = dir.as_raw_fd();
 | 
			
		||||
 | 
			
		||||
        let old_path = std::mem::take(&mut self.path);
 | 
			
		||||
        for file_entry in file_list {
 | 
			
		||||
            (self.callback)(Path::new(OsStr::from_bytes(file_entry.name.to_bytes())))?;
 | 
			
		||||
            self.path = file_entry.path;
 | 
			
		||||
            self.add_entry(encoder, dir_fd, &file_entry.name, &file_entry.stat)
 | 
			
		||||
                .map_err(|err| self.wrap_err(err))?;
 | 
			
		||||
        }
 | 
			
		||||
        self.path = old_path;
 | 
			
		||||
        self.entry_counter = entry_counter;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn generate_directory_file_list(&mut self, dir: &mut Dir) -> Result<Vec<FileListEntry>, Error> {
 | 
			
		||||
        let dir_fd = dir.as_raw_fd();
 | 
			
		||||
 | 
			
		||||
        let mut file_list = Vec::new();
 | 
			
		||||
 | 
			
		||||
        for file in dir.iter() {
 | 
			
		||||
            let file = file?;
 | 
			
		||||
 | 
			
		||||
            let file_name = file.file_name().to_owned();
 | 
			
		||||
            let file_name_bytes = file_name.to_bytes();
 | 
			
		||||
            if file_name_bytes == b"." || file_name_bytes == b".." {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // FIXME: deal with `.pxarexclude-cli`
 | 
			
		||||
 | 
			
		||||
            if file_name_bytes == b".pxarexclude" {
 | 
			
		||||
                // FIXME: handle this file!
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let os_file_name = OsStr::from_bytes(file_name_bytes);
 | 
			
		||||
            assert_relative_path(os_file_name)?;
 | 
			
		||||
            let full_path = self.path.join(os_file_name);
 | 
			
		||||
 | 
			
		||||
            let stat = match nix::sys::stat::fstatat(
 | 
			
		||||
                dir_fd,
 | 
			
		||||
                file_name.as_c_str(),
 | 
			
		||||
                nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
 | 
			
		||||
            ) {
 | 
			
		||||
                Ok(stat) => stat,
 | 
			
		||||
                Err(ref err) if err.not_found() => continue,
 | 
			
		||||
                Err(err) => bail!("stat failed on {:?}: {}", full_path, err),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if self
 | 
			
		||||
                .excludes
 | 
			
		||||
                .matches(full_path.as_os_str().as_bytes(), Some(stat.st_mode as u32))
 | 
			
		||||
                == Some(MatchType::Exclude)
 | 
			
		||||
            {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            self.entry_counter += 1;
 | 
			
		||||
            if self.entry_counter > self.entry_limit {
 | 
			
		||||
                bail!("exceeded allowed number of file entries (> {})",self.entry_limit);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            file_list.push(FileListEntry {
 | 
			
		||||
                name: file_name,
 | 
			
		||||
                path: full_path,
 | 
			
		||||
                stat
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        file_list.sort_unstable_by(|a, b| a.name.cmp(&b.name));
 | 
			
		||||
 | 
			
		||||
        Ok(file_list)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_entry(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        encoder: &mut Encoder,
 | 
			
		||||
        parent: RawFd,
 | 
			
		||||
        c_file_name: &CStr,
 | 
			
		||||
        stat: &FileStat,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        use pxar::format::mode;
 | 
			
		||||
 | 
			
		||||
        let file_mode = stat.st_mode & libc::S_IFMT;
 | 
			
		||||
        let open_mode = if !(file_mode == libc::S_IFREG || file_mode == libc::S_IFDIR) {
 | 
			
		||||
            OFlag::O_PATH
 | 
			
		||||
        } else {
 | 
			
		||||
            OFlag::empty()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let fd = Fd::openat(
 | 
			
		||||
            &unsafe { RawFdNum::from_raw_fd(parent) },
 | 
			
		||||
            c_file_name,
 | 
			
		||||
            open_mode | OFlag::O_RDONLY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC | OFlag::O_NOCTTY,
 | 
			
		||||
            Mode::empty(),
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
        let metadata = get_metadata(fd.as_raw_fd(), &stat, self.flags(), self.fs_magic)?;
 | 
			
		||||
 | 
			
		||||
        if self
 | 
			
		||||
            .excludes
 | 
			
		||||
            .matches(self.path.as_os_str().as_bytes(), Some(stat.st_mode as u32))
 | 
			
		||||
            == Some(MatchType::Exclude)
 | 
			
		||||
        {
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let file_name: &Path = OsStr::from_bytes(c_file_name.to_bytes()).as_ref();
 | 
			
		||||
        match metadata.file_type() {
 | 
			
		||||
            mode::IFREG => {
 | 
			
		||||
                let link_info = HardLinkInfo {
 | 
			
		||||
                    st_dev: stat.st_dev,
 | 
			
		||||
                    st_ino: stat.st_ino,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                if stat.st_nlink > 1 {
 | 
			
		||||
                    if let Some((path, offset)) = self.hardlinks.get(&link_info) {
 | 
			
		||||
                        if let Some(ref mut catalog) = self.catalog {
 | 
			
		||||
                            catalog.add_hardlink(c_file_name)?;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        encoder.add_hardlink(file_name, path, *offset)?;
 | 
			
		||||
 | 
			
		||||
                        return Ok(());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let file_size = stat.st_size as u64;
 | 
			
		||||
                if let Some(ref mut catalog) = self.catalog {
 | 
			
		||||
                    catalog.add_file(c_file_name, file_size, metadata.stat.mtime)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let offset: LinkOffset =
 | 
			
		||||
                    self.add_regular_file(encoder, fd, file_name, &metadata, file_size)?;
 | 
			
		||||
 | 
			
		||||
                if stat.st_nlink > 1 {
 | 
			
		||||
                    self.hardlinks.insert(link_info, (self.path.clone(), offset));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Ok(())
 | 
			
		||||
            }
 | 
			
		||||
            mode::IFDIR => {
 | 
			
		||||
                let dir = Dir::from_fd(fd.into_raw_fd())?;
 | 
			
		||||
                self.add_directory(encoder, dir, c_file_name, &metadata, stat)
 | 
			
		||||
            }
 | 
			
		||||
            mode::IFSOCK => {
 | 
			
		||||
                if let Some(ref mut catalog) = self.catalog {
 | 
			
		||||
                    catalog.add_socket(c_file_name)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Ok(encoder.add_socket(&metadata, file_name)?)
 | 
			
		||||
            }
 | 
			
		||||
            mode::IFIFO => {
 | 
			
		||||
                if let Some(ref mut catalog) = self.catalog {
 | 
			
		||||
                    catalog.add_fifo(c_file_name)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Ok(encoder.add_fifo(&metadata, file_name)?)
 | 
			
		||||
            }
 | 
			
		||||
            mode::IFLNK => {
 | 
			
		||||
                if let Some(ref mut catalog) = self.catalog {
 | 
			
		||||
                    catalog.add_symlink(c_file_name)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                self.add_symlink(encoder, fd, file_name, &metadata)
 | 
			
		||||
            }
 | 
			
		||||
            mode::IFBLK => {
 | 
			
		||||
                if let Some(ref mut catalog) = self.catalog {
 | 
			
		||||
                    catalog.add_block_device(c_file_name)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                self.add_device(encoder, file_name, &metadata, &stat)
 | 
			
		||||
            }
 | 
			
		||||
            mode::IFCHR => {
 | 
			
		||||
                if let Some(ref mut catalog) = self.catalog {
 | 
			
		||||
                    catalog.add_char_device(c_file_name)?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                self.add_device(encoder, file_name, &metadata, &stat)
 | 
			
		||||
            }
 | 
			
		||||
            other => bail!(
 | 
			
		||||
                "encountered unknown file type: 0x{:x} (0o{:o})",
 | 
			
		||||
                other,
 | 
			
		||||
                other
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_directory(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        encoder: &mut Encoder,
 | 
			
		||||
        dir: Dir,
 | 
			
		||||
        dir_name: &CStr,
 | 
			
		||||
        metadata: &Metadata,
 | 
			
		||||
        stat: &FileStat,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        let dir_name = OsStr::from_bytes(dir_name.to_bytes());
 | 
			
		||||
 | 
			
		||||
        let mut encoder = encoder.create_directory(dir_name, &metadata)?;
 | 
			
		||||
 | 
			
		||||
        let old_fs_magic = self.fs_magic;
 | 
			
		||||
        let old_fs_feature_flags = self.fs_feature_flags;
 | 
			
		||||
        let old_st_dev = self.current_st_dev;
 | 
			
		||||
 | 
			
		||||
        let mut skip_contents = false;
 | 
			
		||||
        if old_st_dev != stat.st_dev {
 | 
			
		||||
            self.fs_magic = detect_fs_type(dir.as_raw_fd())?;
 | 
			
		||||
            self.fs_feature_flags = flags::feature_flags_from_magic(self.fs_magic);
 | 
			
		||||
            self.current_st_dev = stat.st_dev;
 | 
			
		||||
 | 
			
		||||
            if is_virtual_file_system(self.fs_magic) {
 | 
			
		||||
                skip_contents = true;
 | 
			
		||||
            } else if let Some(set) = &self.device_set {
 | 
			
		||||
                skip_contents = !set.contains(&stat.st_dev);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let result = if skip_contents {
 | 
			
		||||
            Ok(())
 | 
			
		||||
        } else {
 | 
			
		||||
            self.archive_dir_contents(&mut encoder, dir)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        self.fs_magic = old_fs_magic;
 | 
			
		||||
        self.fs_feature_flags = old_fs_feature_flags;
 | 
			
		||||
        self.current_st_dev = old_st_dev;
 | 
			
		||||
 | 
			
		||||
        encoder.finish()?;
 | 
			
		||||
        result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_regular_file(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        encoder: &mut Encoder,
 | 
			
		||||
        fd: Fd,
 | 
			
		||||
        file_name: &Path,
 | 
			
		||||
        metadata: &Metadata,
 | 
			
		||||
        file_size: u64,
 | 
			
		||||
    ) -> Result<LinkOffset, Error> {
 | 
			
		||||
        let mut file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
 | 
			
		||||
        let offset = encoder.add_file(metadata, file_name, file_size, &mut file)?;
 | 
			
		||||
        Ok(offset)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_symlink(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        encoder: &mut Encoder,
 | 
			
		||||
        fd: Fd,
 | 
			
		||||
        file_name: &Path,
 | 
			
		||||
        metadata: &Metadata,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        let dest = nix::fcntl::readlinkat(fd.as_raw_fd(), &b""[..])?;
 | 
			
		||||
        encoder.add_symlink(metadata, file_name, dest)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn add_device(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        encoder: &mut Encoder,
 | 
			
		||||
        file_name: &Path,
 | 
			
		||||
        metadata: &Metadata,
 | 
			
		||||
        stat: &FileStat,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        Ok(encoder.add_device(
 | 
			
		||||
            metadata,
 | 
			
		||||
            file_name,
 | 
			
		||||
            pxar::format::Device::from_dev_t(stat.st_rdev),
 | 
			
		||||
        )?)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_metadata(fd: RawFd, stat: &FileStat, flags: u64, fs_magic: i64) -> Result<Metadata, Error> {
 | 
			
		||||
    // required for some of these
 | 
			
		||||
    let proc_path = Path::new("/proc/self/fd/").join(fd.to_string());
 | 
			
		||||
 | 
			
		||||
    let mtime = u64::try_from(stat.st_mtime * 1_000_000_000 + stat.st_mtime_nsec)
 | 
			
		||||
        .map_err(|_| format_err!("file with negative mtime"))?;
 | 
			
		||||
 | 
			
		||||
    let mut meta = Metadata {
 | 
			
		||||
        stat: pxar::Stat {
 | 
			
		||||
            mode: u64::from(stat.st_mode),
 | 
			
		||||
            flags: 0,
 | 
			
		||||
            uid: stat.st_uid,
 | 
			
		||||
            gid: stat.st_gid,
 | 
			
		||||
            mtime,
 | 
			
		||||
        },
 | 
			
		||||
        ..Default::default()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    get_xattr_fcaps_acl(&mut meta, fd, &proc_path, flags)?;
 | 
			
		||||
    get_chattr(&mut meta, fd)?;
 | 
			
		||||
    get_fat_attr(&mut meta, fd, fs_magic)?;
 | 
			
		||||
    get_quota_project_id(&mut meta, fd, flags, fs_magic)?;
 | 
			
		||||
    Ok(meta)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn errno_is_unsupported(errno: Errno) -> bool {
 | 
			
		||||
    match errno {
 | 
			
		||||
        Errno::ENOTTY | Errno::ENOSYS | Errno::EBADF | Errno::EOPNOTSUPP | Errno::EINVAL => true,
 | 
			
		||||
        _ => false,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_fcaps(meta: &mut Metadata, fd: RawFd, flags: u64) -> Result<(), Error> {
 | 
			
		||||
    if 0 == (flags & flags::WITH_FCAPS) {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match xattr::fgetxattr(fd, xattr::xattr_name_fcaps()) {
 | 
			
		||||
        Ok(data) => {
 | 
			
		||||
            meta.fcaps = Some(pxar::format::FCaps { data });
 | 
			
		||||
            Ok(())
 | 
			
		||||
        }
 | 
			
		||||
        Err(Errno::ENODATA) => Ok(()),
 | 
			
		||||
        Err(Errno::EOPNOTSUPP) => Ok(()),
 | 
			
		||||
        Err(Errno::EBADF) => Ok(()), // symlinks
 | 
			
		||||
        Err(err) => bail!("failed to read file capabilities: {}", err),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_xattr_fcaps_acl(
 | 
			
		||||
    meta: &mut Metadata,
 | 
			
		||||
    fd: RawFd,
 | 
			
		||||
    proc_path: &Path,
 | 
			
		||||
    flags: u64,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    if 0 == (flags & flags::WITH_XATTRS) {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let xattrs = match xattr::flistxattr(fd) {
 | 
			
		||||
        Ok(names) => names,
 | 
			
		||||
        Err(Errno::EOPNOTSUPP) => return Ok(()),
 | 
			
		||||
        Err(Errno::EBADF) => return Ok(()), // symlinks
 | 
			
		||||
        Err(err) => bail!("failed to read xattrs: {}", err),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    for attr in &xattrs {
 | 
			
		||||
        if xattr::is_security_capability(&attr) {
 | 
			
		||||
            get_fcaps(meta, fd, flags)?;
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if xattr::is_acl(&attr) {
 | 
			
		||||
            get_acl(meta, proc_path, flags)?;
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !xattr::is_valid_xattr_name(&attr) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match xattr::fgetxattr(fd, attr) {
 | 
			
		||||
            Ok(data) => meta
 | 
			
		||||
                .xattrs
 | 
			
		||||
                .push(pxar::format::XAttr::new(attr.to_bytes(), data)),
 | 
			
		||||
            Err(Errno::ENODATA) => (), // it got removed while we were iterating...
 | 
			
		||||
            Err(Errno::EOPNOTSUPP) => (), // shouldn't be possible so just ignore this
 | 
			
		||||
            Err(Errno::EBADF) => (),   // symlinks, shouldn't be able to reach this either
 | 
			
		||||
            Err(err) => bail!("error reading extended attribute {:?}: {}", attr, err),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_chattr(metadata: &mut Metadata, fd: RawFd) -> Result<(), Error> {
 | 
			
		||||
    let mut attr: usize = 0;
 | 
			
		||||
 | 
			
		||||
    match unsafe { fs::read_attr_fd(fd, &mut attr) } {
 | 
			
		||||
        Ok(_) => (),
 | 
			
		||||
        Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
        Err(err) => bail!("failed to read file attributes: {}", err),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    metadata.stat.flags |= flags::feature_flags_from_chattr(attr as u32);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_fat_attr(metadata: &mut Metadata, fd: RawFd, fs_magic: i64) -> Result<(), Error> {
 | 
			
		||||
    use proxmox::sys::linux::magic::*;
 | 
			
		||||
 | 
			
		||||
    if fs_magic != MSDOS_SUPER_MAGIC && fs_magic != FUSE_SUPER_MAGIC {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut attr: u32 = 0;
 | 
			
		||||
 | 
			
		||||
    match unsafe { fs::read_fat_attr_fd(fd, &mut attr) } {
 | 
			
		||||
        Ok(_) => (),
 | 
			
		||||
        Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
        Err(err) => bail!("failed to read fat attributes: {}", err),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    metadata.stat.flags |= flags::feature_flags_from_fat_attr(attr);
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Read the quota project id for an inode, supported on ext4/XFS/FUSE/ZFS filesystems
 | 
			
		||||
fn get_quota_project_id(
 | 
			
		||||
    metadata: &mut Metadata,
 | 
			
		||||
    fd: RawFd,
 | 
			
		||||
    flags: u64,
 | 
			
		||||
    magic: i64,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    if !(metadata.is_dir() || metadata.is_regular_file()) {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if 0 == (flags & flags::WITH_QUOTA_PROJID) {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    use proxmox::sys::linux::magic::*;
 | 
			
		||||
 | 
			
		||||
    match magic {
 | 
			
		||||
        EXT4_SUPER_MAGIC | XFS_SUPER_MAGIC | FUSE_SUPER_MAGIC | ZFS_SUPER_MAGIC => (),
 | 
			
		||||
        _ => return Ok(()),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut fsxattr = fs::FSXAttr::default();
 | 
			
		||||
    let res = unsafe { fs::fs_ioc_fsgetxattr(fd, &mut fsxattr) };
 | 
			
		||||
 | 
			
		||||
    // On some FUSE filesystems it can happen that ioctl is not supported.
 | 
			
		||||
    // For these cases projid is set to 0 while the error is ignored.
 | 
			
		||||
    if let Err(err) = res {
 | 
			
		||||
        let errno = err
 | 
			
		||||
            .as_errno()
 | 
			
		||||
            .ok_or_else(|| format_err!("error while reading quota project id"))?;
 | 
			
		||||
        if errno_is_unsupported(errno) {
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        } else {
 | 
			
		||||
            bail!("error while reading quota project id ({})", errno);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let projid = fsxattr.fsx_projid as u64;
 | 
			
		||||
    if projid != 0 {
 | 
			
		||||
        metadata.quota_project_id = Some(pxar::format::QuotaProjectId { projid });
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_acl(metadata: &mut Metadata, proc_path: &Path, flags: u64) -> Result<(), Error> {
 | 
			
		||||
    if 0 == (flags & flags::WITH_ACL) {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if metadata.is_symlink() {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get_acl_do(metadata, proc_path, acl::ACL_TYPE_ACCESS)?;
 | 
			
		||||
 | 
			
		||||
    if metadata.is_dir() {
 | 
			
		||||
        get_acl_do(metadata, proc_path, acl::ACL_TYPE_DEFAULT)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_acl_do(
 | 
			
		||||
    metadata: &mut Metadata,
 | 
			
		||||
    proc_path: &Path,
 | 
			
		||||
    acl_type: acl::ACLType,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    // In order to be able to get ACLs with type ACL_TYPE_DEFAULT, we have
 | 
			
		||||
    // to create a path for acl_get_file(). acl_get_fd() only allows to get
 | 
			
		||||
    // ACL_TYPE_ACCESS attributes.
 | 
			
		||||
    let acl = match acl::ACL::get_file(&proc_path, acl_type) {
 | 
			
		||||
        Ok(acl) => acl,
 | 
			
		||||
        // Don't bail if underlying endpoint does not support acls
 | 
			
		||||
        Err(Errno::EOPNOTSUPP) => return Ok(()),
 | 
			
		||||
        // Don't bail if the endpoint cannot carry acls
 | 
			
		||||
        Err(Errno::EBADF) => return Ok(()),
 | 
			
		||||
        // Don't bail if there is no data
 | 
			
		||||
        Err(Errno::ENODATA) => return Ok(()),
 | 
			
		||||
        Err(err) => bail!("error while reading ACL - {}", err),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    process_acl(metadata, acl, acl_type)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn process_acl(
 | 
			
		||||
    metadata: &mut Metadata,
 | 
			
		||||
    acl: acl::ACL,
 | 
			
		||||
    acl_type: acl::ACLType,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    use pxar::format::acl as pxar_acl;
 | 
			
		||||
    use pxar::format::acl::{Group, GroupObject, Permissions, User};
 | 
			
		||||
 | 
			
		||||
    let mut acl_user = Vec::new();
 | 
			
		||||
    let mut acl_group = Vec::new();
 | 
			
		||||
    let mut acl_group_obj = None;
 | 
			
		||||
    let mut acl_default = None;
 | 
			
		||||
    let mut user_obj_permissions = None;
 | 
			
		||||
    let mut group_obj_permissions = None;
 | 
			
		||||
    let mut other_permissions = None;
 | 
			
		||||
    let mut mask_permissions = None;
 | 
			
		||||
 | 
			
		||||
    for entry in &mut acl.entries() {
 | 
			
		||||
        let tag = entry.get_tag_type()?;
 | 
			
		||||
        let permissions = entry.get_permissions()?;
 | 
			
		||||
        match tag {
 | 
			
		||||
            acl::ACL_USER_OBJ => user_obj_permissions = Some(Permissions(permissions)),
 | 
			
		||||
            acl::ACL_GROUP_OBJ => group_obj_permissions = Some(Permissions(permissions)),
 | 
			
		||||
            acl::ACL_OTHER => other_permissions = Some(Permissions(permissions)),
 | 
			
		||||
            acl::ACL_MASK => mask_permissions = Some(Permissions(permissions)),
 | 
			
		||||
            acl::ACL_USER => {
 | 
			
		||||
                acl_user.push(User {
 | 
			
		||||
                    uid: entry.get_qualifier()?,
 | 
			
		||||
                    permissions: Permissions(permissions),
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            acl::ACL_GROUP => {
 | 
			
		||||
                acl_group.push(Group {
 | 
			
		||||
                    gid: entry.get_qualifier()?,
 | 
			
		||||
                    permissions: Permissions(permissions),
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            _ => bail!("Unexpected ACL tag encountered!"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    acl_user.sort();
 | 
			
		||||
    acl_group.sort();
 | 
			
		||||
 | 
			
		||||
    match acl_type {
 | 
			
		||||
        acl::ACL_TYPE_ACCESS => {
 | 
			
		||||
            // The mask permissions are mapped to the stat group permissions
 | 
			
		||||
            // in case that the ACL group permissions were set.
 | 
			
		||||
            // Only in that case we need to store the group permissions,
 | 
			
		||||
            // in the other cases they are identical to the stat group permissions.
 | 
			
		||||
            if let (Some(gop), true) = (group_obj_permissions, mask_permissions.is_some()) {
 | 
			
		||||
                acl_group_obj = Some(GroupObject { permissions: gop });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            metadata.acl.users = acl_user;
 | 
			
		||||
            metadata.acl.groups = acl_group;
 | 
			
		||||
        }
 | 
			
		||||
        acl::ACL_TYPE_DEFAULT => {
 | 
			
		||||
            if user_obj_permissions != None
 | 
			
		||||
                || group_obj_permissions != None
 | 
			
		||||
                || other_permissions != None
 | 
			
		||||
                || mask_permissions != None
 | 
			
		||||
            {
 | 
			
		||||
                acl_default = Some(pxar_acl::Default {
 | 
			
		||||
                    // The value is set to UINT64_MAX as placeholder if one
 | 
			
		||||
                    // of the permissions is not set
 | 
			
		||||
                    user_obj_permissions: user_obj_permissions.unwrap_or(Permissions::NO_MASK),
 | 
			
		||||
                    group_obj_permissions: group_obj_permissions.unwrap_or(Permissions::NO_MASK),
 | 
			
		||||
                    other_permissions: other_permissions.unwrap_or(Permissions::NO_MASK),
 | 
			
		||||
                    mask_permissions: mask_permissions.unwrap_or(Permissions::NO_MASK),
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            metadata.acl.default_users = acl_user;
 | 
			
		||||
            metadata.acl.default_groups = acl_group;
 | 
			
		||||
        }
 | 
			
		||||
        _ => bail!("Unexpected ACL type encountered"),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    metadata.acl.group_obj = acl_group_obj;
 | 
			
		||||
    metadata.acl.default = acl_default;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
@ -1,365 +0,0 @@
 | 
			
		||||
//! *pxar* format decoder for seekable files
 | 
			
		||||
//!
 | 
			
		||||
//! This module contain the code to decode *pxar* archive files.
 | 
			
		||||
 | 
			
		||||
use std::convert::TryFrom;
 | 
			
		||||
use std::ffi::{OsString, OsStr};
 | 
			
		||||
use std::io::{Read, Seek, SeekFrom};
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::os::unix::ffi::OsStrExt;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use libc;
 | 
			
		||||
 | 
			
		||||
use super::binary_search_tree::search_binary_tree_by;
 | 
			
		||||
use super::format_definition::*;
 | 
			
		||||
use super::sequential_decoder::SequentialDecoder;
 | 
			
		||||
use super::match_pattern::MatchPattern;
 | 
			
		||||
 | 
			
		||||
use proxmox::tools::io::ReadExt;
 | 
			
		||||
 | 
			
		||||
pub struct DirectoryEntry {
 | 
			
		||||
    /// Points to the `PxarEntry` of the directory
 | 
			
		||||
    start: u64,
 | 
			
		||||
    /// Points past the goodbye table tail
 | 
			
		||||
    end: u64,
 | 
			
		||||
    /// Filename of entry
 | 
			
		||||
    pub filename: OsString,
 | 
			
		||||
    /// Entry (mode, permissions)
 | 
			
		||||
    pub entry: PxarEntry,
 | 
			
		||||
    /// Extended attributes
 | 
			
		||||
    pub xattr: PxarAttributes,
 | 
			
		||||
    /// Payload size
 | 
			
		||||
    pub size: u64,
 | 
			
		||||
    /// Target path for symbolic links
 | 
			
		||||
    pub target: Option<PathBuf>,
 | 
			
		||||
    /// Start offset of the payload if present.
 | 
			
		||||
    pub payload_offset: Option<u64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Trait to create ReadSeek Decoder trait objects.
 | 
			
		||||
trait ReadSeek: Read + Seek {}
 | 
			
		||||
impl <R: Read + Seek> ReadSeek for R {}
 | 
			
		||||
 | 
			
		||||
// This one needs Read+Seek
 | 
			
		||||
pub struct Decoder {
 | 
			
		||||
    inner: SequentialDecoder<Box<dyn ReadSeek + Send>>,
 | 
			
		||||
    root_start: u64,
 | 
			
		||||
    root_end: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const HEADER_SIZE: u64 = std::mem::size_of::<PxarHeader>() as u64;
 | 
			
		||||
const GOODBYE_ITEM_SIZE: u64 = std::mem::size_of::<PxarGoodbyeItem>() as u64;
 | 
			
		||||
 | 
			
		||||
impl Decoder {
 | 
			
		||||
    pub fn new<R: Read + Seek + Send + 'static>(mut reader: R) -> Result<Self, Error> {
 | 
			
		||||
        let root_end = reader.seek(SeekFrom::End(0))?;
 | 
			
		||||
        let boxed_reader: Box<dyn ReadSeek + 'static + Send> = Box::new(reader);
 | 
			
		||||
        let inner = SequentialDecoder::new(boxed_reader, super::flags::DEFAULT);
 | 
			
		||||
  
 | 
			
		||||
        Ok(Self { inner, root_start: 0, root_end })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_callback<F: Fn(&Path) -> Result<(), Error> + Send + 'static>(&mut self, callback: F ) {
 | 
			
		||||
        self.inner.set_callback(callback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn root(&mut self) -> Result<DirectoryEntry, Error> {
 | 
			
		||||
        self.seek(SeekFrom::Start(0))?;
 | 
			
		||||
        let header: PxarHeader = self.inner.read_item()?;
 | 
			
		||||
        check_ca_header::<PxarEntry>(&header, PXAR_ENTRY)?;
 | 
			
		||||
        let entry: PxarEntry = self.inner.read_item()?;
 | 
			
		||||
        let (header, xattr) = self.inner.read_attributes()?;
 | 
			
		||||
        let (size, payload_offset) = match header.htype {
 | 
			
		||||
            PXAR_PAYLOAD => (header.size - HEADER_SIZE, Some(self.seek(SeekFrom::Current(0))?)),
 | 
			
		||||
            _ => (0, None),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Ok(DirectoryEntry {
 | 
			
		||||
            start: self.root_start,
 | 
			
		||||
            end: self.root_end,
 | 
			
		||||
            filename: OsString::new(), // Empty
 | 
			
		||||
            entry,
 | 
			
		||||
            xattr,
 | 
			
		||||
            size,
 | 
			
		||||
            target: None,
 | 
			
		||||
            payload_offset,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn seek(&mut self, pos: SeekFrom) -> Result<u64, Error> {
 | 
			
		||||
        let pos = self.inner.get_reader_mut().seek(pos)?;
 | 
			
		||||
        Ok(pos)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn root_end_offset(&self) -> u64 {
 | 
			
		||||
        self.root_end
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Restore the subarchive starting at `dir` to the provided target `path`.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Only restore the content matched by the MatchPattern `pattern`.
 | 
			
		||||
    /// An empty Vec `pattern` means restore all.
 | 
			
		||||
    pub fn restore(&mut self, dir: &DirectoryEntry, path: &Path, pattern: &Vec<MatchPattern>) -> Result<(), Error> {
 | 
			
		||||
        let start = dir.start;
 | 
			
		||||
        self.seek(SeekFrom::Start(start))?;
 | 
			
		||||
        self.inner.restore(path, pattern)?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn read_directory_entry(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        start: u64,
 | 
			
		||||
        end: u64,
 | 
			
		||||
    ) -> Result<DirectoryEntry, Error> {
 | 
			
		||||
        self.seek(SeekFrom::Start(start))?;
 | 
			
		||||
 | 
			
		||||
        let head: PxarHeader = self.inner.read_item()?;
 | 
			
		||||
 | 
			
		||||
        if head.htype != PXAR_FILENAME {
 | 
			
		||||
            bail!("wrong filename header type for object [{}..{}]", start, end);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let entry_start = start + head.size;
 | 
			
		||||
 | 
			
		||||
        let filename = self.inner.read_filename(head.size)?;
 | 
			
		||||
 | 
			
		||||
        let head: PxarHeader = self.inner.read_item()?;
 | 
			
		||||
        if head.htype == PXAR_FORMAT_HARDLINK {
 | 
			
		||||
            let (_, offset) = self.inner.read_hardlink(head.size)?;
 | 
			
		||||
            // TODO: Howto find correct end offset for hardlink target?
 | 
			
		||||
            // This is a bit tricky since we cannot find correct end in an efficient
 | 
			
		||||
            // way, on the other hand it doesn't really matter (for now) since target
 | 
			
		||||
            // is never a directory and end is not used in such cases.
 | 
			
		||||
            return self.read_directory_entry(start - offset, end);
 | 
			
		||||
        }
 | 
			
		||||
        check_ca_header::<PxarEntry>(&head, PXAR_ENTRY)?;
 | 
			
		||||
        let entry: PxarEntry = self.inner.read_item()?;
 | 
			
		||||
        let (header, xattr) = self.inner.read_attributes()?;
 | 
			
		||||
        let (size, payload_offset, target) = match header.htype {
 | 
			
		||||
            PXAR_PAYLOAD =>
 | 
			
		||||
                (header.size - HEADER_SIZE, Some(self.seek(SeekFrom::Current(0))?), None),
 | 
			
		||||
            PXAR_SYMLINK =>
 | 
			
		||||
                (header.size - HEADER_SIZE, None, Some(self.inner.read_link(header.size)?)),
 | 
			
		||||
            _ => (0, None, None),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Ok(DirectoryEntry {
 | 
			
		||||
            start: entry_start,
 | 
			
		||||
            end,
 | 
			
		||||
            filename,
 | 
			
		||||
            entry,
 | 
			
		||||
            xattr,
 | 
			
		||||
            size,
 | 
			
		||||
            target,
 | 
			
		||||
            payload_offset,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Return the goodbye table based on the provided end offset.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Get the goodbye table entries and the start and end offsets of the
 | 
			
		||||
    /// items they reference.
 | 
			
		||||
    /// If the start offset is provided, we use that to check the consistency of
 | 
			
		||||
    /// the data, else the start offset calculated based on the goodbye tail is
 | 
			
		||||
    /// used.
 | 
			
		||||
    pub(crate) fn goodbye_table(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        start: Option<u64>,
 | 
			
		||||
        end: u64,
 | 
			
		||||
    ) -> Result<Vec<(PxarGoodbyeItem, u64, u64)>, Error> {
 | 
			
		||||
        self.seek(SeekFrom::Start(end - GOODBYE_ITEM_SIZE))?;
 | 
			
		||||
 | 
			
		||||
        let tail: PxarGoodbyeItem = self.inner.read_item()?;
 | 
			
		||||
        if tail.hash != PXAR_GOODBYE_TAIL_MARKER {
 | 
			
		||||
            bail!("missing goodbye tail marker for object at offset {}", end);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If the start offset was provided, we use and check based on that.
 | 
			
		||||
        // If not, we rely on the offset calculated from the goodbye table entry.
 | 
			
		||||
        let start = start.unwrap_or(end - tail.offset - tail.size);
 | 
			
		||||
        let goodbye_table_size = tail.size;
 | 
			
		||||
        if goodbye_table_size < (HEADER_SIZE + GOODBYE_ITEM_SIZE) {
 | 
			
		||||
            bail!("short goodbye table size for object [{}..{}]", start, end);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let goodbye_inner_size = goodbye_table_size - HEADER_SIZE - GOODBYE_ITEM_SIZE;
 | 
			
		||||
        if (goodbye_inner_size % GOODBYE_ITEM_SIZE) != 0 {
 | 
			
		||||
            bail!(
 | 
			
		||||
                "wrong goodbye inner table size for entry [{}..{}]",
 | 
			
		||||
                start,
 | 
			
		||||
                end
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let goodbye_start = end - goodbye_table_size;
 | 
			
		||||
        if tail.offset != (goodbye_start - start) {
 | 
			
		||||
            bail!(
 | 
			
		||||
                "wrong offset in goodbye tail marker for entry [{}..{}]",
 | 
			
		||||
                start,
 | 
			
		||||
                end
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.seek(SeekFrom::Start(goodbye_start))?;
 | 
			
		||||
        let head: PxarHeader = self.inner.read_item()?;
 | 
			
		||||
        if head.htype != PXAR_GOODBYE {
 | 
			
		||||
            bail!(
 | 
			
		||||
                "wrong goodbye table header type for entry [{}..{}]",
 | 
			
		||||
                start,
 | 
			
		||||
                end
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if head.size != goodbye_table_size {
 | 
			
		||||
            bail!("wrong goodbye table size for entry [{}..{}]", start, end);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut gb_entries = Vec::new();
 | 
			
		||||
        for i in 0..goodbye_inner_size / GOODBYE_ITEM_SIZE {
 | 
			
		||||
            let item: PxarGoodbyeItem = self.inner.read_item()?;
 | 
			
		||||
            if item.offset > (goodbye_start - start) {
 | 
			
		||||
                bail!(
 | 
			
		||||
                    "goodbye entry {} offset out of range [{}..{}] {} {} {}",
 | 
			
		||||
                    i,
 | 
			
		||||
                    start,
 | 
			
		||||
                    end,
 | 
			
		||||
                    item.offset,
 | 
			
		||||
                    goodbye_start,
 | 
			
		||||
                    start
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            let item_start = goodbye_start - item.offset;
 | 
			
		||||
            let item_end = item_start + item.size;
 | 
			
		||||
            if item_end > goodbye_start {
 | 
			
		||||
                bail!("goodbye entry {} end out of range [{}..{}]", i, start, end);
 | 
			
		||||
            }
 | 
			
		||||
            gb_entries.push((item, item_start, item_end));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(gb_entries)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn list_dir(&mut self, dir: &DirectoryEntry) -> Result<Vec<DirectoryEntry>, Error> {
 | 
			
		||||
        let start = dir.start;
 | 
			
		||||
        let end = dir.end;
 | 
			
		||||
 | 
			
		||||
        //println!("list_dir1: {} {}", start, end);
 | 
			
		||||
 | 
			
		||||
        if (end - start) < (HEADER_SIZE + GOODBYE_ITEM_SIZE) {
 | 
			
		||||
            bail!("detected short object [{}..{}]", start, end);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut result = vec![];
 | 
			
		||||
        let goodbye_table = self.goodbye_table(Some(start), end)?;
 | 
			
		||||
        for (_, item_start, item_end) in goodbye_table {
 | 
			
		||||
            let entry = self.read_directory_entry(item_start, item_end)?;
 | 
			
		||||
            //println!("ENTRY: {} {} {:?}", item_start, item_end, entry.filename);
 | 
			
		||||
            result.push(entry);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(result)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn print_filenames<W: std::io::Write>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        output: &mut W,
 | 
			
		||||
        prefix: &mut PathBuf,
 | 
			
		||||
        dir: &DirectoryEntry,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        let mut list = self.list_dir(dir)?;
 | 
			
		||||
 | 
			
		||||
        list.sort_unstable_by(|a, b| a.filename.cmp(&b.filename));
 | 
			
		||||
 | 
			
		||||
        for item in &list {
 | 
			
		||||
            prefix.push(item.filename.clone());
 | 
			
		||||
 | 
			
		||||
            let mode = item.entry.mode as u32;
 | 
			
		||||
 | 
			
		||||
            let ifmt = mode & libc::S_IFMT;
 | 
			
		||||
 | 
			
		||||
            writeln!(output, "{:?}", prefix)?;
 | 
			
		||||
 | 
			
		||||
            match ifmt {
 | 
			
		||||
                libc::S_IFDIR => self.print_filenames(output, prefix, item)?,
 | 
			
		||||
                libc::S_IFREG | libc::S_IFLNK | libc::S_IFBLK | libc::S_IFCHR => {}
 | 
			
		||||
                _ => bail!("unknown item mode/type for {:?}", prefix),
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            prefix.pop();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Lookup the item identified by `filename` in the provided `DirectoryEntry`.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Calculates the hash of the filename and searches for matching entries in
 | 
			
		||||
    /// the goodbye table of the provided `DirectoryEntry`.
 | 
			
		||||
    /// If found, also the filename is compared to avoid hash collision.
 | 
			
		||||
    /// If the filename does not match, the search resumes with the next entry in
 | 
			
		||||
    /// the goodbye table.
 | 
			
		||||
    /// If there is no entry with matching `filename`, `Ok(None)` is returned.
 | 
			
		||||
    pub fn lookup(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        dir: &DirectoryEntry,
 | 
			
		||||
        filename: &OsStr,
 | 
			
		||||
    ) -> Result<Option<DirectoryEntry>, Error> {
 | 
			
		||||
        let gbt = self.goodbye_table(Some(dir.start), dir.end)?;
 | 
			
		||||
        let hash = compute_goodbye_hash(filename.as_bytes());
 | 
			
		||||
 | 
			
		||||
        let mut start_idx = 0;
 | 
			
		||||
        let mut skip_multiple = 0;
 | 
			
		||||
        loop {
 | 
			
		||||
            // Search for the next goodbye entry with matching hash.
 | 
			
		||||
            let idx = search_binary_tree_by(
 | 
			
		||||
                start_idx,
 | 
			
		||||
                gbt.len(),
 | 
			
		||||
                skip_multiple,
 | 
			
		||||
                |idx| hash.cmp(&gbt[idx].0.hash),
 | 
			
		||||
            );
 | 
			
		||||
            let (_item, start, end) = match idx {
 | 
			
		||||
                Some(idx) => &gbt[idx],
 | 
			
		||||
                None => return Ok(None),
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let entry = self.read_directory_entry(*start, *end)?;
 | 
			
		||||
 | 
			
		||||
            // Possible hash collision, need to check if the found entry is indeed
 | 
			
		||||
            // the filename to lookup.
 | 
			
		||||
            if entry.filename == filename {
 | 
			
		||||
                return Ok(Some(entry));
 | 
			
		||||
            }
 | 
			
		||||
            // Hash collision, check the next entry in the goodbye table by starting
 | 
			
		||||
            // from given index but skipping one more match (so hash at index itself).
 | 
			
		||||
            start_idx = idx.unwrap();
 | 
			
		||||
            skip_multiple = 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Read the payload of the file given by `entry`.
 | 
			
		||||
    ///
 | 
			
		||||
    /// This will read a files payload as raw bytes starting from `offset` after
 | 
			
		||||
    /// the payload marker, reading `size` bytes.
 | 
			
		||||
    /// If the payload from `offset` to EOF is smaller than `size` bytes, the
 | 
			
		||||
    /// buffer with reduced size is returned.
 | 
			
		||||
    /// If `offset` is larger than the payload size of the `DirectoryEntry`, an
 | 
			
		||||
    /// empty buffer is returned.
 | 
			
		||||
    pub fn read(&mut self, entry: &DirectoryEntry, size: usize, offset: u64) -> Result<Vec<u8>, Error> {
 | 
			
		||||
        let start_offset = entry.payload_offset
 | 
			
		||||
            .ok_or_else(|| format_err!("entry has no payload offset"))?;
 | 
			
		||||
        if offset >= entry.size {
 | 
			
		||||
            return Ok(Vec::new());
 | 
			
		||||
        }
 | 
			
		||||
        let len = if u64::try_from(size)? > entry.size {
 | 
			
		||||
            usize::try_from(entry.size)?
 | 
			
		||||
        } else {
 | 
			
		||||
            size
 | 
			
		||||
        };
 | 
			
		||||
        self.seek(SeekFrom::Start(start_offset + offset))?;
 | 
			
		||||
        let data = self.inner.get_reader_mut().read_exact_allocated(len)?;
 | 
			
		||||
 | 
			
		||||
        Ok(data)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,118 +1,141 @@
 | 
			
		||||
use std::ffi::{OsStr, OsString};
 | 
			
		||||
use std::ffi::OsString;
 | 
			
		||||
use std::os::unix::io::{AsRawFd, RawFd};
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use anyhow::{format_err, Error};
 | 
			
		||||
use nix::errno::Errno;
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use nix::dir::Dir;
 | 
			
		||||
use nix::fcntl::OFlag;
 | 
			
		||||
use nix::sys::stat::Mode;
 | 
			
		||||
use nix::NixPath;
 | 
			
		||||
use nix::sys::stat::{mkdirat, Mode};
 | 
			
		||||
 | 
			
		||||
use super::format_definition::{PxarAttributes, PxarEntry};
 | 
			
		||||
use proxmox::sys::error::SysError;
 | 
			
		||||
use pxar::Metadata;
 | 
			
		||||
 | 
			
		||||
use crate::pxar::tools::{assert_relative_path, perms_from_metadata};
 | 
			
		||||
 | 
			
		||||
pub struct PxarDir {
 | 
			
		||||
    pub filename: OsString,
 | 
			
		||||
    pub entry: PxarEntry,
 | 
			
		||||
    pub attr: PxarAttributes,
 | 
			
		||||
    pub dir: Option<nix::dir::Dir>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct PxarDirStack {
 | 
			
		||||
    root: RawFd,
 | 
			
		||||
    data: Vec<PxarDir>,
 | 
			
		||||
    file_name: OsString,
 | 
			
		||||
    metadata: Metadata,
 | 
			
		||||
    dir: Option<Dir>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PxarDir {
 | 
			
		||||
    pub fn new(filename: &OsStr, entry: PxarEntry, attr: PxarAttributes) -> Self {
 | 
			
		||||
    pub fn new(file_name: OsString, metadata: Metadata) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            filename: filename.to_os_string(),
 | 
			
		||||
            entry,
 | 
			
		||||
            attr,
 | 
			
		||||
            file_name,
 | 
			
		||||
            metadata,
 | 
			
		||||
            dir: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn create_dir(&self, parent: RawFd, create_new: bool) -> Result<nix::dir::Dir, nix::Error> {
 | 
			
		||||
        let res = self
 | 
			
		||||
            .filename
 | 
			
		||||
            .with_nix_path(|cstr| unsafe { libc::mkdirat(parent, cstr.as_ptr(), libc::S_IRWXU) })?;
 | 
			
		||||
    pub fn with_dir(dir: Dir, metadata: Metadata) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            file_name: OsString::from("."),
 | 
			
		||||
            metadata,
 | 
			
		||||
            dir: Some(dir),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        match Errno::result(res) {
 | 
			
		||||
            Ok(_) => {}
 | 
			
		||||
    fn create_dir(&mut self, parent: RawFd, allow_existing_dirs: bool) -> Result<RawFd, Error> {
 | 
			
		||||
        match mkdirat(
 | 
			
		||||
            parent,
 | 
			
		||||
            self.file_name.as_os_str(),
 | 
			
		||||
            perms_from_metadata(&self.metadata)?,
 | 
			
		||||
        ) {
 | 
			
		||||
            Ok(()) => (),
 | 
			
		||||
            Err(err) => {
 | 
			
		||||
                if err == nix::Error::Sys(nix::errno::Errno::EEXIST) {
 | 
			
		||||
                    if create_new {
 | 
			
		||||
                        return Err(err);
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    return Err(err);
 | 
			
		||||
                if !(allow_existing_dirs && err.already_exists()) {
 | 
			
		||||
                    return Err(err.into());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let dir = nix::dir::Dir::openat(
 | 
			
		||||
        self.open_dir(parent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn open_dir(&mut self, parent: RawFd) -> Result<RawFd, Error> {
 | 
			
		||||
        let dir = Dir::openat(
 | 
			
		||||
            parent,
 | 
			
		||||
            self.filename.as_os_str(),
 | 
			
		||||
            self.file_name.as_os_str(),
 | 
			
		||||
            OFlag::O_DIRECTORY,
 | 
			
		||||
            Mode::empty(),
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
        Ok(dir)
 | 
			
		||||
        let fd = dir.as_raw_fd();
 | 
			
		||||
        self.dir = Some(dir);
 | 
			
		||||
 | 
			
		||||
        Ok(fd)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn try_as_raw_fd(&self) -> Option<RawFd> {
 | 
			
		||||
        self.dir.as_ref().map(AsRawFd::as_raw_fd)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn metadata(&self) -> &Metadata {
 | 
			
		||||
        &self.metadata
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct PxarDirStack {
 | 
			
		||||
    dirs: Vec<PxarDir>,
 | 
			
		||||
    path: PathBuf,
 | 
			
		||||
    created: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PxarDirStack {
 | 
			
		||||
    pub fn new(parent: RawFd) -> Self {
 | 
			
		||||
    pub fn new(root: Dir, metadata: Metadata) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            root: parent,
 | 
			
		||||
            data: Vec::new(),
 | 
			
		||||
            dirs: vec![PxarDir::with_dir(root, metadata)],
 | 
			
		||||
            path: PathBuf::from("/"),
 | 
			
		||||
            created: 1, // the root directory exists
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn push(&mut self, dir: PxarDir) {
 | 
			
		||||
        self.data.push(dir);
 | 
			
		||||
    pub fn is_empty(&self) -> bool {
 | 
			
		||||
        self.dirs.is_empty()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn pop(&mut self) -> Option<PxarDir> {
 | 
			
		||||
        self.data.pop()
 | 
			
		||||
    pub fn push(&mut self, file_name: OsString, metadata: Metadata) -> Result<(), Error> {
 | 
			
		||||
        assert_relative_path(&file_name)?;
 | 
			
		||||
        self.path.push(&file_name);
 | 
			
		||||
        self.dirs.push(PxarDir::new(file_name, metadata));
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn as_path_buf(&self) -> PathBuf {
 | 
			
		||||
        let path: PathBuf = self.data.iter().map(|d| d.filename.clone()).collect();
 | 
			
		||||
        path
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn last(&self) -> Option<&PxarDir> {
 | 
			
		||||
        self.data.last()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn last_mut(&mut self) -> Option<&mut PxarDir> {
 | 
			
		||||
        self.data.last_mut()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn last_dir_fd(&self) -> Option<RawFd> {
 | 
			
		||||
        let last_dir = self.data.last()?;
 | 
			
		||||
        match &last_dir.dir {
 | 
			
		||||
            Some(d) => Some(d.as_raw_fd()),
 | 
			
		||||
            None => None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn create_all_dirs(&mut self, create_new: bool) -> Result<RawFd, Error> {
 | 
			
		||||
        let mut current_fd = self.root;
 | 
			
		||||
        for d in &mut self.data {
 | 
			
		||||
            match &d.dir {
 | 
			
		||||
                Some(dir) => current_fd = dir.as_raw_fd(),
 | 
			
		||||
                None => {
 | 
			
		||||
                    let dir = d
 | 
			
		||||
                        .create_dir(current_fd, create_new)
 | 
			
		||||
                        .map_err(|err| format_err!("create dir failed - {}", err))?;
 | 
			
		||||
                    current_fd = dir.as_raw_fd();
 | 
			
		||||
                    d.dir = Some(dir);
 | 
			
		||||
                }
 | 
			
		||||
    pub fn pop(&mut self) -> Result<Option<PxarDir>, Error> {
 | 
			
		||||
        let out = self.dirs.pop();
 | 
			
		||||
        if !self.path.pop() {
 | 
			
		||||
            if self.path.as_os_str() == "/" {
 | 
			
		||||
                // we just finished the root directory, make sure this can only happen once:
 | 
			
		||||
                self.path = PathBuf::new();
 | 
			
		||||
            } else {
 | 
			
		||||
                bail!("lost track of path");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        self.created = self.created.min(self.dirs.len());
 | 
			
		||||
        Ok(out)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        Ok(current_fd)
 | 
			
		||||
    pub fn last_dir_fd(&mut self, allow_existing_dirs: bool) -> Result<RawFd, Error> {
 | 
			
		||||
        // should not be possible given the way we use it:
 | 
			
		||||
        assert!(!self.dirs.is_empty(), "PxarDirStack underrun");
 | 
			
		||||
 | 
			
		||||
        let mut fd = self.dirs[self.created - 1]
 | 
			
		||||
            .try_as_raw_fd()
 | 
			
		||||
            .ok_or_else(|| format_err!("lost track of directory file descriptors"))?;
 | 
			
		||||
        while self.created < self.dirs.len() {
 | 
			
		||||
            fd = self.dirs[self.created].create_dir(fd, allow_existing_dirs)?;
 | 
			
		||||
            self.created += 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(fd)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn root_dir_fd(&self) -> Result<RawFd, Error> {
 | 
			
		||||
        // should not be possible given the way we use it:
 | 
			
		||||
        assert!(!self.dirs.is_empty(), "PxarDirStack underrun");
 | 
			
		||||
 | 
			
		||||
        self.dirs[0]
 | 
			
		||||
            .try_as_raw_fd()
 | 
			
		||||
            .ok_or_else(|| format_err!("lost track of directory file descriptors"))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1332
									
								
								src/pxar/encoder.rs
									
									
									
									
									
								
							
							
						
						
									
										1332
									
								
								src/pxar/encoder.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										306
									
								
								src/pxar/extract.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								src/pxar/extract.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,306 @@
 | 
			
		||||
//! Code for extraction of pxar contents onto the file system.
 | 
			
		||||
 | 
			
		||||
use std::convert::TryFrom;
 | 
			
		||||
use std::ffi::{CStr, CString, OsStr};
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::os::unix::ffi::OsStrExt;
 | 
			
		||||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use nix::dir::Dir;
 | 
			
		||||
use nix::fcntl::OFlag;
 | 
			
		||||
use nix::sys::stat::Mode;
 | 
			
		||||
 | 
			
		||||
use pathpatterns::{MatchEntry, MatchList, MatchType};
 | 
			
		||||
use pxar::format::Device;
 | 
			
		||||
use pxar::Metadata;
 | 
			
		||||
 | 
			
		||||
use proxmox::c_result;
 | 
			
		||||
use proxmox::tools::fs::{create_path, CreateOptions};
 | 
			
		||||
 | 
			
		||||
use crate::pxar::dir_stack::PxarDirStack;
 | 
			
		||||
use crate::pxar::flags;
 | 
			
		||||
use crate::pxar::metadata;
 | 
			
		||||
 | 
			
		||||
struct Extractor<'a> {
 | 
			
		||||
    /// FIXME: use bitflags!() for feature_flags
 | 
			
		||||
    feature_flags: u64,
 | 
			
		||||
    allow_existing_dirs: bool,
 | 
			
		||||
    callback: &'a mut dyn FnMut(&Path),
 | 
			
		||||
    dir_stack: PxarDirStack,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Extractor<'a> {
 | 
			
		||||
    fn with_flag(&self, flag: u64) -> bool {
 | 
			
		||||
        flag == (self.feature_flags & flag)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn extract_archive<T, F>(
 | 
			
		||||
    mut decoder: pxar::decoder::Decoder<T>,
 | 
			
		||||
    destination: &Path,
 | 
			
		||||
    match_list: &[MatchEntry],
 | 
			
		||||
    feature_flags: u64,
 | 
			
		||||
    allow_existing_dirs: bool,
 | 
			
		||||
    mut callback: F,
 | 
			
		||||
) -> Result<(), Error>
 | 
			
		||||
where
 | 
			
		||||
    T: pxar::decoder::SeqRead,
 | 
			
		||||
    F: FnMut(&Path),
 | 
			
		||||
{
 | 
			
		||||
    // we use this to keep track of our directory-traversal
 | 
			
		||||
    decoder.enable_goodbye_entries(true);
 | 
			
		||||
 | 
			
		||||
    let root = decoder
 | 
			
		||||
        .next()
 | 
			
		||||
        .ok_or_else(|| format_err!("found empty pxar archive"))?
 | 
			
		||||
        .map_err(|err| format_err!("error reading pxar archive: {}", err))?;
 | 
			
		||||
 | 
			
		||||
    if !root.is_dir() {
 | 
			
		||||
        bail!("pxar archive does not start with a directory entry!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    create_path(
 | 
			
		||||
        &destination,
 | 
			
		||||
        None,
 | 
			
		||||
        Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
 | 
			
		||||
    )
 | 
			
		||||
    .map_err(|err| format_err!("error creating directory {:?}: {}", destination, err))?;
 | 
			
		||||
 | 
			
		||||
    let dir = Dir::open(
 | 
			
		||||
        destination,
 | 
			
		||||
        OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
 | 
			
		||||
        Mode::empty(),
 | 
			
		||||
    )
 | 
			
		||||
    .map_err(|err| format_err!("unable to open target directory {:?}: {}", destination, err,))?;
 | 
			
		||||
 | 
			
		||||
    let mut extractor = Extractor {
 | 
			
		||||
        feature_flags,
 | 
			
		||||
        allow_existing_dirs,
 | 
			
		||||
        callback: &mut callback,
 | 
			
		||||
        dir_stack: PxarDirStack::new(dir, root.metadata().clone()),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut match_stack = Vec::new();
 | 
			
		||||
    let mut current_match = true;
 | 
			
		||||
    while let Some(entry) = decoder.next() {
 | 
			
		||||
        use pxar::EntryKind;
 | 
			
		||||
 | 
			
		||||
        let entry = entry.map_err(|err| format_err!("error reading pxar archive: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let file_name_os = entry.file_name();
 | 
			
		||||
 | 
			
		||||
        // safety check: a file entry in an archive must never contain slashes:
 | 
			
		||||
        if file_name_os.as_bytes().contains(&b'/') {
 | 
			
		||||
            bail!("archive file entry contains slashes, which is invalid and a security concern");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let file_name = CString::new(file_name_os.as_bytes())
 | 
			
		||||
            .map_err(|_| format_err!("encountered file name with null-bytes"))?;
 | 
			
		||||
 | 
			
		||||
        let metadata = entry.metadata();
 | 
			
		||||
 | 
			
		||||
        let match_result = match_list.matches(
 | 
			
		||||
            entry.path().as_os_str().as_bytes(),
 | 
			
		||||
            Some(metadata.file_type() as u32),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let did_match = match match_result {
 | 
			
		||||
            Some(MatchType::Include) => true,
 | 
			
		||||
            Some(MatchType::Exclude) => false,
 | 
			
		||||
            None => current_match,
 | 
			
		||||
        };
 | 
			
		||||
        match (did_match, entry.kind()) {
 | 
			
		||||
            (_, EntryKind::Directory) => {
 | 
			
		||||
                extractor.callback(entry.path());
 | 
			
		||||
 | 
			
		||||
                extractor
 | 
			
		||||
                    .dir_stack
 | 
			
		||||
                    .push(file_name_os.to_owned(), metadata.clone())?;
 | 
			
		||||
 | 
			
		||||
                if current_match && match_result != Some(MatchType::Exclude) {
 | 
			
		||||
                    // We're currently in a positive match and this directory does not match an
 | 
			
		||||
                    // exclude entry, so make sure it is created:
 | 
			
		||||
                    let _ = extractor
 | 
			
		||||
                        .dir_stack
 | 
			
		||||
                        .last_dir_fd(extractor.allow_existing_dirs)
 | 
			
		||||
                        .map_err(|err| {
 | 
			
		||||
                            format_err!("error creating entry {:?}: {}", file_name_os, err)
 | 
			
		||||
                        })?;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // We're starting a new directory, push our old matching state and replace it with
 | 
			
		||||
                // our new one:
 | 
			
		||||
                match_stack.push(current_match);
 | 
			
		||||
                current_match = did_match;
 | 
			
		||||
 | 
			
		||||
                Ok(())
 | 
			
		||||
            }
 | 
			
		||||
            (_, EntryKind::GoodbyeTable) => {
 | 
			
		||||
                // go up a directory
 | 
			
		||||
                let dir = extractor
 | 
			
		||||
                    .dir_stack
 | 
			
		||||
                    .pop()
 | 
			
		||||
                    .map_err(|err| format_err!("unexpected end of directory entry: {}", err))?
 | 
			
		||||
                    .ok_or_else(|| format_err!("broken pxar archive (directory stack underrun)"))?;
 | 
			
		||||
                // We left a directory, also get back our previous matching state. This is in sync
 | 
			
		||||
                // with `dir_stack` so this should never be empty except for the final goodbye
 | 
			
		||||
                // table, in which case we get back to the default of `true`.
 | 
			
		||||
                current_match = match_stack.pop().unwrap_or(true);
 | 
			
		||||
 | 
			
		||||
                if let Some(fd) = dir.try_as_raw_fd() {
 | 
			
		||||
                    metadata::apply(extractor.feature_flags, dir.metadata(), fd, &file_name)
 | 
			
		||||
                } else {
 | 
			
		||||
                    Ok(())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            (true, EntryKind::Symlink(link)) => {
 | 
			
		||||
                extractor.callback(entry.path());
 | 
			
		||||
                extractor.extract_symlink(&file_name, metadata, link.as_ref())
 | 
			
		||||
            }
 | 
			
		||||
            (true, EntryKind::Hardlink(link)) => {
 | 
			
		||||
                extractor.callback(entry.path());
 | 
			
		||||
                extractor.extract_hardlink(&file_name, metadata, link.as_os_str())
 | 
			
		||||
            }
 | 
			
		||||
            (true, EntryKind::Device(dev)) => {
 | 
			
		||||
                if extractor.with_flag(flags::WITH_DEVICE_NODES) {
 | 
			
		||||
                    extractor.callback(entry.path());
 | 
			
		||||
                    extractor.extract_device(&file_name, metadata, dev)
 | 
			
		||||
                } else {
 | 
			
		||||
                    Ok(())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            (true, EntryKind::Fifo) => {
 | 
			
		||||
                if extractor.with_flag(flags::WITH_FIFOS) {
 | 
			
		||||
                    extractor.callback(entry.path());
 | 
			
		||||
                    extractor.extract_special(&file_name, metadata, 0)
 | 
			
		||||
                } else {
 | 
			
		||||
                    Ok(())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            (true, EntryKind::Socket) => {
 | 
			
		||||
                if extractor.with_flag(flags::WITH_SOCKETS) {
 | 
			
		||||
                    extractor.callback(entry.path());
 | 
			
		||||
                    extractor.extract_special(&file_name, metadata, 0)
 | 
			
		||||
                } else {
 | 
			
		||||
                    Ok(())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            (true, EntryKind::File { size, .. }) => extractor.extract_file(
 | 
			
		||||
                &file_name,
 | 
			
		||||
                metadata,
 | 
			
		||||
                *size,
 | 
			
		||||
                &mut decoder.contents().ok_or_else(|| {
 | 
			
		||||
                    format_err!("found regular file entry without contents in archive")
 | 
			
		||||
                })?,
 | 
			
		||||
            ),
 | 
			
		||||
            (false, _) => Ok(()), // skip this
 | 
			
		||||
        }
 | 
			
		||||
        .map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !extractor.dir_stack.is_empty() {
 | 
			
		||||
        bail!("unexpected eof while decoding pxar archive");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> Extractor<'a> {
 | 
			
		||||
    fn parent_fd(&mut self) -> Result<RawFd, Error> {
 | 
			
		||||
        self.dir_stack.last_dir_fd(self.allow_existing_dirs)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn callback(&mut self, path: &Path) {
 | 
			
		||||
        (self.callback)(path)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn extract_symlink(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        file_name: &CStr,
 | 
			
		||||
        metadata: &Metadata,
 | 
			
		||||
        link: &OsStr,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        let parent = self.parent_fd()?;
 | 
			
		||||
        nix::unistd::symlinkat(link, Some(parent), file_name)?;
 | 
			
		||||
        metadata::apply_at(self.feature_flags, metadata, parent, file_name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn extract_hardlink(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        file_name: &CStr,
 | 
			
		||||
        _metadata: &Metadata, // for now we don't use this because hardlinks don't need it...
 | 
			
		||||
        link: &OsStr,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        crate::pxar::tools::assert_relative_path(link)?;
 | 
			
		||||
 | 
			
		||||
        let parent = self.parent_fd()?;
 | 
			
		||||
        let root = self.dir_stack.root_dir_fd()?;
 | 
			
		||||
        let target = CString::new(link.as_bytes())?;
 | 
			
		||||
        nix::unistd::linkat(
 | 
			
		||||
            Some(root),
 | 
			
		||||
            target.as_c_str(),
 | 
			
		||||
            Some(parent),
 | 
			
		||||
            file_name,
 | 
			
		||||
            nix::unistd::LinkatFlags::NoSymlinkFollow,
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn extract_device(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        file_name: &CStr,
 | 
			
		||||
        metadata: &Metadata,
 | 
			
		||||
        device: &Device,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        self.extract_special(file_name, metadata, device.to_dev_t())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn extract_special(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        file_name: &CStr,
 | 
			
		||||
        metadata: &Metadata,
 | 
			
		||||
        device: libc::dev_t,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        let mode = metadata.stat.mode;
 | 
			
		||||
        let mode = u32::try_from(mode).map_err(|_| {
 | 
			
		||||
            format_err!(
 | 
			
		||||
                "device node's mode contains illegal bits: 0x{:x} (0o{:o})",
 | 
			
		||||
                mode,
 | 
			
		||||
                mode,
 | 
			
		||||
            )
 | 
			
		||||
        })?;
 | 
			
		||||
        let parent = self.parent_fd()?;
 | 
			
		||||
        unsafe { c_result!(libc::mknodat(parent, file_name.as_ptr(), mode, device)) }
 | 
			
		||||
            .map_err(|err| format_err!("failed to create device node: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        metadata::apply_at(self.feature_flags, metadata, parent, file_name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn extract_file(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        file_name: &CStr,
 | 
			
		||||
        metadata: &Metadata,
 | 
			
		||||
        size: u64,
 | 
			
		||||
        contents: &mut dyn io::Read,
 | 
			
		||||
    ) -> Result<(), Error> {
 | 
			
		||||
        let parent = self.parent_fd()?;
 | 
			
		||||
        let mut file = unsafe {
 | 
			
		||||
            std::fs::File::from_raw_fd(nix::fcntl::openat(
 | 
			
		||||
                parent,
 | 
			
		||||
                file_name,
 | 
			
		||||
                OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_CLOEXEC,
 | 
			
		||||
                Mode::from_bits(0o600).unwrap(),
 | 
			
		||||
            )?)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let extracted = io::copy(&mut *contents, &mut file)?;
 | 
			
		||||
        if size != extracted {
 | 
			
		||||
            bail!("extracted {} bytes of a file of {} bytes", extracted, size);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        metadata::apply(self.feature_flags, metadata, file.as_raw_fd(), file_name)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,8 @@
 | 
			
		||||
//! Flags for known supported features for a given filesystem can be derived
 | 
			
		||||
//! from the superblocks magic number.
 | 
			
		||||
 | 
			
		||||
// FIXME: use bitflags!() here!
 | 
			
		||||
 | 
			
		||||
/// FAT-style 2s time granularity
 | 
			
		||||
pub const WITH_2SEC_TIME: u64                   = 0x40;
 | 
			
		||||
/// Preserve read only flag of files
 | 
			
		||||
 | 
			
		||||
@ -1,263 +0,0 @@
 | 
			
		||||
//! *pxar* binary format definition
 | 
			
		||||
//!
 | 
			
		||||
//! Please note the all values are stored in little endian ordering.
 | 
			
		||||
//!
 | 
			
		||||
//! The Archive contains a list of items. Each item starts with a
 | 
			
		||||
//! `PxarHeader`, followed by the item data.
 | 
			
		||||
use std::cmp::Ordering;
 | 
			
		||||
 | 
			
		||||
use endian_trait::Endian;
 | 
			
		||||
use anyhow::{bail, Error};
 | 
			
		||||
use siphasher::sip::SipHasher24;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Header types identifying items stored in the archive
 | 
			
		||||
pub const PXAR_ENTRY: u64 = 0x1396fabcea5bbb51;
 | 
			
		||||
pub const PXAR_FILENAME: u64 = 0x6dbb6ebcb3161f0b;
 | 
			
		||||
pub const PXAR_SYMLINK: u64 = 0x664a6fb6830e0d6c;
 | 
			
		||||
pub const PXAR_DEVICE: u64 = 0xac3dace369dfe643;
 | 
			
		||||
pub const PXAR_XATTR: u64 = 0xb8157091f80bc486;
 | 
			
		||||
pub const PXAR_ACL_USER: u64 = 0x297dc88b2ef12faf;
 | 
			
		||||
pub const PXAR_ACL_GROUP: u64 = 0x36f2acb56cb3dd0b;
 | 
			
		||||
pub const PXAR_ACL_GROUP_OBJ: u64 = 0x23047110441f38f3;
 | 
			
		||||
pub const PXAR_ACL_DEFAULT: u64 = 0xfe3eeda6823c8cd0;
 | 
			
		||||
pub const PXAR_ACL_DEFAULT_USER: u64 = 0xbdf03df9bd010a91;
 | 
			
		||||
pub const PXAR_ACL_DEFAULT_GROUP: u64 = 0xa0cb1168782d1f51;
 | 
			
		||||
pub const PXAR_FCAPS: u64 = 0xf7267db0afed0629;
 | 
			
		||||
pub const PXAR_QUOTA_PROJID: u64 = 0x161baf2d8772a72b;
 | 
			
		||||
 | 
			
		||||
/// Marks item as hardlink
 | 
			
		||||
/// compute_goodbye_hash(b"__PROXMOX_FORMAT_HARDLINK__");
 | 
			
		||||
pub const PXAR_FORMAT_HARDLINK: u64 = 0x2c5e06f634f65b86;
 | 
			
		||||
/// Marks the beginning of the payload (actual content) of regular files
 | 
			
		||||
pub const PXAR_PAYLOAD: u64 = 0x8b9e1d93d6dcffc9;
 | 
			
		||||
/// Marks item as entry of goodbye table
 | 
			
		||||
pub const PXAR_GOODBYE: u64 = 0xdfd35c5e8327c403;
 | 
			
		||||
/// The end marker used in the GOODBYE object
 | 
			
		||||
pub const PXAR_GOODBYE_TAIL_MARKER: u64 = 0x57446fa533702943;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Endian)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarHeader {
 | 
			
		||||
    /// The item type (see `PXAR_` constants).
 | 
			
		||||
    pub htype: u64,
 | 
			
		||||
    /// The size of the item, including the size of `PxarHeader`.
 | 
			
		||||
    pub size: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Endian)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarEntry {
 | 
			
		||||
    pub mode: u64,
 | 
			
		||||
    pub flags: u64,
 | 
			
		||||
    pub uid: u32,
 | 
			
		||||
    pub gid: u32,
 | 
			
		||||
    pub mtime: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Endian)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarDevice {
 | 
			
		||||
    pub major: u64,
 | 
			
		||||
    pub minor: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Endian)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarGoodbyeItem {
 | 
			
		||||
    /// SipHash24 of the directory item name. The last GOODBYE item
 | 
			
		||||
    /// uses the special hash value `PXAR_GOODBYE_TAIL_MARKER`.
 | 
			
		||||
    pub hash: u64,
 | 
			
		||||
    /// The offset from the start of the GOODBYE object to the start
 | 
			
		||||
    /// of the matching directory item (point to a FILENAME). The last
 | 
			
		||||
    /// GOODBYE item points to the start of the matching ENTRY
 | 
			
		||||
    /// object.
 | 
			
		||||
    pub offset: u64,
 | 
			
		||||
    /// The overall size of the directory item. The last GOODBYE item
 | 
			
		||||
    /// repeats the size of the GOODBYE item.
 | 
			
		||||
    pub size: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Helper function to extract file names from binary archive.
 | 
			
		||||
pub fn read_os_string(buffer: &[u8]) -> std::ffi::OsString {
 | 
			
		||||
    let len = buffer.len();
 | 
			
		||||
 | 
			
		||||
    use std::os::unix::ffi::OsStrExt;
 | 
			
		||||
 | 
			
		||||
    let name = if len > 0 && buffer[len - 1] == 0 {
 | 
			
		||||
        std::ffi::OsStr::from_bytes(&buffer[0..len - 1])
 | 
			
		||||
    } else {
 | 
			
		||||
        std::ffi::OsStr::from_bytes(&buffer)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    name.into()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Eq)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarXAttr {
 | 
			
		||||
    pub name: Vec<u8>,
 | 
			
		||||
    pub value: Vec<u8>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Ord for PxarXAttr {
 | 
			
		||||
    fn cmp(&self, other: &PxarXAttr) -> Ordering {
 | 
			
		||||
        self.name.cmp(&other.name)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialOrd for PxarXAttr {
 | 
			
		||||
    fn partial_cmp(&self, other: &PxarXAttr) -> Option<Ordering> {
 | 
			
		||||
        Some(self.cmp(other))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialEq for PxarXAttr {
 | 
			
		||||
    fn eq(&self, other: &PxarXAttr) -> bool {
 | 
			
		||||
        self.name == other.name
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarFCaps {
 | 
			
		||||
    pub data: Vec<u8>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Endian, Eq)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarACLUser {
 | 
			
		||||
    pub uid: u64,
 | 
			
		||||
    pub permissions: u64,
 | 
			
		||||
    //pub name: Vec<u64>, not impl for now
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO if also name is impl, sort by uid, then by name and last by permissions
 | 
			
		||||
impl Ord for PxarACLUser {
 | 
			
		||||
    fn cmp(&self, other: &PxarACLUser) -> Ordering {
 | 
			
		||||
        match self.uid.cmp(&other.uid) {
 | 
			
		||||
            // uids are equal, entries ordered by permissions
 | 
			
		||||
            Ordering::Equal => self.permissions.cmp(&other.permissions),
 | 
			
		||||
            // uids are different, entries ordered by uid
 | 
			
		||||
            uid_order => uid_order,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialOrd for PxarACLUser {
 | 
			
		||||
    fn partial_cmp(&self, other: &PxarACLUser) -> Option<Ordering> {
 | 
			
		||||
        Some(self.cmp(other))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialEq for PxarACLUser {
 | 
			
		||||
    fn eq(&self, other: &PxarACLUser) -> bool {
 | 
			
		||||
        self.uid == other.uid && self.permissions == other.permissions
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Endian, Eq)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarACLGroup {
 | 
			
		||||
    pub gid: u64,
 | 
			
		||||
    pub permissions: u64,
 | 
			
		||||
    //pub name: Vec<u64>, not impl for now
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO if also name is impl, sort by gid, then by name and last by permissions
 | 
			
		||||
impl Ord for PxarACLGroup {
 | 
			
		||||
    fn cmp(&self, other: &PxarACLGroup) -> Ordering {
 | 
			
		||||
        match self.gid.cmp(&other.gid) {
 | 
			
		||||
            // gids are equal, entries are ordered by permissions
 | 
			
		||||
            Ordering::Equal => self.permissions.cmp(&other.permissions),
 | 
			
		||||
            // gids are different, entries ordered by gid
 | 
			
		||||
            gid_ordering => gid_ordering,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialOrd for PxarACLGroup {
 | 
			
		||||
    fn partial_cmp(&self, other: &PxarACLGroup) -> Option<Ordering> {
 | 
			
		||||
        Some(self.cmp(other))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl PartialEq for PxarACLGroup {
 | 
			
		||||
    fn eq(&self, other: &PxarACLGroup) -> bool {
 | 
			
		||||
        self.gid == other.gid && self.permissions == other.permissions
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Endian)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarACLGroupObj {
 | 
			
		||||
    pub permissions: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Endian)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarACLDefault {
 | 
			
		||||
    pub user_obj_permissions: u64,
 | 
			
		||||
    pub group_obj_permissions: u64,
 | 
			
		||||
    pub other_permissions: u64,
 | 
			
		||||
    pub mask_permissions: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) struct PxarACL {
 | 
			
		||||
    pub users: Vec<PxarACLUser>,
 | 
			
		||||
    pub groups: Vec<PxarACLGroup>,
 | 
			
		||||
    pub group_obj: Option<PxarACLGroupObj>,
 | 
			
		||||
    pub default: Option<PxarACLDefault>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub const PXAR_ACL_PERMISSION_READ: u64 = 4;
 | 
			
		||||
pub const PXAR_ACL_PERMISSION_WRITE: u64 = 2;
 | 
			
		||||
pub const PXAR_ACL_PERMISSION_EXECUTE: u64 = 1;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Endian)]
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
pub struct PxarQuotaProjID {
 | 
			
		||||
    pub projid: u64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Default)]
 | 
			
		||||
pub struct PxarAttributes {
 | 
			
		||||
    pub xattrs: Vec<PxarXAttr>,
 | 
			
		||||
    pub fcaps: Option<PxarFCaps>,
 | 
			
		||||
    pub quota_projid: Option<PxarQuotaProjID>,
 | 
			
		||||
    pub acl_user: Vec<PxarACLUser>,
 | 
			
		||||
    pub acl_group: Vec<PxarACLGroup>,
 | 
			
		||||
    pub acl_group_obj: Option<PxarACLGroupObj>,
 | 
			
		||||
    pub acl_default: Option<PxarACLDefault>,
 | 
			
		||||
    pub acl_default_user: Vec<PxarACLUser>,
 | 
			
		||||
    pub acl_default_group: Vec<PxarACLGroup>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create SipHash values for goodby tables.
 | 
			
		||||
//pub fn compute_goodbye_hash(name: &std::ffi::CStr) -> u64 {
 | 
			
		||||
pub fn compute_goodbye_hash(name: &[u8]) -> u64 {
 | 
			
		||||
    use std::hash::Hasher;
 | 
			
		||||
    let mut hasher = SipHasher24::new_with_keys(0x8574442b0f1d84b3, 0x2736ed30d1c22ec1);
 | 
			
		||||
    hasher.write(name);
 | 
			
		||||
    hasher.finish()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn check_ca_header<T>(head: &PxarHeader, htype: u64) -> Result<(), Error> {
 | 
			
		||||
    if head.htype != htype {
 | 
			
		||||
        bail!(
 | 
			
		||||
            "got wrong header type ({:016x} != {:016x})",
 | 
			
		||||
            head.htype,
 | 
			
		||||
            htype
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    if head.size != (std::mem::size_of::<T>() + std::mem::size_of::<PxarHeader>()) as u64 {
 | 
			
		||||
        bail!("got wrong header size for type {:016x}", htype);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// The format requires to build sorted directory lookup tables in
 | 
			
		||||
/// memory, so we restrict the number of allowed entries to limit
 | 
			
		||||
/// maximum memory usage.
 | 
			
		||||
pub const ENCODER_MAX_ENTRIES: usize = 1024 * 1024;
 | 
			
		||||
							
								
								
									
										1421
									
								
								src/pxar/fuse.rs
									
									
									
									
									
								
							
							
						
						
									
										1421
									
								
								src/pxar/fuse.rs
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,36 +0,0 @@
 | 
			
		||||
use libc;
 | 
			
		||||
use nix::sys::stat::FileStat;
 | 
			
		||||
 | 
			
		||||
#[inline(always)]
 | 
			
		||||
pub fn is_directory(stat: &FileStat) -> bool {
 | 
			
		||||
    (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[inline(always)]
 | 
			
		||||
pub fn is_symlink(stat: &FileStat) -> bool {
 | 
			
		||||
    (stat.st_mode & libc::S_IFMT) == libc::S_IFLNK
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[inline(always)]
 | 
			
		||||
pub fn is_reg_file(stat: &FileStat) -> bool {
 | 
			
		||||
    (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[inline(always)]
 | 
			
		||||
pub fn is_block_dev(stat: &FileStat) -> bool {
 | 
			
		||||
    (stat.st_mode & libc::S_IFMT) == libc::S_IFBLK
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[inline(always)]
 | 
			
		||||
pub fn is_char_dev(stat: &FileStat) -> bool {
 | 
			
		||||
    (stat.st_mode & libc::S_IFMT) == libc::S_IFCHR
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[inline(always)]
 | 
			
		||||
pub fn is_fifo(stat: &FileStat) -> bool {
 | 
			
		||||
    (stat.st_mode & libc::S_IFMT) == libc::S_IFIFO
 | 
			
		||||
}
 | 
			
		||||
#[inline(always)]
 | 
			
		||||
pub fn is_socket(stat: &FileStat) -> bool {
 | 
			
		||||
    (stat.st_mode & libc::S_IFMT) == libc::S_IFSOCK
 | 
			
		||||
}
 | 
			
		||||
@ -1,514 +0,0 @@
 | 
			
		||||
//! `MatchPattern` defines a match pattern used to match filenames encountered
 | 
			
		||||
//! during encoding or decoding of a `pxar` archive.
 | 
			
		||||
//! `fnmatch` is used internally to match filenames against the patterns.
 | 
			
		||||
//! Shell wildcard pattern can be used to match multiple filenames, see manpage
 | 
			
		||||
//! `glob(7)`.
 | 
			
		||||
//! `**` is treated special, as it matches multiple directories in a path.
 | 
			
		||||
 | 
			
		||||
use std::ffi::{CStr, CString};
 | 
			
		||||
use std::fs::File;
 | 
			
		||||
use std::io::Read;
 | 
			
		||||
use std::os::unix::io::{FromRawFd, RawFd};
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, Error};
 | 
			
		||||
use libc::{c_char, c_int};
 | 
			
		||||
use nix::errno::Errno;
 | 
			
		||||
use nix::fcntl;
 | 
			
		||||
use nix::fcntl::{AtFlags, OFlag};
 | 
			
		||||
use nix::sys::stat;
 | 
			
		||||
use nix::sys::stat::{FileStat, Mode};
 | 
			
		||||
use nix::NixPath;
 | 
			
		||||
 | 
			
		||||
pub const FNM_NOMATCH: c_int = 1;
 | 
			
		||||
 | 
			
		||||
extern "C" {
 | 
			
		||||
    fn fnmatch(pattern: *const c_char, string: *const c_char, flags: c_int) -> c_int;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, PartialEq, Clone, Copy)]
 | 
			
		||||
pub enum MatchType {
 | 
			
		||||
    None,
 | 
			
		||||
    Positive,
 | 
			
		||||
    Negative,
 | 
			
		||||
    PartialPositive,
 | 
			
		||||
    PartialNegative,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// `MatchPattern` provides functionality for filename glob pattern matching
 | 
			
		||||
/// based on glibc's `fnmatch`.
 | 
			
		||||
/// Positive matches return `MatchType::PartialPositive` or `MatchType::Positive`.
 | 
			
		||||
/// Patterns starting with `!` are interpreted as negation, meaning they will
 | 
			
		||||
/// return `MatchType::PartialNegative` or `MatchType::Negative`.
 | 
			
		||||
/// No matches result in `MatchType::None`.
 | 
			
		||||
/// # Examples:
 | 
			
		||||
/// ```
 | 
			
		||||
/// # use std::ffi::CString;
 | 
			
		||||
/// # use self::proxmox_backup::pxar::{MatchPattern, MatchType};
 | 
			
		||||
/// # fn main() -> Result<(), anyhow::Error> {
 | 
			
		||||
/// let filename = CString::new("some.conf")?;
 | 
			
		||||
/// let is_dir = false;
 | 
			
		||||
///
 | 
			
		||||
/// /// Positive match of any file ending in `.conf` in any subdirectory
 | 
			
		||||
/// let positive = MatchPattern::from_line(b"**/*.conf")?.unwrap();
 | 
			
		||||
/// let m_positive = positive.as_slice().matches_filename(&filename, is_dir)?;
 | 
			
		||||
/// assert!(m_positive == MatchType::Positive);
 | 
			
		||||
///
 | 
			
		||||
/// /// Negative match of filenames starting with `s`
 | 
			
		||||
/// let negative = MatchPattern::from_line(b"![s]*")?.unwrap();
 | 
			
		||||
/// let m_negative = negative.as_slice().matches_filename(&filename, is_dir)?;
 | 
			
		||||
/// assert!(m_negative == MatchType::Negative);
 | 
			
		||||
/// # Ok(())
 | 
			
		||||
/// # }
 | 
			
		||||
/// ```
 | 
			
		||||
#[derive(Clone, Eq, PartialOrd)]
 | 
			
		||||
pub struct MatchPattern {
 | 
			
		||||
    pattern: Vec<u8>,
 | 
			
		||||
    match_positive: bool,
 | 
			
		||||
    match_dir_only: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::cmp::PartialEq for MatchPattern {
 | 
			
		||||
    fn eq(&self, other: &Self) -> bool {
 | 
			
		||||
        self.pattern == other.pattern
 | 
			
		||||
        && self.match_positive == other.match_positive
 | 
			
		||||
        && self.match_dir_only == other.match_dir_only
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::cmp::Ord for MatchPattern {
 | 
			
		||||
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
 | 
			
		||||
        (&self.pattern, &self.match_positive, &self.match_dir_only)
 | 
			
		||||
            .cmp(&(&other.pattern, &other.match_positive, &other.match_dir_only))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MatchPattern {
 | 
			
		||||
    /// Read a list of `MatchPattern` from file.
 | 
			
		||||
    /// The file is read line by line (lines terminated by newline character),
 | 
			
		||||
    /// each line may only contain one pattern.
 | 
			
		||||
    /// Leading `/` are ignored and lines starting with `#` are interpreted as
 | 
			
		||||
    /// comments and not included in the resulting list.
 | 
			
		||||
    /// Patterns ending in `/` will match only directories.
 | 
			
		||||
    ///
 | 
			
		||||
    /// On success, a list of match pattern is returned as well as the raw file
 | 
			
		||||
    /// byte buffer together with the files stats.
 | 
			
		||||
    /// This is done in order to avoid reading the file more than once during
 | 
			
		||||
    /// encoding of the archive.
 | 
			
		||||
    pub fn from_file<P: ?Sized + NixPath>(
 | 
			
		||||
        parent_fd: RawFd,
 | 
			
		||||
        filename: &P,
 | 
			
		||||
    ) -> Result<Option<(Vec<MatchPattern>, Vec<u8>, FileStat)>, nix::Error> {
 | 
			
		||||
        let stat = match stat::fstatat(parent_fd, filename, AtFlags::AT_SYMLINK_NOFOLLOW) {
 | 
			
		||||
            Ok(stat) => stat,
 | 
			
		||||
            Err(nix::Error::Sys(Errno::ENOENT)) => return Ok(None),
 | 
			
		||||
            Err(err) => return Err(err),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let filefd = fcntl::openat(parent_fd, filename, OFlag::O_NOFOLLOW, Mode::empty())?;
 | 
			
		||||
        let mut file = unsafe { File::from_raw_fd(filefd) };
 | 
			
		||||
 | 
			
		||||
        let mut content_buffer = Vec::new();
 | 
			
		||||
        let _bytes = file.read_to_end(&mut content_buffer)
 | 
			
		||||
            .map_err(|_| Errno::EIO)?;
 | 
			
		||||
 | 
			
		||||
        let mut match_pattern = Vec::new();
 | 
			
		||||
        for line in content_buffer.split(|&c| c == b'\n') {
 | 
			
		||||
            if line.is_empty() {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            if let Some(pattern) = Self::from_line(line)? {
 | 
			
		||||
                match_pattern.push(pattern);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(Some((match_pattern, content_buffer, stat)))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Interpret a byte buffer as a sinlge line containing a valid
 | 
			
		||||
    /// `MatchPattern`.
 | 
			
		||||
    /// Pattern starting with `#` are interpreted as comments, returning `Ok(None)`.
 | 
			
		||||
    /// Pattern starting with '!' are interpreted as negative match pattern.
 | 
			
		||||
    /// Pattern with trailing `/` match only against directories.
 | 
			
		||||
    /// `.` as well as `..` and any pattern containing `\0` are invalid and will
 | 
			
		||||
    /// result in an error with Errno::EINVAL.
 | 
			
		||||
    pub fn from_line(line: &[u8]) -> Result<Option<MatchPattern>, nix::Error> {
 | 
			
		||||
        let mut input = line;
 | 
			
		||||
 | 
			
		||||
        if input.starts_with(b"#") {
 | 
			
		||||
            return Ok(None);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let match_positive = if input.starts_with(b"!") {
 | 
			
		||||
            // Reduce slice view to exclude "!"
 | 
			
		||||
            input = &input[1..];
 | 
			
		||||
            false
 | 
			
		||||
        } else {
 | 
			
		||||
            true
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Paths ending in / match only directory names (no filenames)
 | 
			
		||||
        let match_dir_only = if input.ends_with(b"/") {
 | 
			
		||||
            let len = input.len();
 | 
			
		||||
            input = &input[..len - 1];
 | 
			
		||||
            true
 | 
			
		||||
        } else {
 | 
			
		||||
            false
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Ignore initial slash
 | 
			
		||||
        if input.starts_with(b"/") {
 | 
			
		||||
            input = &input[1..];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if input.is_empty() || input == b"." || input == b".." || input.contains(&b'\0') {
 | 
			
		||||
            return Err(nix::Error::Sys(Errno::EINVAL));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(Some(MatchPattern {
 | 
			
		||||
            pattern: input.to_vec(),
 | 
			
		||||
            match_positive,
 | 
			
		||||
            match_dir_only,
 | 
			
		||||
        }))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// Create a `MatchPatternSlice` of the `MatchPattern` to give a view of the
 | 
			
		||||
    /// `MatchPattern` without copying its content.
 | 
			
		||||
    pub fn as_slice<'a>(&'a self) -> MatchPatternSlice<'a> {
 | 
			
		||||
        MatchPatternSlice {
 | 
			
		||||
            pattern: self.pattern.as_slice(),
 | 
			
		||||
            match_positive: self.match_positive,
 | 
			
		||||
            match_dir_only: self.match_dir_only,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Dump the content of the `MatchPattern` to stdout.
 | 
			
		||||
    /// Intended for debugging purposes only.
 | 
			
		||||
    pub fn dump(&self) {
 | 
			
		||||
        match (self.match_positive, self.match_dir_only) {
 | 
			
		||||
            (true, true) => println!("{:#?}/", self.pattern),
 | 
			
		||||
            (true, false) => println!("{:#?}", self.pattern),
 | 
			
		||||
            (false, true) => println!("!{:#?}/", self.pattern),
 | 
			
		||||
            (false, false) => println!("!{:#?}", self.pattern),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Convert a list of MatchPattern to bytes in order to write them to e.g.
 | 
			
		||||
    /// a file.
 | 
			
		||||
    pub fn to_bytes(patterns: &[MatchPattern]) -> Vec<u8> {
 | 
			
		||||
        let mut slices = Vec::new();
 | 
			
		||||
        for pattern in patterns {
 | 
			
		||||
            slices.push(pattern.as_slice());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        MatchPatternSlice::to_bytes(&slices)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Invert the match type for this MatchPattern.
 | 
			
		||||
    pub fn invert(&mut self) {
 | 
			
		||||
        self.match_positive = !self.match_positive;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct MatchPatternSlice<'a> {
 | 
			
		||||
    pattern: &'a [u8],
 | 
			
		||||
    match_positive: bool,
 | 
			
		||||
    match_dir_only: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<'a> MatchPatternSlice<'a> {
 | 
			
		||||
    /// Returns the pattern before the first `/` encountered as `MatchPatternSlice`.
 | 
			
		||||
    /// If no slash is encountered, the `MatchPatternSlice` will be a copy of the
 | 
			
		||||
    /// original pattern.
 | 
			
		||||
    /// ```
 | 
			
		||||
    /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
 | 
			
		||||
    /// # fn main() -> Result<(), anyhow::Error> {
 | 
			
		||||
    /// let pattern = MatchPattern::from_line(b"some/match/pattern/")?.unwrap();
 | 
			
		||||
    /// let slice = pattern.as_slice();
 | 
			
		||||
    /// let front = slice.get_front_pattern();
 | 
			
		||||
    /// /// ... will be the same as ...
 | 
			
		||||
    /// let front_pattern = MatchPattern::from_line(b"some")?.unwrap();
 | 
			
		||||
    /// let front_slice = front_pattern.as_slice();
 | 
			
		||||
    /// # Ok(())
 | 
			
		||||
    /// # }
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn get_front_pattern(&'a self) -> MatchPatternSlice<'a> {
 | 
			
		||||
        let (front, _) = self.split_at_slash();
 | 
			
		||||
        MatchPatternSlice {
 | 
			
		||||
            pattern: front,
 | 
			
		||||
            match_positive: self.match_positive,
 | 
			
		||||
            match_dir_only: self.match_dir_only,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Returns the pattern after the first encountered `/` as `MatchPatternSlice`.
 | 
			
		||||
    /// If no slash is encountered, the `MatchPatternSlice` will be empty.
 | 
			
		||||
    /// ```
 | 
			
		||||
    /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
 | 
			
		||||
    /// # fn main() -> Result<(), anyhow::Error> {
 | 
			
		||||
    /// let pattern = MatchPattern::from_line(b"some/match/pattern/")?.unwrap();
 | 
			
		||||
    /// let slice = pattern.as_slice();
 | 
			
		||||
    /// let rest = slice.get_rest_pattern();
 | 
			
		||||
    /// /// ... will be the same as ...
 | 
			
		||||
    /// let rest_pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
 | 
			
		||||
    /// let rest_slice = rest_pattern.as_slice();
 | 
			
		||||
    /// # Ok(())
 | 
			
		||||
    /// # }
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn get_rest_pattern(&'a self) -> MatchPatternSlice<'a> {
 | 
			
		||||
        let (_, rest) = self.split_at_slash();
 | 
			
		||||
        MatchPatternSlice {
 | 
			
		||||
            pattern: rest,
 | 
			
		||||
            match_positive: self.match_positive,
 | 
			
		||||
            match_dir_only: self.match_dir_only,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Splits the `MatchPatternSlice` at the first slash encountered and returns the
 | 
			
		||||
    /// content before (front pattern) and after the slash (rest pattern),
 | 
			
		||||
    /// omitting the slash itself.
 | 
			
		||||
    /// Slices starting with `**/` are an exception to this, as the corresponding
 | 
			
		||||
    /// `MatchPattern` is intended to match multiple directories.
 | 
			
		||||
    /// These pattern slices therefore return a `*` as front pattern and the original
 | 
			
		||||
    /// pattern itself as rest pattern.
 | 
			
		||||
    fn split_at_slash(&'a self) -> (&'a [u8], &'a [u8]) {
 | 
			
		||||
        let pattern = if self.pattern.starts_with(b"./") {
 | 
			
		||||
            &self.pattern[2..]
 | 
			
		||||
        } else {
 | 
			
		||||
            self.pattern
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let (mut front, mut rest) = match pattern.iter().position(|&c| c == b'/') {
 | 
			
		||||
            Some(ind) => {
 | 
			
		||||
                let (front, rest) = pattern.split_at(ind);
 | 
			
		||||
                (front, &rest[1..])
 | 
			
		||||
            }
 | 
			
		||||
            None => (pattern, &pattern[0..0]),
 | 
			
		||||
        };
 | 
			
		||||
        // '**' is treated such that it maches any directory
 | 
			
		||||
        if front == b"**" {
 | 
			
		||||
            front = b"*";
 | 
			
		||||
            rest = pattern;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        (front, rest)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Convert a list of `MatchPatternSlice`s to bytes in order to write them to e.g.
 | 
			
		||||
    /// a file.
 | 
			
		||||
    pub fn to_bytes(patterns: &[MatchPatternSlice]) -> Vec<u8> {
 | 
			
		||||
        let mut buffer = Vec::new();
 | 
			
		||||
        for pattern in patterns {
 | 
			
		||||
            if !pattern.match_positive { buffer.push(b'!'); }
 | 
			
		||||
            buffer.extend_from_slice(&pattern.pattern);
 | 
			
		||||
            if pattern.match_dir_only { buffer.push(b'/'); }
 | 
			
		||||
            buffer.push(b'\n');
 | 
			
		||||
        }
 | 
			
		||||
        buffer
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Match the given filename against this `MatchPatternSlice`.
 | 
			
		||||
    /// If the filename matches the pattern completely, `MatchType::Positive` or
 | 
			
		||||
    /// `MatchType::Negative` is returned, depending if the match pattern is was
 | 
			
		||||
    /// declared as positive (no `!` prefix) or negative (`!` prefix).
 | 
			
		||||
    /// If the pattern matched only up to the first slash of the pattern,
 | 
			
		||||
    /// `MatchType::PartialPositive` or `MatchType::PartialNegatie` is returned.
 | 
			
		||||
    /// If the pattern was postfixed by a trailing `/` a match is only valid if
 | 
			
		||||
    /// the parameter `is_dir` equals `true`.
 | 
			
		||||
    /// No match results in `MatchType::None`.
 | 
			
		||||
    pub fn matches_filename(&self, filename: &CStr, is_dir: bool) -> Result<MatchType, Error> {
 | 
			
		||||
        let mut res = MatchType::None;
 | 
			
		||||
        let (front, _) = self.split_at_slash();
 | 
			
		||||
 | 
			
		||||
        let front = CString::new(front).unwrap();
 | 
			
		||||
        let fnmatch_res = unsafe {
 | 
			
		||||
            let front_ptr = front.as_ptr() as *const libc::c_char;
 | 
			
		||||
            let filename_ptr = filename.as_ptr() as *const libc::c_char;
 | 
			
		||||
            fnmatch(front_ptr, filename_ptr, 0)
 | 
			
		||||
        };
 | 
			
		||||
        if fnmatch_res < 0 {
 | 
			
		||||
            bail!("error in fnmatch inside of MatchPattern");
 | 
			
		||||
        }
 | 
			
		||||
        if fnmatch_res == 0 {
 | 
			
		||||
            res = if self.match_positive {
 | 
			
		||||
                MatchType::PartialPositive
 | 
			
		||||
            } else {
 | 
			
		||||
                MatchType::PartialNegative
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let full = if self.pattern.starts_with(b"**/") {
 | 
			
		||||
            CString::new(&self.pattern[3..]).unwrap()
 | 
			
		||||
        } else {
 | 
			
		||||
            CString::new(&self.pattern[..]).unwrap()
 | 
			
		||||
        };
 | 
			
		||||
        let fnmatch_res = unsafe {
 | 
			
		||||
            let full_ptr = full.as_ptr() as *const libc::c_char;
 | 
			
		||||
            let filename_ptr = filename.as_ptr() as *const libc::c_char;
 | 
			
		||||
            fnmatch(full_ptr, filename_ptr, 0)
 | 
			
		||||
        };
 | 
			
		||||
        if fnmatch_res < 0 {
 | 
			
		||||
            bail!("error in fnmatch inside of MatchPattern");
 | 
			
		||||
        }
 | 
			
		||||
        if fnmatch_res == 0 {
 | 
			
		||||
            res = if self.match_positive {
 | 
			
		||||
                MatchType::Positive
 | 
			
		||||
            } else {
 | 
			
		||||
                MatchType::Negative
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !is_dir && self.match_dir_only {
 | 
			
		||||
            res = MatchType::None;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !is_dir && (res == MatchType::PartialPositive || res == MatchType::PartialNegative) {
 | 
			
		||||
            res = MatchType::None;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(res)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Match the given filename against the set of `MatchPatternSlice`s.
 | 
			
		||||
    ///
 | 
			
		||||
    /// A positive match is intended to includes the full subtree (unless another
 | 
			
		||||
    /// negative match excludes entries later).
 | 
			
		||||
    /// The `MatchType` together with an updated `MatchPatternSlice` list for passing
 | 
			
		||||
    /// to the matched child is returned.
 | 
			
		||||
    /// ```
 | 
			
		||||
    /// # use std::ffi::CString;
 | 
			
		||||
    /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
 | 
			
		||||
    /// # fn main() -> Result<(), anyhow::Error> {
 | 
			
		||||
    /// let patterns = vec![
 | 
			
		||||
    ///     MatchPattern::from_line(b"some/match/pattern/")?.unwrap(),
 | 
			
		||||
    ///     MatchPattern::from_line(b"to_match/")?.unwrap()
 | 
			
		||||
    /// ];
 | 
			
		||||
    /// let mut slices = Vec::new();
 | 
			
		||||
    /// for pattern in &patterns {
 | 
			
		||||
    ///     slices.push(pattern.as_slice());
 | 
			
		||||
    /// }
 | 
			
		||||
    /// let filename = CString::new("some")?;
 | 
			
		||||
    /// let is_dir = true;
 | 
			
		||||
    /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_include(
 | 
			
		||||
    ///     &filename,
 | 
			
		||||
    ///     is_dir,
 | 
			
		||||
    ///     &slices
 | 
			
		||||
    /// )?;
 | 
			
		||||
    /// assert_eq!(match_type, MatchType::PartialPositive);
 | 
			
		||||
    /// /// child pattern will be the same as ...
 | 
			
		||||
    /// let pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
 | 
			
		||||
    /// let slice = pattern.as_slice();
 | 
			
		||||
    ///
 | 
			
		||||
    /// let filename = CString::new("to_match")?;
 | 
			
		||||
    /// let is_dir = true;
 | 
			
		||||
    /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_include(
 | 
			
		||||
    ///     &filename,
 | 
			
		||||
    ///     is_dir,
 | 
			
		||||
    ///     &slices
 | 
			
		||||
    /// )?;
 | 
			
		||||
    /// assert_eq!(match_type, MatchType::Positive);
 | 
			
		||||
    /// /// child pattern will be the same as ...
 | 
			
		||||
    /// let pattern = MatchPattern::from_line(b"**/*")?.unwrap();
 | 
			
		||||
    /// let slice = pattern.as_slice();
 | 
			
		||||
    /// # Ok(())
 | 
			
		||||
    /// # }
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn match_filename_include(
 | 
			
		||||
        filename: &CStr,
 | 
			
		||||
        is_dir: bool,
 | 
			
		||||
        match_pattern: &'a [MatchPatternSlice<'a>],
 | 
			
		||||
    ) -> Result<(MatchType, Vec<MatchPatternSlice<'a>>), Error> {
 | 
			
		||||
        let mut child_pattern = Vec::new();
 | 
			
		||||
        let mut match_state = MatchType::None;
 | 
			
		||||
 | 
			
		||||
        for pattern in match_pattern {
 | 
			
		||||
            match pattern.matches_filename(filename, is_dir)? {
 | 
			
		||||
                MatchType::None => continue,
 | 
			
		||||
                MatchType::Positive => match_state = MatchType::Positive,
 | 
			
		||||
                MatchType::Negative => match_state = MatchType::Negative,
 | 
			
		||||
                MatchType::PartialPositive => {
 | 
			
		||||
                    if match_state != MatchType::Negative && match_state != MatchType::Positive {
 | 
			
		||||
                        match_state = MatchType::PartialPositive;
 | 
			
		||||
                    }
 | 
			
		||||
                    child_pattern.push(pattern.get_rest_pattern());
 | 
			
		||||
                }
 | 
			
		||||
                MatchType::PartialNegative => {
 | 
			
		||||
                    if match_state == MatchType::PartialPositive {
 | 
			
		||||
                        match_state = MatchType::PartialNegative;
 | 
			
		||||
                    }
 | 
			
		||||
                    child_pattern.push(pattern.get_rest_pattern());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok((match_state, child_pattern))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Match the given filename against the set of `MatchPatternSlice`s.
 | 
			
		||||
    ///
 | 
			
		||||
    /// A positive match is intended to exclude the full subtree, independent of
 | 
			
		||||
    /// matches deeper down the tree.
 | 
			
		||||
    /// The `MatchType` together with an updated `MatchPattern` list for passing
 | 
			
		||||
    /// to the matched child is returned.
 | 
			
		||||
    /// ```
 | 
			
		||||
    /// # use std::ffi::CString;
 | 
			
		||||
    /// # use self::proxmox_backup::pxar::{MatchPattern, MatchPatternSlice, MatchType};
 | 
			
		||||
    /// # fn main() -> Result<(), anyhow::Error> {
 | 
			
		||||
    /// let patterns = vec![
 | 
			
		||||
    ///     MatchPattern::from_line(b"some/match/pattern/")?.unwrap(),
 | 
			
		||||
    ///     MatchPattern::from_line(b"to_match/")?.unwrap()
 | 
			
		||||
    /// ];
 | 
			
		||||
    /// let mut slices = Vec::new();
 | 
			
		||||
    /// for pattern in &patterns {
 | 
			
		||||
    ///     slices.push(pattern.as_slice());
 | 
			
		||||
    /// }
 | 
			
		||||
    /// let filename = CString::new("some")?;
 | 
			
		||||
    /// let is_dir = true;
 | 
			
		||||
    /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_exclude(
 | 
			
		||||
    ///     &filename,
 | 
			
		||||
    ///     is_dir,
 | 
			
		||||
    ///     &slices,
 | 
			
		||||
    /// )?;
 | 
			
		||||
    /// assert_eq!(match_type, MatchType::PartialPositive);
 | 
			
		||||
    /// /// child pattern will be the same as ...
 | 
			
		||||
    /// let pattern = MatchPattern::from_line(b"match/pattern/")?.unwrap();
 | 
			
		||||
    /// let slice = pattern.as_slice();
 | 
			
		||||
    ///
 | 
			
		||||
    /// let filename = CString::new("to_match")?;
 | 
			
		||||
    /// let is_dir = true;
 | 
			
		||||
    /// let (match_type, child_pattern) = MatchPatternSlice::match_filename_exclude(
 | 
			
		||||
    ///     &filename,
 | 
			
		||||
    ///     is_dir,
 | 
			
		||||
    ///     &slices,
 | 
			
		||||
    /// )?;
 | 
			
		||||
    /// assert_eq!(match_type, MatchType::Positive);
 | 
			
		||||
    /// /// child pattern will be empty
 | 
			
		||||
    /// # Ok(())
 | 
			
		||||
    /// # }
 | 
			
		||||
    /// ```
 | 
			
		||||
    pub fn match_filename_exclude(
 | 
			
		||||
        filename: &CStr,
 | 
			
		||||
        is_dir: bool,
 | 
			
		||||
        match_pattern: &'a [MatchPatternSlice<'a>],
 | 
			
		||||
    ) -> Result<(MatchType, Vec<MatchPatternSlice<'a>>), Error> {
 | 
			
		||||
        let mut child_pattern = Vec::new();
 | 
			
		||||
        let mut match_state = MatchType::None;
 | 
			
		||||
 | 
			
		||||
        for pattern in match_pattern {
 | 
			
		||||
            match pattern.matches_filename(filename, is_dir)? {
 | 
			
		||||
                MatchType::None => {}
 | 
			
		||||
                MatchType::Positive => match_state = MatchType::Positive,
 | 
			
		||||
                MatchType::Negative => match_state = MatchType::Negative,
 | 
			
		||||
                match_type => {
 | 
			
		||||
                    if match_state != MatchType::Positive && match_state != MatchType::Negative {
 | 
			
		||||
                        match_state = match_type;
 | 
			
		||||
                    }
 | 
			
		||||
                    child_pattern.push(pattern.get_rest_pattern());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok((match_state, child_pattern))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										342
									
								
								src/pxar/metadata.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										342
									
								
								src/pxar/metadata.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,342 @@
 | 
			
		||||
use std::ffi::{CStr, CString};
 | 
			
		||||
use std::os::unix::ffi::OsStrExt;
 | 
			
		||||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use nix::errno::Errno;
 | 
			
		||||
use nix::fcntl::OFlag;
 | 
			
		||||
use nix::sys::stat::Mode;
 | 
			
		||||
 | 
			
		||||
use pxar::Metadata;
 | 
			
		||||
 | 
			
		||||
use proxmox::sys::error::SysError;
 | 
			
		||||
use proxmox::tools::fd::RawFdNum;
 | 
			
		||||
use proxmox::{c_result, c_try};
 | 
			
		||||
 | 
			
		||||
use crate::pxar::flags;
 | 
			
		||||
use crate::pxar::tools::perms_from_metadata;
 | 
			
		||||
use crate::tools::{acl, fs, xattr};
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// utility functions
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
fn flags_contain(flags: u64, test_flag: u64) -> bool {
 | 
			
		||||
    0 != (flags & test_flag)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn allow_notsupp<E: SysError>(err: E) -> Result<(), E> {
 | 
			
		||||
    if err.is_errno(Errno::EOPNOTSUPP) {
 | 
			
		||||
        Ok(())
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(err)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn allow_notsupp_remember<E: SysError>(err: E, not_supp: &mut bool) -> Result<(), E> {
 | 
			
		||||
    if err.is_errno(Errno::EOPNOTSUPP) {
 | 
			
		||||
        *not_supp = true;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(err)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn nsec_to_update_timespec(mtime_nsec: u64) -> [libc::timespec; 2] {
 | 
			
		||||
    // restore mtime
 | 
			
		||||
    const UTIME_OMIT: i64 = (1 << 30) - 2;
 | 
			
		||||
    const NANOS_PER_SEC: i64 = 1_000_000_000;
 | 
			
		||||
 | 
			
		||||
    let sec = (mtime_nsec as i64) / NANOS_PER_SEC;
 | 
			
		||||
    let nsec = (mtime_nsec as i64) % NANOS_PER_SEC;
 | 
			
		||||
 | 
			
		||||
    let times: [libc::timespec; 2] = [
 | 
			
		||||
        libc::timespec {
 | 
			
		||||
            tv_sec: 0,
 | 
			
		||||
            tv_nsec: UTIME_OMIT,
 | 
			
		||||
        },
 | 
			
		||||
        libc::timespec {
 | 
			
		||||
            tv_sec: sec,
 | 
			
		||||
            tv_nsec: nsec,
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    times
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// metadata application:
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
pub fn apply_at(
 | 
			
		||||
    flags: u64,
 | 
			
		||||
    metadata: &Metadata,
 | 
			
		||||
    parent: RawFd,
 | 
			
		||||
    file_name: &CStr,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    let fd = proxmox::tools::fd::Fd::openat(
 | 
			
		||||
        &unsafe { RawFdNum::from_raw_fd(parent) },
 | 
			
		||||
        file_name,
 | 
			
		||||
        OFlag::O_PATH | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW,
 | 
			
		||||
        Mode::empty(),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    apply(flags, metadata, fd.as_raw_fd(), file_name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn apply_with_path<T: AsRef<Path>>(
 | 
			
		||||
    flags: u64,
 | 
			
		||||
    metadata: &Metadata,
 | 
			
		||||
    fd: RawFd,
 | 
			
		||||
    file_name: T,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    apply(
 | 
			
		||||
        flags,
 | 
			
		||||
        metadata,
 | 
			
		||||
        fd,
 | 
			
		||||
        &CString::new(file_name.as_ref().as_os_str().as_bytes())?,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn apply(flags: u64, metadata: &Metadata, fd: RawFd, file_name: &CStr) -> Result<(), Error> {
 | 
			
		||||
    let c_proc_path = CString::new(format!("/proc/self/fd/{}", fd)).unwrap();
 | 
			
		||||
    let c_proc_path = c_proc_path.as_ptr();
 | 
			
		||||
 | 
			
		||||
    if metadata.stat.flags != 0 {
 | 
			
		||||
        todo!("apply flags!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    unsafe {
 | 
			
		||||
        // UID and GID first, as this fails if we lose access anyway.
 | 
			
		||||
        c_result!(libc::chown(
 | 
			
		||||
            c_proc_path,
 | 
			
		||||
            metadata.stat.uid,
 | 
			
		||||
            metadata.stat.gid
 | 
			
		||||
        ))
 | 
			
		||||
        .map(drop)
 | 
			
		||||
        .or_else(allow_notsupp)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut skip_xattrs = false;
 | 
			
		||||
    apply_xattrs(flags, c_proc_path, metadata, &mut skip_xattrs)?;
 | 
			
		||||
    add_fcaps(flags, c_proc_path, metadata, &mut skip_xattrs)?;
 | 
			
		||||
    apply_acls(flags, c_proc_path, metadata)?;
 | 
			
		||||
    apply_quota_project_id(flags, fd, metadata)?;
 | 
			
		||||
 | 
			
		||||
    // Finally mode and time. We may lose access with mode, but the changing the mode also
 | 
			
		||||
    // affects times.
 | 
			
		||||
    if !metadata.is_symlink() {
 | 
			
		||||
        c_result!(unsafe { libc::chmod(c_proc_path, perms_from_metadata(metadata)?.bits()) })
 | 
			
		||||
            .map(drop)
 | 
			
		||||
            .or_else(allow_notsupp)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let res = c_result!(unsafe {
 | 
			
		||||
        libc::utimensat(
 | 
			
		||||
            libc::AT_FDCWD,
 | 
			
		||||
            c_proc_path,
 | 
			
		||||
            nsec_to_update_timespec(metadata.stat.mtime).as_ptr(),
 | 
			
		||||
            0,
 | 
			
		||||
        )
 | 
			
		||||
    });
 | 
			
		||||
    match res {
 | 
			
		||||
        Ok(_) => (),
 | 
			
		||||
        Err(ref err) if err.is_errno(Errno::EOPNOTSUPP) => (),
 | 
			
		||||
        Err(ref err) if err.is_errno(Errno::EPERM) => {
 | 
			
		||||
            println!(
 | 
			
		||||
                "failed to restore mtime attribute on {:?}: {}",
 | 
			
		||||
                file_name, err
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        Err(err) => return Err(err.into()),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn add_fcaps(
 | 
			
		||||
    flags: u64,
 | 
			
		||||
    c_proc_path: *const libc::c_char,
 | 
			
		||||
    metadata: &Metadata,
 | 
			
		||||
    skip_xattrs: &mut bool,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    if *skip_xattrs || !flags_contain(flags, flags::WITH_FCAPS) {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
    let fcaps = match metadata.fcaps.as_ref() {
 | 
			
		||||
        Some(fcaps) => fcaps,
 | 
			
		||||
        None => return Ok(()),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    c_result!(unsafe {
 | 
			
		||||
        libc::setxattr(
 | 
			
		||||
            c_proc_path,
 | 
			
		||||
            xattr::xattr_name_fcaps().as_ptr(),
 | 
			
		||||
            fcaps.data.as_ptr() as *const libc::c_void,
 | 
			
		||||
            fcaps.data.len(),
 | 
			
		||||
            0,
 | 
			
		||||
        )
 | 
			
		||||
    })
 | 
			
		||||
    .map(drop)
 | 
			
		||||
    .or_else(|err| allow_notsupp_remember(err, skip_xattrs))?;
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn apply_xattrs(
 | 
			
		||||
    flags: u64,
 | 
			
		||||
    c_proc_path: *const libc::c_char,
 | 
			
		||||
    metadata: &Metadata,
 | 
			
		||||
    skip_xattrs: &mut bool,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    if *skip_xattrs || !flags_contain(flags, flags::WITH_XATTRS) {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for xattr in &metadata.xattrs {
 | 
			
		||||
        if *skip_xattrs {
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !xattr::is_valid_xattr_name(xattr.name()) {
 | 
			
		||||
            println!("skipping invalid xattr named {:?}", xattr.name());
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        c_result!(unsafe {
 | 
			
		||||
            libc::setxattr(
 | 
			
		||||
                c_proc_path,
 | 
			
		||||
                xattr.name().as_ptr() as *const libc::c_char,
 | 
			
		||||
                xattr.value().as_ptr() as *const libc::c_void,
 | 
			
		||||
                xattr.value().len(),
 | 
			
		||||
                0,
 | 
			
		||||
            )
 | 
			
		||||
        })
 | 
			
		||||
        .map(drop)
 | 
			
		||||
        .or_else(|err| allow_notsupp_remember(err, &mut *skip_xattrs))?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn apply_acls(
 | 
			
		||||
    flags: u64,
 | 
			
		||||
    c_proc_path: *const libc::c_char,
 | 
			
		||||
    metadata: &Metadata,
 | 
			
		||||
) -> Result<(), Error> {
 | 
			
		||||
    if !flags_contain(flags, flags::WITH_ACL) || metadata.acl.is_empty() {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut acl = acl::ACL::init(5)?;
 | 
			
		||||
 | 
			
		||||
    // acl type access:
 | 
			
		||||
    acl.add_entry_full(
 | 
			
		||||
        acl::ACL_USER_OBJ,
 | 
			
		||||
        None,
 | 
			
		||||
        acl::mode_user_to_acl_permissions(metadata.stat.mode),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    acl.add_entry_full(
 | 
			
		||||
        acl::ACL_OTHER,
 | 
			
		||||
        None,
 | 
			
		||||
        acl::mode_other_to_acl_permissions(metadata.stat.mode),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    match metadata.acl.group_obj.as_ref() {
 | 
			
		||||
        Some(group_obj) => {
 | 
			
		||||
            acl.add_entry_full(
 | 
			
		||||
                acl::ACL_MASK,
 | 
			
		||||
                None,
 | 
			
		||||
                acl::mode_group_to_acl_permissions(metadata.stat.mode),
 | 
			
		||||
            )?;
 | 
			
		||||
            acl.add_entry_full(acl::ACL_GROUP_OBJ, None, group_obj.permissions.0)?;
 | 
			
		||||
        }
 | 
			
		||||
        None => {
 | 
			
		||||
            acl.add_entry_full(
 | 
			
		||||
                acl::ACL_GROUP_OBJ,
 | 
			
		||||
                None,
 | 
			
		||||
                acl::mode_group_to_acl_permissions(metadata.stat.mode),
 | 
			
		||||
            )?;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for user in &metadata.acl.users {
 | 
			
		||||
        acl.add_entry_full(acl::ACL_USER, Some(user.uid), user.permissions.0)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for group in &metadata.acl.groups {
 | 
			
		||||
        acl.add_entry_full(acl::ACL_GROUP, Some(group.gid), group.permissions.0)?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if !acl.is_valid() {
 | 
			
		||||
        bail!("Error while restoring ACL - ACL invalid");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    c_try!(unsafe { acl::acl_set_file(c_proc_path, acl::ACL_TYPE_ACCESS, acl.ptr,) });
 | 
			
		||||
    drop(acl);
 | 
			
		||||
 | 
			
		||||
    // acl type default:
 | 
			
		||||
    if let Some(default) = metadata.acl.default.as_ref() {
 | 
			
		||||
        let mut acl = acl::ACL::init(5)?;
 | 
			
		||||
 | 
			
		||||
        acl.add_entry_full(acl::ACL_USER_OBJ, None, default.user_obj_permissions.0)?;
 | 
			
		||||
 | 
			
		||||
        acl.add_entry_full(acl::ACL_GROUP_OBJ, None, default.group_obj_permissions.0)?;
 | 
			
		||||
 | 
			
		||||
        acl.add_entry_full(acl::ACL_OTHER, None, default.other_permissions.0)?;
 | 
			
		||||
 | 
			
		||||
        if default.mask_permissions != pxar::format::acl::Permissions::NO_MASK {
 | 
			
		||||
            acl.add_entry_full(acl::ACL_MASK, None, default.mask_permissions.0)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for user in &metadata.acl.default_users {
 | 
			
		||||
            acl.add_entry_full(acl::ACL_USER, Some(user.uid), user.permissions.0)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for group in &metadata.acl.default_groups {
 | 
			
		||||
            acl.add_entry_full(acl::ACL_GROUP, Some(group.gid), group.permissions.0)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !acl.is_valid() {
 | 
			
		||||
            bail!("Error while restoring ACL - ACL invalid");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        c_try!(unsafe { acl::acl_set_file(c_proc_path, acl::ACL_TYPE_DEFAULT, acl.ptr,) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn apply_quota_project_id(flags: u64, fd: RawFd, metadata: &Metadata) -> Result<(), Error> {
 | 
			
		||||
    if !flags_contain(flags, flags::WITH_QUOTA_PROJID) {
 | 
			
		||||
        return Ok(());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let projid = match metadata.quota_project_id {
 | 
			
		||||
        Some(projid) => projid,
 | 
			
		||||
        None => return Ok(()),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut fsxattr = fs::FSXAttr::default();
 | 
			
		||||
    unsafe {
 | 
			
		||||
        fs::fs_ioc_fsgetxattr(fd, &mut fsxattr).map_err(|err| {
 | 
			
		||||
            format_err!(
 | 
			
		||||
                "error while getting fsxattr to restore quota project id - {}",
 | 
			
		||||
                err
 | 
			
		||||
            )
 | 
			
		||||
        })?;
 | 
			
		||||
 | 
			
		||||
        fsxattr.fsx_projid = projid.projid as u32;
 | 
			
		||||
 | 
			
		||||
        fs::fs_ioc_fssetxattr(fd, &fsxattr).map_err(|err| {
 | 
			
		||||
            format_err!(
 | 
			
		||||
                "error while setting fsxattr to restore quota project id - {}",
 | 
			
		||||
                err
 | 
			
		||||
            )
 | 
			
		||||
        })?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										192
									
								
								src/pxar/tools.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								src/pxar/tools.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,192 @@
 | 
			
		||||
//! Some common methods used within the pxar code.
 | 
			
		||||
 | 
			
		||||
use std::convert::TryFrom;
 | 
			
		||||
use std::ffi::OsStr;
 | 
			
		||||
use std::os::unix::ffi::OsStrExt;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use nix::sys::stat::Mode;
 | 
			
		||||
 | 
			
		||||
use pxar::{mode, Entry, EntryKind, Metadata};
 | 
			
		||||
 | 
			
		||||
/// Get the file permissions as `nix::Mode`
 | 
			
		||||
pub fn perms_from_metadata(meta: &Metadata) -> Result<Mode, Error> {
 | 
			
		||||
    let mode = meta.stat.get_permission_bits();
 | 
			
		||||
    u32::try_from(mode)
 | 
			
		||||
        .map_err(drop)
 | 
			
		||||
        .and_then(|mode| Mode::from_bits(mode).ok_or(()))
 | 
			
		||||
        .map_err(|_| format_err!("mode contains illegal bits: 0x{:x} (0o{:o})", mode, mode))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Make sure path is relative and not '.' or '..'.
 | 
			
		||||
pub fn assert_relative_path<S: AsRef<OsStr> + ?Sized>(path: &S) -> Result<(), Error> {
 | 
			
		||||
    assert_relative_path_do(Path::new(path))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn assert_relative_path_do(path: &Path) -> Result<(), Error> {
 | 
			
		||||
    if !path.is_relative() {
 | 
			
		||||
        bail!("bad absolute file name in archive: {:?}", path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let mut components = path.components();
 | 
			
		||||
    match components.next() {
 | 
			
		||||
        Some(std::path::Component::Normal(_)) => (),
 | 
			
		||||
        _ => bail!("invalid path component in archive: {:?}", path),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if components.next().is_some() {
 | 
			
		||||
        bail!(
 | 
			
		||||
            "invalid path with multiple components in archive: {:?}",
 | 
			
		||||
            path
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[rustfmt::skip]
 | 
			
		||||
fn symbolic_mode(c: u64, special: bool, special_x: u8, special_no_x: u8) -> [u8; 3] {
 | 
			
		||||
    [
 | 
			
		||||
        if 0 != c & 4 { b'r' } else { b'-' },
 | 
			
		||||
        if 0 != c & 2 { b'w' } else { b'-' },
 | 
			
		||||
        match (c & 1, special) {
 | 
			
		||||
            (0, false) => b'-',
 | 
			
		||||
            (0, true) => special_no_x,
 | 
			
		||||
            (_, false) => b'x',
 | 
			
		||||
            (_, true) => special_x,
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn mode_string(entry: &Entry) -> String {
 | 
			
		||||
    // https://www.gnu.org/software/coreutils/manual/html_node/What-information-is-listed.html#What-information-is-listed
 | 
			
		||||
    // additionally we use:
 | 
			
		||||
    //     file type capital 'L' hard links
 | 
			
		||||
    //     a second '+' after the mode to show non-acl xattr presence
 | 
			
		||||
    //
 | 
			
		||||
    // Trwxrwxrwx++ uid/gid size mtime filename [-> destination]
 | 
			
		||||
 | 
			
		||||
    let meta = entry.metadata();
 | 
			
		||||
    let mode = meta.stat.mode;
 | 
			
		||||
    let type_char = if entry.is_hardlink() {
 | 
			
		||||
        'L'
 | 
			
		||||
    } else {
 | 
			
		||||
        match mode & mode::IFMT {
 | 
			
		||||
            mode::IFREG => '-',
 | 
			
		||||
            mode::IFBLK => 'b',
 | 
			
		||||
            mode::IFCHR => 'c',
 | 
			
		||||
            mode::IFDIR => 'd',
 | 
			
		||||
            mode::IFLNK => 'l',
 | 
			
		||||
            mode::IFIFO => 'p',
 | 
			
		||||
            mode::IFSOCK => 's',
 | 
			
		||||
            _ => '?',
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let fmt_u = symbolic_mode((mode >> 6) & 7, 0 != mode & mode::ISUID, b's', b'S');
 | 
			
		||||
    let fmt_g = symbolic_mode((mode >> 3) & 7, 0 != mode & mode::ISGID, b's', b'S');
 | 
			
		||||
    let fmt_o = symbolic_mode((mode >> 3) & 7, 0 != mode & mode::ISVTX, b't', b'T');
 | 
			
		||||
 | 
			
		||||
    let has_acls = if meta.acl.is_empty() { ' ' } else { '+' };
 | 
			
		||||
 | 
			
		||||
    let has_xattrs = if meta.xattrs.is_empty() { ' ' } else { '+' };
 | 
			
		||||
 | 
			
		||||
    format!(
 | 
			
		||||
        "{}{}{}{}{}{}",
 | 
			
		||||
        type_char,
 | 
			
		||||
        unsafe { std::str::from_utf8_unchecked(&fmt_u) },
 | 
			
		||||
        unsafe { std::str::from_utf8_unchecked(&fmt_g) },
 | 
			
		||||
        unsafe { std::str::from_utf8_unchecked(&fmt_o) },
 | 
			
		||||
        has_acls,
 | 
			
		||||
        has_xattrs,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn format_single_line_entry(entry: &Entry) -> String {
 | 
			
		||||
    use chrono::offset::TimeZone;
 | 
			
		||||
 | 
			
		||||
    let mode_string = mode_string(entry);
 | 
			
		||||
 | 
			
		||||
    let meta = entry.metadata();
 | 
			
		||||
    let mtime = meta.mtime_as_duration();
 | 
			
		||||
    let mtime = chrono::Local.timestamp(mtime.as_secs() as i64, mtime.subsec_nanos());
 | 
			
		||||
 | 
			
		||||
    let (size, link) = match entry.kind() {
 | 
			
		||||
        EntryKind::File { size, .. } => (format!("{}", *size), String::new()),
 | 
			
		||||
        EntryKind::Symlink(link) => ("0".to_string(), format!(" -> {:?}", link.as_os_str())),
 | 
			
		||||
        EntryKind::Hardlink(link) => ("0".to_string(), format!(" -> {:?}", link.as_os_str())),
 | 
			
		||||
        EntryKind::Device(dev) => (format!("{},{}", dev.major, dev.minor), String::new()),
 | 
			
		||||
        _ => ("0".to_string(), String::new()),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    format!(
 | 
			
		||||
        "{} {:<13} {} {:>8} {:?}{}",
 | 
			
		||||
        mode_string,
 | 
			
		||||
        format!("{}/{}", meta.stat.uid, meta.stat.gid),
 | 
			
		||||
        mtime.format("%Y-%m-%d %H:%M:%S"),
 | 
			
		||||
        size,
 | 
			
		||||
        entry.path(),
 | 
			
		||||
        link,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn format_multi_line_entry(entry: &Entry) -> String {
 | 
			
		||||
    use chrono::offset::TimeZone;
 | 
			
		||||
 | 
			
		||||
    let mode_string = mode_string(entry);
 | 
			
		||||
 | 
			
		||||
    let meta = entry.metadata();
 | 
			
		||||
    let mtime = meta.mtime_as_duration();
 | 
			
		||||
    let mtime = chrono::Local.timestamp(mtime.as_secs() as i64, mtime.subsec_nanos());
 | 
			
		||||
 | 
			
		||||
    let (size, link, type_name) = match entry.kind() {
 | 
			
		||||
        EntryKind::File { size, .. } => (format!("{}", *size), String::new(), "file"),
 | 
			
		||||
        EntryKind::Symlink(link) => (
 | 
			
		||||
            "0".to_string(),
 | 
			
		||||
            format!(" -> {:?}", link.as_os_str()),
 | 
			
		||||
            "symlink",
 | 
			
		||||
        ),
 | 
			
		||||
        EntryKind::Hardlink(link) => (
 | 
			
		||||
            "0".to_string(),
 | 
			
		||||
            format!(" -> {:?}", link.as_os_str()),
 | 
			
		||||
            "symlink",
 | 
			
		||||
        ),
 | 
			
		||||
        EntryKind::Device(dev) => (
 | 
			
		||||
            format!("{},{}", dev.major, dev.minor),
 | 
			
		||||
            String::new(),
 | 
			
		||||
            if meta.stat.is_chardev() {
 | 
			
		||||
                "characters pecial file"
 | 
			
		||||
            } else if meta.stat.is_blockdev() {
 | 
			
		||||
                "block special file"
 | 
			
		||||
            } else {
 | 
			
		||||
                "device"
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        EntryKind::Socket => ("0".to_string(), String::new(), "socket"),
 | 
			
		||||
        EntryKind::Fifo => ("0".to_string(), String::new(), "fifo"),
 | 
			
		||||
        EntryKind::Directory => ("0".to_string(), String::new(), "directory"),
 | 
			
		||||
        EntryKind::GoodbyeTable => ("0".to_string(), String::new(), "bad entry"),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let file_name = match std::str::from_utf8(entry.path().as_os_str().as_bytes()) {
 | 
			
		||||
        Ok(name) => std::borrow::Cow::Borrowed(name),
 | 
			
		||||
        Err(_) => std::borrow::Cow::Owned(format!("{:?}", entry.path())),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    format!(
 | 
			
		||||
        "  File: {}{}\n  \
 | 
			
		||||
           Size: {:<13} Type: {}\n\
 | 
			
		||||
         Access: ({:o}/{})  Uid: {:<5} Gid: {:<5}\n\
 | 
			
		||||
         Modify: {}\n",
 | 
			
		||||
        file_name,
 | 
			
		||||
        link,
 | 
			
		||||
        size,
 | 
			
		||||
        type_name,
 | 
			
		||||
        meta.file_mode(),
 | 
			
		||||
        mode_string,
 | 
			
		||||
        meta.stat.uid,
 | 
			
		||||
        meta.stat.gid,
 | 
			
		||||
        mtime.format("%Y-%m-%d %H:%M:%S"),
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@ -49,7 +49,8 @@ pub const ACL_EA_VERSION: u32 = 0x0002;
 | 
			
		||||
#[link(name = "acl")]
 | 
			
		||||
extern "C" {
 | 
			
		||||
    fn acl_get_file(path: *const c_char, acl_type: ACLType) -> *mut c_void;
 | 
			
		||||
    fn acl_set_file(path: *const c_char, acl_type: ACLType, acl: *mut c_void) -> c_int;
 | 
			
		||||
    // FIXME: remove 'pub' after the cleanup
 | 
			
		||||
    pub(crate) fn acl_set_file(path: *const c_char, acl_type: ACLType, acl: *mut c_void) -> c_int;
 | 
			
		||||
    fn acl_get_fd(fd: RawFd) -> *mut c_void;
 | 
			
		||||
    fn acl_get_entry(acl: *const c_void, entry_id: c_int, entry: *mut *mut c_void) -> c_int;
 | 
			
		||||
    fn acl_create_entry(acl: *mut *mut c_void, entry: *mut *mut c_void) -> c_int;
 | 
			
		||||
@ -68,7 +69,8 @@ extern "C" {
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct ACL {
 | 
			
		||||
    ptr: *mut c_void,
 | 
			
		||||
    // FIXME: remove 'pub' after the cleanup
 | 
			
		||||
    pub(crate) ptr: *mut c_void,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Drop for ACL {
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,22 @@ pub fn xattr_name_fcaps() -> &'static CStr {
 | 
			
		||||
    c_str!("security.capability")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// `"system.posix_acl_access"` as a CStr to avoid typos.
 | 
			
		||||
///
 | 
			
		||||
/// This cannot be `const` until `const_cstr_unchecked` is stable.
 | 
			
		||||
#[inline]
 | 
			
		||||
pub fn xattr_acl_access() -> &'static CStr {
 | 
			
		||||
    c_str!("system.posix_acl_access")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// `"system.posix_acl_default"` as a CStr to avoid typos.
 | 
			
		||||
///
 | 
			
		||||
/// This cannot be `const` until `const_cstr_unchecked` is stable.
 | 
			
		||||
#[inline]
 | 
			
		||||
pub fn xattr_acl_default() -> &'static CStr {
 | 
			
		||||
    c_str!("system.posix_acl_default")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Result of `flistxattr`, allows iterating over the attributes as a list of `&CStr`s.
 | 
			
		||||
///
 | 
			
		||||
/// Listing xattrs produces a list separated by zeroes, inherently making them available as `&CStr`
 | 
			
		||||
@ -139,6 +155,11 @@ pub fn is_security_capability(name: &CStr) -> bool {
 | 
			
		||||
    name.to_bytes() == xattr_name_fcaps().to_bytes()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn is_acl(name: &CStr) -> bool {
 | 
			
		||||
    name.to_bytes() == xattr_acl_access().to_bytes()
 | 
			
		||||
    || name.to_bytes() == xattr_acl_default().to_bytes()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Check if the passed name buffer starts with a valid xattr namespace prefix
 | 
			
		||||
/// and is within the length limit of 255 bytes
 | 
			
		||||
pub fn is_valid_xattr_name(c_name: &CStr) -> bool {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user