totebag 0.8.14

An API for extracting/archiving files and directories in multiple formats.
Documentation
//! Output formatting utilities for archive entries.
//!
//! This module provides functions to format archive entries in various formats.

use crate::Result;
use crate::extractor::{Entries, Entry};

/// Convert entries to a simple string format (one filename per line).
///
/// # Arguments
///
/// * `entries` - The archive entries to format
///
/// # Returns
///
/// Returns a string with one filename per line.
pub fn to_string(entries: &Entries) -> Result<String> {
    Ok(entries
        .iter()
        .map(|entry| entry.name.to_string())
        .collect::<Vec<String>>()
        .join("\n"))
}

/// Convert entries to a detailed long format string.
///
/// Each line includes permissions, compressed/original sizes, date, and filename.
///
/// # Arguments
///
/// * `entries` - The archive entries to format
///
/// # Returns
///
/// Returns a formatted string with detailed information for each entry.
pub fn to_string_long(entries: &Entries) -> Result<String> {
    Ok(entries
        .iter()
        .map(to_long_format)
        .collect::<Vec<String>>()
        .join("\n"))
}

fn to_long_format(entry: &Entry) -> String {
    let r1 = to_unix_mode(entry.unix_mode);
    let r2 = format_size(entry.compressed_size, entry.original_size);
    let r3 = format_date(entry.date);
    format!("{} {} {} {}", r1, r2, r3, entry.name)
}

fn format_date(date: Option<NaiveDateTime>) -> String {
    match date {
        Some(d) => d.format("%Y-%m-%d %H:%M:%S").to_string(),
        None => "                    ".to_string(),
    }
}

fn format_size(compressed: Option<u64>, original: Option<u64>) -> String {
    let formatter = humansize::make_format(humansize::DECIMAL);
    match (compressed, original) {
        (Some(c), Some(o)) => format!("{:>10}/{:>10}", formatter(c), formatter(o)),
        (Some(c), None) => format!("{:>10}/ -------- ", formatter(c)),
        (None, Some(o)) => format!(" -------- /{:>10}", formatter(o)),
        (None, None) => " -------- / -------- ".to_string(),
    }
}

/// Convert a Unix permission mode to a human-readable string.
///
/// # Arguments
///
/// * `mode` - Optional Unix permission mode (e.g., 0o644)
///
/// # Returns
///
/// Returns a string like "-rw-r--r--" representing the permission mode.
pub fn to_unix_mode(mode: Option<u32>) -> String {
    if let Some(mode) = mode {
        format!(
            "-{}{}{}",
            format_mode((mode >> 6 & 0x7) as u8),
            format_mode((mode >> 3 & 0x7) as u8),
            format_mode((mode & 0x7) as u8)
        )
    } else {
        "----------".to_string()
    }
}

fn format_mode(mode: u8) -> String {
    match mode {
        0 => "---",
        1 => "--x",
        2 => "-w-",
        3 => "-wx",
        4 => "r--",
        5 => "r-x",
        6 => "rw-",
        7 => "rwx",
        _ => "???",
    }
    .to_string()
}

use chrono::NaiveDateTime;
use serde::Serializer;

/// Serialize an optional u32 value as an octal string for Serde.
///
/// This is used to serialize Unix permission modes in octal format.
///
/// # Arguments
///
/// * `value` - Optional u32 value to serialize
/// * `serializer` - The Serde serializer
///
/// # Returns
///
/// Returns the serialization result.
pub fn serialize_option_u32_octal<S>(
    value: &Option<u32>,
    serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
    S: Serializer,
{
    match value {
        Some(v) => serializer.serialize_str(&format!("{v:o}")),
        None => serializer.serialize_none(),
    }
}

#[cfg(test)]
mod tests {
    use chrono::DateTime;

    use super::*;

    #[test]
    fn test_entry() {
        let entry = Entry {
            name: "Cargo.toml".to_string(),
            compressed_size: Some(100),
            original_size: Some(200),
            unix_mode: Some(0o644),
            date: Some(
                NaiveDateTime::parse_from_str("2021-02-03 04:05:10", "%Y-%m-%d %H:%M:%S").unwrap(),
            ),
        };
        assert_eq!(
            to_long_format(&entry),
            "-rw-r--r--      100 B/     200 B 2021-02-03 04:05:10 Cargo.toml"
        );
    }

    #[test]
    fn trivial_tests() {
        assert_eq!(format_date(None), "                    ");
        assert_eq!(
            format_date(Some(DateTime::from_timestamp(0, 0).unwrap().naive_local())),
            "1970-01-01 00:00:00"
        );

        assert_eq!(format_size(Some(100), Some(200)), "     100 B/     200 B");
        assert_eq!(format_size(None, Some(200)), " -------- /     200 B");
        assert_eq!(format_size(None, None), " -------- / -------- ");
        assert_eq!(format_size(Some(100), None), "     100 B/ -------- ");

        assert_eq!(to_unix_mode(None), "----------");
        assert_eq!(to_unix_mode(Some(0o644)), "-rw-r--r--");
        assert_eq!(to_unix_mode(Some(0o751)), "-rwxr-x--x");
        assert_eq!(to_unix_mode(Some(0o640)), "-rw-r-----");
        assert_eq!(to_unix_mode(Some(0o123)), "---x-w--wx");
        assert_eq!(to_unix_mode(Some(0o456)), "-r--r-xrw-");

        assert_eq!(format_mode(128), "???");
    }
}