trasher 1.3.3

A small command-line utility to replace 'rm' and 'del' by a trash system
use std::path::PathBuf;
use std::fmt;
use std::str;
use crc_any::CRC;
use base64::{encode_config, URL_SAFE_NO_PAD};
use chrono::prelude::*;
use chrono::LocalResult;
use regex::Regex;

lazy_static! {
    static ref DECODER: Regex = Regex::new(
        "^(?P<filename>.*)\\s\\[@\\s(?P<datetime>.*)\\]\\s\\{(?P<id>[\\da-zA-Z_\\-]+)\\}(\\.[^\\.]*)?$"
    ).unwrap();
}

static DATETIME_FORMAT: &'static str = "%Y.%m.%d_%Hh%Mm%Ss.%f%z";

pub struct TrashItem {
    id: String,
    filename: String,
    datetime: DateTime<Local>
}

impl TrashItem {
    pub fn new(filename: String, datetime: DateTime<Local>) -> Self {
        Self { id: Self::hash(datetime), filename, datetime }
    }

    pub fn new_now(filename: String) -> Self {
        Self::new(filename, Local::now())
    }

    pub fn hash(datetime: DateTime<Local>) -> String {
        let mut crc24 = CRC::crc24();
        crc24.digest(&datetime.timestamp().to_le_bytes());
        crc24.digest(&datetime.timestamp_subsec_nanos().to_le_bytes());
        encode_config(crc24.get_crc_vec_le(), URL_SAFE_NO_PAD)
    }

    pub fn id(&self) -> &str {
        &self.id
    }

    pub fn filename(&self) -> &str {
        &self.filename
    }

    // pub fn datetime(&self) -> &DateTime<Local> {
    //     &self.datetime
    // }

    pub fn trash_filename(&self) -> String {
        let extension = match PathBuf::from(&self.filename).extension() {
            // There cannot be any loss here as the provided filename is a valid UTF-8 string
            Some(ext) => format!(".{}", ext.to_string_lossy().to_string()),
            None => "".to_string()
        };

        format!("{} [@ {}] {{{}}}{}", self.filename, self.datetime.format(DATETIME_FORMAT), self.id, extension)
    }

    pub fn decode(final_name: &str) -> Result<TrashItem, TrashItemDecodingError> {
        let captured = DECODER.captures(final_name)
            .ok_or(TrashItemDecodingError::InvalidFilenameFormat)?;

        let datetime = DateTime::parse_from_str(&captured["datetime"], DATETIME_FORMAT)
            .map_err(TrashItemDecodingError::InvalidDateTime)?;
        
        let timezoned = match Local.from_local_datetime(&datetime.naive_local()) {
            LocalResult::None => return Err(TrashItemDecodingError::TimezoneDecodingError),
            LocalResult::Single(datetime) => datetime,
            LocalResult::Ambiguous(_, _) => return Err(TrashItemDecodingError::TimezoneDecodingError)
        };

        let decoded = Self::new(captured["filename"].to_string(), timezoned);

        if decoded.id != captured["id"] {
            return Err(TrashItemDecodingError::IDDoesNotMatch { found: captured["id"].to_string(), expected: decoded.id });
        } else {
            return Ok(decoded);
        }
    }
}

impl fmt::Display for TrashItem {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[ ID: {} ] {} (removed on: {})", self.id, self.filename, self.datetime.to_rfc2822())
    }
}

pub enum TrashItemDecodingError {
    InvalidFilenameFormat,
    InvalidDateTime(chrono::ParseError),
    TimezoneDecodingError,
    IDDoesNotMatch { found: String, expected: String }
}

impl fmt::Debug for TrashItemDecodingError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", match self {
            Self::InvalidFilenameFormat => "File name format is invalid".to_string(),
            Self::InvalidDateTime(err) => format!("Invalid date/time in file name: {:?}", err.to_string()),
            Self::TimezoneDecodingError => format!("Date/time is invalid for the local timezone"),
            Self::IDDoesNotMatch { found, expected } => format!("Found ID '{}' but expected one was '{}'", found, expected)
        })
    }
}