//! 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 { 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 + ?Sized>(path: &S) -> Result<(), Error> { assert_relative_path_do(Path::new(path)) } /// Make sure path is a single component and not '.' or '..'. pub fn assert_single_path_component + ?Sized>(path: &S) -> Result<(), Error> { assert_single_path_component_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); } Ok(()) } fn assert_single_path_component_do(path: &Path) -> Result<(), Error> { assert_relative_path_do(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"), ) }