pash 0.3.0

Simple and easy app for generating and storing passwords
Documentation
//! Module for interacting with `config.toml` and `passwords.toml`.

use crate::files::*;
use crate::generator::Generator;

use anyhow::{Context, Result};
use serde_derive::Serialize;
use std::collections::HashMap;
use std::io::Read;
use std::io::Write;

/// Struct for serializing passwords' file structure data.
#[derive(Debug, Serialize)]
pub struct PasswordsFile(HashMap<String, HashMap<String, String>>);

/// `config.toml` and `passwords.toml` files' content.
pub struct FileContent {
    config_file: fs::File,
    pub password_file: fs::File,
}

impl FileContent {
    /// Open and store files in a struct.
    pub fn default() -> Result<Self> {
        let paths_list = FilePaths::default().context("Failed to fetch default paths")?;
        let config_file = fs::OpenOptions::new()
            .read(true)
            .write(true)
            .open(paths_list.config_path)
            .context("Could not open 'config.toml' file for reading and writing")?;
        let password_file = fs::OpenOptions::new()
            .read(true)
            .write(true)
            .open(paths_list.password_path)
            .context("Could not open 'passwords.toml' file for reading and writing")?;
        Ok(Self {
            config_file,
            password_file,
        })
    }

    /// Initialize default config if file is empty (we are always reading config from file).
    pub fn init_config(mut self) -> Result<Self> {
        let mut config_content: String = String::new();
        self.config_file
            .read_to_string(&mut config_content)
            .context("Failed to read and write 'config.toml' to string")?;
        if config_content.is_empty() {
            let config_struct: Generator = Generator::new(10)
                .lowercase(true)
                .uppercase(true)
                .symbols(true)
                .numbers(true)
                .begin_with_letter(true)
                .category("unknown".to_string());

            config_content = toml::to_string(&config_struct)
                .context("Failed to create string from default config")?;
            let config_byte_content = config_content.as_bytes();

            self.config_file
                .write_all(config_byte_content)
                .context("Failed to write default content to 'config.toml'")?;
        }

        Ok(self)
    }

    /// Load and edit `passwords.toml` file's content.
    /// *NOTE*: this function does not save content, only returns modified string.
    pub fn edit_passwords(
        mut self,
        category_name: Option<&str>,
        password_name: String,
    ) -> Result<String> {
        let mut config_content: String = String::new();
        let mut passwords_content: String = String::new();

        self.config_file
            .read_to_string(&mut config_content)
            .context("Failed to read and write 'config.toml' to string")?;

        self.password_file
            .read_to_string(&mut passwords_content)
            .context("Failed to read and write 'passwords.toml' to string")?;

        let generator: Generator = toml::from_str(&config_content).with_context(|| {
            format!("Failed to generate config from string: {}", &config_content)
        })?;
        let password = generator
            .generate()
            .context("Failed to generate password")?;

        if passwords_content.is_empty() {
            let mut passwords_map: HashMap<String, String> = HashMap::new();
            passwords_map.insert(password_name, password);

            let mut categories_map: HashMap<String, HashMap<String, String>> = HashMap::new();

            if let Some(category_value) = category_name {
                categories_map.insert(category_value.to_string(), passwords_map);
            } else {
                categories_map.insert(generator.category, passwords_map);
            }

            let file_content = PasswordsFile(categories_map);
            Ok(toml::to_string_pretty(&file_content)
                .with_context(|| format!("Failed to create string from: {:?}", &file_content))?)
        } else {
            let mut file_content: HashMap<String, HashMap<String, String>> =
                toml::from_str(&passwords_content)
                    .context("Failed to convert 'passwords.toml' as string to hashmap")?;

            if let Some(category_value) = category_name {
                if let Some(category_map) = file_content.get_mut(category_value) {
                    category_map.insert(password_name, password);
                } else {
                    let mut password_map: HashMap<String, String> = HashMap::new();

                    password_map.insert(password_name, password);
                    file_content.insert(category_value.to_string(), password_map);
                }
            } else if let Some(category_map) = file_content.get_mut(&generator.category) {
                category_map.insert(password_name, password);
            } else {
                let mut password_map: HashMap<String, String> = HashMap::new();

                password_map.insert(password_name, password);
                file_content.insert(generator.category, password_map);
            }

            println!("Password successfully generated!");

            Ok(toml::to_string_pretty(&file_content)
                .with_context(|| format!("Failed to create string from: {:?}", &file_content))?)
        }
    }

    /// Save modified content to file with given options (category and password name).
    pub fn save_passwords(self, category_name: Option<&str>, password_name: String) -> Result<()> {
        // this might be changed: FilesPaths is dropped right after this line
        let password_filepath = {
            FilePaths::default()
                .context("Failed to fetch default paths")?
                .password_path
        };
        let passwords_file_content = self
            .edit_passwords(category_name, password_name)
            .context("Failed to create new 'passwors.toml' content")?;
        let mut passwords_file = fs::OpenOptions::new()
            .write(true)
            .open(password_filepath)
            .context("Failed to open 'passwords.toml' for writing")?;
        passwords_file.write_all(passwords_file_content.as_bytes())
            .context("Failed to write to 'passwords.toml'")?;

        Ok(())
    }
}