#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
pub const RAR_EXTENSION: &str = "rar";
pub const RAR_OLD_PART_EXTENSION: &str = "r00";
pub const RAR_PART1_EXTENSION: &str = "part1.rar";
pub const RAR_EXTENSIONS: &[&str] = &["rar", "r00", "part1.rar"];
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RarVersion {
Rar4,
Rar5,
#[default]
Unknown,
}
impl RarVersion {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Rar4 => "rar4",
Self::Rar5 => "rar5",
Self::Unknown => "unknown",
}
}
}
impl fmt::Display for RarVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RarVolumeKind {
Single,
MultiPart,
Recovery,
#[default]
Unknown,
}
impl RarVolumeKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Single => "single",
Self::MultiPart => "multi-part",
Self::Recovery => "recovery",
Self::Unknown => "unknown",
}
}
}
impl fmt::Display for RarVolumeKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[must_use]
pub fn is_rar_extension(extension: &str) -> bool {
let normalized = normalize_extension(extension);
normalized == "rar" || is_old_part_extension(&normalized) || is_part_rar_extension(&normalized)
}
#[must_use]
pub fn is_rar_filename(name: &str) -> bool {
let parts = filename_parts(name);
match parts.as_slice() {
[.., last] if last == "rar" || is_old_part_extension(last) => true,
[.., previous, last] if is_part_label(previous) && last == "rar" => true,
_ => false,
}
}
fn is_part_rar_extension(extension: &str) -> bool {
let parts = extension
.split('.')
.filter(|part| !part.is_empty())
.collect::<Vec<_>>();
matches!(parts.as_slice(), [part, "rar"] if is_part_label(part))
}
fn is_old_part_extension(extension: &str) -> bool {
let bytes = extension.as_bytes();
bytes.len() == 3 && bytes[0] == b'r' && bytes[1].is_ascii_digit() && bytes[2].is_ascii_digit()
}
fn is_part_label(label: &str) -> bool {
let Some(number) = label.strip_prefix("part") else {
return false;
};
!number.is_empty() && number.bytes().all(|byte| byte.is_ascii_digit())
}
fn normalize_extension(extension: &str) -> String {
extension
.trim()
.trim_start_matches('.')
.to_ascii_lowercase()
}
fn filename_parts(name: &str) -> Vec<String> {
name.trim()
.to_ascii_lowercase()
.rsplit(['/', '\\'])
.next()
.unwrap_or_default()
.trim_start_matches('.')
.split('.')
.filter(|part| !part.is_empty())
.map(str::to_owned)
.collect()
}
#[cfg(test)]
mod tests {
use super::{RAR_EXTENSIONS, RarVersion, RarVolumeKind, is_rar_extension, is_rar_filename};
#[test]
fn detects_rar_extensions() {
assert!(is_rar_extension(".rar"));
assert!(is_rar_extension("r00"));
assert!(is_rar_extension("part1.rar"));
assert_eq!(RAR_EXTENSIONS[0], "rar");
}
#[test]
fn detects_rar_filenames() {
assert!(is_rar_filename("backup.rar"));
assert!(is_rar_filename("backup.r00"));
assert!(is_rar_filename("backup.part1.rar"));
assert!(!is_rar_filename("bundle.zip"));
}
#[test]
fn exposes_default_and_unknown_labels() {
assert_eq!(RarVersion::default(), RarVersion::Unknown);
assert_eq!(RarVersion::Rar4.as_str(), "rar4");
assert_eq!(RarVolumeKind::default(), RarVolumeKind::Unknown);
assert_eq!(RarVolumeKind::Recovery.as_str(), "recovery");
}
}