#[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 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(reader: &mut R, flag: Flag) -> Result { 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 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 { 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(reader: &mut R) -> Result { 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); } }