diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 7f31fc7..5d8fbba 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: https://codeberg.org/TheAwiteb/rust-action@v1.74 + - name: Check MSRV + run: cargo +1.74 build - name: Build the source code run: cargo build - name: Check the code format diff --git a/.forgejo/workflows/git-sumi.yml b/.forgejo/workflows/git-sumi.yml index 2c45af4..a753dc2 100644 --- a/.forgejo/workflows/git-sumi.yml +++ b/.forgejo/workflows/git-sumi.yml @@ -19,4 +19,11 @@ jobs: mv git-sumi-x86_64-unknown-linux-gnu/git-sumi git-sumi chmod +x git-sumi - name: Run git-sumi - run: ./git-sumi "${{ github.event.pull_request.title }}" + run: | + # Check if the PR are WIP or not (Start with WIP: or wip:) + IS_WIP=$(echo ${{ github.event.pull_request.title }} | grep -i "^WIP:" | wc -l) + if [ $IS_WIP -eq 1 ]; then + echo "Is Work In Progress PR" + exit 0 + fi + ./git-sumi "${{ github.event.pull_request.title }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a90c2a..64b1556 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,49 @@ # Contributing to lprs -Thank you for your interest in contributing to lprs! We welcome contributions from the community to help improve the project. +Thank you for your interest in contributing to lprs! We welcome contributions +from the community to help improve the project. ## Reporting Issues -If you encounter any issues or bugs while using lprs, please open a new issue on the Forgejo repository. When reporting an issue, please provide as much detail as possible, including steps to reproduce the issue and any relevant error messages. +If you encounter any issues or bugs while using lprs, please open a new issue on +the Forgejo repository. When reporting an issue, please provide as much detail as +possible, including steps to reproduce the issue and any relevant error messages. ## Feature Requests -If you have a feature request or an idea for improving lprs, we encourage you to open a new issue on the Forgejo repository. Please describe the feature or improvement in detail and provide any relevant context or examples. +If you have a feature request or an idea for improving lprs, we encourage you to +open a new issue on the Forgejo repository. Please describe the feature or +improvement in detail and provide any relevant context or examples. ## Writing Code -Before you start writing code, please open a new issue first to discuss the proposed changes. This will help ensure that your contribution is aligned with the project's goals and that you are not duplicating work that is already in progress or has been completed by someone else. +Before you start writing code, please open a new issue first to discuss the proposed +changes. This will help ensure that your contribution is aligned with the project's +goals and that you are not duplicating work that is already in progress or has +been completed by someone else. + +### Rust Version +In the lprs project, we always try to stay on the lowest MSRV possible for +compatibility, but the development process relies on the nightly release to get +the latest rust-analyzer and rustfmt features. + +You can check the nightly version used in the project in the `rust-toolchain` file. +And the MSRV in the `Cargo.toml` file. ### PR title -Your PR will squash and merge, and your PR title will be used as the commit message. Please make sure your PR title is clear and concise. +Your PR will squash and merge, and your PR title will be used as the commit message. +Please make sure your PR title is clear and concise. -The title must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format. This means that the title should be in the following format: +The title must follow [Conventional Commits] format. This means that the title +should be in the following format: ``` (): ``` -- The `` is optional, and the `` should be a clear and concise summary of the changes. -- You should use the imperative, present tense (e.g., "Add feature" instead of "Added feature"). +- The `` is optional, and the `` should be a clear and + concise summary of the changes. +- You should use the imperative, present tense + (e.g., "Add feature" instead of "Added feature"). - The `` should be one of the following: - `feat`: A new feature - `fix`: A bug fix @@ -35,7 +55,8 @@ The title must follow [Conventional Commits](https://www.conventionalcommits.org - `security`: Changes that affect the security of the code - `perf`: A code change that improves performance - `test`: Adding missing tests or correcting existing tests - - `chore`: Changes to the build process or auxiliary tools and libraries such as documentation generation + - `chore`: Changes to the build process or auxiliary tools and libraries such + as documentation generation #### Example ``` @@ -44,13 +65,18 @@ The title must follow [Conventional Commits](https://www.conventionalcommits.org ``` ### PR description -Your PR description should provide a clear and concise summary of the changes you have made. It should also include any relevant context or background information that will help the project maintainers understand the purpose of the changes. Make sure to reference the issue that your PR is addressing, and note any breaking changes that your PR introduces. +Your PR description should provide a clear and concise summary of the changes you +have made. It should also include any relevant context or background information +that will help the project maintainers understand the purpose of the changes. +Make sure to reference the issue that your PR is addressing, and note any breaking +changes that your PR introduces. Make sure to explain why you made the changes not just what changes you made. ### Code Style -Please follow the existing code style and conventions used in the lprs project. This includes: +Please follow the existing code style and conventions used in the lprs project. +This includes: - Using Rust's official formatting tool, `rustfmt`, to format your code. - Writing clear and concise code with meaningful variable and function names. @@ -63,14 +89,23 @@ Run the CI before submitting your code. You can run the CI with the following co just ci ``` -This will run the tests and check the code formatting. If the CI fail, please fix the issues before submitting your code. +This will run the tests and check the code formatting. If the CI fail, please +fix the issues before submitting your code. ## Code Review -All contributions to lprs will go through a code review process. This ensures that the code meets the project's standards and maintains its quality. Please be open to feedback and suggestions from the project maintainers during the code review process. +All contributions to lprs will go through a code review process. +This ensures that the code meets the project's standards and maintains its quality. +Please be open to feedback and suggestions from the project maintainers during +the code review process. ## License -By contributing to lprs, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE) file. This means that you are granting lprs the right to use, modify, and distribute your contributions under the terms of the license. wich is GPL-3.0 License. +By contributing to lprs, you agree that your contributions will be licensed under +the project's [license](LICENSE). This means that you are granting lprs the +right to use, modify, and distribute your contributions under the terms of the +license. Which is GPL-3.0 License. Happy contributing! + +[Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/ diff --git a/Cargo.toml b/Cargo.toml index 896bfe1..5dee409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,10 @@ edition = "2021" license = "GPL-3.0-only" authors = ["Awiteb "] readme = "README.md" -description = "A local CLI password manager" +description = "A local CLI password/vault manager" repository = "https://git.4rs.nl/awiteb/lprs" rust-version = "1.74.0" -keywords = ["password", "manager", "CLI"] +keywords = ["password", "vault", "manager", "CLI"] categories = ["command-line-utilities"] [dependencies] @@ -35,5 +35,55 @@ default = ["update-notify"] update-notify = ["reqwest/blocking"] reqwest = ["dep:reqwest"] +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" + +[lints.clippy] +# I know is huge, but I like to be explicit, it also provides +# a better DX for new contributors (Make it easier to understand the codebase). +# Also, this is a general linting configuration, it's not specific to this project. +wildcard_imports = "deny" +manual_let_else = "deny" +match_bool = "deny" +match_on_vec_items = "deny" +or_fun_call = "deny" +panic = "deny" +unwrap_used = "deny" + +missing_assert_message = "warn" +missing_const_for_fn = "warn" +missing_errors_doc = "warn" +absolute_paths = "warn" +cast_lossless = "warn" +clone_on_ref_ptr = "warn" +cloned_instead_of_copied = "warn" +dbg_macro = "warn" +default_trait_access = "warn" +empty_enum_variants_with_brackets = "warn" +empty_line_after_doc_comments = "warn" +empty_line_after_outer_attr = "warn" +empty_structs_with_brackets = "warn" +enum_glob_use = "warn" +equatable_if_let = "warn" +explicit_iter_loop = "warn" +filetype_is_file = "warn" +filter_map_next = "warn" +flat_map_option = "warn" +float_cmp = "warn" +format_push_string = "warn" +future_not_send = "warn" +if_not_else = "warn" +if_then_some_else_none = "warn" +implicit_clone = "warn" +inconsistent_struct_constructor = "warn" +indexing_slicing = "warn" +iter_filter_is_ok = "warn" +iter_filter_is_some = "warn" +iter_not_returning_iterator = "warn" +manual_is_variant_and = "warn" +option_if_let_else = "warn" +option_option = "warn" + [profile.release] strip = true # Automatically strip symbols from the binary. diff --git a/Justfile b/Justfile index cc349fb..8b2c4df 100644 --- a/Justfile +++ b/Justfile @@ -18,10 +18,9 @@ _default: # Run the CI @ci: && msrv - cargo +stable build -q - cargo +stable fmt -- --check - cargo +stable clippy -- -D warnings - cargo +{{msrv}} clippy -- -D warnings + cargo build -q + cargo fmt -- --check + cargo clippy -- -D warnings # Check that the current MSRV is correct @msrv: diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..523c157 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,13 @@ +[toolchain] +# We use nightly in development only, the project will always be compliant with +# the latest stable release and the MSRV as defined in `Cargo.toml` file. +channel = "nightly-2024-05-05" +components = [ + "rustc", + "cargo", + "rust-std", + "rust-src", + "rustfmt", + "rust-analyzer", + "clippy", +] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..8735518 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,22 @@ +unstable_features = true +version = "Two" + +blank_lines_upper_bound = 2 +combine_control_expr = false +wrap_comments = true +condense_wildcard_suffixes = true +edition = "2021" +enum_discrim_align_threshold = 20 +force_multiline_blocks = true +format_code_in_doc_comments = true +format_generated_files = false +format_macro_matchers = true +format_strings = true +imports_layout = "HorizontalVertical" +newline_style = "Unix" +normalize_comments = true +reorder_impl_items = true +group_imports = "StdExternalCrate" +single_line_let_else_max_width = 0 +struct_field_align_threshold = 20 +use_try_shorthand = true diff --git a/src/cli/add_command.rs b/src/cli/add_command.rs index d8389a9..02200b5 100644 --- a/src/cli/add_command.rs +++ b/src/cli/add_command.rs @@ -15,20 +15,26 @@ // along with this program. If not, see . use clap::Args; +use inquire::{Password, PasswordDisplayMode}; use crate::{ vault::{Vault, Vaults}, - LprsCommand, LprsError, LprsResult, + LprsCommand, + LprsError, + LprsResult, }; #[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)] - password: Option>, + // FIXME: I think replacing `Option>` with custom type will be better + #[allow(clippy::option_option)] + password: Option>, } impl LprsCommand for Add { @@ -41,10 +47,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..33e0d6b 100644 --- a/src/cli/edit_command.rs +++ b/src/cli/edit_command.rs @@ -17,33 +17,39 @@ use std::num::NonZeroU64; use clap::Args; +use inquire::{Password, PasswordDisplayMode}; use crate::{ vault::{Vault, Vaults}, - LprsCommand, LprsError, LprsResult, + LprsCommand, + LprsError, + LprsResult, }; #[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, #[arg(short, long)] /// The new vault name - name: Option, + name: Option, #[arg(short, long)] /// The new vault username 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 - service: Option, + service: Option, #[arg(short = 'o', long)] /// The new vault note - note: Option, + note: Option, } impl LprsCommand for Edit { @@ -62,13 +68,15 @@ impl LprsCommand for Edit { // Get the password from stdin or from its value if provided let password = match self.password { Some(Some(password)) => Some(password), - Some(None) => Some( - inquire::Password::new("New vault password:") - .without_confirmation() - .with_formatter(&|p| "*".repeat(p.chars().count())) - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .prompt()?, - ), + Some(None) => { + Some( + Password::new("New vault password:") + .without_confirmation() + .with_formatter(&|p| "*".repeat(p.chars().count())) + .with_display_mode(PasswordDisplayMode::Masked) + .prompt()?, + ) + } None => None, }; diff --git a/src/cli/export_command.rs b/src/cli/export_command.rs index 3e2023a..d488a97 100644 --- a/src/cli/export_command.rs +++ b/src/cli/export_command.rs @@ -20,14 +20,18 @@ use clap::Args; use crate::{ vault::{BitWardenPasswords, Format, Vaults}, - LprsCommand, LprsError, LprsResult, + LprsCommand, + LprsError, + LprsResult, }; #[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, + path: PathBuf, /// Format to export vaults in #[arg(short, long, value_name = "FORMAT", default_value_t= Format::Lprs)] format: Format, @@ -51,24 +55,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..10f423c 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())] @@ -35,10 +36,10 @@ pub struct Gen { lowercase: bool, /// With numbers (0-9) #[arg(short, long)] - numbers: bool, + numbers: bool, /// With symbols (!,# ...) #[arg(short, long)] - symbols: bool, + symbols: bool, } impl LprsCommand for Gen { diff --git a/src/cli/import_command.rs b/src/cli/import_command.rs index ff70302..700a8a1 100644 --- a/src/cli/import_command.rs +++ b/src/cli/import_command.rs @@ -25,11 +25,15 @@ use clap::Args; use crate::{ vault::{BitWardenPasswords, Format, Vault, Vaults}, - LprsCommand, LprsError, LprsResult, + LprsCommand, + LprsError, + LprsResult, }; #[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 +81,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..2276850 100644 --- a/src/cli/list_command.rs +++ b/src/cli/list_command.rs @@ -23,16 +23,17 @@ 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")] - get: Option, + get: Option, /// Filter the select list #[arg(short, long, value_name = "TEXT")] filter: Option, /// Enable regex when use `--filter` option #[arg(short, long)] - regex: bool, + regex: bool, } impl LprsCommand for List { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 5f63b44..a9f055a 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -20,13 +20,23 @@ 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,20 +66,29 @@ 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)] pub vaults_file: Option, /// Show the logs in the stdout #[arg(short, long)] - pub verbose: bool, + 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 +102,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..46e1ae3 100644 --- a/src/cli/remove_command.rs +++ b/src/cli/remove_command.rs @@ -22,11 +22,13 @@ 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, - /// Force remove, will not return error if there is no password with this index + /// Force remove, will not return error if there is no password with this + /// index #[arg(short, long)] force: bool, } @@ -37,14 +39,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..e425f56 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -14,15 +14,20 @@ // 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), - #[error("Decryption Error: The given key cannot decrypt the given data. Either the data has been tampered with or the key is incorrect.")] + #[error( + "Decryption Error: The given key cannot decrypt the given data. Either the data has been \ + tampered with or the key is incorrect." + )] Decryption, #[error("Wrong Master Password Error: Wrong decryption password")] WrongMasterPassword, @@ -50,12 +55,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..3bf60f4 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -29,10 +29,7 @@ /// #### Output /// ```rust /// impl crate::LprsCommand for TestCommands { -/// fn run( -/// &self, -/// vault_manager: crate::vault::Vaults, -/// ) -> crate::LprsResult<()> { +/// fn run(&self, vault_manager: crate::vault::Vaults) -> crate::LprsResult<()> { /// match self { /// Self::Test(command) => command.run(vault_manager), /// Self::Some(command) => command.run(vault_manager), @@ -46,7 +43,6 @@ /// } /// } /// } - /// ``` #[macro_export] macro_rules! impl_commands { diff --git a/src/main.rs b/src/main.rs index 1ab2191..ba59a86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,14 +13,21 @@ // // 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 std::env; +use std::process::ExitCode; use clap::Parser; use inquire::InquireError; -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 +37,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..860d797 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,12 +16,19 @@ use std::{fs, path::PathBuf}; -use inquire::validator::Validation; +use inquire::{validator::Validation, PasswordDisplayMode}; +use passwords::{analyzer, scorer}; +#[cfg(feature = "update-notify")] +use reqwest::blocking::Client as BlockingClient; use sha2::Digest; 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 +44,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 +61,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 +94,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 +127,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 +136,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..c91fc15 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}; @@ -6,27 +26,27 @@ use super::{Vault, Vaults}; pub struct BitWardenLoginData { pub username: Option, pub password: Option, - pub uris: Option>, + pub uris: Option>, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BitWardenUri { #[serde(rename = "match")] - pub mt: Option, + pub mt: Option, pub uri: String, } #[derive(Default, Deserialize, Serialize)] pub struct BitWardenFolder { - pub id: String, + pub id: String, pub name: String, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct BitWardenPassword { #[serde(rename = "type")] - pub ty: i32, - pub name: String, + pub ty: i32, + pub name: String, pub login: Option, pub notes: Option, } @@ -35,8 +55,8 @@ pub struct BitWardenPassword { #[derive(Default, Deserialize, Serialize)] pub struct BitWardenPasswords { pub encrypted: bool, - pub folders: Vec, - pub items: Vec, + pub folders: Vec, + pub items: Vec, } impl From for Vault { @@ -58,12 +78,12 @@ impl From for Vault { impl From for BitWardenPassword { fn from(value: Vault) -> Self { Self { - ty: 1, - name: value.name, + ty: 1, + name: value.name, login: Some(BitWardenLoginData { username: value.username, password: value.password, - uris: value + uris: value .service .map(|s| vec![BitWardenUri { mt: None, uri: s }]), }), @@ -76,8 +96,8 @@ impl From for BitWardenPasswords { fn from(value: Vaults) -> Self { Self { encrypted: false, - folders: Vec::new(), - items: value + folders: Vec::new(), + items: value .vaults .into_iter() .map(BitWardenPassword::from) diff --git a/src/vault/cipher.rs b/src/vault/cipher.rs index e71ea75..0bf3819 100644 --- a/src/vault/cipher.rs +++ b/src/vault/cipher.rs @@ -14,9 +14,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::time::{SystemTime, UNIX_EPOCH}; + use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use rand::{rngs::StdRng, Rng, SeedableRng}; -use std::time::{SystemTime, UNIX_EPOCH}; use crate::{LprsError, LprsResult}; diff --git a/src/vault/mod.rs b/src/vault/mod.rs index 11d2595..4bded17 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,14 @@ 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, } @@ -39,7 +46,7 @@ pub enum Format { pub struct Vault { /// The name of the vault #[arg(short, long)] - pub name: String, + pub name: String, /// The username #[arg(short, long)] pub username: Option, @@ -48,10 +55,10 @@ pub struct Vault { pub password: Option, /// The service name. e.g the website url #[arg(short, long)] - pub service: Option, + pub service: Option, /// Add a note to the vault #[arg(short = 'o', long)] - pub note: Option, + pub note: Option, } /// The vaults manager @@ -60,9 +67,9 @@ pub struct Vaults { /// Hash of the master password pub master_password: [u8; 32], /// The json vaults file - pub vaults_file: PathBuf, + pub vaults_file: PathBuf, /// The vaults - pub vaults: Vec, + pub vaults: Vec, } impl Vault { @@ -75,17 +82,17 @@ impl Vault { note: Option>, ) -> Self { Self { - name: name.into(), + name: name.into(), username: username.map(Into::into), password: password.map(Into::into), - service: service.map(Into::into), - note: note.map(Into::into), + service: service.map(Into::into), + note: note.map(Into::into), } } /// 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 +126,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 +158,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 +187,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 +203,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 +221,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 +233,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}")?;