468 lines
16 KiB
Rust
468 lines
16 KiB
Rust
#[cfg(feature = "color")]
|
|
use {
|
|
std::io::Write,
|
|
termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor},
|
|
};
|
|
|
|
use {
|
|
crate::{filetype::Flag, Error, FileType, Node, Special},
|
|
chrono::NaiveDateTime,
|
|
std::{
|
|
cmp::Ordering,
|
|
fmt,
|
|
io::{Read, Seek, SeekFrom},
|
|
},
|
|
};
|
|
|
|
/// An item representing the `Filetype` of a `Node` without
|
|
/// including a file's actual data or checksum.
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum Kind {
|
|
/// A regular file. The associated `u64` represents the files
|
|
/// size in bytes.
|
|
Normal(u64),
|
|
/// A hard link to another member of this archive. The associated
|
|
/// `String` represents the target of the link
|
|
HardLink(String),
|
|
/// A symlink to another file. The associated `String` represents
|
|
/// the target of the link.
|
|
SoftLink(String),
|
|
/// A directory entry
|
|
Directory,
|
|
/// A character (buffered) device file. The associated `Special`
|
|
/// struct represents the **major** and **minor** numbers of the
|
|
/// device file.
|
|
Character(Special),
|
|
/// A block (non-buffered) device file. The associated `Special`
|
|
/// struct represents the **major** and **minor** numbers of the
|
|
/// device file.
|
|
Block(Special),
|
|
/// A named pipe
|
|
Fifo,
|
|
/// The end of the archive
|
|
Eof,
|
|
}
|
|
|
|
impl From<FileType> for Kind {
|
|
fn from(value: FileType) -> Self {
|
|
match value {
|
|
FileType::Normal(f) => Self::Normal(f.len),
|
|
FileType::HardLink(tgt) => Self::HardLink(tgt),
|
|
FileType::SoftLink(tgt) => Self::SoftLink(tgt),
|
|
FileType::Directory => Self::Directory,
|
|
FileType::Character(c) => Self::Character(c),
|
|
FileType::Block(b) => Self::Block(b),
|
|
FileType::Fifo => Self::Fifo,
|
|
FileType::Eof => Self::Eof,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Kind {
|
|
/// Reads an item from a stream of bytes. The stream can be anything implementing
|
|
/// both `Read` and `Seek`
|
|
#[allow(clippy::cast_possible_wrap)]
|
|
fn read<R: Read + Seek>(reader: &mut R, flag: Flag) -> Result<Self, Error> {
|
|
match flag {
|
|
Flag::Normal => {
|
|
let mut len = [0; 8];
|
|
reader.read_exact(&mut len)?;
|
|
let len = u64::from_le_bytes(len);
|
|
let mut buf = [0; 1];
|
|
reader.read_exact(&mut buf)?;
|
|
match buf[0] {
|
|
0 => {
|
|
let _bytes = reader.seek(SeekFrom::Current(len as i64 + 16))?;
|
|
}
|
|
1 => {
|
|
let _bytes = reader.seek(SeekFrom::Current(len as i64 + 20))?;
|
|
}
|
|
2 => {
|
|
let _bytes = reader.seek(SeekFrom::Current(len as i64 + 32))?;
|
|
}
|
|
_ => {
|
|
let _bytes = reader.seek(SeekFrom::Current(len as i64))?;
|
|
}
|
|
}
|
|
Ok(Self::Normal(len))
|
|
}
|
|
Flag::HardLink => {
|
|
let mut len = [0; 2];
|
|
reader.read_exact(&mut len)?;
|
|
let len = u16::from_le_bytes(len);
|
|
let mut buf = Vec::with_capacity(len.into());
|
|
let mut handle = reader.take(len.into());
|
|
handle.read_to_end(&mut buf)?;
|
|
let s = String::from_utf8(buf)?;
|
|
Ok(Self::HardLink(s))
|
|
}
|
|
Flag::SoftLink => {
|
|
let mut len = [0; 2];
|
|
reader.read_exact(&mut len)?;
|
|
let len = u16::from_le_bytes(len);
|
|
let mut buf = Vec::with_capacity(len.into());
|
|
let mut handle = reader.take(len.into());
|
|
handle.read_to_end(&mut buf)?;
|
|
let s = String::from_utf8(buf)?;
|
|
Ok(Self::SoftLink(s))
|
|
}
|
|
Flag::Directory => Ok(Self::Directory),
|
|
Flag::Character => {
|
|
let sp = Special::read(reader)?;
|
|
Ok(Self::Character(sp))
|
|
}
|
|
Flag::Block => {
|
|
let sp = Special::read(reader)?;
|
|
Ok(Self::Block(sp))
|
|
}
|
|
Flag::Fifo => Ok(Self::Fifo),
|
|
Flag::Eof => Ok(Self::Eof),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An item representing a `Node` with all relavent metadata minus
|
|
/// the bytes making up the file and the file's checksum. Useful
|
|
/// for examining the contents of an archive by printing file listings.
|
|
/// > Note: Because this type can be read from an archive while skipping over
|
|
/// > the bytes making up the file's contents and checksum, it can be created
|
|
/// > faster than a `Node` and takes up less space in memory. However, it may
|
|
/// > not always be possible to create a `Listing` directly, as in the case
|
|
/// > where the underlying byte stream does not implement `Seek`, such as with
|
|
/// > a zstd compressed archive. In this case, a `Node` can be read from the
|
|
/// > stream and converted to a `Listing` using `From`, which will still save
|
|
/// > on overall memory usage when listing all files in an archive.
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct Listing {
|
|
pub name: String,
|
|
pub uid: u32,
|
|
pub gid: u32,
|
|
pub mtime: u64,
|
|
pub mode: u16,
|
|
pub kind: Kind,
|
|
}
|
|
|
|
impl From<Node> for Listing {
|
|
fn from(value: Node) -> Self {
|
|
Self {
|
|
name: value.name,
|
|
uid: value.uid,
|
|
gid: value.gid,
|
|
mtime: value.mtime,
|
|
mode: value.mode,
|
|
kind: value.filetype.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for Listing {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for Listing {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
match (&self.kind, &other.kind) {
|
|
(Kind::Directory, Kind::Directory) => self.name.cmp(&other.name),
|
|
(Kind::Directory, _) => Ordering::Less,
|
|
(_, Kind::Directory) => Ordering::Greater,
|
|
_ => self.name.cmp(&other.name),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Listing {
|
|
#[allow(clippy::cast_possible_wrap)]
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
"{}{}{}{}{}{}{}{}{}{} {:>4}:{:<4} ",
|
|
match self.kind {
|
|
Kind::Normal(_) => "-",
|
|
Kind::HardLink(_) => "L",
|
|
Kind::SoftLink(_) => "l",
|
|
Kind::Directory => "d",
|
|
Kind::Character(_) => "c",
|
|
Kind::Block(_) => "b",
|
|
Kind::Fifo => "p",
|
|
Kind::Eof => return Ok(()),
|
|
},
|
|
match self.mode {
|
|
m if m & 0o400 != 0 => "r",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o200 != 0 => "w",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o4000 != 0 => "S",
|
|
m if m & 0o100 != 0 => "x",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o40 != 0 => "r",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o20 != 0 => "w",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o2000 != 0 => "S",
|
|
m if m & 0o10 != 0 => "x",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o4 != 0 => "r",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o2 != 0 => "w",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o1000 != 0 => "t",
|
|
m if m & 0o1 != 0 => "x",
|
|
_ => "-",
|
|
},
|
|
self.uid,
|
|
self.gid,
|
|
)?;
|
|
match self.kind {
|
|
Kind::Normal(s) => write!(f, "{s:>10} "),
|
|
_ => write!(f, "{:>10}", "-"),
|
|
}?;
|
|
match NaiveDateTime::from_timestamp_opt(self.mtime as i64, 0) {
|
|
Some(dt) => write!(f, "{dt} ")?,
|
|
_ => write!(f, "{:>19} ", self.mtime)?,
|
|
}
|
|
match self.kind {
|
|
Kind::Directory | Kind::Fifo | Kind::Normal(_) => write!(f, "{}", self.name),
|
|
Kind::HardLink(ref tgt) => write!(f, "{}=>{}", self.name, tgt),
|
|
Kind::SoftLink(ref tgt) => write!(f, "{}->{}", self.name, tgt),
|
|
Kind::Character(ref sp) | Kind::Block(ref sp) => {
|
|
write!(f, "{} {},{}", self.name, sp.major, sp.minor)
|
|
}
|
|
Kind::Eof => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Listing {
|
|
/// Reads all metadata from a haggis Node without reading the file's actual
|
|
/// data.
|
|
/// # Errors
|
|
/// Can return an error if IO fails
|
|
#[allow(clippy::similar_names)]
|
|
pub fn read<R: Read + Seek>(reader: &mut R) -> Result<Self, Error> {
|
|
let mut len = [0; 2];
|
|
reader.read_exact(&mut len)?;
|
|
let len = u16::from_le_bytes(len);
|
|
if len == 0 {
|
|
return Ok(Self {
|
|
name: String::new(),
|
|
mode: 0,
|
|
uid: 0,
|
|
gid: 0,
|
|
mtime: 0,
|
|
kind: Kind::Eof,
|
|
});
|
|
}
|
|
let mut name = Vec::with_capacity(len.into());
|
|
let mut handle = reader.take(len.into());
|
|
handle.read_to_end(&mut name)?;
|
|
let mut buf = [0; 18];
|
|
reader.read_exact(&mut buf)?;
|
|
let uid: [u8; 4] = buf[0..4].try_into()?;
|
|
let gid: [u8; 4] = buf[4..8].try_into()?;
|
|
let mtime: [u8; 8] = buf[8..16].try_into()?;
|
|
let raw_mode: [u8; 2] = buf[16..18].try_into()?;
|
|
let raw_mode = u16::from_le_bytes(raw_mode);
|
|
let (flag, mode) = Flag::extract_from_raw(raw_mode)?;
|
|
let kind = Kind::read(reader, flag)?;
|
|
Ok(Self {
|
|
name: String::from_utf8(name)?,
|
|
uid: u32::from_le_bytes(uid),
|
|
gid: u32::from_le_bytes(gid),
|
|
mtime: u64::from_le_bytes(mtime),
|
|
mode,
|
|
kind,
|
|
})
|
|
}
|
|
|
|
/// Prints a line with relavent metadata included,
|
|
/// colorizing the filename based on the filetype.
|
|
/// Similar to GNU `ls --long --color auto`
|
|
/// # Errors
|
|
/// Can return an `Error` if stdout is unavailable
|
|
#[allow(clippy::cast_possible_wrap)]
|
|
#[cfg(feature = "color")]
|
|
pub fn print_color(&self) -> Result<(), Error> {
|
|
print!(
|
|
"{}{}{}{}{}{}{}{}{}{} {:>4}:{:<4} ",
|
|
match self.kind {
|
|
Kind::Normal(_) => "-",
|
|
Kind::HardLink(_) => "L",
|
|
Kind::SoftLink(_) => "l",
|
|
Kind::Directory => "d",
|
|
Kind::Character(_) => "c",
|
|
Kind::Block(_) => "b",
|
|
Kind::Fifo => "p",
|
|
Kind::Eof => return Ok(()),
|
|
},
|
|
match self.mode {
|
|
m if m & 0o400 != 0 => "r",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o200 != 0 => "w",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o4000 != 0 => "S",
|
|
m if m & 0o100 != 0 => "x",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o40 != 0 => "r",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o20 != 0 => "w",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o2000 != 0 => "S",
|
|
m if m & 0o10 != 0 => "x",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o4 != 0 => "r",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o2 != 0 => "w",
|
|
_ => "-",
|
|
},
|
|
match self.mode {
|
|
m if m & 0o1000 != 0 => "t",
|
|
m if m & 0o1 != 0 => "x",
|
|
_ => "-",
|
|
},
|
|
self.uid,
|
|
self.gid,
|
|
);
|
|
match self.kind {
|
|
Kind::Normal(s) => print!("{s:>10} "),
|
|
_ => print!("{:>10} ", "-"),
|
|
}
|
|
match NaiveDateTime::from_timestamp_opt(self.mtime as i64, 0) {
|
|
Some(dt) => print!("{dt} "),
|
|
_ => print!("{:>19} ", self.mtime),
|
|
}
|
|
let mut stdout = StandardStream::stdout(ColorChoice::Auto);
|
|
match self.kind {
|
|
Kind::Normal(_) => println!("{}", self.name),
|
|
Kind::Directory => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?;
|
|
writeln!(&mut stdout, "{}", self.name)?;
|
|
stdout.reset()?;
|
|
}
|
|
Kind::Fifo => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
|
|
writeln!(&mut stdout, "{}", self.name)?;
|
|
stdout.reset()?;
|
|
}
|
|
Kind::HardLink(ref tgt) => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
|
|
write!(&mut stdout, "{}", self.name)?;
|
|
stdout.reset()?;
|
|
println!(" => {tgt}");
|
|
}
|
|
Kind::SoftLink(ref tgt) => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
|
|
write!(&mut stdout, "{}", self.name)?;
|
|
stdout.reset()?;
|
|
println!(" -> {tgt}");
|
|
}
|
|
Kind::Character(ref sp) | Kind::Block(ref sp) => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
|
|
write!(&mut stdout, "{}", self.name)?;
|
|
stdout.reset()?;
|
|
println!(" {},{}", sp.major, sp.minor);
|
|
}
|
|
Kind::Eof => unreachable!(),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Prints just the filename representing the `Listing` item,
|
|
/// coloring each line based on the filetype
|
|
/// # Errors
|
|
/// Can return an `Error` if stdout is unavailable
|
|
#[cfg(feature = "color")]
|
|
pub fn print_color_simple(&self) -> Result<(), Error> {
|
|
let mut stdout = StandardStream::stdout(ColorChoice::Auto);
|
|
match self.kind {
|
|
Kind::Normal(_) => println!("{}", self.name),
|
|
Kind::Directory => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?;
|
|
writeln!(&mut stdout, "{}", self.name)?;
|
|
}
|
|
Kind::Fifo => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
|
|
writeln!(&mut stdout, "{}", self.name)?;
|
|
}
|
|
Kind::HardLink(_) | Kind::SoftLink(_) => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
|
|
write!(&mut stdout, "{}", self.name)?;
|
|
}
|
|
Kind::Character(_) | Kind::Block(_) => {
|
|
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
|
|
write!(&mut stdout, "{}", self.name)?;
|
|
}
|
|
Kind::Eof => unreachable!(),
|
|
}
|
|
stdout.reset()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{Algorithm, Node};
|
|
use std::{
|
|
collections::HashMap,
|
|
fs,
|
|
io::{BufReader, BufWriter, Write},
|
|
sync::Mutex,
|
|
};
|
|
|
|
#[test]
|
|
fn listing() {
|
|
let links = Mutex::new(HashMap::new());
|
|
{
|
|
let mut node = Node::from_path("test", Algorithm::Skip, &links).unwrap();
|
|
let fd = fs::File::create("test/ord_test.hag").unwrap();
|
|
let mut writer = BufWriter::new(fd);
|
|
node.write(&mut writer).unwrap();
|
|
node = Node::from_path("Cargo.toml", Algorithm::Skip, &links).unwrap();
|
|
node.write(&mut writer).unwrap();
|
|
node = Node::from_path("Cargo.lock", Algorithm::Skip, &links).unwrap();
|
|
node.write(&mut writer).unwrap();
|
|
writer.flush().unwrap();
|
|
}
|
|
let fd = fs::File::open("test/ord_test.hag").unwrap();
|
|
let mut reader = BufReader::new(fd);
|
|
let test_listing = Listing::read(&mut reader).unwrap();
|
|
let cargo_toml_listing = Listing::read(&mut reader).unwrap();
|
|
assert!(test_listing < cargo_toml_listing);
|
|
let cargo_lock_listing = Listing::read(&mut reader).unwrap();
|
|
assert!(test_listing < cargo_lock_listing);
|
|
assert!(cargo_lock_listing < cargo_toml_listing);
|
|
}
|
|
}
|