lazin 1.0.2

Primarily a dotfiles manager
use std::{
    collections::{BTreeMap, btree_map},
    fs::{self, File},
    io::{BufWriter, ErrorKind, Read, Write},
    path::{Path, PathBuf},
};

use lazin_error::{Context, LazinResult};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

const CACHE_FILE: &str = "lazin.cache";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FileHash(String);

impl FileHash {
    pub fn hash<P: AsRef<Path>>(path: P) -> LazinResult<Self> {
        let mut file = File::open(path).context("Failed to open file for hashing")?;
        let mut hasher = Sha256::new();
        let mut buffer = [0u8; 8192];

        loop {
            let n = file
                .read(&mut buffer)
                .context("Failed to read hashing file contents")?;
            if n == 0 {
                break;
            }
            hasher.update(&buffer[..n]);
        }

        let digest = hasher.finalize();
        Ok(Self(Self::encode(digest)))
    }

    fn encode(digest: sha2::digest::Output<sha2::Sha256>) -> String {
        digest.iter().map(|b| format!("{:02x}", b)).collect()
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct CacheEntries {
    encryption: BTreeMap<PathBuf, FileHash>,
}

#[derive(Debug)]
pub struct Cache {
    file_path: PathBuf,
    entries: CacheEntries,
}

pub enum Entry {
    Encryption(PathBuf, FileHash),
}

impl Cache {
    pub fn try_new<P: AsRef<Path>>(cache_dir: P) -> LazinResult<Self> {
        let cache_path = cache_dir.as_ref().join(CACHE_FILE);

        match cache_path.try_exists() {
            Ok(true) => {}
            Ok(false) => init_cache(&cache_path)?,
            Err(e) if e.kind() == ErrorKind::NotFound => init_cache(&cache_path)?,
            Err(e) => return Err(e).context("Failed to check if cache file exists"),
        };

        let mut file_data = String::new();
        fs::File::open(&cache_path)
            .context("Failed to open cache file")?
            .read_to_string(&mut file_data)
            .context("Failed to read cache file")?;
        let entries = toml::from_str(&file_data).context("Failed to parse cache entries")?;

        Ok(Self {
            file_path: cache_path,
            entries,
        })
    }

    pub fn save(&self) -> LazinResult {
        let serialized_entries = toml::to_string(&self.entries)?;

        let file = fs::OpenOptions::new().write(true).open(&self.file_path)?;
        let mut writer = BufWriter::new(file);

        writeln!(
            writer,
            "# This file is generated and should not be modified by hand."
        )?;
        writeln!(
            writer,
            "# You can safely commit this file into source control."
        )?;
        writeln!(writer)?;

        write!(writer, "{}", serialized_entries)?;

        writer.flush()?;
        Ok(())
    }

    pub fn add_entry(&mut self, entry: Entry) -> LazinResult<Option<Entry>> {
        let old_entry = match entry {
            Entry::Encryption(path_buf, hash) => match self.entries.encryption.entry(path_buf) {
                btree_map::Entry::Vacant(entry) => {
                    entry.insert(hash);
                    None
                }
                btree_map::Entry::Occupied(mut entry) => {
                    let old_hash = entry.insert(hash);
                    let path_buf = entry.key().clone();

                    Some(Entry::Encryption(path_buf, old_hash))
                }
            },
        };

        Ok(old_entry)
    }
}

fn init_cache(cache_file: &Path) -> LazinResult<()> {
    let file = fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(cache_file)?;

    let mut writer = BufWriter::new(file);
    writeln!(writer, "[encryption]")
        .context("Failed to write encryption toml attribute to cache")?;

    Ok(())
}