haggis-rs/src/listing.rs
2024-01-17 01:58:36 -05:00

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);
}
}