diff --git a/Cargo.lock b/Cargo.lock index 895980c07..99030cd58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,22 @@ dependencies = [ "supports-color", ] +[[package]] +name = "bitwarden-collections" +version = "1.0.0" +dependencies = [ + "bitwarden-api-api", + "bitwarden-core", + "bitwarden-crypto", + "bitwarden-error", + "serde", + "thiserror 1.0.69", + "tsify-next", + "uniffi", + "uuid", + "wasm-bindgen", +] + [[package]] name = "bitwarden-core" version = "1.0.0" @@ -459,6 +475,7 @@ name = "bitwarden-exporters" version = "1.0.0" dependencies = [ "base64", + "bitwarden-collections", "bitwarden-core", "bitwarden-crypto", "bitwarden-error", @@ -622,6 +639,7 @@ version = "0.1.0" dependencies = [ "android_logger", "async-trait", + "bitwarden-collections", "bitwarden-core", "bitwarden-crypto", "bitwarden-exporters", @@ -668,6 +686,7 @@ version = "1.0.0" dependencies = [ "base64", "bitwarden-api-api", + "bitwarden-collections", "bitwarden-core", "bitwarden-crypto", "bitwarden-error", diff --git a/Cargo.toml b/Cargo.toml index bbda3dd09..d07c4446b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ bitwarden = { path = "crates/bitwarden", version = "=1.0.0" } bitwarden-api-api = { path = "crates/bitwarden-api-api", version = "=1.0.0" } bitwarden-api-identity = { path = "crates/bitwarden-api-identity", version = "=1.0.0" } bitwarden-cli = { path = "crates/bitwarden-cli", version = "=1.0.0" } +bitwarden-collections = { path = "crates/bitwarden-collections", version = "=1.0.0" } bitwarden-core = { path = "crates/bitwarden-core", version = "=1.0.0" } bitwarden-crypto = { path = "crates/bitwarden-crypto", version = "=1.0.0" } bitwarden-error = { path = "crates/bitwarden-error", version = "=1.0.0" } diff --git a/README.md b/README.md index 5006db3b6..85fa438f0 100644 --- a/README.md +++ b/README.md @@ -52,22 +52,20 @@ You can also browse the latest published documentation: The project is structured as a monorepo using cargo workspaces. Some of the more noteworthy crates are: -- [`bitwarden-api-api`](./crates/bitwarden-api-api/): Auto-generated API bindings for the API - server. -- [`bitwarden-api-identity`](./crates/bitwarden-api-identity/): Auto-generated API bindings for the +- [`bitwarden-api-api`](./crates/bitwarden-api-api): Auto-generated API bindings for the API server. +- [`bitwarden-api-identity`](./crates/bitwarden-api-identity): Auto-generated API bindings for the Identity server. -- [`bitwarden-core`](./crates/bitwarden-core/): The core functionality consumed by the other crates. -- [`bitwarden-crypto`](./crates/bitwarden-crypto/): Crypto library. -- [`bitwarden-wasm-internal`](./crates/bitwarden-wasm-internal/): WASM bindings for the internal - SDK. -- [`bitwarden-uniffi`](./crates/bitwarden-uniffi/): Mobile bindings for swift and kotlin using +- [`bitwarden-core`](./crates/bitwarden-core): The core functionality consumed by the other crates. +- [`bitwarden-crypto`](./crates/bitwarden-crypto): Crypto library. +- [`bitwarden-wasm-internal`](./crates/bitwarden-wasm-internal): WASM bindings for the internal SDK. +- [`bitwarden-uniffi`](./crates/bitwarden-uniffi): Mobile bindings for swift and kotlin using [UniFFI](https://github.com/mozilla/uniffi-rs/). ## API Bindings -We autogenerate the server bindings using -[openapi-generator](https://github.com/OpenAPITools/openapi-generator). To do this we first need to -build the internal swagger documentation. +We autogenerate the server bindings +using[openapi-generator](https://github.com/OpenAPITools/openapi-generator). To do this, we first +need to build the internal swagger documentation. ### Swagger generation @@ -83,15 +81,16 @@ ASPNETCORE_ENVIRONMENT=development dotnet swagger tofile --output ../../identity ### OpenApi Generator -To generate a new version of the bindings run the following script from the root of the SDK project. +To generate a new version of the bindings, run the following script from the root of the SDK +project. ```bash ./support/build-api.sh ``` -This project uses customized templates which lives in the `support/openapi-templates` directory. -These templates resolves some outstanding issues we've experienced with the rust generator. But we -strive towards modifying the templates as little as possible to ease future upgrades. +This project uses customized templates that live in the `support/openapi-templates` directory. These +templates resolve some outstanding issues we've experienced with the rust generator. But we strive +towards modifying the templates as little as possible to ease future upgrades. ### Note @@ -102,9 +101,9 @@ strive towards modifying the templates as little as possible to ease future upgr ## Developer tools -This project recommends the use of certain developer tools, and also includes configurations for -them to make developers lives easier. The use of these tools is optional and they might require a -separate installation step. +This project recommends the use of certain developer tools and includes configurations for them to +make developers' lives easier. The use of these tools is optional, and they might require a separate +installation step. The list of developer tools is: @@ -122,11 +121,11 @@ The list of developer tools is: ## Formatting & Linting This repository uses various tools to check formatting and linting before it's merged. It's -recommended to run the checks prior to submitting a PR. +recommended to run the checks before submitting a PR. ### Installation -Please see the [lint.yml](./.github/workflows/lint.yml) file for example installation commands & +Please see the [lint.yml](./.github/workflows/lint.yml) file, for example, installation commands and versions. Here are the cli tools we use: - Nightly [cargo fmt](https://github.com/rust-lang/rustfmt) and diff --git a/crates/bitwarden-collections/Cargo.toml b/crates/bitwarden-collections/Cargo.toml new file mode 100644 index 000000000..7602cedb1 --- /dev/null +++ b/crates/bitwarden-collections/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "bitwarden-collections" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +readme.workspace = true +keywords.workspace = true + +[features] +uniffi = [ + "bitwarden-core/uniffi", + "bitwarden-crypto/uniffi", + "dep:uniffi" +] # Uniffi bindings +wasm = ["dep:tsify-next", "dep:wasm-bindgen"] # WASM support + +[dependencies] +bitwarden-api-api = { workspace = true } +bitwarden-core = { workspace = true, features = ["internal"] } +bitwarden-crypto = { workspace = true } +bitwarden-error = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +tsify-next = { workspace = true, optional = true } +uniffi = { workspace = true, optional = true } +uuid = { workspace = true } +wasm-bindgen = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/crates/bitwarden-collections/README.md b/crates/bitwarden-collections/README.md new file mode 100644 index 000000000..cb0252f27 --- /dev/null +++ b/crates/bitwarden-collections/README.md @@ -0,0 +1,6 @@ +# Bitwarden Collections + +Defines the data model for collections both encrypted and decrypted. It also handles conversions +between those two states by implementing `Encryptable`. It also provides `Tree` struct which allows +all structs implementing `TreeItem` to be represented in a tree structure along with functions to +access each node. diff --git a/crates/bitwarden-vault/src/collection.rs b/crates/bitwarden-collections/src/collection.rs similarity index 79% rename from crates/bitwarden-vault/src/collection.rs rename to crates/bitwarden-collections/src/collection.rs index 91224c92d..7e2c23241 100644 --- a/crates/bitwarden-vault/src/collection.rs +++ b/crates/bitwarden-collections/src/collection.rs @@ -7,7 +7,7 @@ use bitwarden_crypto::{CryptoError, Decryptable, EncString, IdentifyKey, KeyStor use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::VaultParseError; +use crate::{error::CollectionsParseError, tree::TreeItem}; #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] @@ -31,7 +31,7 @@ pub struct Collection { } #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct CollectionView { @@ -46,12 +46,7 @@ pub struct CollectionView { pub manage: bool, } -impl IdentifyKey for Collection { - fn key_identifier(&self) -> SymmetricKeyId { - SymmetricKeyId::Organization(self.organization_id) - } -} - +#[allow(missing_docs)] impl Decryptable for Collection { fn decrypt( &self, @@ -72,8 +67,9 @@ impl Decryptable for Collection { } } +#[allow(missing_docs)] impl TryFrom for Collection { - type Error = VaultParseError; + type Error = CollectionsParseError; fn try_from(collection: CollectionDetailsResponseModel) -> Result { Ok(Collection { @@ -87,3 +83,30 @@ impl TryFrom for Collection { }) } } + +#[allow(missing_docs)] +impl IdentifyKey for Collection { + fn key_identifier(&self) -> SymmetricKeyId { + SymmetricKeyId::Organization(self.organization_id) + } +} + +#[allow(missing_docs)] +impl TreeItem for CollectionView { + fn id(&self) -> Uuid { + self.id.unwrap_or_default() + } + + fn short_name(&self) -> &str { + self.path().last().unwrap_or(&"") + } + + fn path(&self) -> Vec<&str> { + self.name + .split(Self::DELIMITER) + .filter(|s| !s.is_empty()) + .collect::>() + } + + const DELIMITER: char = '/'; +} diff --git a/crates/bitwarden-collections/src/error.rs b/crates/bitwarden-collections/src/error.rs new file mode 100644 index 000000000..c7e921ec7 --- /dev/null +++ b/crates/bitwarden-collections/src/error.rs @@ -0,0 +1,19 @@ +use bitwarden_error::bitwarden_error; +use thiserror::Error; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum CollectionDecryptError { + #[error(transparent)] + Crypto(#[from] bitwarden_crypto::CryptoError), +} + +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum CollectionsParseError { + #[error(transparent)] + Crypto(#[from] bitwarden_crypto::CryptoError), + #[error(transparent)] + MissingFieldError(#[from] bitwarden_core::MissingFieldError), +} diff --git a/crates/bitwarden-collections/src/lib.rs b/crates/bitwarden-collections/src/lib.rs new file mode 100644 index 000000000..9a44e04a6 --- /dev/null +++ b/crates/bitwarden-collections/src/lib.rs @@ -0,0 +1,19 @@ +#![doc = include_str!("../README.md")] + +#[cfg(feature = "uniffi")] +uniffi::setup_scaffolding!(); +#[cfg(feature = "uniffi")] +mod uniffi_support; + +/// +/// Module containing the collection data models. It also contains the implementations for +/// Encryptable, TryFrom, and TreeItem +pub mod collection; +/// +/// Module containing the error types. +pub mod error; +/// +/// Module containing Tree struct that is a tree representation of all structs implementing TreeItem +/// trait. It is made using an index vector to hold the data and another vector to hold the +/// parent child relationships between those nodes. +pub mod tree; diff --git a/crates/bitwarden-collections/src/tree.rs b/crates/bitwarden-collections/src/tree.rs new file mode 100644 index 000000000..fbd9cc803 --- /dev/null +++ b/crates/bitwarden-collections/src/tree.rs @@ -0,0 +1,300 @@ +use std::{collections::HashMap, fmt::Debug}; + +use uuid::Uuid; + +#[allow(missing_docs)] +pub trait TreeItem: Clone + Debug { + fn id(&self) -> Uuid; + /* + This is the name that will be output when getting the tree nodes + */ + fn short_name(&self) -> &str; + /* + This is the path that the item is stored into a tree + */ + fn path(&self) -> Vec<&str>; + const DELIMITER: char; +} + +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub struct TreeIndex { + pub id: usize, // location in the tree + pub data: T, // this will be the raw value + pub path: Vec, +} + +impl TreeIndex { + #[allow(missing_docs)] + pub fn new(id: usize, data: &T) -> Self { + TreeIndex { + id, + data: data.clone(), + path: data.path().iter().map(|s| s.to_string()).collect(), + } + } +} + +#[allow(missing_docs)] +pub struct NodeItem { + pub item: T, + pub parent: Option, + pub children: Vec, +} + +#[allow(missing_docs)] +pub struct TreeNode { + pub id: usize, + pub item_id: Uuid, + pub parent_idx: Option, + pub children_idx: Vec, + pub path: Vec, +} + +impl TreeNode { + #[allow(missing_docs)] + pub fn new( + id: usize, + parent_idx: Option, + children_idx: Vec, + index: TreeIndex, + ) -> Self { + TreeNode { + id, + item_id: index.data.id(), + parent_idx, + children_idx, + path: index.path, + } + } +} + +#[allow(missing_docs)] +pub struct Tree { + pub nodes: Vec, + pub items: HashMap>, + path_to_node: HashMap, usize>, +} + +impl Tree { + /// Takes vector of TreeItem and stores them into a tree structure. + pub fn from_items(items: Vec) -> Self { + let mut tree = Tree { + nodes: Vec::new(), + items: HashMap::new(), + path_to_node: HashMap::new(), + }; + + // sort items + let sorted_items = { + let mut i = items.clone(); + i.sort_by(|a, b| a.path().cmp(&b.path())); + i + }; + + // add items + for (index, item) in sorted_items.iter().enumerate() { + let tree_index = TreeIndex::new(index, item); + tree.items.insert(item.id(), tree_index.clone()); + tree.add_item(tree_index); + } + + tree + } + + /// This inserts an item into the tree and sets any look-up information that may be needed. + fn add_item(&mut self, index: TreeIndex) { + let parent_path = index.path[0..index.path.len() - 1].to_vec(); + + let parent_id = self.path_to_node.get(&parent_path).map(|&id| { + let parent = &mut self.nodes[id]; + parent.children_idx.push(index.id); + parent.id + }); + + // add new node + let node = TreeNode::new(index.id, parent_id, vec![], index); + self.path_to_node.insert(node.path.clone(), node.id); + self.nodes.push(node); + } + + /// Returns an optional node item for a given tree item id. + /// + /// This contains the item, its children (or an empty vector), and its parent (if it has one) + pub fn get_item_by_id(&self, tree_item_id: Uuid) -> Option> { + let item = self.items.get(&tree_item_id); + + if let Some(item) = item { + let node = self.nodes.get(item.id)?; + + // Get the parent if it exists + let parent = node + .parent_idx + .and_then(|pid| self.nodes.get(pid)) + .and_then(|p| self.items.get(&p.item_id)) + .map(|p| p.data.clone()); + + // Get any children + let children: Vec = node + .children_idx + .iter() + .filter_map(|&child_id| self.nodes.get(child_id)) + .filter_map(|child| self.items.get(&child.item_id)) + .map(|i| i.data.clone()) + .collect(); + + return Some(NodeItem { + item: item.data.clone(), + parent, + children, + }); + } + + None + } + + /// Returns the list of root nodes with their children + pub fn get_root_items(&self) -> Vec> { + self.nodes + .iter() + .filter(|n| n.parent_idx.is_none()) + .filter_map(|n| self.get_item_by_id(n.item_id)) + .collect() + } +} + +#[cfg(test)] +mod tests { + use uuid::Uuid; + + use super::*; + + #[derive(Clone, Debug)] + pub struct TestItem { + pub id: Uuid, + pub name: String, + } + + impl TreeItem for TestItem { + fn id(&self) -> Uuid { + self.id + } + + fn short_name(&self) -> &str { + self.path().last().unwrap() + } + + fn path(&self) -> Vec<&str> { + self.name + .split(Self::DELIMITER) + .filter(|s| !s.is_empty()) + .collect::>() + } + + const DELIMITER: char = '/'; + } + + #[test] + fn given_collection_with_one_parent_and_two_children_when_getting_parent_then_parent_is_returned_with_children_and_no_parent( + ) { + let parent_id = Uuid::new_v4(); + let items = vec![ + TestItem { + id: Uuid::new_v4(), + name: "parent/child1".to_string(), + }, + TestItem { + id: parent_id, + name: "parent".to_string(), + }, + TestItem { + id: Uuid::new_v4(), + name: "parent/child2".to_string(), + }, + ]; + + let node = Tree::from_items(items) + .get_item_by_id(parent_id) + .expect("Node not found"); + + let item = node.item; + let parent = node.parent; + let children = node.children; + + assert_eq!(children.len(), 2); + assert_eq!(item.id(), parent_id); + assert_eq!(item.short_name(), "parent"); + assert_eq!(item.path(), ["parent"]); + assert!(parent.is_none()); + } + + #[test] + fn given_collection_with_one_parent_and_two_children_when_getting_child1_then_child1_is_returned_with_no_children_and_a_parent( + ) { + let child_1_id = Uuid::new_v4(); + let parent_id = Uuid::new_v4(); + let items = vec![ + TestItem { + id: child_1_id, + name: "parent/child1".to_string(), + }, + TestItem { + id: parent_id, + name: "parent".to_string(), + }, + TestItem { + id: Uuid::new_v4(), + name: "parent/child2".to_string(), + }, + ]; + + let node = Tree::from_items(items) + .get_item_by_id(child_1_id) + .expect("Node not found"); + + let item = node.item; + let parent = node.parent; + let children = node.children; + + assert_eq!(children.len(), 0); + assert_eq!(item.id(), child_1_id); + assert_eq!(item.short_name(), "child1"); + assert_eq!(item.path(), ["parent", "child1"]); + assert_eq!(parent.unwrap().id, parent_id); + } + + #[test] + fn given_collection_with_two_children_where_their_parent_node_does_not_exist_children_are_returned_correctly( + ) { + let child_1_id = Uuid::new_v4(); + let grandparent_id = Uuid::new_v4(); + let items = vec![ + TestItem { + id: child_1_id, + name: "grandparent/parent/child1".to_string(), + }, + TestItem { + id: Uuid::new_v4(), + name: "grandparent/parent/child2".to_string(), + }, + TestItem { + id: grandparent_id, + name: "grandparent".to_string(), + }, + ]; + + let node = Tree::from_items(items) + .get_item_by_id(child_1_id) + .expect("Node not found"); + + let item = node.item; + let parent = node.parent; + let children = node.children; + + assert_eq!(children.len(), 0); + assert_eq!(item.id(), child_1_id); + assert_eq!(item.short_name(), "child1"); + assert_eq!(item.path(), ["grandparent", "parent", "child1"]); + assert!(parent.is_none()); + } +} diff --git a/crates/bitwarden-collections/src/uniffi_support.rs b/crates/bitwarden-collections/src/uniffi_support.rs new file mode 100644 index 000000000..3c6aff9f1 --- /dev/null +++ b/crates/bitwarden-collections/src/uniffi_support.rs @@ -0,0 +1,3 @@ +use uuid::Uuid; + +uniffi::use_remote_type!(bitwarden_core::Uuid); diff --git a/crates/bitwarden-exporters/Cargo.toml b/crates/bitwarden-exporters/Cargo.toml index 35eba3fae..0f52280fe 100644 --- a/crates/bitwarden-exporters/Cargo.toml +++ b/crates/bitwarden-exporters/Cargo.toml @@ -25,6 +25,7 @@ wasm = [ [dependencies] base64 = ">=0.22.1, <0.23" +bitwarden-collections = { workspace = true, features = ["wasm"] } bitwarden-core = { workspace = true } bitwarden-crypto = { workspace = true } bitwarden-error = { workspace = true } diff --git a/crates/bitwarden-exporters/src/export.rs b/crates/bitwarden-exporters/src/export.rs index 234e1b3ca..fc1b7d2e8 100644 --- a/crates/bitwarden-exporters/src/export.rs +++ b/crates/bitwarden-exporters/src/export.rs @@ -1,6 +1,7 @@ +use bitwarden_collections::collection::Collection; use bitwarden_core::{key_management::KeyIds, Client}; use bitwarden_crypto::{Encryptable, IdentifyKey, KeyStoreContext}; -use bitwarden_vault::{Cipher, CipherView, Collection, Folder, FolderView}; +use bitwarden_vault::{Cipher, CipherView, Folder, FolderView}; use crate::{ csv::export_csv, diff --git a/crates/bitwarden-exporters/src/exporter_client.rs b/crates/bitwarden-exporters/src/exporter_client.rs index 029f9ae4c..451148179 100644 --- a/crates/bitwarden-exporters/src/exporter_client.rs +++ b/crates/bitwarden-exporters/src/exporter_client.rs @@ -1,5 +1,6 @@ +use bitwarden_collections::collection::Collection; use bitwarden_core::Client; -use bitwarden_vault::{Cipher, Collection, Folder}; +use bitwarden_vault::{Cipher, Folder}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -45,7 +46,7 @@ impl ExporterClient { /// *Warning:* Expect this API to be unstable, and it will change in the future. /// /// For use with Apple using [ASCredentialExportManager](https://developer.apple.com/documentation/authenticationservices/ascredentialexportmanager). - /// Ideally the input should be immediately serialized from [ASImportableAccount](https://developer.apple.com/documentation/authenticationservices/asimportableaccount). + /// Ideally, the input should be immediately serialized from [ASImportableAccount](https://developer.apple.com/documentation/authenticationservices/asimportableaccount). pub fn export_cxf( &self, account: Account, @@ -59,7 +60,7 @@ impl ExporterClient { /// *Warning:* Expect this API to be unstable, and it will change in the future. /// /// For use with Apple using [ASCredentialExportManager](https://developer.apple.com/documentation/authenticationservices/ascredentialexportmanager). - /// Ideally the input should be immediately serialized from [ASImportableAccount](https://developer.apple.com/documentation/authenticationservices/asimportableaccount). + /// Ideally, the input should be immediately serialized from [ASImportableAccount](https://developer.apple.com/documentation/authenticationservices/asimportableaccount). pub fn import_cxf(&self, payload: String) -> Result, ExportError> { import_cxf(&self.client, payload) } diff --git a/crates/bitwarden-uniffi/Cargo.toml b/crates/bitwarden-uniffi/Cargo.toml index 899281984..246c4be6f 100644 --- a/crates/bitwarden-uniffi/Cargo.toml +++ b/crates/bitwarden-uniffi/Cargo.toml @@ -20,6 +20,7 @@ bench = false [dependencies] async-trait = "0.1.80" +bitwarden-collections = { workspace = true, features = ["uniffi"] } bitwarden-core = { workspace = true, features = ["uniffi"] } bitwarden-crypto = { workspace = true, features = ["uniffi"] } bitwarden-exporters = { workspace = true, features = ["uniffi"] } diff --git a/crates/bitwarden-uniffi/src/tool/mod.rs b/crates/bitwarden-uniffi/src/tool/mod.rs index 997c51cbd..dc6a168b1 100644 --- a/crates/bitwarden-uniffi/src/tool/mod.rs +++ b/crates/bitwarden-uniffi/src/tool/mod.rs @@ -1,8 +1,9 @@ +use bitwarden_collections::collection::Collection; use bitwarden_exporters::{Account, ExportFormat}; use bitwarden_generators::{ PassphraseGeneratorRequest, PasswordGeneratorRequest, UsernameGeneratorRequest, }; -use bitwarden_vault::{Cipher, Collection, Folder}; +use bitwarden_vault::{Cipher, Folder}; use crate::error::{Error, Result}; diff --git a/crates/bitwarden-uniffi/src/vault/collections.rs b/crates/bitwarden-uniffi/src/vault/collections.rs index 1875fb453..fb9fa8500 100644 --- a/crates/bitwarden-uniffi/src/vault/collections.rs +++ b/crates/bitwarden-uniffi/src/vault/collections.rs @@ -1,10 +1,16 @@ -use bitwarden_vault::{Collection, CollectionView}; +use std::sync::Arc; + +use bitwarden_collections::{ + collection::{Collection, CollectionView}, + tree::{NodeItem, Tree}, +}; +use uuid::Uuid; use crate::{error::Error, Result}; #[allow(missing_docs)] #[derive(uniffi::Object)] -pub struct CollectionsClient(pub(crate) bitwarden_vault::CollectionsClient); +pub struct CollectionsClient(pub(crate) bitwarden_vault::collection_client::CollectionsClient); #[uniffi::export] impl CollectionsClient { @@ -17,4 +23,42 @@ impl CollectionsClient { pub fn decrypt_list(&self, collections: Vec) -> Result> { Ok(self.0.decrypt_list(collections).map_err(Error::Decrypt)?) } + + /// + /// Returns the vector of CollectionView objects in a tree structure based on its implemented + /// path(). + pub fn get_collection_tree(&self, collections: Vec) -> Arc { + Arc::new(CollectionViewTree { + tree: Tree::from_items(collections), + }) + } +} + +#[derive(uniffi::Object)] +pub struct CollectionViewTree { + tree: Tree, +} + +#[derive(uniffi::Object)] +#[allow(unused)] +pub struct CollectionViewNodeItem { + node_item: NodeItem, +} + +#[uniffi::export] +impl CollectionViewTree { + pub fn get_item_by_id(&self, collection_id: Uuid) -> Option> { + self.tree + .get_item_by_id(collection_id) + .map(|n| Arc::new(CollectionViewNodeItem { node_item: n })) + } + + pub fn get_root_items(&self) -> Vec> { + self.tree + .nodes + .iter() + .filter(|n| n.parent_idx.is_none()) + .filter_map(|n| self.get_item_by_id(n.item_id)) + .collect() + } } diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index 01d496d55..786e7e65e 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -29,6 +29,7 @@ wasm = [ [dependencies] base64 = ">=0.22.1, <0.23" bitwarden-api-api = { workspace = true } +bitwarden-collections = { workspace = true } bitwarden-core = { workspace = true, features = ["internal"] } bitwarden-crypto = { workspace = true } bitwarden-error = { workspace = true } diff --git a/crates/bitwarden-vault/src/collection_client.rs b/crates/bitwarden-vault/src/collection_client.rs index 73be34c59..061a5d6bc 100644 --- a/crates/bitwarden-vault/src/collection_client.rs +++ b/crates/bitwarden-vault/src/collection_client.rs @@ -1,6 +1,10 @@ +use bitwarden_collections::{ + collection::{Collection, CollectionView}, + tree::Tree, +}; use bitwarden_core::Client; -use crate::{error::DecryptError, Collection, CollectionView}; +use crate::DecryptError; #[allow(missing_docs)] pub struct CollectionsClient { @@ -24,6 +28,12 @@ impl CollectionsClient { let views = key_store.decrypt_list(&collections)?; Ok(views) } + + /// Returns the vector of CollectionView objects in a tree structure based on its implemented + /// path(). + pub fn get_collections_tree(&self, collections: Vec) -> Tree { + Tree::from_items(collections) + } } #[cfg(test)] diff --git a/crates/bitwarden-vault/src/lib.rs b/crates/bitwarden-vault/src/lib.rs index e1d893cc2..9d505b303 100644 --- a/crates/bitwarden-vault/src/lib.rs +++ b/crates/bitwarden-vault/src/lib.rs @@ -7,10 +7,6 @@ mod uniffi_support; mod cipher; pub use cipher::*; -mod collection; -pub use collection::{Collection, CollectionView}; -mod collection_client; -pub use collection_client::CollectionsClient; mod folder; pub use folder::{Folder, FolderView}; mod folder_client; @@ -33,5 +29,8 @@ pub use vault_client::{VaultClient, VaultClientExt}; mod sync; pub use sync::{SyncRequest, SyncResponse}; +#[allow(missing_docs)] +pub mod collection_client; mod totp_client; + pub use totp_client::TotpClient; diff --git a/crates/bitwarden-vault/src/sync.rs b/crates/bitwarden-vault/src/sync.rs index df959a7d1..63bdb8ffe 100644 --- a/crates/bitwarden-vault/src/sync.rs +++ b/crates/bitwarden-vault/src/sync.rs @@ -1,6 +1,7 @@ use bitwarden_api_api::models::{ DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel, }; +use bitwarden_collections::{collection::Collection, error::CollectionsParseError}; use bitwarden_core::{ client::encryption_settings::EncryptionSettingsError, require, Client, MissingFieldError, }; @@ -8,7 +9,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; -use crate::{Cipher, Collection, Folder, GlobalDomains, VaultParseError}; +use crate::{Cipher, Folder, GlobalDomains, VaultParseError}; #[derive(Debug, Error)] pub enum SyncError { @@ -19,6 +20,8 @@ pub enum SyncError { #[error(transparent)] VaultParse(#[from] VaultParseError), #[error(transparent)] + CollectionParse(#[from] CollectionsParseError), + #[error(transparent)] EncryptionSettings(#[from] EncryptionSettingsError), } diff --git a/crates/bitwarden-vault/src/vault_client.rs b/crates/bitwarden-vault/src/vault_client.rs index 889c9a91a..5afdfd158 100644 --- a/crates/bitwarden-vault/src/vault_client.rs +++ b/crates/bitwarden-vault/src/vault_client.rs @@ -3,9 +3,10 @@ use bitwarden_core::Client; use wasm_bindgen::prelude::*; use crate::{ + collection_client::CollectionsClient, sync::{sync, SyncError}, - AttachmentsClient, CiphersClient, CollectionsClient, FoldersClient, PasswordHistoryClient, - SyncRequest, SyncResponse, TotpClient, + AttachmentsClient, CiphersClient, FoldersClient, PasswordHistoryClient, SyncRequest, + SyncResponse, TotpClient, }; #[allow(missing_docs)]