diff --git a/Cargo.lock b/Cargo.lock index e9b7646b2..ad314fed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,6 +422,7 @@ dependencies = [ "bitwarden-api-api", "bitwarden-api-identity", "bitwarden-crypto", + "bitwarden-error", "chrono", "getrandom", "hmac", @@ -482,6 +483,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bitwarden-error" +version = "1.0.0" +dependencies = [ + "bitwarden-error-macro", +] + +[[package]] +name = "bitwarden-error-macro" +version = "1.0.0" +dependencies = [ + "bitwarden-error", + "quote", + "syn 2.0.79", +] + [[package]] name = "bitwarden-exporters" version = "1.0.0" @@ -687,6 +704,7 @@ version = "0.1.0" dependencies = [ "bitwarden-core", "bitwarden-crypto", + "bitwarden-error", "bitwarden-vault", "console_error_panic_hook", "console_log", diff --git a/Cargo.toml b/Cargo.toml index ce01a7b7e..ea9a82add 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,6 @@ uniffi = "=0.28.1" uuid = { version = ">=1.3.3, <2.0", features = ["serde", "v4"] } validator = { version = "0.18.1", features = ["derive"] } wasm-bindgen = { version = ">=0.2.91, <0.3", features = ["serde-serialize"] } -wasm-bindgen-futures = "0.4.41" [workspace.lints.clippy] unused_async = "deny" diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index d212afb7a..d2cf7d289 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -49,6 +49,7 @@ wasm-bindgen = { workspace = true, optional = true } zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] } zxcvbn = { version = ">=3.0.1, <4.0", optional = true } tsify-next = { workspace = true, optional = true } +bitwarden-error = { version = "1.0.0", path = "../bitwarden-error" } [target.'cfg(not(target_arch="wasm32"))'.dependencies] # By default, we use rustls as the TLS stack and rust-platform-verifier to support user-installed root certificates diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 9d9549021..d506090e4 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use bitwarden_crypto::{AsymmetricCryptoKey, CryptoError, KeyContainer, SymmetricCryptoKey}; #[cfg(feature = "internal")] use bitwarden_crypto::{AsymmetricEncString, EncString, MasterKey}; +use bitwarden_error::prelude::*; use thiserror::Error; use uuid::Uuid; @@ -10,7 +11,7 @@ use uuid::Uuid; use crate::error::Result; use crate::VaultLocked; -#[derive(Debug, Error)] +#[derive(Debug, Error, FlatError)] pub enum EncryptionSettingsError { #[error("Cryptography error, {0}")] Crypto(#[from] bitwarden_crypto::CryptoError), diff --git a/crates/bitwarden-error-macro/Cargo.toml b/crates/bitwarden-error-macro/Cargo.toml new file mode 100644 index 000000000..f42651e17 --- /dev/null +++ b/crates/bitwarden-error-macro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bitwarden-error-macro" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +keywords.workspace = true + +[dependencies] +quote = "1.0.37" +syn = "2.0.79" + +[dev-dependencies] +bitwarden-error = { path = "../bitwarden-error" } + +[lib] +proc-macro = true + +[lints] +workspace = true diff --git a/crates/bitwarden-error-macro/src/lib.rs b/crates/bitwarden-error-macro/src/lib.rs new file mode 100644 index 000000000..f04a4a745 --- /dev/null +++ b/crates/bitwarden-error-macro/src/lib.rs @@ -0,0 +1,51 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields}; + +#[proc_macro_derive(FlatError)] +pub fn derive_flat_error(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + // Generate match arms without converting variant names to strings + let variant_matches: Vec<_> = if let Data::Enum(data_enum) = &input.data { + data_enum + .variants + .iter() + .map(|variant| { + let variant_ident = &variant.ident; + let message = match &variant.fields { + Fields::Unnamed(_) | Fields::Named(_) => { + format!("Error: {}", variant_ident) + } + Fields::Unit => { + format!("{}", variant_ident) + } + }; + quote! { + #name::#variant_ident { .. } => (stringify!(#variant_ident), #message), + } + }) + .collect() + } else { + panic!("FlatError can only be derived for enums"); + }; + + let expanded = quote! { + impl FlatError for #name { + fn get_variant(&self) -> &str { + match self { + #(#variant_matches)* + }.0 + } + + fn get_message(&self) -> &str { + match self { + #(#variant_matches)* + }.1 + } + } + }; + + TokenStream::from(expanded) +} diff --git a/crates/bitwarden-error-macro/tests/mod.rs b/crates/bitwarden-error-macro/tests/mod.rs new file mode 100644 index 000000000..cd2c13398 --- /dev/null +++ b/crates/bitwarden-error-macro/tests/mod.rs @@ -0,0 +1,50 @@ +use bitwarden_error::prelude::*; + +#[test] +fn flattens_basic_enums() { + #[derive(FlatError)] + enum Errors { + Foo, + Bar, + Baz, + } + + let foo = Errors::Foo; + let bar = Errors::Bar; + let baz = Errors::Baz; + + assert_eq!(foo.get_variant(), "Foo"); + assert_eq!(bar.get_variant(), "Bar"); + assert_eq!(baz.get_variant(), "Baz"); + + assert_eq!(foo.get_message(), "Foo"); + assert_eq!(bar.get_message(), "Bar"); + assert_eq!(baz.get_message(), "Baz"); +} + +#[test] +fn flattens_enums_with_fields() { + #[derive(FlatError)] + enum Errors { + #[allow(dead_code)] + Foo(String), + #[allow(dead_code)] + Bar(u32), + Baz, + } + + let foo = Errors::Foo("hello".to_string()); + let bar = Errors::Bar(42); + let baz = Errors::Baz; + + assert_eq!(foo.get_variant(), "Foo"); + assert_eq!(bar.get_variant(), "Bar"); + assert_eq!(baz.get_variant(), "Baz"); + + // The message is always "Error: " + // TODO: Add support for getting the message from the fields + // or maybe just remove get_message and rely on ToString + assert_eq!(foo.get_message(), "Error: Foo"); + assert_eq!(bar.get_message(), "Error: Bar"); + assert_eq!(baz.get_message(), "Baz"); +} diff --git a/crates/bitwarden-error/Cargo.toml b/crates/bitwarden-error/Cargo.toml new file mode 100644 index 000000000..78af969fa --- /dev/null +++ b/crates/bitwarden-error/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "bitwarden-error" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +keywords.workspace = true + +[dependencies] +bitwarden-error-macro = { path = "../bitwarden-error-macro" } + +[lints] +workspace = true diff --git a/crates/bitwarden-error/src/flat_error.rs b/crates/bitwarden-error/src/flat_error.rs new file mode 100644 index 000000000..afd844a08 --- /dev/null +++ b/crates/bitwarden-error/src/flat_error.rs @@ -0,0 +1,4 @@ +pub trait FlatError { + fn get_variant(&self) -> &str; + fn get_message(&self) -> &str; +} diff --git a/crates/bitwarden-error/src/lib.rs b/crates/bitwarden-error/src/lib.rs new file mode 100644 index 000000000..6a7095449 --- /dev/null +++ b/crates/bitwarden-error/src/lib.rs @@ -0,0 +1,8 @@ +mod flat_error; + +pub use flat_error::FlatError; + +pub mod prelude { + pub use crate::FlatError; + pub use bitwarden_error_macro::FlatError; +} diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 33f8128e7..41639d101 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -17,6 +17,7 @@ crate-type = ["cdylib"] [dependencies] bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } +bitwarden-error = { version = "1.0.0", path = "../bitwarden-error" } bitwarden-vault = { workspace = true, features = ["wasm"] } console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } diff --git a/crates/bitwarden-wasm-internal/src/error.js b/crates/bitwarden-wasm-internal/src/error.js new file mode 100644 index 000000000..60adb5f63 --- /dev/null +++ b/crates/bitwarden-wasm-internal/src/error.js @@ -0,0 +1,13 @@ +/** + * Error thrown by the WASM module. + * @param {string} message - Error message. + * @extends Error + */ +class WasmError extends Error { + constructor(message, name) { + super(message); + this.name = name ?? "WasmError"; + } +} + +exports.WasmError = WasmError; diff --git a/crates/bitwarden-wasm-internal/src/error.rs b/crates/bitwarden-wasm-internal/src/error.rs index 237ee4941..3fe1df5e2 100644 --- a/crates/bitwarden-wasm-internal/src/error.rs +++ b/crates/bitwarden-wasm-internal/src/error.rs @@ -1,28 +1,48 @@ +use bitwarden_error::FlatError; use wasm_bindgen::prelude::*; // Importing an error class defined in JavaScript instead of defining it in Rust // allows us to extend the `Error` class. It also provides much better console output. -#[wasm_bindgen] +#[wasm_bindgen(module = "/src/error.js")] extern "C" { - #[wasm_bindgen(js_name = Error)] - type JsError; + type WasmError; - #[wasm_bindgen(constructor, js_class = Error)] - fn new(message: String) -> JsError; + #[wasm_bindgen(constructor)] + fn new(message: String, name: Option) -> WasmError; } pub type Result = std::result::Result; pub struct GenericError(pub String); +pub struct FlattenedError { + pub variant: String, + pub message: String, +} + impl From for GenericError { fn from(error: T) -> Self { GenericError(error.to_string()) } } +impl From for FlattenedError { + fn from(error: T) -> Self { + FlattenedError { + variant: error.get_variant().to_owned(), + message: error.get_message().to_owned(), + } + } +} + impl From for JsValue { fn from(error: GenericError) -> Self { - JsError::new(error.0).into() + WasmError::new(error.0, None).into() + } +} + +impl From for JsValue { + fn from(error: FlattenedError) -> Self { + WasmError::new(error.message, Some(error.variant)).into() } }