chore(DX): Improve the DX #27

Merged
awiteb merged 9 commits from awiteb/improve-dx into master 2024-05-06 23:19:20 +02:00 AGit
16 changed files with 182 additions and 67 deletions
Showing only changes of commit 17974ce74b - Show all commits

View file

@ -15,6 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
use clap::Args; use clap::Args;
use inquire::{Password, PasswordDisplayMode};
use crate::{ use crate::{
vault::{Vault, Vaults}, vault::{Vault, Vaults},
@ -23,11 +24,14 @@ use crate::{
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Add command, used to add new vault to the vaults file
pub struct Add { pub struct Add {
#[command(flatten)] #[command(flatten)]
vault_info: Vault, vault_info: Vault,
/// The password, if there is no value for it you will prompt it /// The password, if there is no value for it you will prompt it
#[arg(short, long)] #[arg(short, long)]
// FIXME: I think replacing `Option<Option<String>>` with custom type will be better
#[allow(clippy::option_option)]
password: Option<Option<String>>, password: Option<Option<String>>,
} }
@ -41,10 +45,10 @@ impl LprsCommand for Add {
Some(None) => { Some(None) => {
log::debug!("User didn't provide a password, prompting it"); log::debug!("User didn't provide a password, prompting it");
self.vault_info.password = Some( self.vault_info.password = Some(
inquire::Password::new("Vault password:") Password::new("Vault password:")
.without_confirmation() .without_confirmation()
.with_formatter(&|p| "*".repeat(p.chars().count())) .with_formatter(&|p| "*".repeat(p.chars().count()))
.with_display_mode(inquire::PasswordDisplayMode::Masked) .with_display_mode(PasswordDisplayMode::Masked)
.prompt()?, .prompt()?,
); );
} }

View file

@ -22,7 +22,8 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub struct Clean {} /// Clean command, used to clean the vaults file (remove all vaults)
pub struct Clean;
impl LprsCommand for Clean { impl LprsCommand for Clean {
fn run(self, vault_manager: Vaults) -> LprsResult<()> { fn run(self, vault_manager: Vaults) -> LprsResult<()> {

View file

@ -17,6 +17,7 @@
use std::num::NonZeroU64; use std::num::NonZeroU64;
use clap::Args; use clap::Args;
use inquire::{Password, PasswordDisplayMode};
use crate::{ use crate::{
vault::{Vault, Vaults}, vault::{Vault, Vaults},
@ -25,6 +26,7 @@ use crate::{
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Edit command, used to edit the vault content
pub struct Edit { pub struct Edit {
/// The password index. Check it from list command /// The password index. Check it from list command
index: NonZeroU64, index: NonZeroU64,
@ -37,6 +39,8 @@ pub struct Edit {
username: Option<String>, username: Option<String>,
#[arg(short, long)] #[arg(short, long)]
/// The new password, if there is no value for it you will prompt it /// The new password, if there is no value for it you will prompt it
// FIXME: I think replacing `Option<Option<String>>` with custom type will be better
#[allow(clippy::option_option)]
password: Option<Option<String>>, password: Option<Option<String>>,
#[arg(short, long)] #[arg(short, long)]
/// The new vault service /// The new vault service
@ -63,10 +67,10 @@ impl LprsCommand for Edit {
let password = match self.password { let password = match self.password {
Some(Some(password)) => Some(password), Some(Some(password)) => Some(password),
Some(None) => Some( Some(None) => Some(
inquire::Password::new("New vault password:") Password::new("New vault password:")
.without_confirmation() .without_confirmation()
.with_formatter(&|p| "*".repeat(p.chars().count())) .with_formatter(&|p| "*".repeat(p.chars().count()))
.with_display_mode(inquire::PasswordDisplayMode::Masked) .with_display_mode(PasswordDisplayMode::Masked)
.prompt()?, .prompt()?,
), ),
None => None, None => None,

View file

@ -25,6 +25,8 @@ use crate::{
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Export command, used to export the vaults in `lprs` format or `BitWarden` format.
/// The exported file will be a json file.
pub struct Export { pub struct Export {
/// The path to export to /// The path to export to
path: PathBuf, path: PathBuf,
@ -51,24 +53,29 @@ impl LprsCommand for Export {
} }
fn validate_args(&self) -> LprsResult<()> { fn validate_args(&self) -> LprsResult<()> {
if self if !self
.path .path
.extension() .extension()
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
{ {
if !self.path.exists() { return Err(LprsError::Io(IoError::new(
Ok(())
} else {
Err(LprsError::Io(IoError::new(
IoErrorKind::AlreadyExists,
format!("file `{}` is already exists", self.path.display()),
)))
}
} else {
Err(LprsError::Io(IoError::new(
IoErrorKind::InvalidInput, IoErrorKind::InvalidInput,
format!("file `{}` is not a json file", self.path.display()), format!("file `{}` is not a json file", self.path.display()),
))) )));
} }
if self.path.exists() {
return Err(LprsError::Io(IoError::new(
IoErrorKind::AlreadyExists,
format!("file `{}` is already exists", self.path.display()),
)));
}
if self.path.is_dir() {
return Err(LprsError::Io(IoError::new(
IoErrorKind::InvalidInput,
format!("file `{}` is a directory", self.path.display()),
)));
}
Ok(())
} }
} }

View file

@ -22,6 +22,7 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Generate command, used to generate a password
pub struct Gen { pub struct Gen {
/// The password length /// The password length
#[arg(default_value_t = NonZeroU64::new(18).unwrap())] #[arg(default_value_t = NonZeroU64::new(18).unwrap())]

View file

@ -30,6 +30,7 @@ use crate::{
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Import command, used to import vaults from the exported files, `lprs` or `BitWarden`
pub struct Import { pub struct Import {
/// The file path to import from /// The file path to import from
path: PathBuf, path: PathBuf,
@ -77,24 +78,29 @@ impl LprsCommand for Import {
} }
fn validate_args(&self) -> LprsResult<()> { fn validate_args(&self) -> LprsResult<()> {
if self.path.exists() { if self
if self .path
.path .extension()
.extension() .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json"))
.is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) {
{ return Err(LprsError::Io(IoError::new(
Ok(()) IoErrorKind::InvalidInput,
} else { format!("file `{}` is not a json file", self.path.display()),
Err(LprsError::Io(IoError::new( )));
IoErrorKind::InvalidInput, }
format!("file `{}` is not a json file", self.path.display()), if !self.path.exists() {
))) return Err(LprsError::Io(IoError::new(
}
} else {
Err(LprsError::Io(IoError::new(
IoErrorKind::NotFound, IoErrorKind::NotFound,
format!("file `{}` not found", self.path.display()), format!("file `{}` not found", self.path.display()),
))) )));
} }
if self.path.is_dir() {
return Err(LprsError::Io(IoError::new(
IoErrorKind::InvalidInput,
format!("file `{}` is a directory", self.path.display()),
)));
}
Ok(())
} }
} }

View file

@ -23,6 +23,7 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// List command, used to list the vaults and search
pub struct List { pub struct List {
/// Return the password with spesifc index /// Return the password with spesifc index
#[arg(short, long, value_name = "INDEX")] #[arg(short, long, value_name = "INDEX")]

View file

@ -20,13 +20,22 @@ use clap::Parser;
use crate::{impl_commands, utils, vault::Vaults, LprsCommand, LprsResult}; use crate::{impl_commands, utils, vault::Vaults, LprsCommand, LprsResult};
/// Add command, used to add new vault to the vaults file
pub mod add_command; pub mod add_command;
/// Clean command, used to clean the vaults file (remove all vaults)
pub mod clean_command; pub mod clean_command;
/// Edit command, used to edit the vault content
pub mod edit_command; pub mod edit_command;
/// Export command, used to export the vaults
/// in `lprs` format or `BitWarden` format
pub mod export_command; pub mod export_command;
/// Generate command, used to generate a password
pub mod gen_command; pub mod gen_command;
/// Import command, used to import vaults from the exported files, `lprs` or `BitWarden`
pub mod import_command; pub mod import_command;
/// List command, used to list the vaults and search
pub mod list_command; pub mod list_command;
/// Remove command, used to remove vault from the vaults file
pub mod remove_command; pub mod remove_command;
/// The lprs commands /// The lprs commands
@ -56,6 +65,7 @@ impl_commands!(Commands, Add Remove List Clean Edit Gen Export Import);
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// The lprs cli, manage the CLI arguments and run the commands
pub struct Cli { pub struct Cli {
/// The vaults json file /// The vaults json file
#[arg(short = 'f', long)] #[arg(short = 'f', long)]
@ -65,11 +75,19 @@ pub struct Cli {
pub verbose: bool, pub verbose: bool,
#[command(subcommand)] #[command(subcommand)]
/// The provided command to run
pub command: Commands, pub command: Commands,
} }
impl Cli { impl Cli {
/// Run the cli /// Run the cli
///
/// # Errors
/// - If can't get the default vaults file
/// - If the vaults file can't be created
/// - If the user provide a worng CLI arguments
/// - If failed to write in the vaults file
/// - (errors from the commands)
pub fn run(self) -> LprsResult<()> { pub fn run(self) -> LprsResult<()> {
let vaults_file = if let Some(path) = self.vaults_file { let vaults_file = if let Some(path) = self.vaults_file {
log::info!("Using the given vaults file"); log::info!("Using the given vaults file");
@ -83,7 +101,7 @@ impl Cli {
path path
} else { } else {
log::info!("Using the default vaults file"); log::info!("Using the default vaults file");
crate::utils::vaults_file()? utils::vaults_file()?
}; };
log::debug!("Vaults file: {}", vaults_file.display()); log::debug!("Vaults file: {}", vaults_file.display());

View file

@ -22,6 +22,7 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult};
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
/// Remove command, used to remove a vault from the vaults file
pub struct Remove { pub struct Remove {
/// The password index /// The password index
index: NonZeroU64, index: NonZeroU64,
@ -37,14 +38,14 @@ impl LprsCommand for Remove {
log::debug!("Removing vault at index: {index}"); log::debug!("Removing vault at index: {index}");
if index > vault_manager.vaults.len() { if index > vault_manager.vaults.len() {
if !self.force { if self.force {
return Err(LprsError::Other(
"The index is greater than the passwords counts".to_owned(),
));
} else {
log::error!( log::error!(
"The index is greater than the passwords counts, but the force flag is enabled" "The index is greater than the passwords counts, but the force flag is enabled"
); );
} else {
return Err(LprsError::Other(
"The index is greater than the passwords counts".to_owned(),
));
} }
} else { } else {
vault_manager.vaults.remove(index); vault_manager.vaults.remove(index);

View file

@ -14,11 +14,13 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
use std::{process::ExitCode, string::FromUtf8Error}; use std::{io, process::ExitCode, result, string::FromUtf8Error};
pub type Result<T> = std::result::Result<T, Error>; /// The result type used in the whole project
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error { pub enum Error {
#[error("Encryption Error: {0}")] #[error("Encryption Error: {0}")]
Encryption(String), Encryption(String),
@ -50,12 +52,12 @@ pub enum Error {
#[error("Project Folder Error: {0}")] #[error("Project Folder Error: {0}")]
ProjectDir(String), ProjectDir(String),
#[error("IO Error: {0}")] #[error("IO Error: {0}")]
Io(#[from] std::io::Error), Io(#[from] io::Error),
} }
impl Error { impl Error {
/// Return the exit code of the error /// Return the exit code of the error
pub fn exit_code(&self) -> ExitCode { pub const fn exit_code(&self) -> ExitCode {
// TODO: Exit with more specific exit code // TODO: Exit with more specific exit code
ExitCode::FAILURE ExitCode::FAILURE
} }

View file

@ -46,7 +46,6 @@
/// } /// }
/// } /// }
/// } /// }
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! impl_commands { macro_rules! impl_commands {

View file

@ -13,14 +13,20 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
#![doc = include_str!("../README.md")]
use clap::Parser; use clap::Parser;
use inquire::InquireError; use inquire::InquireError;
use std::env;
use std::process::ExitCode; use std::process::ExitCode;
/// The main module of the lprs crate, contains the cli and the commands.
pub mod cli; pub mod cli;
/// The errors module, contains the errors and the result type.
pub mod errors; pub mod errors;
/// The utils module, contains the utility functions of all the modules.
pub mod utils; pub mod utils;
/// The vault module, contains the vault struct and the vaults manager.
pub mod vault; pub mod vault;
mod macros; mod macros;
@ -30,17 +36,20 @@ pub use base64::engine::general_purpose::STANDARD as BASE64;
pub use errors::{Error as LprsError, Result as LprsResult}; pub use errors::{Error as LprsError, Result as LprsResult};
pub use traits::*; pub use traits::*;
/// The default vaults file name. Used to store the vaults.
pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs"; pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs";
#[cfg(feature = "update-notify")] #[cfg(feature = "update-notify")]
/// The version of the lprs crate.
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(feature = "update-notify")] #[cfg(feature = "update-notify")]
/// The last version check file. Used to store the last version check time.
pub const LAST_VERSION_CHECK_FILE: &str = ".last_version_check"; pub const LAST_VERSION_CHECK_FILE: &str = ".last_version_check";
fn main() -> ExitCode { fn main() -> ExitCode {
let lprs_cli = cli::Cli::parse(); let lprs_cli = cli::Cli::parse();
if lprs_cli.verbose { if lprs_cli.verbose {
std::env::set_var("RUST_LOG", "lprs"); env::set_var("RUST_LOG", "lprs");
} }
pretty_env_logger::init(); pretty_env_logger::init();

View file

@ -17,6 +17,7 @@
use crate::{vault::Vaults, LprsResult}; use crate::{vault::Vaults, LprsResult};
/// Trait to work with the commands /// Trait to work with the commands
#[allow(clippy::missing_errors_doc)]
pub trait LprsCommand { pub trait LprsCommand {
/// Run the command, should do all the logic, even the export /// Run the command, should do all the logic, even the export
fn run(self, vault_manager: Vaults) -> LprsResult<()>; fn run(self, vault_manager: Vaults) -> LprsResult<()>;

View file

@ -16,12 +16,20 @@
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use inquire::validator::Validation; use inquire::{validator::Validation, PasswordDisplayMode};
use passwords::{analyzer, scorer};
use sha2::Digest; use sha2::Digest;
#[cfg(feature = "update-notify")]
use reqwest::blocking::Client as BlockingClient;
use crate::{LprsError, LprsResult}; use crate::{LprsError, LprsResult};
/// Returns the local project dir joined with the given file name /// Returns the local project dir joined with the given file name
///
/// ## Errors
/// - If the project dir can't be extracted from the OS
/// - If the local project dir can't be created
pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> { pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
let local_dir = directories::ProjectDirs::from("", "", "lprs") let local_dir = directories::ProjectDirs::from("", "", "lprs")
.map(|d| d.data_local_dir().to_path_buf()) .map(|d| d.data_local_dir().to_path_buf())
@ -37,6 +45,10 @@ pub fn local_project_file(filename: &str) -> LprsResult<PathBuf> {
} }
/// Returns the default vaults json file /// Returns the default vaults json file
///
/// ## Errors
/// - If the project dir can't be extracted from the OS
/// - If the vaults file can't be created
pub fn vaults_file() -> LprsResult<PathBuf> { pub fn vaults_file() -> LprsResult<PathBuf> {
let vaults_file = local_project_file(crate::DEFAULT_VAULTS_FILE)?; let vaults_file = local_project_file(crate::DEFAULT_VAULTS_FILE)?;
if !vaults_file.exists() { if !vaults_file.exists() {
@ -50,22 +62,26 @@ pub fn vaults_file() -> LprsResult<PathBuf> {
/// ## To pass /// ## To pass
/// - The length must be higher than 14 (>=15) /// - The length must be higher than 14 (>=15)
/// - Its score must be greater than 80.0 /// - Its score must be greater than 80.0
///
/// ## Errors
/// - There is no errors, just the return type of inquire validator
/// must be Result<Validation, inquire::CustomUserError>
pub fn password_validator(password: &str) -> Result<Validation, inquire::CustomUserError> { pub fn password_validator(password: &str) -> Result<Validation, inquire::CustomUserError> {
let analyzed = passwords::analyzer::analyze(password); let analyzed = analyzer::analyze(password);
if analyzed.length() < 15 { Ok(if analyzed.length() < 15 {
return Ok(Validation::Invalid( Validation::Invalid("The master password length must be beggier then 15".into())
"The master password length must be beggier then 15".into(), } else if scorer::score(&analyzed) < 80.0 {
)); Validation::Invalid("Your master password is not stronge enough".into())
} else if passwords::scorer::score(&analyzed) < 80.0 { } else {
return Ok(Validation::Invalid( Validation::Valid
"Your master password is not stronge enough".into(), })
));
}
Ok(Validation::Valid)
} }
/// Ask the user for the master password, then returns it /// Ask the user for the master password, then returns it
/// ///
/// ## Errors
/// - Can't read the password from the user
///
/// Return's the password as 32 bytes after hash it (256 bit) /// Return's the password as 32 bytes after hash it (256 bit)
pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]> { pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]> {
inquire::Password { inquire::Password {
@ -79,13 +95,17 @@ pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]>
..inquire::Password::new("") ..inquire::Password::new("")
} }
.with_formatter(&|p| "*".repeat(p.chars().count())) .with_formatter(&|p| "*".repeat(p.chars().count()))
.with_display_mode(inquire::PasswordDisplayMode::Masked) .with_display_mode(PasswordDisplayMode::Masked)
.prompt() .prompt()
.map(|p| sha2::Sha256::digest(p).into()) .map(|p| sha2::Sha256::digest(p).into())
.map_err(Into::into) .map_err(Into::into)
} }
/// Retuns the current lprs version from `crates.io` /// Retuns the current lprs version from `crates.io`
///
/// ## Errors
/// - The project dir can't be extracted from the OS
/// - If the last version check file can't be created
#[cfg(feature = "update-notify")] #[cfg(feature = "update-notify")]
pub fn lprs_version() -> LprsResult<Option<String>> { pub fn lprs_version() -> LprsResult<Option<String>> {
use std::time::SystemTime; use std::time::SystemTime;
@ -108,7 +128,7 @@ pub fn lprs_version() -> LprsResult<Option<String>> {
// Check if the last check is before one hour or not // Check if the last check is before one hour or not
if (current_time - last_check) >= (60 * 60) || current_time == last_check { if (current_time - last_check) >= (60 * 60) || current_time == last_check {
if let Ok(Ok(response)) = reqwest::blocking::Client::new() if let Ok(Ok(response)) = BlockingClient::new()
.get("https://crates.io/api/v1/crates/lprs") .get("https://crates.io/api/v1/crates/lprs")
.header( .header(
"User-Agent", "User-Agent",
@ -117,8 +137,8 @@ pub fn lprs_version() -> LprsResult<Option<String>> {
.send() .send()
.map(|r| r.text()) .map(|r| r.text())
{ {
let re = let re = regex::Regex::new(r#""max_stable_version":"(?<version>\d+\.\d+\.\d+)""#)
regex::Regex::new(r#""max_stable_version":"(?<version>\d+\.\d+\.\d+)""#).unwrap(); .expect("The regex is correct");
if let Some(cap) = re.captures(&response) { if let Some(cap) = re.captures(&response) {
return Ok(cap.name("version").map(|m| m.as_str().to_string())); return Ok(cap.name("version").map(|m| m.as_str().to_string()));
} }

View file

@ -1,3 +1,23 @@
// Lprs - A local CLI vault manager
// Copyright (C) 2024 Awiteb <a@4rs.nl>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
// This file is not important, it is just a struct that is used to serialize and deserialize the vaults
// from and to the BitWarden format.
#![allow(missing_docs)]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{Vault, Vaults}; use super::{Vault, Vaults};

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
use std::{fs, path::PathBuf}; use std::{fmt, fs, path::PathBuf};
use base64::Engine; use base64::Engine;
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize};
use crate::{LprsError, LprsResult}; use crate::{LprsError, LprsResult};
/// The chiper module, used to encrypt and decrypt the vaults
pub mod cipher; pub mod cipher;
mod bitwarden; mod bitwarden;
@ -29,8 +30,12 @@ mod bitwarden;
pub use bitwarden::*; pub use bitwarden::*;
#[derive(Clone, Debug, ValueEnum)] #[derive(Clone, Debug, ValueEnum)]
/// The vaults format
pub enum Format { pub enum Format {
/// The lprs format, which is the default format
/// and is is the result of the serialization/deserialization of the Vaults struct
Lprs, Lprs,
/// The BitWarden format, which is the result of the serialization/deserialization of the BitWardenPasswords struct
BitWarden, BitWarden,
} }
@ -85,7 +90,7 @@ impl Vault {
/// Return the name of the vault with the service if there /// Return the name of the vault with the service if there
pub fn list_name(&self) -> String { pub fn list_name(&self) -> String {
use std::fmt::Write; use fmt::Write;
let mut list_name = self.name.clone(); let mut list_name = self.name.clone();
if let Some(ref username) = self.username { if let Some(ref username) = self.username {
write!(&mut list_name, " <{username}>").expect("String never fail"); write!(&mut list_name, " <{username}>").expect("String never fail");
@ -119,6 +124,10 @@ impl Vaults {
/// ///
/// This function used to backup the vaults. /// This function used to backup the vaults.
/// ///
/// ## Errors
/// - If the serialization failed
/// - if the encryption failed
///
/// Note: The returned string is `Vec<Vault>` /// Note: The returned string is `Vec<Vault>`
pub fn json_export(&self) -> LprsResult<String> { pub fn json_export(&self) -> LprsResult<String> {
let encrypt = |val: &str| { let encrypt = |val: &str| {
@ -147,6 +156,10 @@ impl Vaults {
/// Reload the vaults from json data. /// Reload the vaults from json data.
/// ///
/// ## Errors
/// - If base64 decoding failed (of the vault field encrypted data)
/// - If decryption failed (wrong master password or the data is corrupted)
///
/// This function used to import backup vaults. /// This function used to import backup vaults.
pub fn json_reload(master_password: &[u8; 32], json_data: &[u8]) -> LprsResult<Vec<Vault>> { pub fn json_reload(master_password: &[u8; 32], json_data: &[u8]) -> LprsResult<Vec<Vault>> {
let decrypt = |val: &str| { let decrypt = |val: &str| {
@ -172,6 +185,9 @@ impl Vaults {
} }
/// Encrypt the vaults then export it to the file /// Encrypt the vaults then export it to the file
///
/// ## Errors
/// - Writing to the file failed
pub fn try_export(self) -> LprsResult<()> { pub fn try_export(self) -> LprsResult<()> {
log::debug!( log::debug!(
"Trying to export the vaults to the file: {}", "Trying to export the vaults to the file: {}",
@ -185,6 +201,11 @@ impl Vaults {
} }
/// Reload the vaults from the file then decrypt it /// Reload the vaults from the file then decrypt it
///
/// ## Errors
/// - Reading the file failed
/// - Decryption failed (wrong master password or the data is corrupted)
/// - Bytecode deserialization failed (the data is corrupted)
pub fn try_reload(vaults_file: PathBuf, master_password: [u8; 32]) -> LprsResult<Self> { pub fn try_reload(vaults_file: PathBuf, master_password: [u8; 32]) -> LprsResult<Self> {
let vaults_data = fs::read(&vaults_file)?; let vaults_data = fs::read(&vaults_file)?;
@ -198,8 +219,8 @@ impl Vaults {
} }
} }
impl std::fmt::Display for Format { impl fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
"{}", "{}",
@ -210,8 +231,8 @@ impl std::fmt::Display for Format {
} }
} }
impl std::fmt::Display for Vault { impl fmt::Display for Vault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Name: {}", self.name)?; write!(f, "Name: {}", self.name)?;
if let Some(ref username) = self.username { if let Some(ref username) = self.username {
write!(f, "\nUsername: {username}")?; write!(f, "\nUsername: {username}")?;