portable-network-archive 0.32.2

Portable-Network-Archive cli
Documentation
use crate::{
    cli::PasswordArgs,
    command::{
        Command, ask_password,
        core::{SplitArchiveReader, collect_split_archives},
    },
    utils::{PathPartExt, env::NamedTempFile},
};
use clap::{Parser, ValueHint};
use pna::{Archive, NormalEntry};
use std::{
    fmt::{self, Display, Formatter},
    path::PathBuf,
    str::FromStr,
};

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) enum SortBy {
    Name,
    Ctime,
    Mtime,
    Atime,
}

impl Display for SortBy {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            SortBy::Name => "name",
            SortBy::Ctime => "ctime",
            SortBy::Mtime => "mtime",
            SortBy::Atime => "atime",
        })
    }
}

impl FromStr for SortBy {
    type Err = String;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "name" => Ok(Self::Name),
            "ctime" => Ok(Self::Ctime),
            "mtime" => Ok(Self::Mtime),
            "atime" => Ok(Self::Atime),
            _ => Err("allowed values: name, ctime, mtime, atime".into()),
        }
    }
}

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) enum SortOrder {
    Asc,
    Desc,
}

impl Display for SortOrder {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            SortOrder::Asc => "asc",
            SortOrder::Desc => "desc",
        })
    }
}

impl FromStr for SortOrder {
    type Err = String;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "asc" => Ok(Self::Asc),
            "desc" => Ok(Self::Desc),
            _ => Err("only allowed `asc` or `desc`".into()),
        }
    }
}

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) struct SortKey {
    by: SortBy,
    order: SortOrder,
}

impl Default for SortKey {
    fn default() -> Self {
        Self {
            by: SortBy::Name,
            order: SortOrder::Asc,
        }
    }
}

impl Display for SortKey {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        if self.order == SortOrder::Asc {
            write!(f, "{}", self.by)
        } else {
            write!(f, "{}:{}", self.by, self.order)
        }
    }
}

impl FromStr for SortKey {
    type Err = String;

    #[inline]
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (by, order) = match s.split_once(':') {
            None => (s, SortOrder::Asc),
            Some((b, "")) => (b, SortOrder::Asc),
            Some((b, o)) => (b, SortOrder::from_str(o)?),
        };
        let by = SortBy::from_str(by)?;
        Ok(Self { by, order })
    }
}

#[derive(Parser, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) struct SortCommand {
    #[arg(short = 'f', long = "file", help = "Archive file path", value_hint = ValueHint::FilePath)]
    archive: PathBuf,
    #[arg(long, help = "Output archive file path", value_hint = ValueHint::FilePath)]
    output: Option<PathBuf>,
    #[arg(
        long = "by",
        value_name = "KEY",
        num_args = 1..,
        default_values_t = [SortKey::default()],
        help = "Sort key in format KEY[:ORDER] (e.g., name, mtime:desc) [keys: name, ctime, mtime, atime] [orders: asc, desc]"
    )]
    by: Vec<SortKey>,
    #[command(flatten)]
    password: PasswordArgs,
}

impl Command for SortCommand {
    #[inline]
    fn execute(self, _ctx: &crate::cli::GlobalContext) -> anyhow::Result<()> {
        sort_archive(self)
    }
}

#[hooq::hooq(anyhow)]
fn sort_archive(args: SortCommand) -> anyhow::Result<()> {
    let password = ask_password(args.password)?;
    let archives = collect_split_archives(&args.archive)?;
    let mut source = SplitArchiveReader::new(archives)?;
    let mut entries = Vec::<NormalEntry<_>>::new();
    source.for_each_entry(
        password.as_deref(),
        #[hooq::skip_all]
        |entry| {
            entries.push(entry?);
            Ok(())
        },
    )?;

    entries.sort_by(|a, b| {
        for key in &args.by {
            let ord = match key.by {
                SortBy::Name => a.name().cmp(b.name()),
                SortBy::Ctime => a.metadata().created().cmp(&b.metadata().created()),
                SortBy::Mtime => a.metadata().modified().cmp(&b.metadata().modified()),
                SortBy::Atime => a.metadata().accessed().cmp(&b.metadata().accessed()),
            };
            if ord != std::cmp::Ordering::Equal {
                return match key.order {
                    SortOrder::Asc => ord,
                    SortOrder::Desc => ord.reverse(),
                };
            }
        }
        std::cmp::Ordering::Equal
    });

    let mut temp_file =
        NamedTempFile::new(|| args.archive.parent().unwrap_or_else(|| ".".as_ref()))?;
    let mut archive = Archive::write_header(temp_file.as_file_mut())?;
    for entry in entries {
        archive.add_entry(entry)?;
    }
    archive.finalize()?;

    drop(source);

    let output = args.output.unwrap_or_else(|| args.archive.remove_part());
    temp_file.persist(output)?;

    Ok(())
}

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

    #[test]
    fn parse_sort_key_default_order() {
        assert_eq!(
            SortKey::from_str("name").unwrap(),
            SortKey {
                by: SortBy::Name,
                order: SortOrder::Asc,
            }
        );
        assert_eq!(
            SortKey::from_str("name:").unwrap(),
            SortKey {
                by: SortBy::Name,
                order: SortOrder::Asc,
            }
        );
    }

    #[test]
    fn parse_sort_key_explicit_orders() {
        assert_eq!(
            SortKey::from_str("name:asc").unwrap(),
            SortKey {
                by: SortBy::Name,
                order: SortOrder::Asc,
            }
        );
        assert_eq!(
            SortKey::from_str("name:desc").unwrap(),
            SortKey {
                by: SortBy::Name,
                order: SortOrder::Desc,
            }
        );
    }

    #[test]
    fn parse_sort_key_invalid() {
        assert!(SortKey::from_str("name:foo").is_err());
        assert!(SortKey::from_str("foo").is_err());
    }
}