From 17974ce74be99060ce14a286d578e4e78a7c2d62 Mon Sep 17 00:00:00 2001 From: Awiteb Date: Sun, 5 May 2024 22:42:16 +0300 Subject: [PATCH] chore(lint): Impl the clippy lints --- src/cli/add_command.rs | 8 ++++-- src/cli/clean_command.rs | 3 ++- src/cli/edit_command.rs | 8 ++++-- src/cli/export_command.rs | 31 ++++++++++++++--------- src/cli/gen_command.rs | 1 + src/cli/import_command.rs | 38 ++++++++++++++++------------ src/cli/list_command.rs | 1 + src/cli/mod.rs | 20 ++++++++++++++- src/cli/remove_command.rs | 11 +++++---- src/errors.rs | 10 +++++--- src/macros.rs | 1 - src/main.rs | 11 ++++++++- src/traits.rs | 1 + src/utils.rs | 52 +++++++++++++++++++++++++++------------ src/vault/bitwarden.rs | 20 +++++++++++++++ src/vault/mod.rs | 33 ++++++++++++++++++++----- 16 files changed, 182 insertions(+), 67 deletions(-) diff --git a/src/cli/add_command.rs b/src/cli/add_command.rs index d8389a9..579c753 100644 --- a/src/cli/add_command.rs +++ b/src/cli/add_command.rs @@ -15,6 +15,7 @@ // along with this program. If not, see . use clap::Args; +use inquire::{Password, PasswordDisplayMode}; use crate::{ vault::{Vault, Vaults}, @@ -23,11 +24,14 @@ use crate::{ #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] +/// Add command, used to add new vault to the vaults file pub struct Add { #[command(flatten)] vault_info: Vault, /// The password, if there is no value for it you will prompt it #[arg(short, long)] + // FIXME: I think replacing `Option>` with custom type will be better + #[allow(clippy::option_option)] password: Option>, } @@ -41,10 +45,10 @@ impl LprsCommand for Add { Some(None) => { log::debug!("User didn't provide a password, prompting it"); self.vault_info.password = Some( - inquire::Password::new("Vault password:") + Password::new("Vault password:") .without_confirmation() .with_formatter(&|p| "*".repeat(p.chars().count())) - .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_display_mode(PasswordDisplayMode::Masked) .prompt()?, ); } diff --git a/src/cli/clean_command.rs b/src/cli/clean_command.rs index 2e8cd7b..a01872e 100644 --- a/src/cli/clean_command.rs +++ b/src/cli/clean_command.rs @@ -22,7 +22,8 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult}; #[derive(Debug, Args)] #[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 { fn run(self, vault_manager: Vaults) -> LprsResult<()> { diff --git a/src/cli/edit_command.rs b/src/cli/edit_command.rs index fba2433..fbed9a6 100644 --- a/src/cli/edit_command.rs +++ b/src/cli/edit_command.rs @@ -17,6 +17,7 @@ use std::num::NonZeroU64; use clap::Args; +use inquire::{Password, PasswordDisplayMode}; use crate::{ vault::{Vault, Vaults}, @@ -25,6 +26,7 @@ use crate::{ #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] +/// Edit command, used to edit the vault content pub struct Edit { /// The password index. Check it from list command index: NonZeroU64, @@ -37,6 +39,8 @@ pub struct Edit { username: Option, #[arg(short, long)] /// The new password, if there is no value for it you will prompt it + // FIXME: I think replacing `Option>` with custom type will be better + #[allow(clippy::option_option)] password: Option>, #[arg(short, long)] /// The new vault service @@ -63,10 +67,10 @@ impl LprsCommand for Edit { let password = match self.password { Some(Some(password)) => Some(password), Some(None) => Some( - inquire::Password::new("New vault password:") + Password::new("New vault password:") .without_confirmation() .with_formatter(&|p| "*".repeat(p.chars().count())) - .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_display_mode(PasswordDisplayMode::Masked) .prompt()?, ), None => None, diff --git a/src/cli/export_command.rs b/src/cli/export_command.rs index 3e2023a..304ad1c 100644 --- a/src/cli/export_command.rs +++ b/src/cli/export_command.rs @@ -25,6 +25,8 @@ use crate::{ #[derive(Debug, Args)] #[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 { /// The path to export to path: PathBuf, @@ -51,24 +53,29 @@ impl LprsCommand for Export { } fn validate_args(&self) -> LprsResult<()> { - if self + if !self .path .extension() .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) { - if !self.path.exists() { - Ok(()) - } else { - Err(LprsError::Io(IoError::new( - IoErrorKind::AlreadyExists, - format!("file `{}` is already exists", self.path.display()), - ))) - } - } else { - Err(LprsError::Io(IoError::new( + return 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( + 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(()) } } diff --git a/src/cli/gen_command.rs b/src/cli/gen_command.rs index ba4ff5f..2e4ad05 100644 --- a/src/cli/gen_command.rs +++ b/src/cli/gen_command.rs @@ -22,6 +22,7 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult}; #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] +/// Generate command, used to generate a password pub struct Gen { /// The password length #[arg(default_value_t = NonZeroU64::new(18).unwrap())] diff --git a/src/cli/import_command.rs b/src/cli/import_command.rs index ff70302..7b5e993 100644 --- a/src/cli/import_command.rs +++ b/src/cli/import_command.rs @@ -30,6 +30,7 @@ use crate::{ #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] +/// Import command, used to import vaults from the exported files, `lprs` or `BitWarden` pub struct Import { /// The file path to import from path: PathBuf, @@ -77,24 +78,29 @@ impl LprsCommand for Import { } fn validate_args(&self) -> LprsResult<()> { - if self.path.exists() { - if self - .path - .extension() - .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) - { - Ok(()) - } else { - Err(LprsError::Io(IoError::new( - IoErrorKind::InvalidInput, - format!("file `{}` is not a json file", self.path.display()), - ))) - } - } else { - Err(LprsError::Io(IoError::new( + if self + .path + .extension() + .is_some_and(|e| e.to_string_lossy().eq_ignore_ascii_case("json")) + { + return 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( IoErrorKind::NotFound, 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(()) } } diff --git a/src/cli/list_command.rs b/src/cli/list_command.rs index 10e8bc3..7edc27f 100644 --- a/src/cli/list_command.rs +++ b/src/cli/list_command.rs @@ -23,6 +23,7 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult}; #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] +/// List command, used to list the vaults and search pub struct List { /// Return the password with spesifc index #[arg(short, long, value_name = "INDEX")] diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 5f63b44..4db418c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -20,13 +20,22 @@ use clap::Parser; use crate::{impl_commands, utils, vault::Vaults, LprsCommand, LprsResult}; +/// Add command, used to add new vault to the vaults file pub mod add_command; +/// Clean command, used to clean the vaults file (remove all vaults) pub mod clean_command; +/// Edit command, used to edit the vault content pub mod edit_command; +/// Export command, used to export the vaults +/// in `lprs` format or `BitWarden` format pub mod export_command; +/// Generate command, used to generate a password pub mod gen_command; +/// Import command, used to import vaults from the exported files, `lprs` or `BitWarden` pub mod import_command; +/// List command, used to list the vaults and search pub mod list_command; +/// Remove command, used to remove vault from the vaults file pub mod remove_command; /// The lprs commands @@ -56,6 +65,7 @@ impl_commands!(Commands, Add Remove List Clean Edit Gen Export Import); #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] +/// The lprs cli, manage the CLI arguments and run the commands pub struct Cli { /// The vaults json file #[arg(short = 'f', long)] @@ -65,11 +75,19 @@ pub struct Cli { pub verbose: bool, #[command(subcommand)] + /// The provided command to run pub command: Commands, } impl 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<()> { let vaults_file = if let Some(path) = self.vaults_file { log::info!("Using the given vaults file"); @@ -83,7 +101,7 @@ impl Cli { path } else { log::info!("Using the default vaults file"); - crate::utils::vaults_file()? + utils::vaults_file()? }; log::debug!("Vaults file: {}", vaults_file.display()); diff --git a/src/cli/remove_command.rs b/src/cli/remove_command.rs index df97091..c563ae2 100644 --- a/src/cli/remove_command.rs +++ b/src/cli/remove_command.rs @@ -22,6 +22,7 @@ use crate::{vault::Vaults, LprsCommand, LprsError, LprsResult}; #[derive(Debug, Args)] #[command(author, version, about, long_about = None)] +/// Remove command, used to remove a vault from the vaults file pub struct Remove { /// The password index index: NonZeroU64, @@ -37,14 +38,14 @@ impl LprsCommand for Remove { log::debug!("Removing vault at index: {index}"); if index > vault_manager.vaults.len() { - if !self.force { - return Err(LprsError::Other( - "The index is greater than the passwords counts".to_owned(), - )); - } else { + if self.force { log::error!( "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 { vault_manager.vaults.remove(index); diff --git a/src/errors.rs b/src/errors.rs index e7bde78..774b8a4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -14,11 +14,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::{process::ExitCode, string::FromUtf8Error}; +use std::{io, process::ExitCode, result, string::FromUtf8Error}; -pub type Result = std::result::Result; +/// The result type used in the whole project +pub type Result = result::Result; #[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] pub enum Error { #[error("Encryption Error: {0}")] Encryption(String), @@ -50,12 +52,12 @@ pub enum Error { #[error("Project Folder Error: {0}")] ProjectDir(String), #[error("IO Error: {0}")] - Io(#[from] std::io::Error), + Io(#[from] io::Error), } impl 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 ExitCode::FAILURE } diff --git a/src/macros.rs b/src/macros.rs index 7507f8d..624ccb7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -46,7 +46,6 @@ /// } /// } /// } - /// ``` #[macro_export] macro_rules! impl_commands { diff --git a/src/main.rs b/src/main.rs index 1ab2191..dc1d274 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,14 +13,20 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#![doc = include_str!("../README.md")] use clap::Parser; use inquire::InquireError; +use std::env; use std::process::ExitCode; +/// The main module of the lprs crate, contains the cli and the commands. pub mod cli; +/// The errors module, contains the errors and the result type. pub mod errors; +/// The utils module, contains the utility functions of all the modules. pub mod utils; +/// The vault module, contains the vault struct and the vaults manager. pub mod vault; 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 traits::*; +/// The default vaults file name. Used to store the vaults. pub const DEFAULT_VAULTS_FILE: &str = "vaults.lprs"; #[cfg(feature = "update-notify")] +/// The version of the lprs crate. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); #[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"; fn main() -> ExitCode { let lprs_cli = cli::Cli::parse(); if lprs_cli.verbose { - std::env::set_var("RUST_LOG", "lprs"); + env::set_var("RUST_LOG", "lprs"); } pretty_env_logger::init(); diff --git a/src/traits.rs b/src/traits.rs index 4c7985e..09cf7ad 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -17,6 +17,7 @@ use crate::{vault::Vaults, LprsResult}; /// Trait to work with the commands +#[allow(clippy::missing_errors_doc)] pub trait LprsCommand { /// Run the command, should do all the logic, even the export fn run(self, vault_manager: Vaults) -> LprsResult<()>; diff --git a/src/utils.rs b/src/utils.rs index 98e38d7..57a7bdc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,12 +16,20 @@ use std::{fs, path::PathBuf}; -use inquire::validator::Validation; +use inquire::{validator::Validation, PasswordDisplayMode}; +use passwords::{analyzer, scorer}; use sha2::Digest; +#[cfg(feature = "update-notify")] +use reqwest::blocking::Client as BlockingClient; + use crate::{LprsError, LprsResult}; /// 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 { let local_dir = directories::ProjectDirs::from("", "", "lprs") .map(|d| d.data_local_dir().to_path_buf()) @@ -37,6 +45,10 @@ pub fn local_project_file(filename: &str) -> LprsResult { } /// 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 { let vaults_file = local_project_file(crate::DEFAULT_VAULTS_FILE)?; if !vaults_file.exists() { @@ -50,22 +62,26 @@ pub fn vaults_file() -> LprsResult { /// ## To pass /// - The length must be higher than 14 (>=15) /// - Its score must be greater than 80.0 +/// +/// ## Errors +/// - There is no errors, just the return type of inquire validator +/// must be Result pub fn password_validator(password: &str) -> Result { - let analyzed = passwords::analyzer::analyze(password); - if analyzed.length() < 15 { - return Ok(Validation::Invalid( - "The master password length must be beggier then 15".into(), - )); - } else if passwords::scorer::score(&analyzed) < 80.0 { - return Ok(Validation::Invalid( - "Your master password is not stronge enough".into(), - )); - } - Ok(Validation::Valid) + let analyzed = analyzer::analyze(password); + Ok(if analyzed.length() < 15 { + Validation::Invalid("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 { + Validation::Valid + }) } /// 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) pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]> { inquire::Password { @@ -79,13 +95,17 @@ pub fn master_password_prompt(is_new_vaults_file: bool) -> LprsResult<[u8; 32]> ..inquire::Password::new("") } .with_formatter(&|p| "*".repeat(p.chars().count())) - .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_display_mode(PasswordDisplayMode::Masked) .prompt() .map(|p| sha2::Sha256::digest(p).into()) .map_err(Into::into) } /// 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")] pub fn lprs_version() -> LprsResult> { use std::time::SystemTime; @@ -108,7 +128,7 @@ pub fn lprs_version() -> LprsResult> { // Check if the last check is before one hour or not 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") .header( "User-Agent", @@ -117,8 +137,8 @@ pub fn lprs_version() -> LprsResult> { .send() .map(|r| r.text()) { - let re = - regex::Regex::new(r#""max_stable_version":"(?\d+\.\d+\.\d+)""#).unwrap(); + let re = regex::Regex::new(r#""max_stable_version":"(?\d+\.\d+\.\d+)""#) + .expect("The regex is correct"); if let Some(cap) = re.captures(&response) { return Ok(cap.name("version").map(|m| m.as_str().to_string())); } diff --git a/src/vault/bitwarden.rs b/src/vault/bitwarden.rs index 962127c..e898e0e 100644 --- a/src/vault/bitwarden.rs +++ b/src/vault/bitwarden.rs @@ -1,3 +1,23 @@ +// Lprs - A local CLI vault manager +// Copyright (C) 2024 Awiteb +// +// 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 . + +// 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 super::{Vault, Vaults}; diff --git a/src/vault/mod.rs b/src/vault/mod.rs index 11d2595..38deef9 100644 --- a/src/vault/mod.rs +++ b/src/vault/mod.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::{fs, path::PathBuf}; +use std::{fmt, fs, path::PathBuf}; use base64::Engine; use clap::{Parser, ValueEnum}; @@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize}; use crate::{LprsError, LprsResult}; +/// The chiper module, used to encrypt and decrypt the vaults pub mod cipher; mod bitwarden; @@ -29,8 +30,12 @@ mod bitwarden; pub use bitwarden::*; #[derive(Clone, Debug, ValueEnum)] +/// The vaults 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, + /// The BitWarden format, which is the result of the serialization/deserialization of the BitWardenPasswords struct BitWarden, } @@ -85,7 +90,7 @@ impl Vault { /// Return the name of the vault with the service if there pub fn list_name(&self) -> String { - use std::fmt::Write; + use fmt::Write; let mut list_name = self.name.clone(); if let Some(ref username) = self.username { write!(&mut list_name, " <{username}>").expect("String never fail"); @@ -119,6 +124,10 @@ impl Vaults { /// /// This function used to backup the vaults. /// + /// ## Errors + /// - If the serialization failed + /// - if the encryption failed + /// /// Note: The returned string is `Vec` pub fn json_export(&self) -> LprsResult { let encrypt = |val: &str| { @@ -147,6 +156,10 @@ impl Vaults { /// 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. pub fn json_reload(master_password: &[u8; 32], json_data: &[u8]) -> LprsResult> { let decrypt = |val: &str| { @@ -172,6 +185,9 @@ impl Vaults { } /// Encrypt the vaults then export it to the file + /// + /// ## Errors + /// - Writing to the file failed pub fn try_export(self) -> LprsResult<()> { log::debug!( "Trying to export the vaults to the file: {}", @@ -185,6 +201,11 @@ impl Vaults { } /// 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 { let vaults_data = fs::read(&vaults_file)?; @@ -198,8 +219,8 @@ impl Vaults { } } -impl std::fmt::Display for Format { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Format { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", @@ -210,8 +231,8 @@ impl std::fmt::Display for Format { } } -impl std::fmt::Display for Vault { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Vault { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Name: {}", self.name)?; if let Some(ref username) = self.username { write!(f, "\nUsername: {username}")?;