zarust 0.2.1

Rust implementation of the ZArchive format
Documentation
//! The "how much is in here" tally shared by every command.
//!
//! `pack`, `extract`, and `list` all report the same three numbers in the same
//! phrasing, so the counting and the wording live here rather than in each
//! command.

use std::io::{Read, Seek};

use zarust::{ArchiveReader, EntryKind, NodeHandle};

use crate::cli::{
    Failure,
    ui::{counted, format_bytes},
};

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Summary {
    pub files: u64,
    pub directories: u64,
    pub bytes: u64,
}

impl Summary {
    /// Tallies everything under `root` without touching the compressed data —
    /// the tree is already in memory once the archive is open.
    pub fn of<R: Read + Seek>(
        reader: &ArchiveReader<R>,
        root: NodeHandle,
    ) -> Result<Self, Failure> {
        let mut summary = Self::default();
        summary.accumulate(reader, root)?;
        Ok(summary)
    }

    fn accumulate<R: Read + Seek>(
        &mut self,
        reader: &ArchiveReader<R>,
        directory: NodeHandle,
    ) -> Result<(), Failure> {
        for entry in reader.directory_entries(directory)? {
            match entry.kind {
                EntryKind::Directory => {
                    self.directories += 1;
                    self.accumulate(reader, entry.handle)?;
                }
                EntryKind::File => self.file(entry.size),
            }
        }
        Ok(())
    }

    pub fn file(&mut self, size: u64) {
        self.files += 1;
        self.bytes = self.bytes.saturating_add(size);
    }

    pub fn directory(&mut self) {
        self.directories += 1;
    }

    /// The one-line tally, e.g. `12 files, 3 directories · 1.4 MiB`.
    pub fn describe(&self) -> String {
        format!(
            "{}, {} · {}",
            counted(self.files, "file", "files"),
            counted(self.directories, "directory", "directories"),
            format_bytes(self.bytes as f64),
        )
    }
}

/// `part` as a percentage of `whole`, treating an empty whole as 100%.
pub fn ratio(part: u64, whole: u64) -> f64 {
    if whole == 0 {
        100.0
    } else {
        part as f64 * 100.0 / whole as f64
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn describes_counts_with_matching_plurals() {
        let one = Summary {
            files: 1,
            directories: 1,
            bytes: 1024,
        };
        assert_eq!(one.describe(), "1 file, 1 directory · 1.0 KiB");

        let many = Summary {
            files: 0,
            directories: 2,
            bytes: 0,
        };
        assert_eq!(many.describe(), "0 files, 2 directories · 0 B");
    }

    #[test]
    fn ratio_treats_an_empty_whole_as_complete() {
        assert_eq!(ratio(0, 0), 100.0);
        assert_eq!(ratio(1, 4), 25.0);
    }
}