From 39e172169a329bf1c1525a6ab8de53d75e66b178 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 09:19:14 +0800 Subject: [PATCH 01/61] Create stub sos-changes crate. --- Cargo.lock | 42 ++++++++++++++++++++++++++++ Cargo.toml | 5 ++++ crates/changes/Cargo.toml | 17 ++++++++++++ crates/changes/src/consumer.rs | 50 ++++++++++++++++++++++++++++++++++ crates/changes/src/error.rs | 4 +++ crates/changes/src/lib.rs | 15 ++++++++++ crates/changes/src/producer.rs | 25 +++++++++++++++++ crates/ipc/Cargo.toml | 1 + tests/integration/Cargo.toml | 8 ++++++ 9 files changed, 167 insertions(+) create mode 100644 crates/changes/Cargo.toml create mode 100644 crates/changes/src/consumer.rs create mode 100644 crates/changes/src/error.rs create mode 100644 crates/changes/src/lib.rs create mode 100644 crates/changes/src/producer.rs diff --git a/Cargo.lock b/Cargo.lock index 5c64a42d71..fb2725bff6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1375,6 +1375,12 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "ecdsa" version = "0.16.9" @@ -2483,6 +2489,21 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "intl-memoizer" version = "0.5.2" @@ -4075,6 +4096,12 @@ dependencies = [ "yasna", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.10" @@ -5115,6 +5142,15 @@ dependencies = [ "urn", ] +[[package]] +name = "sos-changes" +version = "0.17.0" +dependencies = [ + "interprocess", + "sos-core", + "thiserror 2.0.12", +] + [[package]] name = "sos-cli-helpers" version = "0.1.1" @@ -7107,6 +7143,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 07a94b5ce7..1596cd6f34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/artifact", "crates/audit", "crates/backend", + "crates/changes", "crates/cli_helpers", "crates/clipboard", "crates/core", @@ -56,6 +57,7 @@ sos-archive = { version = "0.17", path = "crates/archive" } sos-backend = { version = "0.17", path = "crates/backend" } sos-cli-helpers = { version = "0.1", path = "crates/cli_helpers" } sos-core = { version = "0.17", path = "crates/core" } +sos-changes = { version = "0.17", path = "crates/changes" } sos-database = { version = "0.17", path = "crates/database" } sos-database-upgrader = { version = "0.17", path = "crates/database_upgrader" } sos-external-files = { version = "0.17.0", path = "crates/external_files" } @@ -136,6 +138,9 @@ hex = { version = "0.4", features = ["serde"] } k256 = { version = "0.13.1", features = ["ecdsa"] } ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } +# ipc changes +interprocess = "2" + # matches the version in k256 sha1 = "0.10.6" sha2 = "0.10.6" diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml new file mode 100644 index 0000000000..3b73570e36 --- /dev/null +++ b/crates/changes/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "sos-changes" +version = "0.17.0" +edition = "2021" +description = "Local socket change notification producer and consumer for the Save Our Secrets SDK." +homepage = "https://saveoursecrets.com" +license = "MIT OR Apache-2.0" +repository = "https://github.com/saveoursecrets/sdk" + +[features] +changes-consumer = ["dep:interprocess"] +changes-producer = ["dep:interprocess"] + +[dependencies] +thiserror.workspace = true +sos-core.workspace = true +interprocess = { workspace = true, optional = true, features = ["tokio"] } diff --git a/crates/changes/src/consumer.rs b/crates/changes/src/consumer.rs new file mode 100644 index 0000000000..e17f1cbb61 --- /dev/null +++ b/crates/changes/src/consumer.rs @@ -0,0 +1,50 @@ +//! Consumer for change notifications on a named pipe. +use crate::Result; +use interprocess::local_socket::{ + tokio::prelude::*, GenericNamespaced, ListenerOptions, +}; + +/// Consumer socket connection for inter-process communication. +pub struct ChangeConsumer; + +impl ChangeConsumer { + /// Listen on a named pipe. + pub async fn listen(socket_name: &str) -> Result<()> { + let name = socket_name.to_ns_name::()?; + let opts = ListenerOptions::new().name(name); + let listener = match opts.create_tokio() { + Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => { + tracing::error!( + "Error: could not start server because the socket file is occupied. Please check if {socket_name} is in use by another process and try again." + ); + return Err(e.into()); + } + x => x?, + }; + + todo!(); + + /* + let service = LocalWebService::new(app_info, accounts); + let svc = Arc::new(service); + + loop { + let socket = listener.accept().await?; + let svc = svc.clone(); + tokio::task::spawn(async move { + let socket = TokioIo::new(socket); + let http = Builder::new(); + + tracing::debug!("local_socket_server::new_connection"); + let conn = http.serve_connection(socket, svc); + if let Err(err) = conn.await { + tracing::error!( + error = %err, + "local_socket_server::connection_error"); + } + tracing::debug!("local_socket_server::connection_close"); + }); + } + */ + } +} diff --git a/crates/changes/src/error.rs b/crates/changes/src/error.rs new file mode 100644 index 0000000000..40b899b04b --- /dev/null +++ b/crates/changes/src/error.rs @@ -0,0 +1,4 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error {} diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs new file mode 100644 index 0000000000..c327a3f12c --- /dev/null +++ b/crates/changes/src/lib.rs @@ -0,0 +1,15 @@ +//! Local socket change notification producer and consumer. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] + +#[cfg(any(feature = "changes-producer", feature = "changes-consumer"))] +mod error; + +#[cfg(any(feature = "changes-producer", feature = "changes-consumer"))] +pub use error::Error; + +#[cfg(feature = "changes-consumer")] +pub mod consumer; +#[cfg(feature = "changes-producer")] +pub mod producer; diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs new file mode 100644 index 0000000000..2a7c87e63a --- /dev/null +++ b/crates/changes/src/producer.rs @@ -0,0 +1,25 @@ +//! Producer for change notifications on a named pipe. +use crate::Result; +use interprocess::local_socket::{tokio::prelude::*, GenericNamespaced}; + +/// Producer socket connect for inter-process communication. +pub struct ChangeProducer { + socket_name: String, +} + +impl ChangeProducer { + /// Create a connection to the named pipe. + pub async fn connect(socket_name: impl Into) -> Result { + Ok(Self { + socket_name: socket_name.into(), + }) + } + + /// Send a local request. + pub async fn send_request(&mut self) -> Result<()> { + let name = + self.socket_name.clone().to_ns_name::()?; + let io = LocalSocketStream::connect(name).await?; + todo!(); + } +} diff --git a/crates/ipc/Cargo.toml b/crates/ipc/Cargo.toml index 69008fda30..c1432de8c2 100644 --- a/crates/ipc/Cargo.toml +++ b/crates/ipc/Cargo.toml @@ -16,6 +16,7 @@ local-transport = [ "serde_with", "async-trait", ] + memory-http-server = [] clipboard = ["sos-account/clipboard"] contacts = ["sos-protocol/contacts"] diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 36e512f188..4111b00ae5 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -40,9 +40,17 @@ tokio = { workspace = true, features = ["rt-multi-thread"] } http.workspace = true anyhow.workspace = true +[dependences.sos-changes] +workspace = true +features = [ + "changes-producer", + "changes-consumer", +] + [dependencies.sos-ipc] workspace = true features = [ + "extension-helper-client", "extension-helper-server", "extension-helper-client", "contacts", From ffd4d9a29bba01c2b6917a20ddd0388c78a84bf9 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 09:23:34 +0800 Subject: [PATCH 02/61] Rename ChangeNotification -> NetworkChangeEvent. --- Cargo.lock | 2 ++ crates/changes/Cargo.toml | 1 + crates/changes/src/error.rs | 7 ++++++- crates/changes/src/lib.rs | 4 ++-- crates/net/src/account/listen.rs | 6 +++--- crates/net/src/account/remote.rs | 4 ++-- crates/protocol/src/bindings/mod.rs | 2 +- crates/protocol/src/bindings/notifications.rs | 20 +++++++++---------- crates/protocol/src/network_client/http.rs | 4 ++-- .../protocol/src/network_client/websocket.rs | 16 +++++++-------- .../protocol/src/protobuf/notifications.proto | 2 +- crates/server/src/handlers/account.rs | 6 +++--- crates/server/src/handlers/mod.rs | 4 ++-- tests/integration/Cargo.toml | 2 +- tests/unit/src/tests/protocol_encoding.rs | 6 +++--- 15 files changed, 47 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb2725bff6..4da5d8f2eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5149,6 +5149,7 @@ dependencies = [ "interprocess", "sos-core", "thiserror 2.0.12", + "tracing", ] [[package]] @@ -5396,6 +5397,7 @@ dependencies = [ "sos-account", "sos-audit", "sos-backend", + "sos-changes", "sos-client-storage", "sos-core", "sos-database", diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml index 3b73570e36..ebdbc7a184 100644 --- a/crates/changes/Cargo.toml +++ b/crates/changes/Cargo.toml @@ -14,4 +14,5 @@ changes-producer = ["dep:interprocess"] [dependencies] thiserror.workspace = true sos-core.workspace = true +tracing.workspace = true interprocess = { workspace = true, optional = true, features = ["tokio"] } diff --git a/crates/changes/src/error.rs b/crates/changes/src/error.rs index 40b899b04b..530647a9fb 100644 --- a/crates/changes/src/error.rs +++ b/crates/changes/src/error.rs @@ -1,4 +1,9 @@ use thiserror::Error; +/// Errors generated by the library. #[derive(Debug, Error)] -pub enum Error {} +pub enum Error { + /// Errors generated by the IO module. + #[error(transparent)] + Io(#[from] std::io::Error), +} diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index c327a3f12c..d1da8e248f 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -3,13 +3,13 @@ #![forbid(unsafe_code)] #![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -#[cfg(any(feature = "changes-producer", feature = "changes-consumer"))] mod error; -#[cfg(any(feature = "changes-producer", feature = "changes-consumer"))] pub use error::Error; #[cfg(feature = "changes-consumer")] pub mod consumer; #[cfg(feature = "changes-producer")] pub mod producer; + +pub(crate) type Result = std::result::Result; diff --git a/crates/net/src/account/listen.rs b/crates/net/src/account/listen.rs index 5aee907df9..89b5a6c3dc 100644 --- a/crates/net/src/account/listen.rs +++ b/crates/net/src/account/listen.rs @@ -3,7 +3,7 @@ use crate::{Error, NetworkAccount, Result}; use sos_core::Origin; use sos_protocol::{ - network_client::ListenOptions, ChangeNotification, RemoteResult, + network_client::ListenOptions, NetworkChangeEvent, RemoteResult, RemoteSync, }; use sos_sync::SyncStorage; @@ -40,7 +40,7 @@ impl NetworkAccount { origin: &Origin, options: ListenOptions, listener: Option< - mpsc::Sender<(ChangeNotification, RemoteResult)>, + mpsc::Sender<(NetworkChangeEvent, RemoteResult)>, >, ) -> Result<()> { let remotes = self.remotes.read().await; @@ -48,7 +48,7 @@ impl NetworkAccount { self.stop_listening(&origin).await; let remote = Arc::new(remote.clone()); - let (tx, mut rx) = mpsc::channel::(32); + let (tx, mut rx) = mpsc::channel::(32); let local_account = Arc::clone(&self.account); let sync_lock = Arc::clone(&self.sync_lock); diff --git a/crates/net/src/account/remote.rs b/crates/net/src/account/remote.rs index 92c5f48510..b424f2a4df 100644 --- a/crates/net/src/account/remote.rs +++ b/crates/net/src/account/remote.rs @@ -204,7 +204,7 @@ mod listen { use crate::RemoteBridge; use sos_protocol::{ network_client::{ListenOptions, WebSocketHandle}, - ChangeNotification, + NetworkChangeEvent, }; use tokio::sync::mpsc; @@ -223,7 +223,7 @@ mod listen { pub(crate) fn listen( &self, options: ListenOptions, - channel: mpsc::Sender, + channel: mpsc::Sender, ) -> WebSocketHandle { let handle = self.client.listen(options, move |notification| { let tx = channel.clone(); diff --git a/crates/protocol/src/bindings/mod.rs b/crates/protocol/src/bindings/mod.rs index 8288a365ac..0297988653 100644 --- a/crates/protocol/src/bindings/mod.rs +++ b/crates/protocol/src/bindings/mod.rs @@ -11,7 +11,7 @@ mod sync; pub use diff::{DiffRequest, DiffResponse}; #[cfg(feature = "listen")] -pub use notifications::ChangeNotification; +pub use notifications::NetworkChangeEvent; pub use patch::{PatchRequest, PatchResponse}; #[cfg(feature = "pairing")] #[doc(hidden)] diff --git a/crates/protocol/src/bindings/notifications.rs b/crates/protocol/src/bindings/notifications.rs index bfaa0369bf..c517904cef 100644 --- a/crates/protocol/src/bindings/notifications.rs +++ b/crates/protocol/src/bindings/notifications.rs @@ -6,7 +6,7 @@ use sos_sync::MergeOutcome; /// Notification sent by the server when changes were made. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ChangeNotification { +pub struct NetworkChangeEvent { /// Account identifier. account_id: AccountId, /// Connection identifier that made the change. @@ -17,7 +17,7 @@ pub struct ChangeNotification { outcome: MergeOutcome, } -impl ChangeNotification { +impl NetworkChangeEvent { /// Create a new change notification. pub fn new( account_id: &AccountId, @@ -54,10 +54,10 @@ impl ChangeNotification { } } -impl From +impl From for (AccountId, String, CommitHash, MergeOutcome) { - fn from(value: ChangeNotification) -> Self { + fn from(value: NetworkChangeEvent) -> Self { ( value.account_id, value.connection_id, @@ -67,14 +67,14 @@ impl From } } -impl ProtoBinding for ChangeNotification { - type Inner = WireChangeNotification; +impl ProtoBinding for NetworkChangeEvent { + type Inner = WireNetworkChangeEvent; } -impl TryFrom for ChangeNotification { +impl TryFrom for NetworkChangeEvent { type Error = Error; - fn try_from(value: WireChangeNotification) -> Result { + fn try_from(value: WireNetworkChangeEvent) -> Result { let account_id: [u8; 20] = value.account_id.as_slice().try_into()?; Ok(Self { account_id: account_id.into(), @@ -85,8 +85,8 @@ impl TryFrom for ChangeNotification { } } -impl From for WireChangeNotification { - fn from(value: ChangeNotification) -> WireChangeNotification { +impl From for WireNetworkChangeEvent { + fn from(value: NetworkChangeEvent) -> WireNetworkChangeEvent { Self { account_id: value.account_id().as_ref().to_vec(), connection_id: value.connection_id, diff --git a/crates/protocol/src/network_client/http.rs b/crates/protocol/src/network_client/http.rs index a22b89558e..6f0c263561 100644 --- a/crates/protocol/src/network_client/http.rs +++ b/crates/protocol/src/network_client/http.rs @@ -34,7 +34,7 @@ use crate::{ network_client::websocket::{ ListenOptions, WebSocketChangeListener, WebSocketHandle, }, - ChangeNotification, + NetworkChangeEvent, }; #[cfg(feature = "files")] @@ -118,7 +118,7 @@ impl HttpClient { pub fn listen( &self, options: ListenOptions, - handler: impl Fn(ChangeNotification) -> F + Send + Sync + 'static, + handler: impl Fn(NetworkChangeEvent) -> F + Send + Sync + 'static, ) -> WebSocketHandle where F: Future + Send + 'static, diff --git a/crates/protocol/src/network_client/websocket.rs b/crates/protocol/src/network_client/websocket.rs index d99227563e..c94b060fce 100644 --- a/crates/protocol/src/network_client/websocket.rs +++ b/crates/protocol/src/network_client/websocket.rs @@ -2,7 +2,7 @@ use crate::{ network_client::{NetworkRetry, WebSocketRequest}, transfer::CancelReason, - ChangeNotification, Error, Result, WireEncodeDecode, + NetworkChangeEvent, Error, Result, WireEncodeDecode, }; use futures::{ stream::{Map, SplitStream}, @@ -123,13 +123,13 @@ pub fn changes( impl FnMut( std::result::Result, ) -> Result< - Pin> + Send>>, + Pin> + Send>>, >, > { let (_, read) = stream.split(); read.map( move |message| -> Result< - Pin> + Send>>, + Pin> + Send>>, > { match message { Ok(message) => Ok(Box::pin(async move { @@ -141,11 +141,11 @@ pub fn changes( ) } -async fn decode_notification(message: Message) -> Result { +async fn decode_notification(message: Message) -> Result { match message { Message::Binary(buffer) => { let buf: Bytes = buffer.into(); - let notification = ChangeNotification::decode(buf).await?; + let notification = NetworkChangeEvent::decode(buf).await?; Ok(notification) } _ => Err(Error::NotBinaryWebsocketMessageType), @@ -210,7 +210,7 @@ impl WebSocketChangeListener { /// the handler with the notifications. pub fn spawn( self, - handler: impl Fn(ChangeNotification) -> F + Send + Sync + 'static, + handler: impl Fn(NetworkChangeEvent) -> F + Send + Sync + 'static, ) -> WebSocketHandle where F: Future + Send + 'static, @@ -229,7 +229,7 @@ impl WebSocketChangeListener { async fn listen( &self, mut stream: WsStream, - handler: &(impl Fn(ChangeNotification) -> F + Send + Sync + 'static), + handler: &(impl Fn(NetworkChangeEvent) -> F + Send + Sync + 'static), ) -> Result<()> where F: Future + Send + 'static, @@ -289,7 +289,7 @@ impl WebSocketChangeListener { async fn connect_loop( &self, - handler: &(impl Fn(ChangeNotification) -> F + Send + Sync + 'static), + handler: &(impl Fn(NetworkChangeEvent) -> F + Send + Sync + 'static), ) -> Result<()> where F: Future + Send + 'static, diff --git a/crates/protocol/src/protobuf/notifications.proto b/crates/protocol/src/protobuf/notifications.proto index 1933deac53..49ceacbf7d 100644 --- a/crates/protocol/src/protobuf/notifications.proto +++ b/crates/protocol/src/protobuf/notifications.proto @@ -5,7 +5,7 @@ package notifications; import "protobuf/common.proto"; import "protobuf/sync.proto"; -message WireChangeNotification { +message WireNetworkChangeEvent { // Account identifier. bytes account_id = 1; // Connection identifier. diff --git a/crates/server/src/handlers/account.rs b/crates/server/src/handlers/account.rs index bb8cb95465..5dae50502e 100644 --- a/crates/server/src/handlers/account.rs +++ b/crates/server/src/handlers/account.rs @@ -624,7 +624,7 @@ mod handlers { use std::sync::Arc; #[cfg(feature = "listen")] - use sos_protocol::ChangeNotification; + use sos_protocol::NetworkChangeEvent; #[cfg(feature = "listen")] use crate::handlers::send_notification; @@ -818,7 +818,7 @@ mod handlers { if let Some(conn_id) = caller.connection_id() { let reader = account.read().await; let local_status = reader.sync_status().await?; - let notification = ChangeNotification::new( + let notification = NetworkChangeEvent::new( caller.account_id(), conn_id.to_string(), local_status.root, @@ -868,7 +868,7 @@ mod handlers { #[cfg(feature = "listen")] if outcome.changes > 0 { if let Some(conn_id) = caller.connection_id() { - let notification = ChangeNotification::new( + let notification = NetworkChangeEvent::new( caller.account_id(), conn_id.to_string(), packet.status.root, diff --git a/crates/server/src/handlers/mod.rs b/crates/server/src/handlers/mod.rs index 286442aa0d..08da77cdb9 100644 --- a/crates/server/src/handlers/mod.rs +++ b/crates/server/src/handlers/mod.rs @@ -28,7 +28,7 @@ pub(crate) mod websocket; const BODY_LIMIT: usize = 33554432; #[cfg(feature = "listen")] -use sos_protocol::{ChangeNotification, WireEncodeDecode}; +use sos_protocol::{NetworkChangeEvent, WireEncodeDecode}; use crate::server::{ServerState, State}; @@ -133,7 +133,7 @@ async fn authenticate_endpoint( pub(crate) async fn send_notification( reader: &State, caller: &Caller, - notification: ChangeNotification, + notification: NetworkChangeEvent, ) { // Send notification on the websockets channel match notification.encode().await { diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 4111b00ae5..b4f2813192 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -40,7 +40,7 @@ tokio = { workspace = true, features = ["rt-multi-thread"] } http.workspace = true anyhow.workspace = true -[dependences.sos-changes] +[dependencies.sos-changes] workspace = true features = [ "changes-producer", diff --git a/tests/unit/src/tests/protocol_encoding.rs b/tests/unit/src/tests/protocol_encoding.rs index 87dcbd8895..2c53942ac3 100644 --- a/tests/unit/src/tests/protocol_encoding.rs +++ b/tests/unit/src/tests/protocol_encoding.rs @@ -231,13 +231,13 @@ async fn encode_decode_merge_outcom() -> Result<()> { #[cfg(feature = "listen")] #[tokio::test] async fn encode_decode_change_notification() -> Result<()> { - use sos_protocol::ChangeNotification; + use sos_protocol::NetworkChangeEvent; let outcome = MergeOutcome { changes: 7, ..Default::default() }; let account_id: AccountId = [1u8; 20].into(); - let value = ChangeNotification::new( + let value = NetworkChangeEvent::new( &account_id, "mock-connection".to_string(), Default::default(), @@ -245,7 +245,7 @@ async fn encode_decode_change_notification() -> Result<()> { ); let buffer = value.clone().encode().await?; let buffer: Bytes = buffer.into(); - let decoded = ChangeNotification::decode(buffer).await?; + let decoded = NetworkChangeEvent::decode(buffer).await?; assert_eq!(value, decoded); Ok(()) } From c276b63f49f16eb799ba1e6e113dde6d62656deb Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 09:51:02 +0800 Subject: [PATCH 03/61] Sketch local change event. --- Cargo.lock | 1 + crates/changes/Cargo.toml | 5 ++++- crates/changes/build.rs | 14 ++++++++++++++ crates/changes/src/lib.rs | 18 ++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 crates/changes/build.rs diff --git a/Cargo.lock b/Cargo.lock index 4da5d8f2eb..f1c475cd66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5147,6 +5147,7 @@ name = "sos-changes" version = "0.17.0" dependencies = [ "interprocess", + "rustc_version", "sos-core", "thiserror 2.0.12", "tracing", diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml index ebdbc7a184..3d56c1693f 100644 --- a/crates/changes/Cargo.toml +++ b/crates/changes/Cargo.toml @@ -2,7 +2,7 @@ name = "sos-changes" version = "0.17.0" edition = "2021" -description = "Local socket change notification producer and consumer for the Save Our Secrets SDK." +description = "Local socket change event producer and consumer for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" repository = "https://github.com/saveoursecrets/sdk" @@ -16,3 +16,6 @@ thiserror.workspace = true sos-core.workspace = true tracing.workspace = true interprocess = { workspace = true, optional = true, features = ["tokio"] } + +[build-dependencies] +rustc_version.workspace = true diff --git a/crates/changes/build.rs b/crates/changes/build.rs new file mode 100644 index 0000000000..5976a1c6d5 --- /dev/null +++ b/crates/changes/build.rs @@ -0,0 +1,14 @@ +use rustc_version::{version_meta, Channel}; + +fn main() { + println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); + + // Set cfg flags depending on release channel + let channel = match version_meta().unwrap().channel { + Channel::Stable => "CHANNEL_STABLE", + Channel::Beta => "CHANNEL_BETA", + Channel::Nightly => "CHANNEL_NIGHTLY", + Channel::Dev => "CHANNEL_DEV", + }; + println!("cargo:rustc-cfg={}", channel); +} diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index d1da8e248f..cd56706c1d 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -6,6 +6,7 @@ mod error; pub use error::Error; +use sos_core::{events::EventLogType, AccountId}; #[cfg(feature = "changes-consumer")] pub mod consumer; @@ -13,3 +14,20 @@ pub mod consumer; pub mod producer; pub(crate) type Result = std::result::Result; + +/// Change event sent over a local socket. +pub struct LocalChangeEvent { + /// Account identifier. + pub account_id: AccountId, + /// Detail about the event. + pub detail: LocalChangeDetail, +} + +/// Detail for the event. +pub enum LocalChangeDetail { + /// Change to an event log. + EventLog { + /// Type of the event log. + log_type: EventLogType, + }, +} From bb8d013288f52c84ceed9483a57b699bb72f8bac Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 09:54:18 +0800 Subject: [PATCH 04/61] Define CommitSpan type. --- crates/core/src/commit/mod.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/core/src/commit/mod.rs b/crates/core/src/commit/mod.rs index 46b1bfd758..537fa6dd43 100644 --- a/crates/core/src/commit/mod.rs +++ b/crates/core/src/commit/mod.rs @@ -16,5 +16,14 @@ pub use tree::CommitTree; /// Commit state combines the last commit hash with /// a commit proof. -#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] pub struct CommitState(pub CommitHash, pub CommitProof); + +/// Commit span represents a section of an event log. +#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] +pub struct CommitSpan { + /// Commit hash before changes were applied. + pub before: Option, + /// Commit hash after changes were applied. + pub after: CommitHash, +} From 18b31189d07ac4f070fc3a93bd6f20066982e59c Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 10:28:22 +0800 Subject: [PATCH 05/61] Event logs return a commit span. It would have been nice to use a compile time check such as vec1::Vec1 but that over-complicates the API and adds more dependencies so we just bail early and return a default CommitSpan when the event records are empty. --- crates/backend/src/event_log.rs | 11 +++++++---- crates/changes/src/lib.rs | 4 +++- crates/core/src/commit/mod.rs | 6 +++++- crates/core/src/events/event_log.rs | 11 +++++++---- crates/database/src/event_log.rs | 26 ++++++++++++++++++++------ crates/filesystem/src/event_log.rs | 25 ++++++++++++++++++++----- 6 files changed, 62 insertions(+), 21 deletions(-) diff --git a/crates/backend/src/event_log.rs b/crates/backend/src/event_log.rs index d89566be8b..3ca09ef4b6 100644 --- a/crates/backend/src/event_log.rs +++ b/crates/backend/src/event_log.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use binary_stream::futures::{Decodable, Encodable}; use futures::stream::BoxStream; use sos_core::{ - commit::{CommitHash, CommitProof, CommitTree}, + commit::{CommitHash, CommitProof, CommitSpan, CommitTree}, events::{ patch::{CheckedPatch, Diff, Patch}, AccountEvent, DeviceEvent, EventLog, EventRecord, WriteEvent, @@ -293,7 +293,10 @@ where } } - async fn apply(&mut self, events: &[T]) -> Result<(), Self::Error> { + async fn apply( + &mut self, + events: &[T], + ) -> Result { match self { Self::Database(inner) => inner.apply(events).await, Self::FileSystem(inner) => inner.apply(events).await, @@ -303,7 +306,7 @@ where async fn apply_records( &mut self, records: Vec, - ) -> Result<(), Self::Error> { + ) -> Result { match self { Self::Database(inner) => inner.apply_records(records).await, Self::FileSystem(inner) => inner.apply_records(records).await, @@ -338,7 +341,7 @@ where async fn patch_unchecked( &mut self, patch: &Patch, - ) -> Result<(), Self::Error> { + ) -> Result { match self { Self::Database(inner) => inner.patch_unchecked(patch).await, Self::FileSystem(inner) => inner.patch_unchecked(patch).await, diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index cd56706c1d..0f6543b4ef 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -6,7 +6,7 @@ mod error; pub use error::Error; -use sos_core::{events::EventLogType, AccountId}; +use sos_core::{commit::CommitSpan, events::EventLogType, AccountId}; #[cfg(feature = "changes-consumer")] pub mod consumer; @@ -29,5 +29,7 @@ pub enum LocalChangeDetail { EventLog { /// Type of the event log. log_type: EventLogType, + /// Span of commit hashes. + commit_span: CommitSpan, }, } diff --git a/crates/core/src/commit/mod.rs b/crates/core/src/commit/mod.rs index 537fa6dd43..69c6d617a0 100644 --- a/crates/core/src/commit/mod.rs +++ b/crates/core/src/commit/mod.rs @@ -20,10 +20,14 @@ pub use tree::CommitTree; pub struct CommitState(pub CommitHash, pub CommitProof); /// Commit span represents a section of an event log. +/// +/// There can be no before commit hash when applying the first +/// collection of events to an event log. If an empty collection +/// was passed the after commit hash will be `None`. #[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] pub struct CommitSpan { /// Commit hash before changes were applied. pub before: Option, /// Commit hash after changes were applied. - pub after: CommitHash, + pub after: Option, } diff --git a/crates/core/src/events/event_log.rs b/crates/core/src/events/event_log.rs index 2bd9d5e3fc..dfb5ec09bc 100644 --- a/crates/core/src/events/event_log.rs +++ b/crates/core/src/events/event_log.rs @@ -1,5 +1,5 @@ use crate::{ - commit::{CommitHash, CommitProof, CommitTree}, + commit::{CommitHash, CommitProof, CommitSpan, CommitTree}, events::{ patch::{CheckedPatch, Diff, Patch}, EventRecord, @@ -91,7 +91,10 @@ where /// Append a collection of events and commit the tree hashes /// only if all the events were successfully written. - async fn apply(&mut self, events: &[E]) -> Result<(), Self::Error>; + async fn apply( + &mut self, + events: &[E], + ) -> Result; /// Append raw event records to the event log. /// @@ -100,7 +103,7 @@ where async fn apply_records( &mut self, records: Vec, - ) -> Result<(), Self::Error>; + ) -> Result; /// Append a patch to this event log only if the /// head of the tree matches the given proof. @@ -134,7 +137,7 @@ where async fn patch_unchecked( &mut self, patch: &Patch, - ) -> Result<(), Self::Error>; + ) -> Result; /// Diff of event records until a specific commit. /// diff --git a/crates/database/src/event_log.rs b/crates/database/src/event_log.rs index 8917230f2b..282d526df8 100644 --- a/crates/database/src/event_log.rs +++ b/crates/database/src/event_log.rs @@ -22,7 +22,7 @@ use futures::{ stream::{BoxStream, StreamExt, TryStreamExt}, }; use sos_core::{ - commit::{CommitHash, CommitProof, CommitTree, Comparison}, + commit::{CommitHash, CommitProof, CommitSpan, CommitTree, Comparison}, encoding::VERSION1, events::{ patch::{CheckedPatch, Diff, Patch}, @@ -154,7 +154,16 @@ where &mut self, records: &[EventRecord], delete_before: bool, - ) -> Result<(), E> { + ) -> Result { + if records.is_empty() { + return Ok(CommitSpan::default()); + } + + let mut span = CommitSpan { + before: self.tree.last_commit(), + after: None, + }; + let log_type = self.log_type.clone(); let mut insert_rows = Vec::new(); let mut commits = Vec::new(); @@ -194,7 +203,9 @@ where self.tree.append(&mut hashes); self.tree.commit(); - Ok(()) + span.after = self.tree.last_commit(); + + Ok(span) } } @@ -524,7 +535,10 @@ where Ok(()) } - async fn apply(&mut self, events: &[T]) -> Result<(), Self::Error> { + async fn apply( + &mut self, + events: &[T], + ) -> Result { let mut records = Vec::with_capacity(events.len()); for event in events { records.push(EventRecord::encode_event(event).await?); @@ -535,7 +549,7 @@ where async fn apply_records( &mut self, records: Vec, - ) -> Result<(), Self::Error> { + ) -> Result { self.insert_records(records.as_slice(), false).await } @@ -591,7 +605,7 @@ where async fn patch_unchecked( &mut self, patch: &Patch, - ) -> Result<(), Self::Error> { + ) -> Result { self.apply_records(patch.records().to_vec()).await } diff --git a/crates/filesystem/src/event_log.rs b/crates/filesystem/src/event_log.rs index c371450ebf..ead690addf 100644 --- a/crates/filesystem/src/event_log.rs +++ b/crates/filesystem/src/event_log.rs @@ -25,7 +25,7 @@ use async_trait::async_trait; use binary_stream::futures::{BinaryReader, Decodable, Encodable}; use futures::{stream::BoxStream, StreamExt, TryStreamExt}; use sos_core::{ - commit::{CommitHash, CommitProof, CommitTree, Comparison}, + commit::{CommitHash, CommitProof, CommitSpan, CommitTree, Comparison}, encode, encoding::{encoding_options, VERSION1}, events::{ @@ -279,7 +279,10 @@ where Ok(()) } - async fn apply(&mut self, events: &[T]) -> StdResult<(), Self::Error> { + async fn apply( + &mut self, + events: &[T], + ) -> StdResult { let mut records = Vec::with_capacity(events.len()); for event in events { records.push(EventRecord::encode_event(event).await?); @@ -290,7 +293,16 @@ where async fn apply_records( &mut self, records: Vec, - ) -> StdResult<(), Self::Error> { + ) -> StdResult { + if records.is_empty() { + return Ok(CommitSpan::default()); + } + + let mut span = CommitSpan { + before: self.tree.last_commit(), + after: None, + }; + let mut buffer: Vec = Vec::new(); let mut commits = Vec::new(); let mut last_commit_hash = self.tree().last_commit(); @@ -328,7 +340,10 @@ where commits.iter().map(|c| *c.as_ref()).collect::>(); self.tree.append(&mut hashes); self.tree.commit(); - Ok(()) + + span.after = self.tree.last_commit(); + + Ok(span) } Err(e) => Err(e.into()), } @@ -412,7 +427,7 @@ where async fn patch_unchecked( &mut self, patch: &Patch, - ) -> StdResult<(), Self::Error> { + ) -> StdResult { /* if let Some(record) = patch.records().first() { self.check_event_time_ahead(record).await?; From 70f751c5cd375c1bdbe7ee0cedd177236f5e26c0 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 12:13:58 +0800 Subject: [PATCH 06/61] Move LocalChangeEvent to core library. --- crates/account/src/local_account.rs | 2 -- crates/changes/src/lib.rs | 19 ------------------- crates/core/src/events/change.rs | 20 ++++++++++++++++++++ crates/core/src/events/mod.rs | 2 ++ 4 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 crates/core/src/events/change.rs diff --git a/crates/account/src/local_account.rs b/crates/account/src/local_account.rs index 5d549c4fac..cdd0081163 100644 --- a/crates/account/src/local_account.rs +++ b/crates/account/src/local_account.rs @@ -1172,8 +1172,6 @@ impl Account for LocalAccount { vault }; - // TODO: do we need to re-import the identity vault here? - let event = { let event = AccountEvent::UpdateIdentity(encode(&vault).await?); let log = self.account_log().await?; diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index 0f6543b4ef..3d77442fd1 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -14,22 +14,3 @@ pub mod consumer; pub mod producer; pub(crate) type Result = std::result::Result; - -/// Change event sent over a local socket. -pub struct LocalChangeEvent { - /// Account identifier. - pub account_id: AccountId, - /// Detail about the event. - pub detail: LocalChangeDetail, -} - -/// Detail for the event. -pub enum LocalChangeDetail { - /// Change to an event log. - EventLog { - /// Type of the event log. - log_type: EventLogType, - /// Span of commit hashes. - commit_span: CommitSpan, - }, -} diff --git a/crates/core/src/events/change.rs b/crates/core/src/events/change.rs new file mode 100644 index 0000000000..7a50585f2a --- /dev/null +++ b/crates/core/src/events/change.rs @@ -0,0 +1,20 @@ +use crate::{commit::CommitSpan, events::EventLogType, AccountId}; + +/// Change event sent over a local socket. +pub struct LocalChangeEvent { + /// Account identifier. + pub account_id: AccountId, + /// Detail about the event. + pub detail: LocalChangeDetail, +} + +/// Detail for the event. +pub enum LocalChangeDetail { + /// Change to an event log. + EventLog { + /// Type of the event log. + log_type: EventLogType, + /// Span of commit hashes. + commit_span: CommitSpan, + }, +} diff --git a/crates/core/src/events/mod.rs b/crates/core/src/events/mod.rs index b29ddc7536..bd863900b2 100644 --- a/crates/core/src/events/mod.rs +++ b/crates/core/src/events/mod.rs @@ -9,6 +9,7 @@ //! an audit trail of actions. mod account; +mod change; mod device; mod event; mod event_kind; @@ -21,6 +22,7 @@ mod record; mod write; pub use account::AccountEvent; +pub use change::{LocalChangeDetail, LocalChangeEvent}; pub use device::DeviceEvent; pub use event::Event; pub use event_kind::EventKind; From a32c0a3a452fbd76e29f54b824d6987a45754441 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 13:10:46 +0800 Subject: [PATCH 07/61] Initial logic to send events over the changes feed channel. --- crates/account/src/local_account.rs | 15 ++++-- crates/backend/src/compact.rs | 20 ++++---- crates/backend/src/event_log.rs | 21 +++++--- crates/backend/src/folder.rs | 18 +++++-- crates/core/src/events/change.rs | 44 ++++++++++++---- crates/core/src/events/event_log.rs | 9 ++-- crates/core/src/events/mod.rs | 2 +- crates/database/src/event_log.rs | 51 ++++++++++++------ crates/filesystem/src/archive/import.rs | 31 +++++++---- crates/filesystem/src/event_log.rs | 57 ++++++++++++++++----- crates/login/src/identity_folder.rs | 19 +++++-- crates/storage/client/src/filesystem.rs | 12 +++-- crates/storage/client/src/traits.rs | 3 +- tests/unit/src/tests/event_log/load_tree.rs | 14 ++++- tests/unit/src/tests/event_log/mod.rs | 28 ++++++++-- tests/unit/src/tests/event_log/rewind.rs | 17 ++++-- tests/unit/src/tests/folder.rs | 9 +++- tests/unit/src/tests/sdk/reducer.rs | 27 ++++++++-- 18 files changed, 291 insertions(+), 106 deletions(-) diff --git a/crates/account/src/local_account.rs b/crates/account/src/local_account.rs index cdd0081163..7e6d95bd21 100644 --- a/crates/account/src/local_account.rs +++ b/crates/account/src/local_account.rs @@ -23,8 +23,8 @@ use sos_core::{ device::{DevicePublicKey, TrustedDevice}, encode, events::{ - AccountEvent, DeviceEvent, Event, EventKind, EventLog, EventRecord, - ReadEvent, WriteEvent, + changes_feed, AccountEvent, DeviceEvent, Event, EventKind, EventLog, + EventRecord, LocalChangeEvent, ReadEvent, WriteEvent, }, AccountId, AccountRef, AuthenticationError, FolderRef, Paths, SecretId, UtcDateTime, VaultCommit, VaultFlags, VaultId, @@ -188,6 +188,9 @@ impl LocalAccount { storage.create_account(&public_account).await?; tracing::debug!("new_account::created"); + changes_feed() + .send_replace(LocalChangeEvent::AccountCreated(account_id)); + Ok(Self { account_id, paths: target.paths(), @@ -1071,6 +1074,11 @@ impl Account for LocalAccount { } self.sign_out().await?; + + changes_feed().send_replace(LocalChangeEvent::AccountDeleted( + *self.account_id(), + )); + Ok(()) } @@ -1161,7 +1169,8 @@ impl Account for LocalAccount { let vault = { let event_log = self.identity_log().await?; let mut log_file = event_log.write().await; - compact_folder(identity.id(), &mut *log_file).await?; + compact_folder(self.account_id(), identity.id(), &mut *log_file) + .await?; let vault = FolderReducer::new() .reduce(&*log_file) diff --git a/crates/backend/src/compact.rs b/crates/backend/src/compact.rs index b3e73cfa58..feb6004737 100644 --- a/crates/backend/src/compact.rs +++ b/crates/backend/src/compact.rs @@ -3,7 +3,7 @@ use crate::{BackendEventLog, Error, FolderEventLog, Result}; use sos_core::{ events::{ patch::{FolderDiff, Patch}, - EventLog, EventRecord, + EventLog, EventLogType, EventRecord, }, AccountId, VaultId, }; @@ -20,6 +20,7 @@ use tempfile::NamedTempFile; /// Compact a folder event log. pub async fn compact_folder( + account_id: &AccountId, folder_id: &VaultId, event_log: &mut FolderEventLog, ) -> Result<()> { @@ -38,12 +39,9 @@ pub async fn compact_folder( // Ensure the foreign key constrains exist // in the temporary database - let temp_account_id = AccountId::random(); let temp_name = "compact_temp"; - let account_row = AccountRow::new_insert( - &temp_account_id, - temp_name.to_owned(), - )?; + let account_row = + AccountRow::new_insert(account_id, temp_name.to_owned())?; let mut vault = Vault::default(); *vault.header_mut().id_mut() = *folder_id; let folder_row = FolderRow::new_insert(&vault).await?; @@ -64,7 +62,7 @@ pub async fn compact_folder( // Copy the event log using the new temporary owner let mut temp_event_log = event_log.with_new_client( client, - Some(EventLogOwner::Folder(folder_record)), + Some(EventLogOwner::Folder(*account_id, folder_record)), ); temp_event_log.apply(events.as_slice()).await?; @@ -92,8 +90,12 @@ pub async fn compact_folder( // Apply them to a temporary event log file let temp = NamedTempFile::new()?; - let mut temp_event_log = - FsFolderEventLog::::new_folder(temp.path()).await?; + let mut temp_event_log = FsFolderEventLog::::new_folder( + temp.path(), + *account_id, + EventLogType::Folder(*folder_id), + ) + .await?; temp_event_log.apply(events.as_slice()).await?; let mut records = Vec::new(); diff --git a/crates/backend/src/event_log.rs b/crates/backend/src/event_log.rs index 3ca09ef4b6..c60cf8c93f 100644 --- a/crates/backend/src/event_log.rs +++ b/crates/backend/src/event_log.rs @@ -3,10 +3,11 @@ use async_trait::async_trait; use binary_stream::futures::{Decodable, Encodable}; use futures::stream::BoxStream; use sos_core::{ - commit::{CommitHash, CommitProof, CommitSpan, CommitTree}, + commit::{CommitHash, CommitProof, CommitTree}, events::{ patch::{CheckedPatch, Diff, Patch}, - AccountEvent, DeviceEvent, EventLog, EventRecord, WriteEvent, + AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, + WriteEvent, }, AccountId, VaultId, }; @@ -50,6 +51,7 @@ impl BackendEventLog { BackendTarget::FileSystem(paths) => BackendEventLog::FileSystem( FileSystemEventLog::::new_account( paths.with_account_id(account_id).account_events(), + *account_id, ) .await?, ), @@ -77,6 +79,8 @@ impl BackendEventLog { paths .with_account_id(account_id) .event_log_path(folder_id), + *account_id, + EventLogType::Folder(*folder_id), ) .await?, ), @@ -100,6 +104,8 @@ impl BackendEventLog { BackendTarget::FileSystem(paths) => BackendEventLog::FileSystem( FileSystemEventLog::::new_folder( paths.with_account_id(account_id).identity_events(), + *account_id, + EventLogType::Identity, ) .await?, ), @@ -139,6 +145,7 @@ impl BackendEventLog { BackendTarget::FileSystem(paths) => BackendEventLog::FileSystem( FileSystemEventLog::::new_device( paths.with_account_id(account_id).device_events(), + *account_id, ) .await?, ), @@ -164,6 +171,7 @@ impl BackendEventLog { BackendTarget::FileSystem(paths) => BackendEventLog::FileSystem( FileSystemEventLog::::new_file( paths.with_account_id(account_id).file_events(), + *account_id, ) .await?, ), @@ -293,10 +301,7 @@ where } } - async fn apply( - &mut self, - events: &[T], - ) -> Result { + async fn apply(&mut self, events: &[T]) -> Result<(), Self::Error> { match self { Self::Database(inner) => inner.apply(events).await, Self::FileSystem(inner) => inner.apply(events).await, @@ -306,7 +311,7 @@ where async fn apply_records( &mut self, records: Vec, - ) -> Result { + ) -> Result<(), Self::Error> { match self { Self::Database(inner) => inner.apply_records(records).await, Self::FileSystem(inner) => inner.apply_records(records).await, @@ -341,7 +346,7 @@ where async fn patch_unchecked( &mut self, patch: &Patch, - ) -> Result { + ) -> Result<(), Self::Error> { match self { Self::Database(inner) => inner.patch_unchecked(patch).await, Self::FileSystem(inner) => inner.patch_unchecked(patch).await, diff --git a/crates/backend/src/folder.rs b/crates/backend/src/folder.rs index 11c3962225..97380480f5 100644 --- a/crates/backend/src/folder.rs +++ b/crates/backend/src/folder.rs @@ -4,7 +4,7 @@ use sos_core::{ commit::{CommitHash, CommitState}, crypto::AccessKey, encode, - events::{EventLog, EventRecord, ReadEvent, WriteEvent}, + events::{EventLog, EventLogType, EventRecord, ReadEvent, WriteEvent}, AccountId, VaultFlags, VaultId, }; use sos_core::{constants::EVENT_LOG_EXT, decode, VaultCommit}; @@ -43,6 +43,8 @@ impl Folder { BackendTarget::FileSystem(paths) => { Self::from_path( paths.with_account_id(account_id).vault_path(folder_id), + account_id, + EventLogType::Folder(*folder_id), ) .await } @@ -132,13 +134,21 @@ impl Folder { /// and if an event log does not exist it is created. /// /// If an event log exists the commit tree is loaded into memory. - pub async fn from_path(path: impl AsRef) -> Result { + pub async fn from_path( + path: impl AsRef, + account_id: &AccountId, + log_type: EventLogType, + ) -> Result { let mut events_path = path.as_ref().to_owned(); events_path.set_extension(EVENT_LOG_EXT); let mut event_log = - sos_filesystem::FolderEventLog::::new_folder(events_path) - .await?; + sos_filesystem::FolderEventLog::::new_folder( + events_path, + *account_id, + log_type, + ) + .await?; event_log.load_tree().await?; let needs_init = event_log.tree().root().is_none(); diff --git a/crates/core/src/events/change.rs b/crates/core/src/events/change.rs index 7a50585f2a..3a6a4b4a69 100644 --- a/crates/core/src/events/change.rs +++ b/crates/core/src/events/change.rs @@ -1,20 +1,42 @@ use crate::{commit::CommitSpan, events::EventLogType, AccountId}; +use std::sync::OnceLock; +use tokio::sync::watch; -/// Change event sent over a local socket. -pub struct LocalChangeEvent { - /// Account identifier. - pub account_id: AccountId, - /// Detail about the event. - pub detail: LocalChangeDetail, -} +static CHANGES_FEED: OnceLock> = + OnceLock::new(); -/// Detail for the event. -pub enum LocalChangeDetail { - /// Change to an event log. - EventLog { +/// Change event. +/// +/// Used for IPC communication when a process needs +/// to know if changes have been made externally, +/// +/// For example, the browser extension helper executable +/// can detect changes made by the app and update it's +/// view. +#[derive(Default, Debug)] +pub enum LocalChangeEvent { + /// Changes feed was initialized. + #[default] + Init, + /// Account was created. + AccountCreated(AccountId), + /// Account was modified. + AccountModified { + /// Account identifier. + account_id: AccountId, /// Type of the event log. log_type: EventLogType, /// Span of commit hashes. commit_span: CommitSpan, }, + /// Account was deleted. + AccountDeleted(AccountId), +} + +/// Feed of change events. +pub fn changes_feed<'a>() -> &'a watch::Sender { + CHANGES_FEED.get_or_init(|| { + let (tx, _) = watch::channel(LocalChangeEvent::default()); + tx + }) } diff --git a/crates/core/src/events/event_log.rs b/crates/core/src/events/event_log.rs index dfb5ec09bc..a2a3e90ef8 100644 --- a/crates/core/src/events/event_log.rs +++ b/crates/core/src/events/event_log.rs @@ -91,10 +91,7 @@ where /// Append a collection of events and commit the tree hashes /// only if all the events were successfully written. - async fn apply( - &mut self, - events: &[E], - ) -> Result; + async fn apply(&mut self, events: &[E]) -> Result<(), Self::Error>; /// Append raw event records to the event log. /// @@ -103,7 +100,7 @@ where async fn apply_records( &mut self, records: Vec, - ) -> Result; + ) -> Result<(), Self::Error>; /// Append a patch to this event log only if the /// head of the tree matches the given proof. @@ -137,7 +134,7 @@ where async fn patch_unchecked( &mut self, patch: &Patch, - ) -> Result; + ) -> Result<(), Self::Error>; /// Diff of event records until a specific commit. /// diff --git a/crates/core/src/events/mod.rs b/crates/core/src/events/mod.rs index bd863900b2..1bf91b8a5b 100644 --- a/crates/core/src/events/mod.rs +++ b/crates/core/src/events/mod.rs @@ -22,7 +22,7 @@ mod record; mod write; pub use account::AccountEvent; -pub use change::{LocalChangeDetail, LocalChangeEvent}; +pub use change::{changes_feed, LocalChangeEvent}; pub use device::DeviceEvent; pub use event::Event; pub use event_kind::EventKind; diff --git a/crates/database/src/event_log.rs b/crates/database/src/event_log.rs index 282d526df8..1898e97ba5 100644 --- a/crates/database/src/event_log.rs +++ b/crates/database/src/event_log.rs @@ -25,9 +25,10 @@ use sos_core::{ commit::{CommitHash, CommitProof, CommitSpan, CommitTree, Comparison}, encoding::VERSION1, events::{ + changes_feed, patch::{CheckedPatch, Diff, Patch}, AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, - WriteEvent, + LocalChangeEvent, WriteEvent, }, AccountId, VaultId, }; @@ -37,16 +38,26 @@ use sos_core::{ #[doc(hidden)] pub enum EventLogOwner { /// Event log owned by an account. - Account(i64), + Account(AccountId, i64), /// Event log owned by a folder. - Folder(FolderRecord), + Folder(AccountId, FolderRecord), +} + +impl EventLogOwner { + /// Account idenifier. + pub fn account_id(&self) -> &AccountId { + match self { + EventLogOwner::Account(account_id, _) => account_id, + EventLogOwner::Folder(account_id, _) => account_id, + } + } } impl From<&EventLogOwner> for i64 { fn from(value: &EventLogOwner) -> Self { match value { - EventLogOwner::Account(id) => *id, - EventLogOwner::Folder(folder) => folder.row_id, + EventLogOwner::Account(_, id) => *id, + EventLogOwner::Folder(_, folder) => folder.row_id, } } } @@ -143,8 +154,11 @@ where .await?; Ok(match result { - (account_row, None) => EventLogOwner::Account(account_row.row_id), + (account_row, None) => { + EventLogOwner::Account(account_id, account_row.row_id) + } (_, Some(folder_row)) => EventLogOwner::Folder( + account_id, FolderRecord::from_row(folder_row).await?, ), }) @@ -154,9 +168,9 @@ where &mut self, records: &[EventRecord], delete_before: bool, - ) -> Result { + ) -> Result<(), E> { if records.is_empty() { - return Ok(CommitSpan::default()); + return Ok(()); } let mut span = CommitSpan { @@ -205,7 +219,13 @@ where span.after = self.tree.last_commit(); - Ok(span) + changes_feed().send_replace(LocalChangeEvent::AccountModified { + account_id: *self.owner.account_id(), + log_type: self.log_type, + commit_span: span, + }); + + Ok(()) } } @@ -535,10 +555,7 @@ where Ok(()) } - async fn apply( - &mut self, - events: &[T], - ) -> Result { + async fn apply(&mut self, events: &[T]) -> Result<(), Self::Error> { let mut records = Vec::with_capacity(events.len()); for event in events { records.push(EventRecord::encode_event(event).await?); @@ -549,7 +566,7 @@ where async fn apply_records( &mut self, records: Vec, - ) -> Result { + ) -> Result<(), Self::Error> { self.insert_records(records.as_slice(), false).await } @@ -605,7 +622,7 @@ where async fn patch_unchecked( &mut self, patch: &Patch, - ) -> Result { + ) -> Result<(), Self::Error> { self.apply_records(patch.records().to_vec()).await } @@ -644,8 +661,8 @@ where fn version(&self) -> u16 { match &self.owner { - EventLogOwner::Folder(folder) => *folder.summary.version(), - EventLogOwner::Account(_) => VERSION1, + EventLogOwner::Folder(_, folder) => *folder.summary.version(), + EventLogOwner::Account(_, _) => VERSION1, } } } diff --git a/crates/filesystem/src/archive/import.rs b/crates/filesystem/src/archive/import.rs index 616c80c096..85ced1333c 100644 --- a/crates/filesystem/src/archive/import.rs +++ b/crates/filesystem/src/archive/import.rs @@ -6,6 +6,8 @@ use crate::{write_exclusive, FolderEventLog, VaultFileWriter}; use hex; use sha2::{Digest, Sha256}; use sos_archive::{sanitize_file_path, ZipReader}; +use sos_core::events::EventLogType; +use sos_core::AccountId; use sos_core::{ constants::{ ACCOUNT_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR, FILE_EVENTS, @@ -55,8 +57,8 @@ async fn import_archive_reader( )); } - let address_path = restore_targets.manifest.account_id; - let paths = paths.with_account_id(&address_path); + let account_id = restore_targets.manifest.account_id; + let paths = paths.with_account_id(&account_id); // Write out the identity vault let identity_vault: Vault = decode(&restore_targets.identity.1).await?; @@ -65,8 +67,12 @@ async fn import_archive_reader( // Write out the identity event log file let (_, events) = FolderReducer::split::(identity_vault).await?; - let mut event_log = - FolderEventLog::::new_folder(paths.identity_events()).await?; + let mut event_log = FolderEventLog::::new_folder( + paths.identity_events(), + account_id, + EventLogType::Identity, + ) + .await?; event_log.apply(events.as_slice()).await?; // Check if the identity name already exists @@ -97,7 +103,8 @@ async fn import_archive_reader( vfs::create_dir_all(&vaults_dir).await?; restore_system(&paths, &restore_targets).await?; - restore_user_folders(&paths, &restore_targets.vaults).await?; + restore_user_folders(&paths, &account_id, &restore_targets.vaults) + .await?; let account = PublicIdentity::new(restore_targets.manifest.account_id, label); Ok(account) @@ -105,12 +112,14 @@ async fn import_archive_reader( async fn restore_user_folders( paths: &Paths, + account_id: &AccountId, vaults: &Vec<(Vec, Vault)>, ) -> Result<()> { // Write out each vault and the event log for (buffer, vault) in vaults { - let vault_path = paths.vault_path(vault.id()); - let event_log_path = paths.event_log_path(vault.id()); + let folder_id = *vault.id(); + let vault_path = paths.vault_path(&folder_id); + let event_log_path = paths.event_log_path(&folder_id); // Write out the vault buffer write_exclusive(&vault_path, buffer).await?; @@ -119,8 +128,12 @@ async fn restore_user_folders( FolderReducer::split::(vault.clone()).await?; // Write out the event log file - let mut event_log = - FolderEventLog::::new_folder(event_log_path).await?; + let mut event_log = FolderEventLog::::new_folder( + event_log_path, + *account_id, + EventLogType::Folder(folder_id), + ) + .await?; event_log.apply(events.as_slice()).await?; } diff --git a/crates/filesystem/src/event_log.rs b/crates/filesystem/src/event_log.rs index ead690addf..a56352e57a 100644 --- a/crates/filesystem/src/event_log.rs +++ b/crates/filesystem/src/event_log.rs @@ -29,9 +29,12 @@ use sos_core::{ encode, encoding::{encoding_options, VERSION1}, events::{ + changes_feed, patch::{CheckedPatch, Diff, Patch}, - AccountEvent, DeviceEvent, EventRecord, WriteEvent, + AccountEvent, DeviceEvent, EventLogType, EventRecord, + LocalChangeEvent, WriteEvent, }, + AccountId, }; use sos_vfs::{self as vfs, File, OpenOptions}; use std::result::Result as StdResult; @@ -103,6 +106,8 @@ where + Sync + 'static, { + account_id: AccountId, + log_type: EventLogType, tree: CommitTree, data: PathBuf, identity: &'static [u8], @@ -279,10 +284,7 @@ where Ok(()) } - async fn apply( - &mut self, - events: &[T], - ) -> StdResult { + async fn apply(&mut self, events: &[T]) -> StdResult<(), Self::Error> { let mut records = Vec::with_capacity(events.len()); for event in events { records.push(EventRecord::encode_event(event).await?); @@ -293,9 +295,9 @@ where async fn apply_records( &mut self, records: Vec, - ) -> StdResult { + ) -> StdResult<(), Self::Error> { if records.is_empty() { - return Ok(CommitSpan::default()); + return Ok(()); } let mut span = CommitSpan { @@ -343,7 +345,15 @@ where span.after = self.tree.last_commit(); - Ok(span) + changes_feed().send_replace( + LocalChangeEvent::AccountModified { + account_id: self.account_id, + log_type: self.log_type, + commit_span: span, + }, + ); + + Ok(()) } Err(e) => Err(e.into()), } @@ -427,7 +437,7 @@ where async fn patch_unchecked( &mut self, patch: &Patch, - ) -> StdResult { + ) -> StdResult<(), Self::Error> { /* if let Some(record) = patch.records().first() { self.check_event_time_ahead(record).await?; @@ -677,7 +687,11 @@ where + 'static, { /// Create a new folder event log file. - pub async fn new_folder>(path: P) -> StdResult { + pub async fn new_folder>( + path: P, + account_id: AccountId, + log_type: EventLogType, + ) -> StdResult { use sos_core::constants::FOLDER_EVENT_LOG_IDENTITY; // Note that for backwards compatibility we don't // encode a version, later we will need to upgrade @@ -695,6 +709,8 @@ where Ok(Self { data: path.as_ref().to_path_buf(), tree: Default::default(), + log_type, + account_id, identity: &FOLDER_EVENT_LOG_IDENTITY, version: None, phantom: std::marker::PhantomData, @@ -714,7 +730,10 @@ where + 'static, { /// Create a new account event log file. - pub async fn new_account>(path: P) -> StdResult { + pub async fn new_account>( + path: P, + account_id: AccountId, + ) -> StdResult { use sos_core::{ constants::ACCOUNT_EVENT_LOG_IDENTITY, encoding::VERSION, }; @@ -729,6 +748,8 @@ where .await?; Ok(Self { + account_id, + log_type: EventLogType::Account, data: path.as_ref().to_path_buf(), tree: Default::default(), identity: &ACCOUNT_EVENT_LOG_IDENTITY, @@ -750,7 +771,10 @@ where + 'static, { /// Create a new device event log file. - pub async fn new_device(path: impl AsRef) -> StdResult { + pub async fn new_device( + path: impl AsRef, + account_id: AccountId, + ) -> StdResult { use sos_core::{ constants::DEVICE_EVENT_LOG_IDENTITY, encoding::VERSION, }; @@ -766,6 +790,8 @@ where .await?; Ok(Self { + log_type: EventLogType::Device, + account_id, data: path.as_ref().to_path_buf(), tree: Default::default(), identity: &DEVICE_EVENT_LOG_IDENTITY, @@ -788,7 +814,10 @@ where + 'static, { /// Create a new file event log file. - pub async fn new_file(path: impl AsRef) -> StdResult { + pub async fn new_file( + path: impl AsRef, + account_id: AccountId, + ) -> StdResult { use sos_core::{ constants::FILE_EVENT_LOG_IDENTITY, encoding::VERSION, }; @@ -804,6 +833,8 @@ where .await?; Ok(Self { + account_id, + log_type: EventLogType::Files, data: path.as_ref().to_path_buf(), tree: Default::default(), identity: &FILE_EVENT_LOG_IDENTITY, diff --git a/crates/login/src/identity_folder.rs b/crates/login/src/identity_folder.rs index 51a4549dd4..8f9f0ca334 100644 --- a/crates/login/src/identity_folder.rs +++ b/crates/login/src/identity_folder.rs @@ -14,8 +14,9 @@ use sos_backend::{ AccessPoint, BackendTarget, Folder, FolderEventLog, }; use sos_core::{ - constants::LOGIN_AGE_KEY_URN, crypto::AccessKey, encode, AccountId, - AuthenticationError, VaultFlags, VaultId, + constants::LOGIN_AGE_KEY_URN, crypto::AccessKey, encode, + events::EventLogType, AccountId, AuthenticationError, VaultFlags, + VaultId, }; use sos_filesystem::write_exclusive; use sos_vault::Summary; @@ -66,7 +67,12 @@ impl IdentityFolder { BackendTarget::FileSystem(paths) => { let buffer = encode(&vault).await?; write_exclusive(paths.identity_vault(), buffer).await?; - Folder::from_path(paths.identity_vault()).await? + Folder::from_path( + paths.identity_vault(), + &account_id, + EventLogType::Identity, + ) + .await? } BackendTarget::Database(_, client) => { let account_row = AccountRow::new_insert(&account_id, name)?; @@ -466,7 +472,12 @@ impl IdentityFolder { let target = target.clone().with_account_id(account_id); let mut folder = match &target { BackendTarget::FileSystem(paths) => { - Folder::from_path(paths.identity_vault()).await? + Folder::from_path( + paths.identity_vault(), + account_id, + EventLogType::Identity, + ) + .await? } BackendTarget::Database(_, client) => { let (_, login_folder) = diff --git a/crates/storage/client/src/filesystem.rs b/crates/storage/client/src/filesystem.rs index 342a3ae380..8b10089350 100644 --- a/crates/storage/client/src/filesystem.rs +++ b/crates/storage/client/src/filesystem.rs @@ -16,7 +16,7 @@ use sos_core::{ decode, device::TrustedDevice, encode, - events::{DeviceEvent, Event, EventLog, ReadEvent}, + events::{DeviceEvent, Event, EventLog, EventLogType, ReadEvent}, AccountId, Paths, SecretId, VaultFlags, VaultId, }; use sos_filesystem::write_exclusive; @@ -324,8 +324,14 @@ impl ClientFolderStorage for ClientFileSystemStorage { } async fn new_folder(&self, vault: &Vault, _: Internal) -> Result { - let vault_path = self.paths.vault_path(vault.id()); - Ok(Folder::from_path(&vault_path).await?) + let folder_id = *vault.id(); + let vault_path = self.paths.vault_path(&folder_id); + Ok(Folder::from_path( + &vault_path, + self.account_id(), + EventLogType::Folder(folder_id), + ) + .await?) } async fn read_vault(&self, id: &VaultId) -> Result { diff --git a/crates/storage/client/src/traits.rs b/crates/storage/client/src/traits.rs index eab86ca5ff..f797cc1a74 100644 --- a/crates/storage/client/src/traits.rs +++ b/crates/storage/client/src/traits.rs @@ -570,7 +570,8 @@ pub trait ClientFolderStorage: let event_log = folder.event_log(); let mut log_file = event_log.write().await; - compact_folder(folder_id, &mut *log_file).await?; + compact_folder(self.account_id(), folder_id, &mut *log_file) + .await?; } // Refresh in-memory vault and mirrored copy diff --git a/tests/unit/src/tests/event_log/load_tree.rs b/tests/unit/src/tests/event_log/load_tree.rs index 28f60c1440..adf8a17404 100644 --- a/tests/unit/src/tests/event_log/load_tree.rs +++ b/tests/unit/src/tests/event_log/load_tree.rs @@ -2,7 +2,11 @@ use super::mock; use anyhow::Result; use futures::{pin_mut, StreamExt}; use sos_backend::{BackendEventLog, BackendTarget, FolderEventLog}; -use sos_core::{commit::CommitHash, events::EventLog, Paths}; +use sos_core::{ + commit::CommitHash, + events::{EventLog, EventLogType}, + AccountId, Paths, VaultId, +}; use sos_test_utils::mock::file_database; #[tokio::test] @@ -10,8 +14,14 @@ async fn fs_event_log_load_tree() -> Result<()> { let path = "target/event_log_file_load.events"; let (mock_event_log, _) = mock::fs_event_log_standalone(path).await?; let expected_root = mock_event_log.tree().root().unwrap(); + let account_id = AccountId::random(); let event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(path).await?, + sos_filesystem::FolderEventLog::new_folder( + path, + account_id, + EventLogType::Folder(VaultId::new_v4()), + ) + .await?, ); assert_load_tree(event_log, expected_root).await?; Ok(()) diff --git a/tests/unit/src/tests/event_log/mod.rs b/tests/unit/src/tests/event_log/mod.rs index 4596bad63e..fdd2e50a5e 100644 --- a/tests/unit/src/tests/event_log/mod.rs +++ b/tests/unit/src/tests/event_log/mod.rs @@ -15,7 +15,7 @@ pub mod mock { commit::{CommitHash, CommitTree}, crypto::PrivateKey, encode, - events::{EventLog, WriteEvent}, + events::{EventLog, EventLogType, WriteEvent}, AccountId, Paths, SecretId, VaultCommit, VaultEntry, VaultId, }; use sos_database::async_sqlite::Client; @@ -171,9 +171,16 @@ pub mod mock { let (id, data) = mock_secret().await?; + let account_id = AccountId::random(); + // Create a simple event log let mut event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(path.as_ref()).await?, + sos_filesystem::FolderEventLog::new_folder( + path.as_ref(), + account_id, + EventLogType::Folder(VaultId::new_v4()), + ) + .await?, ); event_log .apply(&[ @@ -206,9 +213,17 @@ pub mod mock { let (id, data) = mock_secret().await?; + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + // Create a simple event log let mut server = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(server_file).await?, + sos_filesystem::FolderEventLog::new_folder( + server_file, + account_id, + log_type, + ) + .await?, ); server .apply(&[ @@ -219,7 +234,12 @@ pub mod mock { // Duplicate the server events on the client let mut client = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(client_file).await?, + sos_filesystem::FolderEventLog::new_folder( + client_file, + account_id, + log_type, + ) + .await?, ); { let stream = server.event_stream(false).await; diff --git a/tests/unit/src/tests/event_log/rewind.rs b/tests/unit/src/tests/event_log/rewind.rs index 5586daf591..52c4dcdafd 100644 --- a/tests/unit/src/tests/event_log/rewind.rs +++ b/tests/unit/src/tests/event_log/rewind.rs @@ -1,11 +1,11 @@ use super::mock; use anyhow::Result; use sos_backend::{BackendEventLog, BackendTarget, FolderEventLog}; -use sos_core::Paths; use sos_core::{ commit::CommitHash, encode, - events::{EventLog, WriteEvent}, + events::{EventLog, EventLogType, WriteEvent}, + AccountId, Paths, VaultId, }; use sos_test_utils::mock::memory_database; use sos_vault::Vault; @@ -18,15 +18,24 @@ async fn fs_event_log_rewind() -> Result<()> { vfs::remove_file(path).await?; } + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + let vault: Vault = Default::default(); let event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(path).await?, + sos_filesystem::FolderEventLog::new_folder( + path, account_id, log_type, + ) + .await?, ); let rewind_root = assert_event_log_rewind(event_log, vault).await?; // Create new event log to load the commits and verify the root let event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(path).await?, + sos_filesystem::FolderEventLog::new_folder( + path, account_id, log_type, + ) + .await?, ); assert_event_log_rewound_root(event_log, rewind_root).await?; diff --git a/tests/unit/src/tests/folder.rs b/tests/unit/src/tests/folder.rs index b5d49bf058..9810e79a62 100644 --- a/tests/unit/src/tests/folder.rs +++ b/tests/unit/src/tests/folder.rs @@ -2,10 +2,11 @@ use anyhow::Result; use futures::{pin_mut, StreamExt}; use secrecy::ExposeSecret; use sos_backend::{BackendTarget, Folder, FolderEventLog}; -use sos_core::Paths; +use sos_core::events::EventLogType; use sos_core::{ crypto::AccessKey, encode, events::EventLog, SecretId, VaultFlags, }; +use sos_core::{AccountId, Paths, VaultId}; use sos_test_utils::mock::{ self, file_database, insert_database_vault, vault_file, vault_memory, }; @@ -21,7 +22,11 @@ async fn fs_folder_lifecycle() -> Result<()> { let buffer = encode(&vault).await?; vfs::write(temp.path(), &buffer).await?; - let mut folder = Folder::from_path(temp.path()).await?; + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + + let mut folder = + Folder::from_path(temp.path(), &account_id, log_type).await?; let key: AccessKey = password.into(); assert_folder(&mut folder, key).await?; diff --git a/tests/unit/src/tests/sdk/reducer.rs b/tests/unit/src/tests/sdk/reducer.rs index e28888fea6..d041cd6296 100644 --- a/tests/unit/src/tests/sdk/reducer.rs +++ b/tests/unit/src/tests/sdk/reducer.rs @@ -2,8 +2,10 @@ use anyhow::Result; use secrecy::ExposeSecret; use sos_backend::{BackendEventLog, FolderEventLog}; use sos_core::{ - crypto::PrivateKey, decode, events::EventLog, SecretId, VaultCommit, - VaultEntry, + crypto::PrivateKey, + decode, + events::{EventLog, EventLogType}, + AccountId, SecretId, VaultCommit, VaultEntry, VaultId, }; use sos_reducers::FolderReducer; use sos_test_utils::mock; @@ -73,10 +75,17 @@ async fn event_log_reduce_compact() -> Result<()> { assert_eq!(2, events.len()); + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + let compact_temp = NamedTempFile::new()?; let mut compact = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(compact_temp.path()) - .await?, + sos_filesystem::FolderEventLog::new_folder( + compact_temp.path(), + account_id, + log_type, + ) + .await?, ); compact.apply(events.as_slice()).await?; @@ -96,9 +105,17 @@ async fn mock_event_log_file( let (encryption_key, _, _) = mock::encryption_key()?; let (_, mut vault, _) = mock::vault_file().await?; + let account_id = AccountId::random(); + let log_type = EventLogType::Folder(VaultId::new_v4()); + let temp = NamedTempFile::new()?; let mut event_log = BackendEventLog::FileSystem( - sos_filesystem::FolderEventLog::new_folder(temp.path()).await?, + sos_filesystem::FolderEventLog::new_folder( + temp.path(), + account_id, + log_type, + ) + .await?, ); // Create the vault From b06f469f1aa7dc353418e4419222704828ae1169 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 15:32:37 +0800 Subject: [PATCH 08/61] Prepare producer and consumer for changes feed. --- Cargo.lock | 4 + crates/changes/Cargo.toml | 8 +- crates/changes/src/consumer.rs | 111 ++++++++++++++++++++-------- crates/changes/src/error.rs | 4 + crates/changes/src/lib.rs | 25 ++++++- crates/changes/src/producer.rs | 79 +++++++++++++++++--- crates/core/src/events/change.rs | 4 +- crates/core/src/events/event_log.rs | 2 +- crates/core/src/events/mod.rs | 5 +- 9 files changed, 192 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1c475cd66..47c29cd219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5146,10 +5146,14 @@ dependencies = [ name = "sos-changes" version = "0.17.0" dependencies = [ + "futures", "interprocess", "rustc_version", + "serde_json", "sos-core", "thiserror 2.0.12", + "tokio", + "tokio-util", "tracing", ] diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml index 3d56c1693f..a0d1b70997 100644 --- a/crates/changes/Cargo.toml +++ b/crates/changes/Cargo.toml @@ -8,14 +8,18 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/saveoursecrets/sdk" [features] -changes-consumer = ["dep:interprocess"] -changes-producer = ["dep:interprocess"] +changes-consumer = ["dep:interprocess", "tokio-util"] +changes-producer = ["dep:interprocess", "tokio-util"] [dependencies] thiserror.workspace = true sos-core.workspace = true tracing.workspace = true +futures.workspace = true +tokio.workspace = true +serde_json.workspace = true interprocess = { workspace = true, optional = true, features = ["tokio"] } +tokio-util = { workspace = true, optional = true } [build-dependencies] rustc_version.workspace = true diff --git a/crates/changes/src/consumer.rs b/crates/changes/src/consumer.rs index e17f1cbb61..7dd06791c3 100644 --- a/crates/changes/src/consumer.rs +++ b/crates/changes/src/consumer.rs @@ -1,50 +1,99 @@ -//! Consumer for change notifications on a named pipe. -use crate::Result; +//! Consumer for change notifications on a local socket. +use crate::{Error, Result, SocketFile}; +use futures::stream::StreamExt; use interprocess::local_socket::{ - tokio::prelude::*, GenericNamespaced, ListenerOptions, + tokio::prelude::*, GenericFilePath, ListenerOptions, }; +use sos_core::events::LocalChangeEvent; +use std::path::PathBuf; +use tokio::{ + select, + sync::{mpsc, watch}, +}; +use tokio_util::codec::LengthDelimitedCodec; + +/// Handle to a consumer. +/// +/// Can be used to listen to incoming change events and +/// close the server task. +pub struct ConsumerHandle { + receiver: mpsc::Receiver, + cancel_tx: watch::Sender, +} -/// Consumer socket connection for inter-process communication. +impl ConsumerHandle { + /// Channel for change events. + pub fn changes(&mut self) -> &mut mpsc::Receiver { + &mut self.receiver + } + + /// Stop listening for incoming events. + pub fn cancel(&self) { + self.cancel_tx.send_replace(true); + } +} + +/// Consumer socket connection for change events. pub struct ChangeConsumer; impl ChangeConsumer { - /// Listen on a named pipe. - pub async fn listen(socket_name: &str) -> Result<()> { - let name = socket_name.to_ns_name::()?; + /// Listen on change events. + /// + /// Returns a handle that can be used to consume the + /// incoming events and stop listening. + pub async fn listen(path: PathBuf) -> Result { + let file = SocketFile::from(path); + let name = + file.as_ref().as_os_str().to_fs_name::()?; let opts = ListenerOptions::new().name(name); let listener = match opts.create_tokio() { Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => { tracing::error!( - "Error: could not start server because the socket file is occupied. Please check if {socket_name} is in use by another process and try again." + "Error: could not start server because the socket file is occupied. Please check if {} is in use by another process and try again.", + file.as_ref().display(), ); return Err(e.into()); } x => x?, }; - todo!(); - - /* - let service = LocalWebService::new(app_info, accounts); - let svc = Arc::new(service); - - loop { - let socket = listener.accept().await?; - let svc = svc.clone(); - tokio::task::spawn(async move { - let socket = TokioIo::new(socket); - let http = Builder::new(); - - tracing::debug!("local_socket_server::new_connection"); - let conn = http.serve_connection(socket, svc); - if let Err(err) = conn.await { - tracing::error!( - error = %err, - "local_socket_server::connection_error"); + let (cancel_tx, mut cancel_rx) = watch::channel(false); + let (tx, rx) = mpsc::channel(32); + + #[allow(unreachable_code)] + tokio::task::spawn(async move { + loop { + select! { + _ = cancel_rx.changed() => { + if *cancel_rx.borrow_and_update() { + break; + } + } + socket = listener.accept() => { + let socket = socket?; + let tx = tx.clone(); + tokio::task::spawn(async move { + let mut reader = LengthDelimitedCodec::builder() + .native_endian() + .new_read(socket); + while let Some(Ok(buffer)) = reader.next().await { + let event: LocalChangeEvent = + serde_json::from_slice(&buffer)?; + if let Err(e) = tx.send(event).await { + tracing::warn!(error = %e); + } + } + + Ok::<_, Error>(()) + }); + } } - tracing::debug!("local_socket_server::connection_close"); - }); - } - */ + } + Ok::<_, Error>(()) + }); + Ok(ConsumerHandle { + receiver: rx, + cancel_tx, + }) } } diff --git a/crates/changes/src/error.rs b/crates/changes/src/error.rs index 530647a9fb..d77fe62b1b 100644 --- a/crates/changes/src/error.rs +++ b/crates/changes/src/error.rs @@ -6,4 +6,8 @@ pub enum Error { /// Errors generated by the IO module. #[error(transparent)] Io(#[from] std::io::Error), + + /// Errors generated by the JSON module. + #[error(transparent)] + Json(#[from] serde_json::Error), } diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index 3d77442fd1..35da12d462 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -4,9 +4,7 @@ #![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] mod error; - pub use error::Error; -use sos_core::{commit::CommitSpan, events::EventLogType, AccountId}; #[cfg(feature = "changes-consumer")] pub mod consumer; @@ -14,3 +12,26 @@ pub mod consumer; pub mod producer; pub(crate) type Result = std::result::Result; + +use std::path::PathBuf; + +/// Socket file. +pub(crate) struct SocketFile(PathBuf); + +impl From for SocketFile { + fn from(value: PathBuf) -> Self { + Self(value) + } +} + +impl AsRef for SocketFile { + fn as_ref(&self) -> &PathBuf { + &self.0 + } +} + +impl Drop for SocketFile { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs index 2a7c87e63a..d076eeb176 100644 --- a/crates/changes/src/producer.rs +++ b/crates/changes/src/producer.rs @@ -1,25 +1,80 @@ -//! Producer for change notifications on a named pipe. -use crate::Result; +//! Producer for change notifications on a local socket. +use crate::{Error, Result}; +use futures::sink::SinkExt; use interprocess::local_socket::{tokio::prelude::*, GenericNamespaced}; +use sos_core::events::changes_feed; +use tokio::{select, sync::watch}; +use tokio_util::codec::LengthDelimitedCodec; -/// Producer socket connect for inter-process communication. +/// Handle to a producer. +pub struct ProducerHandle { + cancel_tx: watch::Sender, +} + +impl ProducerHandle { + /// Stop listening for change events. + pub fn cancel(&self) { + self.cancel_tx.send_replace(true); + } +} + +/// Producer socket connection for change events. pub struct ChangeProducer { socket_name: String, } impl ChangeProducer { - /// Create a connection to the named pipe. - pub async fn connect(socket_name: impl Into) -> Result { + /// Create a connection to the socket. + pub fn new(socket_name: &str) -> Result { Ok(Self { - socket_name: socket_name.into(), + socket_name: socket_name.to_owned(), }) } - /// Send a local request. - pub async fn send_request(&mut self) -> Result<()> { - let name = - self.socket_name.clone().to_ns_name::()?; - let io = LocalSocketStream::connect(name).await?; - todo!(); + /// Listen to the changes feed. + /// + /// For each event try to send it over the local socket, + /// returns a handle that can be used to cancel the listener. + #[allow(unreachable_code)] + pub fn listen(&self) -> ProducerHandle { + let (cancel_tx, mut cancel_rx) = watch::channel(false); + let tx = changes_feed(); + let mut rx = tx.subscribe(); + let socket_name = self.socket_name.clone(); + tokio::task::spawn(async move { + loop { + select! { + _ = cancel_rx.changed() => { + if *cancel_rx.borrow_and_update() { + break; + } + } + event = rx.changed() => { + match event { + Ok(_) => { + let event = rx.borrow_and_update().clone(); + let name = socket_name + .clone() + .to_ns_name::()?; + match LocalSocketStream::connect(name).await { + Ok(socket) => { + let mut writer = + LengthDelimitedCodec::builder() + .native_endian() + .new_write(socket); + let message = serde_json::to_vec(&event)?; + writer.send(message.into()).await?; + } + Err(_) => {} + } + } + Err(_) => {} + } + } + } + } + Ok::<_, Error>(()) + }); + ProducerHandle { cancel_tx } } } diff --git a/crates/core/src/events/change.rs b/crates/core/src/events/change.rs index 3a6a4b4a69..675c433d1f 100644 --- a/crates/core/src/events/change.rs +++ b/crates/core/src/events/change.rs @@ -1,4 +1,5 @@ use crate::{commit::CommitSpan, events::EventLogType, AccountId}; +use serde::{Deserialize, Serialize}; use std::sync::OnceLock; use tokio::sync::watch; @@ -13,7 +14,8 @@ static CHANGES_FEED: OnceLock> = /// For example, the browser extension helper executable /// can detect changes made by the app and update it's /// view. -#[derive(Default, Debug)] +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub enum LocalChangeEvent { /// Changes feed was initialized. #[default] diff --git a/crates/core/src/events/event_log.rs b/crates/core/src/events/event_log.rs index a2a3e90ef8..2bd9d5e3fc 100644 --- a/crates/core/src/events/event_log.rs +++ b/crates/core/src/events/event_log.rs @@ -1,5 +1,5 @@ use crate::{ - commit::{CommitHash, CommitProof, CommitSpan, CommitTree}, + commit::{CommitHash, CommitProof, CommitTree}, events::{ patch::{CheckedPatch, Diff, Patch}, EventRecord, diff --git a/crates/core/src/events/mod.rs b/crates/core/src/events/mod.rs index 1bf91b8a5b..e3aa51355e 100644 --- a/crates/core/src/events/mod.rs +++ b/crates/core/src/events/mod.rs @@ -33,8 +33,11 @@ pub use read::ReadEvent; pub use record::EventRecord; pub use write::WriteEvent; +use serde::{Deserialize, Serialize}; + /// Types of event logs. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] pub enum EventLogType { /// Identity folder event log. Identity, From ef5b0a33d9115364fed26f25e0007f9a29c2129c Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 15:52:19 +0800 Subject: [PATCH 09/61] Update producer to support multiple sockets. As there can be multiple browsers (and browser extensions) running and hence multiple consumers. --- crates/changes/Cargo.toml | 6 +++- crates/changes/src/lib.rs | 27 +++++++++++++++++ crates/changes/src/producer.rs | 54 +++++++++++++++------------------- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml index a0d1b70997..fdf7c7e5bb 100644 --- a/crates/changes/Cargo.toml +++ b/crates/changes/Cargo.toml @@ -7,6 +7,10 @@ homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" repository = "https://github.com/saveoursecrets/sdk" +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [features] changes-consumer = ["dep:interprocess", "tokio-util"] changes-producer = ["dep:interprocess", "tokio-util"] @@ -19,7 +23,7 @@ futures.workspace = true tokio.workspace = true serde_json.workspace = true interprocess = { workspace = true, optional = true, features = ["tokio"] } -tokio-util = { workspace = true, optional = true } +tokio-util = { workspace = true, optional = true, features = ["codec"] } [build-dependencies] rustc_version.workspace = true diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index 35da12d462..9f3cdbab4c 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -15,6 +15,9 @@ pub(crate) type Result = std::result::Result; use std::path::PathBuf; +const SOCKS: &str = "socks"; +const SOCK_EXT: &str = "sock"; + /// Socket file. pub(crate) struct SocketFile(PathBuf); @@ -35,3 +38,27 @@ impl Drop for SocketFile { let _ = std::fs::remove_file(&self.0); } } + +/// Standard path for a consumer socket file. +#[cfg(feature = "changes-consumer")] +pub fn socket_file(paths: &sos_core::Paths) -> Result { + let socks = paths.documents_dir().join(SOCKS); + if !socks.exists() { + std::fs::create_dir(&socks)?; + } + let pid = std::process::id(); + let mut path = socks.join(pid.to_string()); + path.set_extension(SOCK_EXT); + Ok(path) +} + +/// Find active socket files for a producer. +#[cfg(feature = "changes-producer")] +pub fn find_active_sockets(paths: &sos_core::Paths) -> Result> { + let socks = paths.documents_dir().join(SOCKS); + if socks.exists() { + todo!(); + } else { + Ok(Vec::new()) + } +} diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs index d076eeb176..5d10099e64 100644 --- a/crates/changes/src/producer.rs +++ b/crates/changes/src/producer.rs @@ -1,9 +1,11 @@ //! Producer for change notifications on a local socket. -use crate::{Error, Result}; +use std::path::PathBuf; + +use crate::Error; use futures::sink::SinkExt; -use interprocess::local_socket::{tokio::prelude::*, GenericNamespaced}; +use interprocess::local_socket::{tokio::prelude::*, GenericFilePath}; use sos_core::events::changes_feed; -use tokio::{select, sync::watch}; +use tokio::{select, sync::watch, sync::Mutex}; use tokio_util::codec::LengthDelimitedCodec; /// Handle to a producer. @@ -19,28 +21,19 @@ impl ProducerHandle { } /// Producer socket connection for change events. -pub struct ChangeProducer { - socket_name: String, -} +pub struct ChangeProducer; impl ChangeProducer { - /// Create a connection to the socket. - pub fn new(socket_name: &str) -> Result { - Ok(Self { - socket_name: socket_name.to_owned(), - }) - } - - /// Listen to the changes feed. + /// Listen to the changes feed and send change events to + /// active sockets. /// - /// For each event try to send it over the local socket, - /// returns a handle that can be used to cancel the listener. + /// Returns a handle that can be used to cancel the listener. #[allow(unreachable_code)] - pub fn listen(&self) -> ProducerHandle { + pub fn listen(&self, sockets: Vec) -> ProducerHandle { let (cancel_tx, mut cancel_rx) = watch::channel(false); let tx = changes_feed(); let mut rx = tx.subscribe(); - let socket_name = self.socket_name.clone(); + let sockets = Mutex::new(sockets); tokio::task::spawn(async move { loop { select! { @@ -53,19 +46,20 @@ impl ChangeProducer { match event { Ok(_) => { let event = rx.borrow_and_update().clone(); - let name = socket_name - .clone() - .to_ns_name::()?; - match LocalSocketStream::connect(name).await { - Ok(socket) => { - let mut writer = - LengthDelimitedCodec::builder() - .native_endian() - .new_write(socket); - let message = serde_json::to_vec(&event)?; - writer.send(message.into()).await?; + let sockets = sockets.lock().await; + for path in &*sockets { + let name = path.as_os_str().to_fs_name::()?; + match LocalSocketStream::connect(name).await { + Ok(socket) => { + let mut writer = + LengthDelimitedCodec::builder() + .native_endian() + .new_write(socket); + let message = serde_json::to_vec(&event)?; + writer.send(message.into()).await?; + } + Err(_) => {} } - Err(_) => {} } } Err(_) => {} From 381df4ecdbe645f057a830ebf24994b028f90331 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 10 Mar 2025 16:37:23 +0800 Subject: [PATCH 10/61] Prepare logic to monitor for active local sockets. --- Cargo.lock | 54 +++++++++++++++++ Cargo.toml | 2 + crates/changes/Cargo.toml | 3 +- crates/changes/src/consumer.rs | 12 +++- crates/changes/src/lib.rs | 22 +++---- crates/changes/src/producer.rs | 103 ++++++++++++++++++++++++++++++--- 6 files changed, 171 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47c29cd219..1419fef0d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1002,6 +1002,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -3100,6 +3110,15 @@ dependencies = [ "serde", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4083,6 +4102,26 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rcgen" version = "0.13.2" @@ -5151,6 +5190,7 @@ dependencies = [ "rustc_version", "serde_json", "sos-core", + "sysinfo", "thiserror 2.0.12", "tokio", "tokio-util", @@ -6188,6 +6228,20 @@ dependencies = [ "syn 2.0.99", ] +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.56.0", +] + [[package]] name = "system-deps" version = "6.2.2" diff --git a/Cargo.toml b/Cargo.toml index 1596cd6f34..fa7941a04a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,6 +121,8 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } secrecy = { version = "0.10", features = ["serde"] } serde = { version = "1", features = ["derive"] } +sysinfo = "0.33" + tokio = { version = "1", features = ["rt", "macros", "time", "sync"]} tokio-util = { version = "0.7", default-features = false, features = ["io", "compat"] } tokio-stream = "0.1" diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml index fdf7c7e5bb..eb1cb48787 100644 --- a/crates/changes/Cargo.toml +++ b/crates/changes/Cargo.toml @@ -13,7 +13,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] changes-consumer = ["dep:interprocess", "tokio-util"] -changes-producer = ["dep:interprocess", "tokio-util"] +changes-producer = ["dep:interprocess", "tokio-util", "dep:sysinfo"] [dependencies] thiserror.workspace = true @@ -22,6 +22,7 @@ tracing.workspace = true futures.workspace = true tokio.workspace = true serde_json.workspace = true +sysinfo = { workspace = true, optional = true } interprocess = { workspace = true, optional = true, features = ["tokio"] } tokio-util = { workspace = true, optional = true, features = ["codec"] } diff --git a/crates/changes/src/consumer.rs b/crates/changes/src/consumer.rs index 7dd06791c3..85449b4e57 100644 --- a/crates/changes/src/consumer.rs +++ b/crates/changes/src/consumer.rs @@ -14,8 +14,9 @@ use tokio_util::codec::LengthDelimitedCodec; /// Handle to a consumer. /// -/// Can be used to listen to incoming change events and -/// close the server task. +/// Provides access to a receive channel for +/// incoming change events and can also be +/// used to cancel the listener. pub struct ConsumerHandle { receiver: mpsc::Receiver, cancel_tx: watch::Sender, @@ -37,11 +38,16 @@ impl ConsumerHandle { pub struct ChangeConsumer; impl ChangeConsumer { - /// Listen on change events. + /// Listen for incoming change events. /// /// Returns a handle that can be used to consume the /// incoming events and stop listening. pub async fn listen(path: PathBuf) -> Result { + tracing::trace!( + socket_file = %path.display(), + "changes::consumer", + ); + let file = SocketFile::from(path); let name = file.as_ref().as_os_str().to_fs_name::()?; diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index 9f3cdbab4c..10b22fbb93 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -15,8 +15,8 @@ pub(crate) type Result = std::result::Result; use std::path::PathBuf; -const SOCKS: &str = "socks"; -const SOCK_EXT: &str = "sock"; +pub(crate) const SOCKS: &str = "socks"; +pub(crate) const SOCK_EXT: &str = "sock"; /// Socket file. pub(crate) struct SocketFile(PathBuf); @@ -40,8 +40,13 @@ impl Drop for SocketFile { } /// Standard path for a consumer socket file. +/// +/// If the parent directory for socket files does not +/// exist it is created. #[cfg(feature = "changes-consumer")] -pub fn socket_file(paths: &sos_core::Paths) -> Result { +pub fn socket_file( + paths: std::sync::Arc, +) -> Result { let socks = paths.documents_dir().join(SOCKS); if !socks.exists() { std::fs::create_dir(&socks)?; @@ -51,14 +56,3 @@ pub fn socket_file(paths: &sos_core::Paths) -> Result { path.set_extension(SOCK_EXT); Ok(path) } - -/// Find active socket files for a producer. -#[cfg(feature = "changes-producer")] -pub fn find_active_sockets(paths: &sos_core::Paths) -> Result> { - let socks = paths.documents_dir().join(SOCKS); - if socks.exists() { - todo!(); - } else { - Ok(Vec::new()) - } -} diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs index 5d10099e64..5f560c3e5f 100644 --- a/crates/changes/src/producer.rs +++ b/crates/changes/src/producer.rs @@ -1,13 +1,25 @@ //! Producer for change notifications on a local socket. -use std::path::PathBuf; - -use crate::Error; +use crate::{Error, Result}; use futures::sink::SinkExt; use interprocess::local_socket::{tokio::prelude::*, GenericFilePath}; -use sos_core::events::changes_feed; -use tokio::{select, sync::watch, sync::Mutex}; +use sos_core::{events::changes_feed, Paths}; +use std::{path::PathBuf, sync::Arc, sync::LazyLock, time::Duration}; +use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System}; +use tokio::{ + select, + sync::{watch, Mutex}, + time, +}; use tokio_util::codec::LengthDelimitedCodec; +static SYSTEM_PROCESSES: LazyLock> = + LazyLock::new(|| { + Mutex::new(System::new_with_specifics( + RefreshKind::nothing() + .with_processes(ProcessRefreshKind::everything()), + )) + }); + /// Handle to a producer. pub struct ProducerHandle { cancel_tx: watch::Sender, @@ -27,21 +39,46 @@ impl ChangeProducer { /// Listen to the changes feed and send change events to /// active sockets. /// + /// The poll interval determines how frequently the socket + /// directory is inspected. The directory is searched for files + /// ending in .sock and with valid PIDs as the file stem. + /// + /// If `sysinfo` is supported it is checked to see if the process + /// is running before being included in the list of socket file paths + /// to attempt to notify of change events. + /// /// Returns a handle that can be used to cancel the listener. #[allow(unreachable_code)] - pub fn listen(&self, sockets: Vec) -> ProducerHandle { + pub async fn listen( + &self, + paths: Arc, + poll_interval: Duration, + ) -> Result { + tracing::debug!( + documents_dir = %paths.documents_dir().display(), + poll_interval = ?poll_interval, + "changes::producer::listen", + ); let (cancel_tx, mut cancel_rx) = watch::channel(false); let tx = changes_feed(); let mut rx = tx.subscribe(); + let sockets = find_active_sockets(paths.clone()).await?; let sockets = Mutex::new(sockets); + let mut interval = time::interval(poll_interval); tokio::task::spawn(async move { loop { + let paths = paths.clone(); select! { _ = cancel_rx.changed() => { if *cancel_rx.borrow_and_update() { break; } } + _ = interval.tick() => { + let active = find_active_sockets(paths).await?; + let mut sockets = sockets.lock().await; + *sockets = active; + } event = rx.changed() => { match event { Ok(_) => { @@ -69,6 +106,58 @@ impl ChangeProducer { } Ok::<_, Error>(()) }); - ProducerHandle { cancel_tx } + Ok(ProducerHandle { cancel_tx }) + } +} + +/// Find active socket files for a producer. +async fn find_active_sockets(paths: Arc) -> Result> { + use std::fs::read_dir; + + if sysinfo::IS_SUPPORTED_SYSTEM { + let mut system = SYSTEM_PROCESSES.lock().await; + system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); + } + + let mut sockets = Vec::new(); + let socks = paths.documents_dir().join(crate::SOCKS); + if socks.exists() { + tracing::trace!( + socks_dir = %socks.display(), + "changes::producer::find_active_sockets", + ); + for entry in read_dir(&socks)? { + let entry = entry?; + if entry.path().ends_with(crate::SOCK_EXT) { + if let Some(stem) = entry.path().file_stem() { + if let Ok(pid) = + stem.to_string_lossy().as_ref().parse::() + { + tracing::trace!( + sock_file = %entry.path().display(), + "changes::producer::find_active_sockets::pid_file", + ); + + if sysinfo::IS_SUPPORTED_SYSTEM { + let system = SYSTEM_PROCESSES.lock().await; + let pid = Pid::from_u32(pid); + tracing::trace!( + pid = %pid, + "changes::producer::find_active_sockets::pid_lookup", + ); + if system.processes().contains_key(&pid) { + sockets.push(entry.path().to_owned()); + } + } else { + #[cfg(any(windows, unix))] + sockets.push(entry.path().to_owned()); + } + } + } + } + } + Ok(sockets) + } else { + Ok(sockets) } } From 172f363b8d40205376e01c79c3009b5c70a85c56 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 09:29:38 +0800 Subject: [PATCH 11/61] Initial changes test spec. --- Cargo.lock | 54 -------------- Cargo.toml | 2 - crates/changes/Cargo.toml | 3 +- crates/changes/src/consumer.rs | 37 +++++++--- crates/changes/src/lib.rs | 22 ++---- crates/changes/src/producer.rs | 71 ++++++------------- tests/integration/tests/changes/main.rs | 1 + .../tests/changes/manual_changes.rs | 71 +++++++++++++++++++ 8 files changed, 127 insertions(+), 134 deletions(-) create mode 100644 tests/integration/tests/changes/main.rs create mode 100644 tests/integration/tests/changes/manual_changes.rs diff --git a/Cargo.lock b/Cargo.lock index 1419fef0d8..47c29cd219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1002,16 +1002,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -3110,15 +3100,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4102,26 +4083,6 @@ dependencies = [ "bitflags 2.9.0", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "rcgen" version = "0.13.2" @@ -5190,7 +5151,6 @@ dependencies = [ "rustc_version", "serde_json", "sos-core", - "sysinfo", "thiserror 2.0.12", "tokio", "tokio-util", @@ -6228,20 +6188,6 @@ dependencies = [ "syn 2.0.99", ] -[[package]] -name = "sysinfo" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" -dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows 0.56.0", -] - [[package]] name = "system-deps" version = "6.2.2" diff --git a/Cargo.toml b/Cargo.toml index fa7941a04a..1596cd6f34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,8 +121,6 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } secrecy = { version = "0.10", features = ["serde"] } serde = { version = "1", features = ["derive"] } -sysinfo = "0.33" - tokio = { version = "1", features = ["rt", "macros", "time", "sync"]} tokio-util = { version = "0.7", default-features = false, features = ["io", "compat"] } tokio-stream = "0.1" diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml index eb1cb48787..fdf7c7e5bb 100644 --- a/crates/changes/Cargo.toml +++ b/crates/changes/Cargo.toml @@ -13,7 +13,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] changes-consumer = ["dep:interprocess", "tokio-util"] -changes-producer = ["dep:interprocess", "tokio-util", "dep:sysinfo"] +changes-producer = ["dep:interprocess", "tokio-util"] [dependencies] thiserror.workspace = true @@ -22,7 +22,6 @@ tracing.workspace = true futures.workspace = true tokio.workspace = true serde_json.workspace = true -sysinfo = { workspace = true, optional = true } interprocess = { workspace = true, optional = true, features = ["tokio"] } tokio-util = { workspace = true, optional = true, features = ["codec"] } diff --git a/crates/changes/src/consumer.rs b/crates/changes/src/consumer.rs index 85449b4e57..b5b9eb1c1e 100644 --- a/crates/changes/src/consumer.rs +++ b/crates/changes/src/consumer.rs @@ -2,10 +2,10 @@ use crate::{Error, Result, SocketFile}; use futures::stream::StreamExt; use interprocess::local_socket::{ - tokio::prelude::*, GenericFilePath, ListenerOptions, + tokio::prelude::*, GenericNamespaced, ListenerOptions, }; -use sos_core::events::LocalChangeEvent; -use std::path::PathBuf; +use sos_core::{events::LocalChangeEvent, Paths}; +use std::{path::PathBuf, sync::Arc}; use tokio::{ select, sync::{mpsc, watch}, @@ -42,15 +42,15 @@ impl ChangeConsumer { /// /// Returns a handle that can be used to consume the /// incoming events and stop listening. - pub async fn listen(path: PathBuf) -> Result { + pub fn listen(paths: Arc) -> Result { + let path = socket_file(paths)?; tracing::trace!( socket_file = %path.display(), - "changes::consumer", + "changes::consumer::listen", ); - + let ps_name = std::process::id().to_string(); let file = SocketFile::from(path); - let name = - file.as_ref().as_os_str().to_fs_name::()?; + let name = ps_name.to_ns_name::()?; let opts = ListenerOptions::new().name(name); let listener = match opts.create_tokio() { Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => { @@ -66,8 +66,14 @@ impl ChangeConsumer { let (cancel_tx, mut cancel_rx) = watch::channel(false); let (tx, rx) = mpsc::channel(32); + // Create the marker file so producers know + // which processes to send change events to + std::fs::File::create(file.as_ref())?; + #[allow(unreachable_code)] tokio::task::spawn(async move { + // Keep the RAII file guard alive + let _guard = file; loop { select! { _ = cancel_rx.changed() => { @@ -103,3 +109,18 @@ impl ChangeConsumer { }) } } + +/// Standard path for a consumer socket file. +/// +/// If the parent directory for socket files does not +/// exist it is created. +fn socket_file(paths: std::sync::Arc) -> Result { + let socks = paths.documents_dir().join(crate::SOCKS); + if !socks.exists() { + std::fs::create_dir(&socks)?; + } + let pid = std::process::id(); + let mut path = socks.join(pid.to_string()); + path.set_extension(crate::SOCK_EXT); + Ok(path) +} diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index 10b22fbb93..02cdb4d93a 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -35,24 +35,10 @@ impl AsRef for SocketFile { impl Drop for SocketFile { fn drop(&mut self) { + tracing::debug!( + file = %self.0.display(), + "changes::socket_file::drop", + ); let _ = std::fs::remove_file(&self.0); } } - -/// Standard path for a consumer socket file. -/// -/// If the parent directory for socket files does not -/// exist it is created. -#[cfg(feature = "changes-consumer")] -pub fn socket_file( - paths: std::sync::Arc, -) -> Result { - let socks = paths.documents_dir().join(SOCKS); - if !socks.exists() { - std::fs::create_dir(&socks)?; - } - let pid = std::process::id(); - let mut path = socks.join(pid.to_string()); - path.set_extension(SOCK_EXT); - Ok(path) -} diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs index 5f560c3e5f..465af98153 100644 --- a/crates/changes/src/producer.rs +++ b/crates/changes/src/producer.rs @@ -1,10 +1,9 @@ //! Producer for change notifications on a local socket. use crate::{Error, Result}; use futures::sink::SinkExt; -use interprocess::local_socket::{tokio::prelude::*, GenericFilePath}; +use interprocess::local_socket::{tokio::prelude::*, GenericNamespaced}; use sos_core::{events::changes_feed, Paths}; -use std::{path::PathBuf, sync::Arc, sync::LazyLock, time::Duration}; -use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System}; +use std::{sync::Arc, time::Duration}; use tokio::{ select, sync::{watch, Mutex}, @@ -12,14 +11,6 @@ use tokio::{ }; use tokio_util::codec::LengthDelimitedCodec; -static SYSTEM_PROCESSES: LazyLock> = - LazyLock::new(|| { - Mutex::new(System::new_with_specifics( - RefreshKind::nothing() - .with_processes(ProcessRefreshKind::everything()), - )) - }); - /// Handle to a producer. pub struct ProducerHandle { cancel_tx: watch::Sender, @@ -50,7 +41,6 @@ impl ChangeProducer { /// Returns a handle that can be used to cancel the listener. #[allow(unreachable_code)] pub async fn listen( - &self, paths: Arc, poll_interval: Duration, ) -> Result { @@ -84,8 +74,9 @@ impl ChangeProducer { Ok(_) => { let event = rx.borrow_and_update().clone(); let sockets = sockets.lock().await; - for path in &*sockets { - let name = path.as_os_str().to_fs_name::()?; + for pid in &*sockets { + let ps_name = pid.to_string(); + let name = ps_name.to_ns_name::()?; match LocalSocketStream::connect(name).await { Ok(socket) => { let mut writer = @@ -111,53 +102,33 @@ impl ChangeProducer { } /// Find active socket files for a producer. -async fn find_active_sockets(paths: Arc) -> Result> { +async fn find_active_sockets(paths: Arc) -> Result> { use std::fs::read_dir; - - if sysinfo::IS_SUPPORTED_SYSTEM { - let mut system = SYSTEM_PROCESSES.lock().await; - system.refresh_processes(sysinfo::ProcessesToUpdate::All, true); - } - let mut sockets = Vec::new(); let socks = paths.documents_dir().join(crate::SOCKS); if socks.exists() { - tracing::trace!( + tracing::debug!( socks_dir = %socks.display(), "changes::producer::find_active_sockets", ); for entry in read_dir(&socks)? { let entry = entry?; - if entry.path().ends_with(crate::SOCK_EXT) { - if let Some(stem) = entry.path().file_stem() { - if let Ok(pid) = - stem.to_string_lossy().as_ref().parse::() - { - tracing::trace!( - sock_file = %entry.path().display(), - "changes::producer::find_active_sockets::pid_file", - ); - - if sysinfo::IS_SUPPORTED_SYSTEM { - let system = SYSTEM_PROCESSES.lock().await; - let pid = Pid::from_u32(pid); - tracing::trace!( - pid = %pid, - "changes::producer::find_active_sockets::pid_lookup", - ); - if system.processes().contains_key(&pid) { - sockets.push(entry.path().to_owned()); - } - } else { - #[cfg(any(windows, unix))] - sockets.push(entry.path().to_owned()); - } - } + if let Some(stem) = entry.path().file_stem() { + if let Ok(pid) = + stem.to_string_lossy().as_ref().parse::() + { + tracing::debug!( + sock_file_pid = %pid, + "changes::producer::find_active_sockets", + ); + sockets.push(pid); } } } - Ok(sockets) - } else { - Ok(sockets) } + tracing::debug!( + sockets_len = %sockets.len(), + "changes::producer::find_active_sockets", + ); + Ok(sockets) } diff --git a/tests/integration/tests/changes/main.rs b/tests/integration/tests/changes/main.rs new file mode 100644 index 0000000000..b6db2d2df1 --- /dev/null +++ b/tests/integration/tests/changes/main.rs @@ -0,0 +1 @@ +mod manual_changes; diff --git a/tests/integration/tests/changes/manual_changes.rs b/tests/integration/tests/changes/manual_changes.rs new file mode 100644 index 0000000000..31296e1575 --- /dev/null +++ b/tests/integration/tests/changes/manual_changes.rs @@ -0,0 +1,71 @@ +use std::time::Duration; + +use anyhow::Result; +use sos_changes::{consumer::ChangeConsumer, producer::ChangeProducer}; +use sos_core::{ + events::{changes_feed, LocalChangeEvent}, + AccountId, Paths, +}; +use sos_test_utils::{setup, teardown}; +use tokio::sync::mpsc; + +/// Dispatch changes via the feed to a producer and consumer. +#[tokio::test] +async fn changes_manual_dispatch() -> Result<()> { + const TEST_ID: &str = "changes_manual_dispatch"; + sos_test_utils::init_tracing(); + + let mut dirs = setup(TEST_ID, 1).await?; + let data_dir = dirs.clients.remove(0); + let paths = Paths::new_client(&data_dir); + + let (tx, mut rx) = mpsc::channel(4); + + // Consumer spawns a task + let mut handle = ChangeConsumer::listen(paths.clone())?; + tokio::task::spawn(async move { + let events = handle.changes(); + while let Some(event) = events.recv().await { + tx.send(event).await.unwrap(); + } + Ok::<_, anyhow::Error>(()) + }); + + // Producer spawns a task + let interval = Duration::from_secs(30); + ChangeProducer::listen(paths.clone(), interval).await?; + + // Simulate producing some events + let send = async move { + let feed = changes_feed(); + feed.send_replace(LocalChangeEvent::AccountCreated( + AccountId::random(), + )); + + // Need to delay between triggering change events + // like in the real world as the changes feed uses + // a watch channel not a broadcast channel + tokio::time::sleep(Duration::from_millis(50)).await; + + feed.send_replace(LocalChangeEvent::AccountDeleted( + AccountId::random(), + )); + }; + + // Listen for the consumer events + let recv = async move { + let mut events = Vec::new(); + while let Some(event) = rx.recv().await { + events.push(event); + if events.len() == 2 { + break; + } + } + }; + + futures::future::join(send, recv).await; + + teardown(TEST_ID).await; + + Ok(()) +} From 4bdcded88aa3657749c8bde957c3323e24bbca37 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 10:13:38 +0800 Subject: [PATCH 12/61] Add script_runner. --- Makefile.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile.toml b/Makefile.toml index 68f3ad5d7b..f97224bfb0 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -110,6 +110,7 @@ mkdir -p target/demo ''' [tasks.test-command-line] +script_runner = "@shell" script = ''' export SOS_TEST=1 export PATH="../../target/debug:$PATH" @@ -136,17 +137,20 @@ command = "cargo" args = ["build", "-p", "sos-integration-tests"] [tasks.clean-tests] +script_runner = "@shell" script = ''' rm -rf target/integration-test ''' [tasks.test] +script_runner = "@shell" script = ''' cargo nextest run -p sos-integration-tests -p sos-unit-tests ''' dependencies = ["clean-tests", "build-test"] [tasks.test-all] +script_runner = "@shell" script = ''' cargo nextest run \ -p sos-integration-tests -p sos-unit-tests @@ -160,6 +164,7 @@ SOS_TEST_CLIENT_DB=1 SOS_TEST_SERVER_DB=1 cargo nextest run \ dependencies = ["clean-tests", "build-test"] [tasks.ci] +script_runner = "@shell" script = ''' cargo nextest run --profile ci \ -p sos-integration-tests -p sos-unit-tests @@ -173,6 +178,7 @@ SOS_TEST_CLIENT_DB=1 SOS_TEST_SERVER_DB=1 cargo nextest run --profile ci \ dependencies = ["clean-tests", "build-test"] [tasks.cover] +script_runner = "@shell" script = ''' cargo llvm-cov clean --workspace cargo llvm-cov nextest \ From 11b1a0112d7081349d08c1248a8759e44eb1d71b Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 10:26:46 +0800 Subject: [PATCH 13/61] Update test fixtures. --- ...0faea9bbc182e3f4fdb3eea7636b5bb31ea9ac.zip | Bin 47454 -> 42313 bytes .../fixtures/backups/v3/multiple-accounts.zip | Bin 50596 -> 45453 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/fixtures/backups/v3/0xba0faea9bbc182e3f4fdb3eea7636b5bb31ea9ac.zip b/tests/fixtures/backups/v3/0xba0faea9bbc182e3f4fdb3eea7636b5bb31ea9ac.zip index dad69c1599f991027b2ce006a758ab3e327e513d..d76afdc78b2be9d084b258afe7cb02527caaf00f 100644 GIT binary patch delta 20741 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 _slJE*xEV%kCynLutD6C0Uc+ohCn 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 z zaIA_oN4{N3_`JInw%f|D>+=Bs-xKU`?wq_)s^f@&Mqh^y^<8;Nu?Mhj=v3$U_$^eg zO}8;SCbzh$d1@IWgGs>H2!AQt$nu?cOf&BI;&APk=H8@$_hng3Z_QGAo^fkULkEtZxFG&62FFCD2-pyCJZuQSWQg{l;U!B1As#Yi$ z8aW`Iuj>R~)^9RXZhWlPczddXMm#UEKvpieE4G;yVHV(v@G;=`n; zF*oPGeyq+Uzui>J>6z&CjYWX&DMnKZ&bk?87pe>T(;u9*6zfKYcv(;nyb3cb&Q0ubb!}28T#H>_u*ijri#1)SrtR=Si*pzh-#qsNBz3#qM!L6l=G!Pt%5h-x zZ02LX>WFVAp~~*5iGri%$`_?IdWv7vrl6qctCsi**&8Z5h{@SrlQ+=h`#<87mlX~@9QQSOr8p|c=ZwHja;)T@w zDdJXayxm}@KaoMA8KVTtotCY86n$V>S@Z5%;F*JoP;PGR! z|0=3PvV+R>Vp)E8)Do#cEtkVhEKXyK^@-S1p+99?-X4Vf=+b-GWC zu84!bR*Hc?q_u?!;D%VCh$0=Y_1>fa@T;C|nD6uLOg?<`ENHOwzTq)mI!+YB{q;Ut zOZf|{vFsrgw6G2VaCLSxK?H*)NLJ=PMoC&9=#n>q9WB43@cq0Rz|GAPC;8@ao6y1> z7F9j)gO}?e>c?Qe6!=-e%yjVlNZv&LL!|li~OGkMLNSsC?B! z9;A(CjX-F%F}?cw*`eKo-MgUFbs{z3vGiE_&pdqJhd9}gl;Wk6^+G=#OfGSff3S|$CXyHzVhYSOt;|8LqQF)^F3()7-p9(WbD?j!p(YrqNQmldtH z9Hbo57NYE^d*;(lpMj%3SdTJ`h}(I@otowzfiN>^A54|tH3bXSWa@p0=xE@PE?uBY zrEB1^LB{nmjb`ZA0`8l>FXq4+b{8qX&^KDu&`$3C?j1{~I;i6R-@{^P?cA@KHlqaZdH>~)iTwc0-EE`W zKCwaV8dp8BHWg=PkAIXmO)KwJbdvm4 z7aof4GZgF_#fECwSrzl(fj&Ps>ZB;4xs4K@vHDCqCnr0m3SU9>*J71lac5xQ^ylcB zk)0k#$DlHmtNuw)hIEevsQPXa=!I)qOuki0e~fOGuTp--tbv)$iJAibWqyd6iYh#k zFv+MZ{BJsDufpZ09r~YodQ}t*9e8&piph53ib_1Z^Ye|LwGfF9uNew$&U?LLX|XP{34aWAI!McS-*qc)2&p#*7+>QPnYM#W9n@MH@1mX z5Q3Fv%aWS~l<-Q6Dq=u=T8;E8@zrE>$Qq;&l((K?lL~KM>l*!=giK^t2e^6pN>BTR z>X2PqF%u8OxRKbfoKr4Q857o#8DXdVS$%fEw57My$&hc^t5h5J(j_*Pv%F;^49;gY=Ae6nOBZuB(9l~h{|jTd6-r)`H~;C%{4-1CQyLH$4p&R~0a zq2BYjhoRKMLOuko$MEn(z-Fc8a6it`qwdihsXm=cj>AX_FeI|>AT>W2DgA!cz%%qk z#WWy724b`gD-m#e%h*_8V$8^>j=zVy&R}9;GG4{(vX~>v_RXgFIm}jz&uz3%616uL zk*mma!cnaGAZ*aza`a6#iAYW1Okp$A%f}6VaTh+&kxY>75h+C!E6D7g5KdgH1rq(< zzdD|0F2l_IV9(xwXIJWVvhB<>m*HHwu9$*HPO4QScMUktqZdv4173skpFDYk2?LUJB}{!|5SFCL2*BQ zwhtPd48h$!c!1y#2n4qQ0t`-Ym%)Dlf?IHh5P}a9+-7hK?(QVGTd=VF@3VDx?~CVd zZC7iwy1L(Vb)EA$y-m2G+QRoE9GIB>bsw;gaeh!%8FYP2exu8#iH*46=2UN7 z5E*pJpotYco)nA-x%uF)bJ;>eLaidE_AVP((?9=;U2!qJkz%0~hR<*H3@HzHVUfL{{8fcy(^OGSC+sZ8wIfny9>1<9nX7;(XHakiV9FD7&+_MdNY2+YoNWf{ zjCOCE+P*Y}k~cr_rI6`}VA*8hY>XHV$doExJGIc9cQi7|>+ki16tW#DcKP3;BrF3+ zryeqlG~My##^4q%8?vcFb#em%8oc^p<_y~B4jlDr4t-akFjhm3@-K(vmDGSJ4|Im=ja}W|B zsB6|Ry{-)ZMeya)N@ZbdRb+T>$NLZffWglCh6mA(mOeov+>ll1>?t~Rfm$st0hUeZo@-Dy`O!!SD7VN(H9oG#mrZK*hp`}RCZ;LLk^EISSrPhK_zsh z4UVl>4aIeG~xx`+h7NCpOjO5(v&YvKiN<9DUJHfp^3h^<|#;zQG70{e#c< zU`PAc*YhP5QKQp$%TW>~xA8Umu>umjf+4?*+riVI_XZPLpLl%7!51(bwORKJ`6t6^l-&G-PfKBn(2Q|z~@ z4a|I=i^5R9U1LUE&ZHfGzRVG`OKLH_vpg?XW}~A0a7k)~&pZtGYCa7KTG|KfPgO4_ zWA`9}{>GZ-a^CY$BhUxnc_()MQ`a=!*-3^&WDhd}ahFER7cl=`xaTDj*XxV~6+qqoZ;i1NRmfd|KH zX?{p8SSnStvfdJ>K1=8(Ux`t(fpnnU;VpF+{>8_f=!FxR(*s}Q10Rn)OaJGEW)JF) zOy2TahoAL2My{Eg0c!zGCx_A9e2)Tgz_VcXCH3S=YJ$<3r66D*B7ea)YT8xB4frW; z&(x3BNt|(CkwMZvq^`}(SG}6sTzkGxMKWVxqz|f1u?_?dUQmb(GH6^z+Z&x_r=rUr zGF3>3arPZZ6xD7|bET7Tm{%YMI^R>WrnL<;3Twz8?(K$QUp|p!3U4{vM~iW7f7Yf@ zr4x{>m5DG0iUI$Uf&70|AjoDM+&+)Rt#4;~nGf$Owfk?I_{jPrEyM6`naQ{_47`tI zt4AmXUQ_!7<77Ye}ezibh`dH5YQ4oXV9ik~hp1Mn?ePucD&Nt{s#GgsD zhbHxhj@sJ6TsyT{MJnHi4OqfT?VCYPE=oYeAQQgBqZ^35`~HZ!1kVYMPnNEagcPC6 zj<>pwD*Btl)m{wO$l)N`$GL`_eaGUJX~1RDp80rlKS_ZMQ8iOovtP2FjqOxAM`&<5 zQ2|FmPw0G~sVNpyah(;BoYmOyNH-&)$4|Q_9I-&aRh69FnT`R%@T} zY&1ZSPa^8NMe zy!{R>M&zqeKVQmP1#Zh*jmP+;mH{*4HO=)Yv<*JkiQ1(v?`yXmK&@ImZ`A9NgZCB7 zSKc&uks?Bc=JIoW-n7&wYeXqcjBVmwvB#N*P_ACIL2IrkFnk`0d#kQ1dcwHD5Z~kF zl>m>jYx`})Y!Mr8xtyX~X*IvoD@+YvBNk4!EV#s8e>Q#a_TvNeMsH_5R*|a5>D(7}?8C)~EJy`LHdqQ#5y9RW5agZ;RO^mJ( zhcvA`JB3v|YQv-|zBxPpz8+qRYOE`A-( z^DiRPi$!uYGDB~^O)MRll%?TjCK5{)1=JI66c<1-(R^|D=O%Q21Iw-cr-+-J4i1~o zT)Suuzsm-b3;8^MXgg?z=l-T|i0blt?g`N%_O0c5ANF%g&{Ia5i3G11jJA(nC`UNp z7&Wx-`-9iL%6ZnY{}N4&*r*|9rEu;}nsaBTZ-^rZSE(TG)%Xanf1Y{Ox&OiZER<6D zc)Y)CLsE5C!my~78E}$l6`d|84V;Ulz|j%Tsw)9ytkb`#wk{aab+H!sV4Amz=lQnr z(Q!8)J4d8EhutiEwM|v-6WOuE3n4G#cN!Q8Z}w`K<8EPytHI`40;(gzB}uC3HrqTbhX?!pEMpVUi-% z>HB6BE9Rp}=-i18|87-$;bQjc1SggpZT|3)?oLU@b(^)fqenw^#>?wzzlD(0%T(Q~ z%4|>08RV0+iUf+d+Eov{0Snx<#ci?Nkd<@|GRVaJt`VO!V9UUUysRw%&D=}qT<&Rk z*p8HqcWT|p2n=yp)Ga%N{9xIYYf@cGule^>7Rt0OY|{c=zLD7p3xStl+uUheSK?iP zWl)qsk>%&`#Tw9x0gYy3@@EQFgLCIM2ZWk(Ips##k)(dYUjV%CVk|mP-$Gd7eGLbl z%c%r+Y=Eb=G-RPoHJ^ok%iHhQ;fxKr2gy(c*|<=WC=H}cW+*kPGawq)>@$Jz_svq# z2nk3Srj)gCQ#0xYy8y!~4Fbq+G_-j|oVV;-h>H(){vT%B3f)M`pXq{%-SVDf^iMY~ zXOxm{EOAf`gf@sdWYgnq-ac-;5RtWw`<^U&kz_kQN}nR55u~uAf^Ik1Obfsruf zofZA5mzYcSGSXvJHd1>>e)&-kn*3rT?hgS%> zZIyt|L@W;$Knjhk@ux-eY)oFrUtU|sa`B&~71d#q-tJIuea9C2uia;`QI)DWxWuX` zyme3Rw4wukm&l|&`*TS@uQ8Q@l?!xxYgm$?u2WQVez_6a*c%C5&y-Bb4L4{7=!O=A zwoi~jC7CK*v=qJkI1eveV!ED75{W51E@G(ISArI zrL?T|P>H*`>B8HksHJ-*<58KZ#+ZY7=Z{dGtvUfStDvU->&~QYT{FGEAwUFELb@`0 zVkF8&u`Cvm^^D7t-Ap#0VTTo+>SVZz6+M|}UI)!Nv-8m+GbrN3dfig?Bl8&t9Zsmz zy6GoR93}ZpOm}qTFDO`Xibb@n+T`Scz2n8C1i1LE>nw{6&yGI)fs12b$*k^YfZF(^ z&$+owA~EC&UKTTo5Om0{lK~iG%^2&g`GEFVY3Dkh`Ri-#P$Tp%Hm>EQ>2A zfUR9HM^c#^#aCoZi=9&q&dAZ@O_zKH3v#3|REhN$?vzNu?GqUPxTQgXcsOy=pmbr8 zkoURvm0gmSxhPL+B549TO?k$A6O7a6T8cf@vJhF+khCD%vD7cJ{f`9c8t=>lk|2dS zC*7*75iQaELRj+5&j2S@oWzJ{Gect-oCI%W)JqfygTnI?SxE^)!{2RCa`255*c=`d zLpCP>pVPlubpcHzDn(Ma#h7u=k1AS?k!yUmv|BcRe)(C=pp6AZ8=N!^htR6WFZ0Ts zW&NcqSqv!b33|@Gx&S8eH~LyVpHtO8v@PAY78$wrySJ2MF##yQOB`N0R!Hq;PR&U> z=QPx*+fq_EBT0v7UXrACM91keav%_~IDU7Qy6j)BJj&r8xihlOr#Q2wjWe#?+tq4C z4`UpQZ8+0xzTuXi5=|O>q~eDbOg&LYL&9W*4-f9V`LK#cdrWf055>iyr?E4QN>lAg zB8%x$F)8?ADZolHL{r_&tjWNTO`mL5(|d9z+qJ~=;i8=T)y8MCMf4Q$BdOdINz!Hy zjnyHREoH4PC-Tv;^)eUa*vei^R1=KA?y+=IX5=1PB=B>2DTZQFk+r4?R#E|?n2#Ik z-2?Rw_YU>;!sO9xLjnv5m7OQ32&}Hee%AlACIju2EVNa!B6!lK^ywHP1WKLp&h7-% z{JYGtKs!1f_<*imQUd3rn^FQf=CufG`UvDd4+8Wrtzq$>4#$gyQBxx1=`k5` zQbHM2>hn>ShIu<2@qFPwI(Y~5`rDk<{ji^h(M|qiWPVXVpXP3&NgUN@0{X;_FC1b` z06byFOf4b zNK1JzNWZ?#2Sr>AT>0EDUwQZMg~#m;i{(@<@Z8@q;lxMh(UK{%uMHBov<6c0U`ls0 z{J>zxLK`a3)cBh;(67B~cHF?syY48Eb1F|`Ymq#1Ng zq+?K0V@Ai4emV~~RjtR|Iz}LM%h@T%^+)R=d}QPQ?1SWb&D8Z6p&3z%axM=J^ytNj zPuA#tv>^eMdNj_27tD*Gv(Y(RKJ(i&L0rD#`?`U)A}P~;9CmcQrUFe6DkX@t7T^!m zSET0)%!vAzW195c1oI;G+4VB{2F7DHFH-_Q6WASp%7~5^)58D~HbFd3)BHLpgL5l8E4X1znYYfUnQL&=+@b(kR zG{W{t1$WMjNNE&ol|%>18w0?JeEs&3zEdw;yV64RAiNvd1#AjiLkZ-0k_`dG&J-Ap zxLY(oa2ZoWDYZ(+6;T4j@sWn1>hIg1?TdN|1VJBq|eAuU)j1BwL zt2N&Lks~C%z-5I-vy9?vO{iNv#IiD68?V+=Ez}>&0$(JpUG-~dA~Dd!lv%_lg9;10 z7uEghmbhe9RIHX;cI%jHePf$C?TJpUb<~aDbeyXrF8)Q@yWs(S@ObyNP|$O;qH5$& z$cw|;yJkrHAv_}2*W?n%0Q^2VWAK-&CrcPA-51ghy!<)racEp;unbfXiauqnoSSl_ z=Sh2@tU`*bnaXm`OAqWEm7e!gCm~qeums?Izx&B*#G*QaSY2lXJU$QF1a4>8#4gCy z32jxpw>J0_A!e*4_?&tY3x1xIcVjx4EqX$Rh4OK7Y-pyS$?XZDRH;3wR@8Rvu7>5gYysQV z`|8AaH*+>|xW8jB@6(5<)DZklv~9{!DNJKS7&5H8O0XfisQ>pWCP~hA)Q$_1>|j77 z%g6XRX<8c&&o=?rv7H4Rj3?~xseMbCyS-U$ktiE_v|q=39c9&WWITWF-Y1&kzLtkU zAJZs0?YP&$tps>NGts zxBst{`g4I9u-@@xFf>lz+H-WZ4L17CPEmJOrJN%nO2Y9=iHBJ)uCUUmA<@D6aer8) zo@=R%ThbP_kMD#fkznU&dR#V@eL7*tKsdy2-ves&(7g5Na-dl&K{%|OX=&sl$L>c93-6W zOWP3n3)pKD)-o^E?XF-nj#+4bD=(NnG%!$*Y$btH9U!Y0RZ!NHp$)btks}`|$?XuS zZkiG7Ek!IYG`BXt-GJ;M?&wZF)e)}tpZ{^eUXUB)?mCGhqiZ7Uv-M$ z=j*Wi%<{SgALg<91bF#iK|?NqO~N?zYUz_b&=}PAqX$%Z#3O0xY+@%JA~RQeE{Ky| zza4WoqOH56msv#W&LvuQa-V*LJ-C`gCh2XJRPLP*?9NgIWODVTKn8uc4ZZk^j&1Fn z2dnR!EHfta>N$o)&KxCq?)W@b|4yfL?h~h2+w4aU4P3116H0LNLd%Kov$8zP3L-85 z{oMWIB}?~ugwZ4erQIK?_j2|N5~+D5d^?sN2(y(At*h)~7Z+J0kzr~v?{BalP+oBH z`=2L)-73TZeX%2q+qRJ9RL!dcUwjYBXp1fFsb+o*_I*MzQ81pP4}sQY@$VG74%H+x z<>>;o%oCBH2#1Gvjv5iMqWTgS*00_I+%{SJ-&XzhR?tNoJd01%>#7kT7`<*o2m8#v z;$awtyaCq$&GNp!Jh|{Jk!dlJiG;ijc@MS$5^r64So7 zR%Zij6o{{|qqMv`Np{PfWF{lP&yP4W+0(K)ol52{IU5%loW9noR3z+5Q?jgEQKRl) zMzfOptaz$jX>i6mq|@ZDlFux<(Y(IXvgR)K_m}98J6f43A3#rGS7>x%d@EsB!4M?3f1w)vXkbroM zJ|ot0?djTkkbv>iXY=n}N|jc^ zNN>?&UZ|(t=Hv=VoRSa_f`Jd)$=F5k(3y@Z-iqU&#o&5~!tS$WdbobqHD`%bWZ_O* zYejkPjLnYKszls#?$BpjSk{;WwO#A1uGjR4cKKk;;8Zk)4Xt5GbpfITUuARnn6h1OY3?e;`^D#>^L9$C5<5$_T~ymx{FX1w zCqrcslcA_4e>$IVj31F51(2LCU$mQvqL9v;U}AjT?{TpL{rF^qToy@se|EbO(jXvn z+=k^ZD#>XtzYJU@6fIkUM)5FeRAwM>itDm|Cm2Yp=UcO@>@1 zEWI)IG@?$oqBFcIgoh#+f8VO?;5Fz&PxfmYFEUnRyg1~^v7O@% z$&oeqHLv2{?M+MN-a-CULCP=oPqLoR^2UJ5rp%LUO%G+D;qJKb#7q$WRukAWISj~> zT+)A{>|;sK792vxCT=;fg{Wz42Wzgte%^8Q+Prgla=SS(?V>2ZV*l2t9OqxdB@Y*5W-P$wYLkn^vM zVavqe)P(`etuZX%DvNLb(#kl5yK&As8D6Gj)L8=_s|h1umMu{Xh9sxo-joJ+58id> zyzr;(FlyNzeKU%Cb?XyOQQ4l+w{74(8Y96PXgBwlwW-MA2-0Bg^?Epq!tl{pTLFc> zPSq8cQ;S}1NgIYQjQT_#XaiJ_C{tbK;$H(urb74Ur^=5x>uO!_a|8 zJ+p%o&&$~*<)+LAPe07WPdn)d5_qN<$K45tDAvWE)Q_*t6Y&BS`L_P66ZSvDUEAj3 zMq!LtOxQ6rIs(;olcQGp9Hqcxj#O0H{UP}CFYn7W#uxHKyF`^bvXBoVUJY8ML^q3*N(X3!MhOetzoDID^n}~I~RuE=JpIw%~w=tV_ zukTa3=#xAhK0Fd{fgro$y;kad3}I;L?P%E?DCnYLb~EC3Bf5@rGYOfJN9=I*=%%+5 zy5{McyA_{KfCT{vpC*;usH%mrl;lN)5kYB@Esy% z5=CG)hL(RrvUEPw#W!1b>*BJW#kOP`J3O-bwXyZ*=exyqr4A*Lh>%E9m-*G5!7S%? zkFW{8$ny{7NEVHb%-^?O)m=6Xp0l`V3^wWi8Eh^=E@c1;=|-*tbw9;n#Teu?96xmJ zDF3cX4mXWx2_;tz@FtQ6apYAN|Ck|2^U}ZhF598rQ{thuPS(c0E&eL8si4WQ;2vx^ zsa*O~!F+Q_7Za7xdpAfqX1#DU?v7~BPn6nCwXu3+BN=8_U*`O&9(C{(#0Ov)gm3)HYtKiuMRE`uH%|EuhBx zMj3c?-E}coVP3U-fUFObC!4{wv8EWRU71meP{I9kyO^k~jV@F*jD$0=AEC^|oVV-3 zNa{*R$dV}kKBD9)r6dt2pZ=e%CGzG|AS;?Ds^$;;pOXAL1@cPtQ^}ub`#pvB|L*x| YiN#X~J@x-;uJwtu1q|qa>;IYh52sDJ761SM diff --git a/tests/fixtures/backups/v3/multiple-accounts.zip b/tests/fixtures/backups/v3/multiple-accounts.zip index 9aba6613303830950480c8e278745fd90ba1e3ad..d4964a10a21c085d4c45cf71696bd37a8372f060 100644 GIT binary patch delta 23882 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 Wx^3Y@UjLC;A`eB%Xf& delta 29082 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 From 8a7ac4b65c9cb049340059bb495c42a94590d87e Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 10:31:56 +0800 Subject: [PATCH 14/61] Update extension helper service to proxy change events. --- Cargo.lock | 1 + crates/changes/src/consumer.rs | 4 ++-- crates/extension_service/Cargo.toml | 1 + crates/extension_service/src/lib.rs | 16 +++++++++++++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47c29cd219..703938959e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5326,6 +5326,7 @@ dependencies = [ "anyhow", "sos-account", "sos-backend", + "sos-changes", "sos-core", "sos-ipc", "sos-net", diff --git a/crates/changes/src/consumer.rs b/crates/changes/src/consumer.rs index b5b9eb1c1e..4ed7a6edf9 100644 --- a/crates/changes/src/consumer.rs +++ b/crates/changes/src/consumer.rs @@ -55,8 +55,8 @@ impl ChangeConsumer { let listener = match opts.create_tokio() { Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => { tracing::error!( - "Error: could not start server because the socket file is occupied. Please check if {} is in use by another process and try again.", - file.as_ref().display(), + socket_file = %file.as_ref().display(), + "changes::consumer::listen::addr_in_use", ); return Err(e.into()); } diff --git a/crates/extension_service/Cargo.toml b/crates/extension_service/Cargo.toml index bcfa18ca3c..c46073a58a 100644 --- a/crates/extension_service/Cargo.toml +++ b/crates/extension_service/Cargo.toml @@ -13,6 +13,7 @@ sos-backend.workspace = true sos-core.workspace = true sos-ipc = { workspace = true, features = ["extension-helper-server", "search", "clipboard"] } sos-net = { workspace = true, features = ["clipboard", "search"] } +sos-changes = { workspace = true, features = ["changes-consumer"] } tokio.workspace = true anyhow.workspace = true xclipboard.workspace = true diff --git a/crates/extension_service/src/lib.rs b/crates/extension_service/src/lib.rs index 81302020b5..0c75256183 100644 --- a/crates/extension_service/src/lib.rs +++ b/crates/extension_service/src/lib.rs @@ -1,6 +1,7 @@ use sos_account::AccountSwitcherOptions; use sos_backend::{BackendTarget, InferOptions}; -use sos_core::Paths; +use sos_changes::consumer::ChangeConsumer; +use sos_core::{events::changes_feed, Paths}; use sos_ipc::{ extension_helper::server::{ ExtensionHelperOptions, ExtensionHelperServer, @@ -30,6 +31,19 @@ pub async fn run() -> anyhow::Result<()> { let extension_id = args.pop().unwrap_or_else(String::new).to_string(); + // Spawn a task to listen for incoming changes and + // proxy them to the standard changes feed. + tokio::task::spawn(async { + let paths = Paths::new_client(Paths::data_dir()?); + let mut changes_handle = ChangeConsumer::listen(paths)?; + let changes_feed = changes_feed(); + let incoming_changes = changes_handle.changes(); + while let Some(event) = incoming_changes.recv().await { + changes_feed.send_replace(event); + } + Ok::<_, anyhow::Error>(()) + }); + let mut accounts = NetworkAccountSwitcher::new_with_options(AccountSwitcherOptions { clipboard: Some(Clipboard::new_timeout(90)?), From 04196318a09028ad90cb33131cbc590bcde110c8 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 10:50:04 +0800 Subject: [PATCH 15/61] Call close on the client in test spec. --- tests/unit/src/tests/folder.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/src/tests/folder.rs b/tests/unit/src/tests/folder.rs index 9810e79a62..df2c5cce4b 100644 --- a/tests/unit/src/tests/folder.rs +++ b/tests/unit/src/tests/folder.rs @@ -45,12 +45,13 @@ async fn db_folder_lifecycle() -> Result<()> { let buffer = encode(&vault).await?; vfs::write(temp.path(), &buffer).await?; let paths = Paths::new_client(dir.path()); - let target = BackendTarget::Database(paths, client); + let target = BackendTarget::Database(paths, client.clone()); let mut folder = Folder::new(target, &account_id, vault.id()).await?; let key: AccessKey = password.into(); assert_folder(&mut folder, key).await?; + client.close().await?; temp.close()?; dir.close()?; Ok(()) From d34ed491f64a3d593cb11773e503b8a80b8afdf0 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 12:27:47 +0800 Subject: [PATCH 16/61] Add tracing warning when fail to connect in producer. --- crates/changes/src/producer.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs index 465af98153..3fcb1d5a61 100644 --- a/crates/changes/src/producer.rs +++ b/crates/changes/src/producer.rs @@ -86,7 +86,12 @@ impl ChangeProducer { let message = serde_json::to_vec(&event)?; writer.send(message.into()).await?; } - Err(_) => {} + Err(e) => { + tracing::warn!( + pid = %pid, + error = %e, + "changes::producer::connect_error"); + } } } } From 8c75691ac5478b9d2b06ee5cc002e701214f6038 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 12:57:51 +0800 Subject: [PATCH 17/61] Tidying calls to scaffold. --- crates/account/src/account_switcher.rs | 2 -- crates/account/src/builder.rs | 8 +------- tests/integration/tests/audit_trail/client.rs | 3 ++- tests/unit/src/tests/account_builder.rs | 1 + tests/unit/src/tests/client_storage.rs | 2 ++ tests/utils/src/lib.rs | 1 + 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/account/src/account_switcher.rs b/crates/account/src/account_switcher.rs index a54dfead9d..e92e9b0f13 100644 --- a/crates/account/src/account_switcher.rs +++ b/crates/account/src/account_switcher.rs @@ -109,8 +109,6 @@ where ) -> Pin>>>, { - Paths::scaffold(data_dir).await?; - let paths = if let Some(data_dir) = data_dir { Paths::new_client(data_dir) } else { diff --git a/crates/account/src/builder.rs b/crates/account/src/builder.rs index ded592c1bf..ca8cff4be0 100644 --- a/crates/account/src/builder.rs +++ b/crates/account/src/builder.rs @@ -9,7 +9,7 @@ use sos_core::{ DEFAULT_CONTACTS_VAULT_NAME, }, crypto::AccessKey, - AccountId, Paths, SecretId, VaultFlags, VaultId, + AccountId, SecretId, VaultFlags, VaultId, }; use sos_login::{DelegatedAccess, FolderKeys, Identity, IdentityFolder}; use sos_vault::{ @@ -300,12 +300,6 @@ impl AccountBuilder { /// Create a new account and write the identity /// folder to backend storage. pub async fn finish(mut self) -> Result { - // TODO: remove this and always scaffold in test specs - #[cfg(debug_assertions)] - if let BackendTarget::FileSystem(paths) = &self.target { - Paths::scaffold(paths.documents_dir()).await?; - } - // Prepare the identity folder let identity_folder = IdentityFolder::new( self.target.clone(), diff --git a/tests/integration/tests/audit_trail/client.rs b/tests/integration/tests/audit_trail/client.rs index 1ab34bced4..61e0f0376f 100644 --- a/tests/integration/tests/audit_trail/client.rs +++ b/tests/integration/tests/audit_trail/client.rs @@ -14,7 +14,7 @@ use sos_vfs as vfs; use std::{path::PathBuf, sync::Arc}; #[tokio::test] -async fn audit_trail_client() -> Result<()> { +async fn audit_trail_client_fs() -> Result<()> { const TEST_ID: &str = "audit_trail_client"; // crate::test_utils::init_tracing(); // @@ -24,6 +24,7 @@ async fn audit_trail_client() -> Result<()> { // Configure the audit provider let paths = Paths::new_client(&data_dir); + Paths::scaffold(paths.documents_dir()).await?; paths.ensure().await?; let provider = sos_backend::audit::new_fs_provider(paths.audit_file().to_owned()); diff --git a/tests/unit/src/tests/account_builder.rs b/tests/unit/src/tests/account_builder.rs index 9cebebccc7..ab19f7ab44 100644 --- a/tests/unit/src/tests/account_builder.rs +++ b/tests/unit/src/tests/account_builder.rs @@ -15,6 +15,7 @@ async fn account_builder_fs() -> Result<()> { let dirs = setup(TEST_ID, 1).await?; let paths = Paths::new_client(&dirs.test_dir); + Paths::scaffold(paths.documents_dir()).await?; let account_name = "fs-account".to_owned(); let password = memorable(); diff --git a/tests/unit/src/tests/client_storage.rs b/tests/unit/src/tests/client_storage.rs index d2f3fca210..42dcf41982 100644 --- a/tests/unit/src/tests/client_storage.rs +++ b/tests/unit/src/tests/client_storage.rs @@ -62,6 +62,8 @@ async fn fs_client_storage() -> Result<()> { ) .await?; + temp.close()?; + Ok(()) } diff --git a/tests/utils/src/lib.rs b/tests/utils/src/lib.rs index 73d3e007c9..74c1474998 100644 --- a/tests/utils/src/lib.rs +++ b/tests/utils/src/lib.rs @@ -61,6 +61,7 @@ pub async fn make_client_backend( sos_database::migrations::migrate_client(&mut client).await?; BackendTarget::Database(paths.clone(), client) } else { + Paths::scaffold(paths.documents_dir()).await?; BackendTarget::FileSystem(paths.clone()) }) } From f776d9de295d60e25947b8bde26c1d43ff6b4e0e Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 13:32:58 +0800 Subject: [PATCH 18/61] Avoid tempdir_in() in test spec. Yet another Windows quirk. --- tests/unit/src/tests/client_storage.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/src/tests/client_storage.rs b/tests/unit/src/tests/client_storage.rs index 42dcf41982..a6f2ae6ff5 100644 --- a/tests/unit/src/tests/client_storage.rs +++ b/tests/unit/src/tests/client_storage.rs @@ -26,7 +26,7 @@ use sos_sync::{CreateSet, StorageEventLogs, SyncStorage}; use sos_test_utils::{mock, setup, teardown}; use sos_vault::secret::SecretRow; use std::collections::HashMap; -use tempfile::tempdir_in; +use tempfile::tempdir; const ACCOUNT_NAME: &str = "client_storage"; const MAIN_NAME: &str = "main"; @@ -41,7 +41,9 @@ const MOCK_VALUE_UPDATED: &str = "mock-value-updated"; #[tokio::test] async fn fs_client_storage() -> Result<()> { - let temp = tempdir_in("target")?; + // Windows doesn't like using tempdir_in("target") here + // but it works in other tests! + let temp = tempdir()?; Paths::scaffold(&temp.path().to_owned()).await?; let account_id = AccountId::random(); From e860e1edef92a826d16e91d6aa231372a17dd3ec Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 13:44:40 +0800 Subject: [PATCH 19/61] Add migration for global_config table. --- .../sql_migrations/V2__global_config.sql | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 crates/database/sql_migrations/V2__global_config.sql diff --git a/crates/database/sql_migrations/V2__global_config.sql b/crates/database/sql_migrations/V2__global_config.sql new file mode 100644 index 0000000000..95018c5091 --- /dev/null +++ b/crates/database/sql_migrations/V2__global_config.sql @@ -0,0 +1,16 @@ +-- Global config. +-- +-- Application wide configuration settings such as the binary encoding +-- version. We also include an encoding version for vaults but we should +-- always be using the same encoding across the app so it's better to +-- store it here. +CREATE TABLE IF NOT EXISTS global_config +( + config_id INTEGER PRIMARY KEY NOT NULL, + created_at DATETIME NOT NULL, + modified_at DATETIME NOT NULL, + -- Binary encoding version + -- Version 1 is the binary stream implementation + -- Version 2 is protobuf encoding + binary_encoding INTEGER DEFAULT 1 +); From 07eede4c1f6595293e6e663a0d4f21a23508858f Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 13:48:06 +0800 Subject: [PATCH 20/61] Make column NOT NULL. --- crates/database/sql_migrations/V2__global_config.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/database/sql_migrations/V2__global_config.sql b/crates/database/sql_migrations/V2__global_config.sql index 95018c5091..bd5880ae42 100644 --- a/crates/database/sql_migrations/V2__global_config.sql +++ b/crates/database/sql_migrations/V2__global_config.sql @@ -12,5 +12,5 @@ CREATE TABLE IF NOT EXISTS global_config -- Binary encoding version -- Version 1 is the binary stream implementation -- Version 2 is protobuf encoding - binary_encoding INTEGER DEFAULT 1 + binary_encoding INTEGER NOT NULL DEFAULT 1 ); From 911b009c7df2d66593e54d43e4525671ead73c87 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 14:51:17 +0800 Subject: [PATCH 21/61] Tidy call to ensure(). --- crates/account/src/account_switcher.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/account/src/account_switcher.rs b/crates/account/src/account_switcher.rs index e92e9b0f13..d7794c3670 100644 --- a/crates/account/src/account_switcher.rs +++ b/crates/account/src/account_switcher.rs @@ -123,8 +123,6 @@ where let account = builder(identity).await.unwrap(); let paths = account.paths(); - // tracing::info!(paths = ?paths); - paths.ensure().await?; self.add_account(account); } Ok(()) From 05d2ee4ea802ddb46604317e2276cc89c10c627c Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 15:51:07 +0800 Subject: [PATCH 22/61] Fix bug with db audit provider stream. --- crates/database/src/audit_provider.rs | 60 ++++++++------ tests/integration/tests/audit_trail/client.rs | 83 +++++++++++++------ 2 files changed, 89 insertions(+), 54 deletions(-) diff --git a/crates/database/src/audit_provider.rs b/crates/database/src/audit_provider.rs index 9d7ac7573d..193cd1fae9 100644 --- a/crates/database/src/audit_provider.rs +++ b/crates/database/src/audit_provider.rs @@ -84,35 +84,41 @@ where std::result::Result, >(16); - self.client - .conn_and_then(move |conn| { - let mut stmt = if reverse { - conn.prepare( - "SELECT * FROM audit_logs ORDER BY log_id DESC", - )? - } else { - conn.prepare( - "SELECT * FROM audit_logs ORDER BY log_id ASC", - )? - }; - let mut rows = stmt.query([])?; + let client = self.client.clone(); + tokio::task::spawn(async move { + client + .conn_and_then(move |conn| { + let mut stmt = if reverse { + conn.prepare( + "SELECT * FROM audit_logs ORDER BY log_id DESC", + )? + } else { + conn.prepare( + "SELECT * FROM audit_logs ORDER BY log_id ASC", + )? + }; + let mut rows = stmt.query([])?; - while let Some(row) = rows.next()? { - let row: AuditRow = row.try_into()?; - let record: AuditRecord = row.try_into()?; - let inner_tx = tx.clone(); - futures::executor::block_on(async move { - if let Err(e) = inner_tx.send(Ok(record.event)).await - { - tracing::error!(error = %e); - } - }); - } + while let Some(row) = rows.next()? { + let row: AuditRow = row.try_into()?; + let record: AuditRecord = row.try_into()?; + let inner_tx = tx.clone(); + futures::executor::block_on(async move { + if let Err(e) = + inner_tx.send(Ok(record.event)).await + { + tracing::error!(error = %e); + } + }); + } - Ok::<_, Error>(()) - }) - .await - .map_err(Error::from)?; + Ok::<_, Error>(()) + }) + .await + .map_err(Error::from)?; + + Ok::<_, Self::Error>(()) + }); Ok(Box::pin(ReceiverStream::new(rx))) } diff --git a/tests/integration/tests/audit_trail/client.rs b/tests/integration/tests/audit_trail/client.rs index 61e0f0376f..5e5b7a02df 100644 --- a/tests/integration/tests/audit_trail/client.rs +++ b/tests/integration/tests/audit_trail/client.rs @@ -1,8 +1,7 @@ use anyhow::Result; use sos_backend::BackendTarget; use sos_client_storage::NewFolderOptions; -use sos_database::open_file; -use sos_test_utils::make_client_backend; +use sos_database::{migrations::migrate_client, open_file}; use crate::test_utils::{mock, setup, teardown}; use futures::{pin_mut, StreamExt}; @@ -11,13 +10,12 @@ use sos_audit::AuditEvent; use sos_migrate::import::ImportTarget; use sos_sdk::prelude::*; use sos_vfs as vfs; -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; #[tokio::test] async fn audit_trail_client_fs() -> Result<()> { - const TEST_ID: &str = "audit_trail_client"; - // crate::test_utils::init_tracing(); - // + const TEST_ID: &str = "audit_trail_client_fs"; + // sos_test_utils::init_tracing(); let mut dirs = setup(TEST_ID, 1).await?; let data_dir = dirs.clients.remove(0); @@ -30,12 +28,45 @@ async fn audit_trail_client_fs() -> Result<()> { sos_backend::audit::new_fs_provider(paths.audit_file().to_owned()); sos_backend::audit::init_providers(vec![provider]); - let account_name = TEST_ID.to_string(); + let target = BackendTarget::FileSystem(paths); + run_audit_test(TEST_ID, target).await?; + + teardown(TEST_ID).await; + + Ok(()) +} + +#[tokio::test] +async fn audit_trail_client_db() -> Result<()> { + const TEST_ID: &str = "audit_trail_client_db"; + // sos_test_utils::init_tracing(); + + let mut dirs = setup(TEST_ID, 1).await?; + let data_dir = dirs.clients.remove(0); + + // Configure the audit provider + let paths = Paths::new_client(&data_dir); + let mut client = open_file(paths.database_file()).await?; + migrate_client(&mut client).await?; + + let provider = sos_backend::audit::new_db_provider(client.clone()); + sos_backend::audit::init_providers(vec![provider]); + + let target = BackendTarget::Database(paths, client); + run_audit_test(TEST_ID, target).await?; + + teardown(TEST_ID).await; + + Ok(()) +} + +async fn run_audit_test(name: &str, target: BackendTarget) -> Result<()> { + let account_name = name.to_string(); let (passphrase, _) = generate_passphrase()?; let mut account = LocalAccount::new_account_with_builder( account_name.to_owned(), passphrase.clone(), - make_client_backend(&paths).await?, + target.clone(), |builder| { builder .save_passphrase(false) @@ -52,14 +83,14 @@ async fn audit_trail_client_fs() -> Result<()> { let summary = account.default_folder().await.unwrap(); // Make changes to generate audit logs - simulate_session(&mut account, &summary, &paths).await?; + simulate_session(&mut account, &summary, &target).await?; // Read in the audit log events let events = read_audit_events().await?; let mut kinds: Vec<_> = events.iter().map(|e| e.event_kind()).collect(); //println!("events {:#?}", events); - println!("kinds {:#?}", kinds); + // println!("kinds {:#?}", kinds); // Created the account assert!(matches!(kinds.remove(0), EventKind::CreateAccount)); @@ -122,15 +153,14 @@ async fn audit_trail_client_fs() -> Result<()> { // Imported an account archive assert!(matches!(kinds.remove(0), EventKind::ImportBackupArchive)); - teardown(TEST_ID).await; - Ok(()) } async fn simulate_session( account: &mut LocalAccount, default_folder: &Summary, - paths: &Arc, + target: &BackendTarget, + // paths: &Arc, ) -> Result<()> { // Create a secret let (meta, secret) = mock::note("Audit note", "Note value"); @@ -173,8 +203,10 @@ async fn simulate_session( .rename_folder(new_folder.id(), "New name".to_string()) .await?; - let exported_folder = - paths.documents_dir().join("audit-trail-vault-export.vault"); + let exported_folder = target + .paths() + .documents_dir() + .join("audit-trail-vault-export.vault"); let (export_passphrase, _) = generate_passphrase()?; account .export_folder( @@ -197,13 +229,16 @@ async fn simulate_session( account.delete_folder(new_folder.id()).await?; // Export an account backup archive - let archive = paths + let archive = target + .paths() .documents_dir() .join("audit-trail-exported-archive.zip"); account.export_backup_archive(&archive).await?; - let unsafe_archive = - paths.documents_dir().join("audit-trail-unsafe-archive.zip"); + let unsafe_archive = target + .paths() + .documents_dir() + .join("audit-trail-unsafe-archive.zip"); account.export_unsafe_archive(unsafe_archive).await?; let import_file = "../fixtures/migrate/bitwarden-export.csv"; @@ -218,7 +253,8 @@ async fn simulate_session( let vcard = vfs::read_to_string(contacts).await?; account.import_contacts(&vcard, |_| {}).await?; - let exported_contacts = paths + let exported_contacts = target + .paths() .documents_dir() .join("audit-trail-exported-contacts.vcf"); account.export_all_contacts(exported_contacts).await?; @@ -226,14 +262,7 @@ async fn simulate_session( // Delete the account account.delete_account().await?; - let target = if paths.is_using_db() { - let client = open_file(paths.database_file()).await?; - BackendTarget::Database(paths.clone(), client) - } else { - BackendTarget::FileSystem(paths.clone()) - }; - - LocalAccount::import_backup_archive(archive, &target).await?; + LocalAccount::import_backup_archive(archive, target).await?; Ok(()) } From d95a92995468979e935d03ea2a112c813d707a97 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 18:13:50 +0800 Subject: [PATCH 23/61] Update default log level. --- crates/backend/src/lib.rs | 2 +- crates/logs/src/logger.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index d653ea8353..44c1fe4c4e 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -168,7 +168,7 @@ impl BackendTarget { pub async fn dump_info(&self) -> Result<()> { tracing::debug!( backend_target = %self, - data_dir = %self.paths().documents_dir().display(), + documents_dir = %self.paths().documents_dir().display(), "backend::dump_info", ); diff --git a/crates/logs/src/logger.rs b/crates/logs/src/logger.rs index a8fc45807a..d039e2384c 100644 --- a/crates/logs/src/logger.rs +++ b/crates/logs/src/logger.rs @@ -14,7 +14,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; const LOG_FILE_NAME: &str = "saveoursecrets.log"; const DEFAULT_LOG_LEVEL: &str = - "sos=info,sos_sdk=debug,sos_net=debug,sos_bindings=debug"; + "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug"; /// State of the log files on disc. pub struct LogFileStatus { From db225e6c8e7a4001d82776347dac5ea147719a9c Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 18:30:56 +0800 Subject: [PATCH 24/61] Update tracing when running migrations. --- crates/database/src/migrations.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/database/src/migrations.rs b/crates/database/src/migrations.rs index 16050d8620..6b0d20a158 100644 --- a/crates/database/src/migrations.rs +++ b/crates/database/src/migrations.rs @@ -13,7 +13,15 @@ mod embedded { pub fn migrate_connection( conn: &mut Connection, ) -> std::result::Result { - embedded::migrations::runner().run(conn) + let report = embedded::migrations::runner().run(conn)?; + for migration in report.applied_migrations() { + tracing::debug!( + name = %migration.name(), + version = %migration.version(), + "migration", + ); + } + Ok(report) } /// Run migrations for a client. @@ -22,7 +30,7 @@ pub async fn migrate_client(client: &mut Client) -> Result { oneshot::channel::>(); client .conn_mut(|conn| { - let result = embedded::migrations::runner().run(conn); + let result = migrate_connection(conn); tx.send(result).unwrap(); Ok(()) }) From bc230315c9be8871ee687e89c303446365b1d1ee Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 18:34:04 +0800 Subject: [PATCH 25/61] Tweak tracing output. --- crates/backend/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index 44c1fe4c4e..3fc71cddba 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -168,7 +168,6 @@ impl BackendTarget { pub async fn dump_info(&self) -> Result<()> { tracing::debug!( backend_target = %self, - documents_dir = %self.paths().documents_dir().display(), "backend::dump_info", ); From d223115fc682521609462a6c110416a595e815c5 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 11 Mar 2025 18:36:05 +0800 Subject: [PATCH 26/61] Update migration output. --- crates/database/src/migrations.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/database/src/migrations.rs b/crates/database/src/migrations.rs index 6b0d20a158..9ac19eb099 100644 --- a/crates/database/src/migrations.rs +++ b/crates/database/src/migrations.rs @@ -13,14 +13,19 @@ mod embedded { pub fn migrate_connection( conn: &mut Connection, ) -> std::result::Result { + tracing::debug!("migration::started"); let report = embedded::migrations::runner().run(conn)?; - for migration in report.applied_migrations() { + let applied = report.applied_migrations(); + for migration in applied { tracing::debug!( name = %migration.name(), version = %migration.version(), - "migration", + "migration::applied", ); } + tracing::debug!( + applied_migrations = %applied.len(), + "migration::finished"); Ok(report) } From ecdff2a91b4751ea9029cd9e468d1c9c4244a6bf Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 12 Mar 2025 12:21:22 +0800 Subject: [PATCH 27/61] Tweak error messages. --- crates/database/src/archive/error.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/database/src/archive/error.rs b/crates/database/src/archive/error.rs index ce11cf1a2e..5318b41c12 100644 --- a/crates/database/src/archive/error.rs +++ b/crates/database/src/archive/error.rs @@ -24,13 +24,15 @@ pub enum Error { NoDatabaseFile(PathBuf, String), /// Error generated attempting to import an account that - /// already exists in the target database. - #[error("import failed, account '{0}' does not exist in source db")] + /// does not exist in the source database. + #[error( + "import failed, account '{0}' does not exist in the backup archive" + )] ImportSourceNotExists(AccountId), /// Error generated attempting to import an account that /// already exists in the target database. - #[error("import failed, account '{0}' already exists in target db")] + #[error("import failed, account '{0}' already exists")] ImportTargetExists(AccountId), /// Error generated when the checksum for a database does not From 9897b4e7dc7ee0ceb804ada681c29f994c8f9fb7 Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 12 Mar 2025 15:06:13 +0800 Subject: [PATCH 28/61] Support creating a debug snapshot ZIP archive. --- Cargo.lock | 16 ++++ Cargo.toml | 4 +- crates/account/src/account_switcher.rs | 2 - crates/archive/src/writer.rs | 31 +++---- crates/audit/src/event.rs | 3 +- crates/database/src/archive/export.rs | 13 ++- crates/debug_snapshot/Cargo.toml | 19 ++++ crates/debug_snapshot/src/error.rs | 25 ++++++ crates/debug_snapshot/src/lib.rs | 114 ++++++++++++++++++++++++ crates/filesystem/src/archive/export.rs | 73 +++++++++------ crates/logs/src/lib.rs | 2 +- crates/logs/src/logger.rs | 5 +- crates/sos/Cargo.toml | 1 + crates/sos/src/commands/tools/debug.rs | 35 ++++++++ crates/sos/src/error.rs | 4 + 15 files changed, 290 insertions(+), 57 deletions(-) create mode 100644 crates/debug_snapshot/Cargo.toml create mode 100644 crates/debug_snapshot/src/error.rs create mode 100644 crates/debug_snapshot/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 703938959e..a0ed1d3d5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5000,6 +5000,7 @@ dependencies = [ "sos-core", "sos-database", "sos-database-upgrader", + "sos-debug-snapshot", "sos-external-files", "sos-integrity", "sos-login", @@ -5319,6 +5320,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "sos-debug-snapshot" +version = "0.17.0" +dependencies = [ + "futures", + "serde_json", + "sos-archive", + "sos-backend", + "sos-client-storage", + "sos-logs", + "sos-sync", + "sos-vfs", + "thiserror 2.0.12", +] + [[package]] name = "sos-extension-service" version = "0.17.0" diff --git a/Cargo.toml b/Cargo.toml index 1596cd6f34..1ead650a92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/core", "crates/database", "crates/database_upgrader", + "crates/debug_snapshot", "crates/extension_service", "crates/external_files", "crates/filesystem", @@ -46,7 +47,7 @@ members = [ "tests/command_line", "tests/integration", "tests/unit", - "tests/utils", + "tests/utils", ] [workspace.dependencies] @@ -60,6 +61,7 @@ sos-core = { version = "0.17", path = "crates/core" } sos-changes = { version = "0.17", path = "crates/changes" } sos-database = { version = "0.17", path = "crates/database" } sos-database-upgrader = { version = "0.17", path = "crates/database_upgrader" } +sos-debug-snapshot = { version = "0.17", path = "crates/debug_snapshot" } sos-external-files = { version = "0.17.0", path = "crates/external_files" } sos-filesystem = { version = "0.17", path = "crates/filesystem" } sos-integrity = { version = "0.17", path = "crates/integrity" } diff --git a/crates/account/src/account_switcher.rs b/crates/account/src/account_switcher.rs index d7794c3670..a6b08dc128 100644 --- a/crates/account/src/account_switcher.rs +++ b/crates/account/src/account_switcher.rs @@ -121,8 +121,6 @@ where tracing::info!( account_id = %identity.account_id(), "add_account"); let account = builder(identity).await.unwrap(); - - let paths = account.paths(); self.add_account(account); } Ok(()) diff --git a/crates/archive/src/writer.rs b/crates/archive/src/writer.rs index f3f10b8a0a..b75133e4ed 100644 --- a/crates/archive/src/writer.rs +++ b/crates/archive/src/writer.rs @@ -1,33 +1,25 @@ -use crate::{Result, ARCHIVE_MANIFEST}; +use crate::Result; use async_zip::{ tokio::write::ZipFileWriter, Compression, ZipDateTimeBuilder, ZipEntryBuilder, }; -use serde::Serialize; use time::OffsetDateTime; use tokio::io::AsyncWrite; use tokio_util::compat::Compat; /// Write to an archive. -pub struct Writer { +pub struct Writer { writer: ZipFileWriter, - manifest: M, } -impl Writer { - /// Create a new writer. - pub fn new(inner: W, manifest: M) -> Self { +impl Writer { + /// Create a new writer with a manifest. + pub fn new(inner: W) -> Self { Self { writer: ZipFileWriter::with_tokio(inner), - manifest, } } - /// Mutable archive manifest. - pub fn manifest_mut(&mut self) -> &mut M { - &mut self.manifest - } - /// Add a file to the archive. pub async fn add_file( &mut self, @@ -42,14 +34,6 @@ impl Writer { self.append_file_buffer(path, content).await } - /// Add the manifest and finish building the archive. - pub async fn finish(mut self) -> Result> { - let manifest = serde_json::to_vec_pretty(&self.manifest)?; - self.append_file_buffer(ARCHIVE_MANIFEST, manifest.as_slice()) - .await?; - Ok(self.writer.close().await?) - } - async fn append_file_buffer( &mut self, path: &str, @@ -73,4 +57,9 @@ impl Writer { self.writer.write_entry_whole(entry, buffer).await?; Ok(()) } + + /// Finish building the archive. + pub async fn finish(self) -> Result> { + Ok(self.writer.close().await?) + } } diff --git a/crates/audit/src/event.rs b/crates/audit/src/event.rs index 9b4be48bd9..6fb6dfa935 100644 --- a/crates/audit/src/event.rs +++ b/crates/audit/src/event.rs @@ -40,6 +40,7 @@ bitflags! { /// * 20 bytes for the public account_id. /// * 16, 32 or 64 bytes for the context data (one, two or four UUIDs). #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct AuditEvent { /// Time the event was created. pub(crate) time: UtcDateTime, @@ -202,7 +203,7 @@ impl From<(&AccountId, &AccountEvent)> for AuditEvent { /// Associated data for an audit log record. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] +#[serde(rename_all = "camelCase")] pub enum AuditData { /// Data for an associated vault. Vault(VaultId), diff --git a/crates/database/src/archive/export.rs b/crates/database/src/archive/export.rs index bb591eac72..0ee3417d96 100644 --- a/crates/database/src/archive/export.rs +++ b/crates/database/src/archive/export.rs @@ -2,7 +2,7 @@ use super::{types::ManifestVersion3, Error, Result}; use crate::entity::{AccountEntity, AccountRecord, AccountRow}; use async_sqlite::rusqlite::{backup, Connection}; use sha2::{Digest, Sha256}; -use sos_archive::ZipWriter; +use sos_archive::{ZipWriter, ARCHIVE_MANIFEST}; use sos_core::{ commit::CommitHash, constants::{BLOBS_DIR, DATABASE_FILE}, @@ -34,7 +34,8 @@ pub(crate) async fn create( } let zip_file = vfs::File::create(output.as_ref()).await?; - let mut zip_writer = ZipWriter::new(zip_file, ManifestVersion3::new_v3()); + let mut manifest = ManifestVersion3::new_v3(); + let mut zip_writer = ZipWriter::new(zip_file); // Find blobs that we need to add to the archive let accounts = list_accounts(source_db.as_ref())?; @@ -45,8 +46,7 @@ pub(crate) async fn create( let db_buffer = vfs::read(db_temp.path()).await?; let db_checksum = Sha256::digest(&db_buffer); - zip_writer.manifest_mut().checksum = - CommitHash(db_checksum.as_slice().try_into()?); + manifest.checksum = CommitHash(db_checksum.as_slice().try_into()?); zip_writer.add_file(DATABASE_FILE, &db_buffer).await?; // Add external file blobs to the archive @@ -68,6 +68,11 @@ pub(crate) async fn create( } } + let manifest = serde_json::to_vec_pretty(&manifest)?; + zip_writer + .add_file(ARCHIVE_MANIFEST, manifest.as_slice()) + .await?; + zip_writer.finish().await?; Ok(()) } diff --git a/crates/debug_snapshot/Cargo.toml b/crates/debug_snapshot/Cargo.toml new file mode 100644 index 0000000000..e61235e683 --- /dev/null +++ b/crates/debug_snapshot/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "sos-debug-snapshot" +version = "0.17.0" +edition = "2021" +description = "Create debug snapshot ZIP archives for the Save Our Secrets SDK" +homepage = "https://saveoursecrets.com" +license = "MIT OR Apache-2.0" +repository = "https://github.com/saveoursecrets/sdk" + +[dependencies] +thiserror.workspace = true +futures.workspace = true +sos-backend.workspace = true +sos-client-storage.workspace = true +sos-archive.workspace = true +sos-logs.workspace = true +sos-sync.workspace = true +sos-vfs.workspace = true +serde_json.workspace = true diff --git a/crates/debug_snapshot/src/error.rs b/crates/debug_snapshot/src/error.rs new file mode 100644 index 0000000000..5d729fe3e7 --- /dev/null +++ b/crates/debug_snapshot/src/error.rs @@ -0,0 +1,25 @@ +use thiserror::Error; + +/// Errors generated by the library. +#[derive(Debug, Error)] +pub enum Error { + /// Errors generated by the IO module. + #[error(transparent)] + Io(#[from] std::io::Error), + + /// Errors generated by the JSON library. + #[error(transparent)] + Json(#[from] serde_json::Error), + + /// Errors generated by the backend library. + #[error(transparent)] + Backend(#[from] sos_backend::Error), + + /// Errors generated by the client storage library. + #[error(transparent)] + ClientStorage(#[from] sos_client_storage::Error), + + /// Errors generated by the archive library. + #[error(transparent)] + Archive(#[from] sos_archive::Error), +} diff --git a/crates/debug_snapshot/src/lib.rs b/crates/debug_snapshot/src/lib.rs new file mode 100644 index 0000000000..7bc5e93ef3 --- /dev/null +++ b/crates/debug_snapshot/src/lib.rs @@ -0,0 +1,114 @@ +use futures::{pin_mut, StreamExt}; +use sos_archive::ZipWriter; +use sos_client_storage::{ + ClientBaseStorage, ClientFolderStorage, ClientStorage, +}; +use sos_logs::LOG_FILE_NAME; +use sos_sync::SyncStorage; +use sos_vfs as vfs; +use std::path::Path; + +mod error; +pub use error::Error; + +/// Options for debug snapshots. +#[derive(Debug)] +pub struct DebugSnapshotOptions { + /// Include log files in the archive. + pub include_log_files: bool, + /// Include audit trail for the first configured + /// audit provider. + pub include_audit_trail: bool, +} + +impl Default for DebugSnapshotOptions { + fn default() -> Self { + Self { + include_log_files: true, + include_audit_trail: false, + } + } +} + +/// Export a ZIP archive containing a snapshot of an +/// account state; if the file exists it is overwritten. +/// +/// # Privacy +/// +/// No secret information is included but it does include the +/// account identifier and folder names. +pub async fn export_debug_snapshot( + source: &ClientStorage, + file: impl AsRef, + options: DebugSnapshotOptions, +) -> Result<(), Error> { + let zip_file = vfs::File::create(file.as_ref()).await?; + let mut zip_writer = ZipWriter::new(zip_file); + + let account_id = *source.account_id(); + let debug_tree = source.debug_account_tree(account_id).await?; + + let buffer = serde_json::to_vec_pretty(&debug_tree)?; + zip_writer.add_file("account.json", &buffer).await?; + + let login = source.read_login_vault().await?; + let buffer = serde_json::to_vec_pretty(login.summary())?; + zip_writer.add_file("login.json", &buffer).await?; + + if let Some(device) = source.read_device_vault().await? { + let buffer = serde_json::to_vec_pretty(device.summary())?; + zip_writer.add_file("device.json", &buffer).await?; + } + + let target = source.backend_target(); + let paths = target.paths(); + + if options.include_log_files { + let logs = paths.logs_dir(); + let mut dir = vfs::read_dir(logs).await?; + while let Some(entry) = dir.next_entry().await? { + let path = entry.path(); + if let Some(name) = path.file_name() { + if name.to_string_lossy().starts_with(LOG_FILE_NAME) { + let buffer = vfs::read(&path).await?; + zip_writer + .add_file( + &format!("logs/{}.jsonl", name.to_string_lossy()), + &buffer, + ) + .await?; + } + } + } + } + + if options.include_audit_trail { + if let Some(providers) = sos_backend::audit::providers() { + for (index, provider) in providers.iter().enumerate() { + let stream = provider.audit_stream(false).await?; + pin_mut!(stream); + + let events = stream + .filter_map(|e| async move { e.ok() }) + .filter_map(|e| async move { + if e.account_id() == &account_id { + Some(e) + } else { + None + } + }) + .collect::>() + .await; + + let buffer = serde_json::to_vec_pretty(&events)?; + zip_writer + .add_file(&format!("audit/{}.json", index), &buffer) + .await?; + } + } + } + + zip_writer.finish().await?; + + Ok(()) +} diff --git a/crates/filesystem/src/archive/export.rs b/crates/filesystem/src/archive/export.rs index 659c94ea07..06b07e8566 100644 --- a/crates/filesystem/src/archive/export.rs +++ b/crates/filesystem/src/archive/export.rs @@ -2,7 +2,7 @@ use crate::archive::{Error, ManifestVersion1, Result}; use hex; use sha2::{Digest, Sha256}; -use sos_archive::ZipWriter; +use sos_archive::{ZipWriter, ARCHIVE_MANIFEST}; use sos_core::{ constants::{ ACCOUNT_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILE_EVENTS, JSON_EXT, @@ -43,13 +43,13 @@ async fn export_archive_buffer( let vaults = list_local_folders(paths).await?; let mut archive = Vec::new(); - let mut writer = - ZipWriter::new(Cursor::new(&mut archive), ManifestVersion1::new_v2()); - set_identity(&mut writer, address, &identity).await?; + let mut manifest = ManifestVersion1::new_v2(); + let mut writer = ZipWriter::new(Cursor::new(&mut archive)); + set_identity(&mut writer, &mut manifest, address, &identity).await?; for (summary, path) in vaults { let buffer = vfs::read(path).await?; - add_vault(&mut writer, *summary.id(), &buffer).await?; + add_vault(&mut writer, &mut manifest, *summary.id(), &buffer).await?; } let device_info = if vfs::try_exists(paths.device_file()).await? @@ -63,27 +63,37 @@ async fn export_archive_buffer( }; if let Some((vault, events)) = device_info { - add_devices(&mut writer, vault.as_slice(), events.as_slice()).await?; + add_devices( + &mut writer, + &mut manifest, + vault.as_slice(), + events.as_slice(), + ) + .await?; } if vfs::try_exists(paths.account_events()).await? { let buffer = vfs::read(paths.account_events()).await?; - add_account_events(&mut writer, buffer.as_slice()).await?; + add_account_events(&mut writer, &mut manifest, buffer.as_slice()) + .await?; } if vfs::try_exists(paths.file_events()).await? { let buffer = vfs::read(paths.file_events()).await?; - add_file_events(&mut writer, buffer.as_slice()).await?; + add_file_events(&mut writer, &mut manifest, buffer.as_slice()) + .await?; } if vfs::try_exists(paths.preferences_file()).await? { let buffer = vfs::read(paths.preferences_file()).await?; - add_preferences(&mut writer, buffer.as_slice()).await?; + add_preferences(&mut writer, &mut manifest, buffer.as_slice()) + .await?; } if vfs::try_exists(paths.remote_origins()).await? { let buffer = vfs::read(paths.remote_origins()).await?; - add_remote_servers(&mut writer, buffer.as_slice()).await?; + add_remote_servers(&mut writer, &mut manifest, buffer.as_slice()) + .await?; } let external_files = @@ -96,22 +106,27 @@ async fn export_archive_buffer( .await?; } + let manifest = serde_json::to_vec_pretty(&manifest)?; + writer + .add_file(ARCHIVE_MANIFEST, manifest.as_slice()) + .await?; + writer.finish().await?; Ok(archive) } /// Set the identity vault for the archive. async fn set_identity( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, account_id: &AccountId, vault: &[u8], ) -> Result<()> { let mut path = PathBuf::from(account_id.to_string()); path.set_extension(VAULT_EXT); - writer.manifest_mut().account_id = *account_id; - writer.manifest_mut().checksum = - hex::encode(Sha256::digest(vault).as_slice()); + manifest.account_id = *account_id; + manifest.checksum = hex::encode(Sha256::digest(vault).as_slice()); writer .add_file(path.to_string_lossy().as_ref(), vault) .await?; @@ -121,7 +136,8 @@ async fn set_identity( /// Add a vault to the archive. async fn add_vault( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, vault_id: VaultId, vault: &[u8], ) -> Result<()> { @@ -129,7 +145,7 @@ async fn add_vault( path.set_extension(VAULT_EXT); let checksum = hex::encode(Sha256::digest(vault).as_slice()); - writer.manifest_mut().vaults.insert(vault_id, checksum); + manifest.vaults.insert(vault_id, checksum); writer .add_file(path.to_string_lossy().as_ref(), vault) .await?; @@ -139,13 +155,14 @@ async fn add_vault( /// Add a devices vault to the archive. async fn add_devices( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, vault: &[u8], events: &[u8], ) -> Result<()> { let vault_checksum = hex::encode(Sha256::digest(vault).as_slice()); let event_checksum = hex::encode(Sha256::digest(events).as_slice()); - writer.manifest_mut().devices = Some((vault_checksum, event_checksum)); + manifest.devices = Some((vault_checksum, event_checksum)); // Create the device vault file let mut path = PathBuf::from(DEVICE_FILE); @@ -166,11 +183,12 @@ async fn add_devices( /// Add account events to the archive. async fn add_account_events( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, events: &[u8], ) -> Result<()> { let event_checksum = hex::encode(Sha256::digest(events).as_slice()); - writer.manifest_mut().account = Some(event_checksum); + manifest.account = Some(event_checksum); // Create the account events file let mut path = PathBuf::from(ACCOUNT_EVENTS); @@ -184,11 +202,12 @@ async fn add_account_events( /// Add file events to the archive. async fn add_file_events( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, events: &[u8], ) -> Result<()> { let event_checksum = hex::encode(Sha256::digest(events).as_slice()); - writer.manifest_mut().files = Some(event_checksum); + manifest.files = Some(event_checksum); // Create the file events file let mut path = PathBuf::from(FILE_EVENTS); @@ -202,11 +221,12 @@ async fn add_file_events( /// Add account-specific preferences. async fn add_preferences( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, prefs: &[u8], ) -> Result<()> { let checksum = hex::encode(Sha256::digest(prefs).as_slice()); - writer.manifest_mut().preferences = Some(checksum); + manifest.preferences = Some(checksum); // Create the file events file let mut path = PathBuf::from(PREFERENCES_FILE); @@ -221,11 +241,12 @@ async fn add_preferences( /// Add remote server settings. async fn add_remote_servers( - writer: &mut ZipWriter>, ManifestVersion1>, + writer: &mut ZipWriter>>, + manifest: &mut ManifestVersion1, remotes: &[u8], ) -> Result<()> { let checksum = hex::encode(Sha256::digest(remotes).as_slice()); - writer.manifest_mut().remotes = Some(checksum); + manifest.remotes = Some(checksum); // Create the file events file let mut path = PathBuf::from(REMOTES_FILE); diff --git a/crates/logs/src/lib.rs b/crates/logs/src/lib.rs index 3f851d6335..91895260b3 100644 --- a/crates/logs/src/lib.rs +++ b/crates/logs/src/lib.rs @@ -6,6 +6,6 @@ mod error; mod logger; pub use error::Error; -pub use logger::{LogFileStatus, Logger}; +pub use logger::{LogFileStatus, Logger, LOG_FILE_NAME}; pub(crate) type Result = std::result::Result; diff --git a/crates/logs/src/logger.rs b/crates/logs/src/logger.rs index d039e2384c..da3da1c6cc 100644 --- a/crates/logs/src/logger.rs +++ b/crates/logs/src/logger.rs @@ -12,7 +12,10 @@ use time::OffsetDateTime; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -const LOG_FILE_NAME: &str = "saveoursecrets.log"; +/// File name prefix for log files. +#[doc(hidden)] +pub const LOG_FILE_NAME: &str = "saveoursecrets.log"; + const DEFAULT_LOG_LEVEL: &str = "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug"; diff --git a/crates/sos/Cargo.toml b/crates/sos/Cargo.toml index ab526d184a..ad08122199 100644 --- a/crates/sos/Cargo.toml +++ b/crates/sos/Cargo.toml @@ -21,6 +21,7 @@ sos-cli-helpers.workspace = true sos-client-storage = { workspace = true, features = ["full"] } sos-database = { workspace = true, features = ["full"] } sos-database-upgrader = { workspace = true, features = ["full"] } +sos-debug-snapshot.workspace = true sos-external-files.workspace = true sos-integrity.workspace = true sos-login = { workspace = true, features = ["full"] } diff --git a/crates/sos/src/commands/tools/debug.rs b/crates/sos/src/commands/tools/debug.rs index 4ad292aabf..4200f48e18 100644 --- a/crates/sos/src/commands/tools/debug.rs +++ b/crates/sos/src/commands/tools/debug.rs @@ -3,7 +3,9 @@ use clap::Subcommand; use sos_backend::BackendTarget; use sos_client_storage::ClientStorage; use sos_core::{AccountRef, Paths}; +use sos_debug_snapshot::{export_debug_snapshot, DebugSnapshotOptions}; use sos_sync::SyncStorage; +use std::path::PathBuf; #[derive(Subcommand, Debug)] pub enum Command { @@ -13,6 +15,19 @@ pub enum Command { #[clap(short, long)] account: AccountRef, }, + /// Create a debug snapshot ZIP bundle. + Snapshot { + /// Account name or identifier. + #[clap(short, long)] + account: AccountRef, + + /// Include audit trail. + #[clap(long)] + include_audit_trail: bool, + + /// Output ZIP file. + file: PathBuf, + }, } pub async fn run(cmd: Command) -> Result<()> { @@ -30,6 +45,26 @@ pub async fn run(cmd: Command) -> Result<()> { let debug_tree = storage.debug_account_tree(account_id).await?; serde_json::to_writer_pretty(std::io::stdout(), &debug_tree)?; } + Command::Snapshot { + account, + include_audit_trail, + file, + } => { + let account_id = resolve_account_address(Some(&account)).await?; + let paths = Paths::new_client(Paths::data_dir()?) + .with_account_id(&account_id); + let target = BackendTarget::from_paths(&paths).await?; + + let storage = + ClientStorage::new_unauthenticated(target, &account_id) + .await?; + + let options = DebugSnapshotOptions { + include_audit_trail, + ..Default::default() + }; + export_debug_snapshot(&storage, file, options).await?; + } } Ok(()) diff --git a/crates/sos/src/error.rs b/crates/sos/src/error.rs index 8afeac1121..212e328018 100644 --- a/crates/sos/src/error.rs +++ b/crates/sos/src/error.rs @@ -211,6 +211,10 @@ pub enum Error { #[error(transparent)] Backend(#[from] sos_backend::Error), + /// Error generated by the debug snapshot library. + #[error(transparent)] + DebugSnapshot(#[from] sos_debug_snapshot::Error), + /// Error generated by the backend storage. #[error(transparent)] BackendStorage(#[from] sos_backend::StorageError), From 465ea17f1c02306dd29f7cde1f75022c2eee2ddc Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 12 Mar 2025 15:08:27 +0800 Subject: [PATCH 29/61] Update SDK readme. --- crates/sdk/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/sdk/README.md b/crates/sdk/README.md index 0a534b00ec..e13f659955 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -27,6 +27,7 @@ This crate exports a prelude of common types for low-level access but we encoura | [sos-core](https://docs.rs/sos-core/latest/sos_core/) | Core types and traits; cryptography functions, commit trees and event definitions. | | [sos-database](https://docs.rs/sos-database/latest/sos_database/) | SQLite database backend | | [sos-database-upgrader](https://docs.rs/sos-database-upgrader/latest/sos_database_upgrader/) | Upgrade filesystem backend to database backend | +| [sos-debug-snapshot](https://docs.rs/sos-debug-snapshot/latest/sos_debug_snapshot/) | Create debug snapshot ZIP archives | | [sos-extension-service](https://docs.rs/sos-extension-service/latest/sos_extension_service/) | Browser extension helper service | | [sos-external-files](https://docs.rs/sos-external-files/latest/sos_external_files/) | Helper functions for managing external encrypted file blobs | | [sos-filesystem](https://docs.rs/sos-filesystem/latest/sos_filesystem/) | Legacy filesystem backend | From c0c186bcf59298e3aca30c6d1cfc517551e904e5 Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 13 Mar 2025 08:52:48 +0800 Subject: [PATCH 30/61] Update sandbox configs, add PairTargetUrl. --- crates/net/src/pairing/share_url.rs | 55 +++++++++++++++++++ sandbox/.gitignore | 4 +- sandbox/accounts/.gitignore | 2 - .../{database.toml => config-database.toml} | 0 .../{config.toml => config-filesystem.toml} | 0 5 files changed, 57 insertions(+), 4 deletions(-) delete mode 100644 sandbox/accounts/.gitignore rename sandbox/{database.toml => config-database.toml} (100%) rename sandbox/{config.toml => config-filesystem.toml} (100%) diff --git a/crates/net/src/pairing/share_url.rs b/crates/net/src/pairing/share_url.rs index 3a40c25642..7b639b3770 100644 --- a/crates/net/src/pairing/share_url.rs +++ b/crates/net/src/pairing/share_url.rs @@ -5,6 +5,61 @@ use sos_core::{csprng, AccountId}; use std::str::FromStr; use url::Url; +/// Pair target URL encapsulates a server and account ID. +/// +/// It is used to transfer a server and account identifier +/// from a mobile device to a desktop for the inverted pairing flow. +/// +/// The URL format is `?aid=`. +pub struct PairTargetUrl { + /// Server used to transfer the account data. + server: Url, + /// Account identifier. + account_id: AccountId, +} + +impl PairTargetUrl { + /// Create a new pair target URL. + pub fn new(server: Url, account_id: AccountId) -> Self { + Self { server, account_id } + } +} + +impl From for Url { + fn from(value: PairTargetUrl) -> Self { + let mut url = value.server; + url.query_pairs_mut() + .append_pair(AID, &value.account_id.to_string()); + url + } +} + +impl TryFrom for PairTargetUrl { + type Error = Error; + + fn try_from(mut url: Url) -> Result { + let mut pairs = url.query_pairs(); + + let account_id = pairs.find_map(|q| { + if q.0.as_ref() == AID { + Some(q.1) + } else { + None + } + }); + let account_id = account_id.ok_or(Error::InvalidShareUrl)?; + let account_id: AccountId = account_id.as_ref().parse()?; + + url.set_query(None); + url.set_path(""); + + Ok(PairTargetUrl { + server: url, + account_id, + }) + } +} + /// Account identifier. const AID: &str = "aid"; /// Server URL. diff --git a/sandbox/.gitignore b/sandbox/.gitignore index 5c532857cf..296754d681 100644 --- a/sandbox/.gitignore +++ b/sandbox/.gitignore @@ -1,10 +1,10 @@ * !.gitignore -!config.toml +!config-filesystem.toml !config-backup.toml !acme.toml !accounts !accounts-backup !acme-cache -!database.toml +!config-database.toml !db_accounts diff --git a/sandbox/accounts/.gitignore b/sandbox/accounts/.gitignore deleted file mode 100644 index d6b7ef32c8..0000000000 --- a/sandbox/accounts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/sandbox/database.toml b/sandbox/config-database.toml similarity index 100% rename from sandbox/database.toml rename to sandbox/config-database.toml diff --git a/sandbox/config.toml b/sandbox/config-filesystem.toml similarity index 100% rename from sandbox/config.toml rename to sandbox/config-filesystem.toml From b661e55e227a55fb1d267cb266824d8364a58bbe Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 13 Mar 2025 09:14:57 +0800 Subject: [PATCH 31/61] Improve pairing URL test spec. --- crates/net/src/pairing/mod.rs | 2 +- crates/net/src/pairing/share_url.rs | 11 +++ tests/unit/src/tests/mod.rs | 2 +- tests/unit/src/tests/pairing_share_url.rs | 23 ------- tests/unit/src/tests/pairing_url.rs | 82 +++++++++++++++++++++++ 5 files changed, 95 insertions(+), 25 deletions(-) delete mode 100644 tests/unit/src/tests/pairing_share_url.rs create mode 100644 tests/unit/src/tests/pairing_url.rs diff --git a/crates/net/src/pairing/mod.rs b/crates/net/src/pairing/mod.rs index bf27af5ce8..dcd25d3efa 100644 --- a/crates/net/src/pairing/mod.rs +++ b/crates/net/src/pairing/mod.rs @@ -6,7 +6,7 @@ mod websocket; pub use enrollment::DeviceEnrollment; pub use error::Error; -pub use share_url::ServerPairUrl; +pub use share_url::{PairTargetUrl, ServerPairUrl}; pub use websocket::{AcceptPairing, OfferPairing}; /// Result type for the pairing module. diff --git a/crates/net/src/pairing/share_url.rs b/crates/net/src/pairing/share_url.rs index 7b639b3770..de2a1c6d37 100644 --- a/crates/net/src/pairing/share_url.rs +++ b/crates/net/src/pairing/share_url.rs @@ -11,6 +11,7 @@ use url::Url; /// from a mobile device to a desktop for the inverted pairing flow. /// /// The URL format is `?aid=`. +#[derive(Debug, PartialEq, Eq)] pub struct PairTargetUrl { /// Server used to transfer the account data. server: Url, @@ -23,6 +24,16 @@ impl PairTargetUrl { pub fn new(server: Url, account_id: AccountId) -> Self { Self { server, account_id } } + + /// Server URL. + pub fn server(&self) -> &Url { + &self.server + } + + /// Account identifier. + pub fn account_id(&self) -> &AccountId { + &self.account_id + } } impl From for Url { diff --git a/tests/unit/src/tests/mod.rs b/tests/unit/src/tests/mod.rs index 4fe71f6942..b7469b9ba1 100644 --- a/tests/unit/src/tests/mod.rs +++ b/tests/unit/src/tests/mod.rs @@ -14,7 +14,7 @@ mod identity; mod keychain; mod migrate; mod not_authenticated; -mod pairing_share_url; +mod pairing_url; mod password; mod preferences; mod protocol_encoding; diff --git a/tests/unit/src/tests/pairing_share_url.rs b/tests/unit/src/tests/pairing_share_url.rs deleted file mode 100644 index 879e6c60ed..0000000000 --- a/tests/unit/src/tests/pairing_share_url.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::Result; -use sos_core::AccountId; -use sos_net::pairing::ServerPairUrl; -use url::Url; - -#[test] -fn server_pair_url() -> Result<()> { - let mock_account_id = AccountId::random(); - let mock_url = Url::parse("http://192.168.1.8:5053/foo?bar=baz+qux")?; - let mock_key = vec![1, 2, 3, 4]; - let share = ServerPairUrl::new( - mock_account_id.clone(), - mock_url.clone(), - mock_key.clone(), - ); - let share_url: Url = share.into(); - let share_url = share_url.to_string(); - let parsed_share: ServerPairUrl = share_url.parse()?; - assert_eq!(&mock_account_id, parsed_share.account_id()); - assert_eq!(&mock_url, parsed_share.server()); - assert_eq!(&mock_key, parsed_share.public_key()); - Ok(()) -} diff --git a/tests/unit/src/tests/pairing_url.rs b/tests/unit/src/tests/pairing_url.rs new file mode 100644 index 0000000000..e0a5d93083 --- /dev/null +++ b/tests/unit/src/tests/pairing_url.rs @@ -0,0 +1,82 @@ +use anyhow::Result; +use sos_core::AccountId; +use sos_net::pairing::{PairTargetUrl, ServerPairUrl}; +use url::Url; + +const SERVER: &str = "http://192.168.1.8:5053"; + +#[test] +fn pair_target_url() -> Result<()> { + let mock_server: Url = SERVER.parse()?; + let mock_account_id = AccountId::random(); + let mock_url = + Url::parse(&format!("{}/?aid={}", mock_server, mock_account_id))?; + let pairing_target = + PairTargetUrl::new(mock_server.clone(), mock_account_id.clone()); + + assert_eq!(&mock_server, pairing_target.server()); + assert_eq!(&mock_account_id, pairing_target.account_id()); + + let parsed_target: PairTargetUrl = mock_url.try_into()?; + assert_eq!(pairing_target, parsed_target); + + Ok(()) +} + +#[test] +fn pair_target_url_errors() -> Result<()> { + // No account identifier in query string + assert!(PairTargetUrl::try_from(Url::parse(SERVER).unwrap()).is_err()); + Ok(()) +} + +#[test] +fn pair_server_url() -> Result<()> { + let mock_account_id = AccountId::random(); + let mock_url = Url::parse(&format!("{}/foo?bar=baz+qux", SERVER))?; + let mock_key = vec![1, 2, 3, 4]; + let share = ServerPairUrl::new( + mock_account_id.clone(), + mock_url.clone(), + mock_key.clone(), + ); + let share_url: Url = share.into(); + let share_url = share_url.to_string(); + let parsed_share: ServerPairUrl = share_url.parse()?; + assert_eq!(&mock_account_id, parsed_share.account_id()); + assert_eq!(&mock_url, parsed_share.server()); + assert_eq!(&mock_key, parsed_share.public_key()); + Ok(()) +} + +#[test] +fn pair_server_url_errors() -> Result<()> { + // Not data:// scheme + assert!(SERVER.parse::().is_err()); + // Invalid path for MIME type info + assert!("data://image/png,sos-pair" + .parse::() + .is_err()); + // No `aid` query string + assert!("data://text/plain,sos-pair" + .parse::() + .is_err()); + // No server `url` query string + assert!("data://text/plain,sos-pair?aid=0x020172140827f060693a1c9a2f5d9639ec299d4c" + .parse::() + .is_err()); + // No noise public `key` query string + assert!("data://text/plain,sos-pair?aid=0x020172140827f060693a1c9a2f5d9639ec299d4c&url=http://localhost" + .parse::() + .is_err()); + // No pre-shared private `psk` query string + assert!("data://text/plain,sos-pair?aid=0x020172140827f060693a1c9a2f5d9639ec299d4c&url=http://localhost&key=0xff" + .parse::() + .is_err()); + // The `psk` query string is not 32 bytes + assert!("data://text/plain,sos-pair?aid=0x020172140827f060693a1c9a2f5d9639ec299d4c&url=http://localhost&key=0xff&psk=0xff" + .parse::() + .is_err()); + + Ok(()) +} From 34b99ce4770ff661339aba620806fae2144e9dab Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 13 Mar 2025 12:12:08 +0800 Subject: [PATCH 32/61] Update tracing warnings. --- crates/changes/src/consumer.rs | 4 +++- crates/net/src/account/file_transfers/mod.rs | 24 ++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/crates/changes/src/consumer.rs b/crates/changes/src/consumer.rs index 4ed7a6edf9..73d6020cbe 100644 --- a/crates/changes/src/consumer.rs +++ b/crates/changes/src/consumer.rs @@ -92,7 +92,9 @@ impl ChangeConsumer { let event: LocalChangeEvent = serde_json::from_slice(&buffer)?; if let Err(e) = tx.send(event).await { - tracing::warn!(error = %e); + tracing::warn!( + error = %e, + "changes::consumer::send_error"); } } diff --git a/crates/net/src/account/file_transfers/mod.rs b/crates/net/src/account/file_transfers/mod.rs index 1fbeb30f01..d361bc7c7f 100644 --- a/crates/net/src/account/file_transfers/mod.rs +++ b/crates/net/src/account/file_transfers/mod.rs @@ -194,21 +194,27 @@ impl FileTransfersHandle { /// Send a collection of items to be added to the queue. pub async fn send(&self, items: FileTransferQueueRequest) { - let res = self.queue_tx.send(items).await; - if let Err(error) = res { - tracing::warn!(error = ?error); + if let Err(error) = self.queue_tx.send(items).await { + tracing::warn!( + error = ?error, + "file_transfers::queue_send_error", + ); } } /// Shutdown the file transfers loop. pub async fn shutdown(self) { - let res = self.shutdown_tx.send(()).await; - if let Err(error) = res { - tracing::warn!(error = ?error); + if let Err(error) = self.shutdown_tx.send(()).await { + tracing::warn!( + error = ?error, + "file_transfers::shutdown_tx::send_error", + ); } - let res = self.shutdown_rx.await; - if let Err(error) = res { - tracing::warn!(error = ?error); + if let Err(error) = self.shutdown_rx.await { + tracing::warn!( + error = ?error, + "file_transfers::shutdown_tx::recv_error", + ); } } } From 606c4df4a57a84d086e16937e502f641700d1e77 Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 13 Mar 2025 13:36:14 +0800 Subject: [PATCH 33/61] Improve handling and tracing of file uploads. --- crates/logs/src/logger.rs | 2 +- crates/protocol/src/network_client/http.rs | 49 +++++++++++++------ .../protocol/src/network_client/websocket.rs | 7 ++- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/crates/logs/src/logger.rs b/crates/logs/src/logger.rs index da3da1c6cc..2e153c81b1 100644 --- a/crates/logs/src/logger.rs +++ b/crates/logs/src/logger.rs @@ -17,7 +17,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub const LOG_FILE_NAME: &str = "saveoursecrets.log"; const DEFAULT_LOG_LEVEL: &str = - "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug"; + "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug"; /// State of the log files on disc. pub struct LogFileStatus { diff --git a/crates/protocol/src/network_client/http.rs b/crates/protocol/src/network_client/http.rs index 6f0c263561..aa27584e0f 100644 --- a/crates/protocol/src/network_client/http.rs +++ b/crates/protocol/src/network_client/http.rs @@ -473,32 +473,51 @@ impl FileSyncClient for HttpClient { let mut bytes_sent = 0; if let Err(error) = progress.send((bytes_sent, Some(file_size))).await { - tracing::warn!(error = ?error); + tracing::warn!( + error = ?error, + "http::progress_send_initial_size", + ); } - let mut reader_stream = ReaderStream::new(file); - - let (tx, rx) = mpsc::channel(8); + let (tx, rx) = mpsc::channel(128); tokio::task::spawn(async move { + let mut reader_stream = ReaderStream::new(file); + let upload_channel = tx.clone(); loop { tokio::select! { biased; - _ = cancel.changed() => { - let reason = cancel.borrow().clone(); - tracing::debug!(reason = ?reason, "upload::canceled"); - if let Err(e) = tx.send(Err(Error::TransferCanceled(reason))).await { - tracing::warn!(error = %e); + _= cancel.changed() => { + let reason = cancel.borrow_and_update().clone(); + if reason != crate::transfer::CancelReason::default() { + tracing::debug!( + reason = ?reason, + "upload::canceled", + ); + if let Err(error) = upload_channel.send(Err(Error::TransferCanceled(reason))).await { + tracing::warn!( + error = %error, + "http::send_transfer_canceled", + ); + } + + break; } } Some(chunk) = reader_stream.next() => { if let Ok(bytes) = &chunk { bytes_sent += bytes.len() as u64; - if let Err(e) = progress.send((bytes_sent, Some(file_size))).await { - tracing::warn!(error = %e); + if let Err(error) = progress.send((bytes_sent, Some(file_size))).await { + tracing::warn!( + error = %error, + "http::send_transfer_progress_update", + ); } } - if let Err(e) = tx.send(chunk.map_err(Error::from)).await { - tracing::error!(error = %e); + if let Err(error) = upload_channel.send(chunk.map_err(Error::from)).await { + tracing::error!( + error = %error, + "http::send_transfer_chunk", + ); break; } } @@ -506,7 +525,7 @@ impl FileSyncClient for HttpClient { } }); - let progress_stream = ReceiverStream::new(rx); + let upload_stream = ReceiverStream::new(rx); // Use a client without the read timeout // as this may be a long running request @@ -522,7 +541,7 @@ impl FileSyncClient for HttpClient { self.request_headers(request, sign_url.as_bytes()).await?; let response = request - .body(Body::wrap_stream(progress_stream)) + .body(Body::wrap_stream(upload_stream)) .send() .await?; let status = response.status(); diff --git a/crates/protocol/src/network_client/websocket.rs b/crates/protocol/src/network_client/websocket.rs index c94b060fce..46c5cc05f0 100644 --- a/crates/protocol/src/network_client/websocket.rs +++ b/crates/protocol/src/network_client/websocket.rs @@ -2,7 +2,7 @@ use crate::{ network_client::{NetworkRetry, WebSocketRequest}, transfer::CancelReason, - NetworkChangeEvent, Error, Result, WireEncodeDecode, + Error, NetworkChangeEvent, Result, WireEncodeDecode, }; use futures::{ stream::{Map, SplitStream}, @@ -246,7 +246,10 @@ impl WebSocketChangeListener { code: CloseCode::Normal, reason: Utf8Bytes::from_static("closed"), })).await { - tracing::warn!(error = ?error); + tracing::warn!( + error = ?error, + "ws_client::websocket::close_error", + ); } tracing::debug!("ws_client::shutdown"); return Ok(()); From 1e34d72c3dbbd904ff21bd29fac13861a2d4b2f6 Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 13 Mar 2025 14:29:42 +0800 Subject: [PATCH 34/61] Update default log level. --- crates/logs/src/logger.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/logs/src/logger.rs b/crates/logs/src/logger.rs index 2e153c81b1..2e2f8981e6 100644 --- a/crates/logs/src/logger.rs +++ b/crates/logs/src/logger.rs @@ -17,7 +17,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub const LOG_FILE_NAME: &str = "saveoursecrets.log"; const DEFAULT_LOG_LEVEL: &str = - "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug"; + "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug,sos_app=debug"; /// State of the log files on disc. pub struct LogFileStatus { From 660a87ebe6beca834a61adfcafaa298a80a1c9bc Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 13 Mar 2025 14:35:57 +0800 Subject: [PATCH 35/61] Do not call ensure() on paths on sign_in(). --- crates/account/src/local_account.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/account/src/local_account.rs b/crates/account/src/local_account.rs index 7e6d95bd21..a0cff9ec38 100644 --- a/crates/account/src/local_account.rs +++ b/crates/account/src/local_account.rs @@ -944,7 +944,6 @@ impl Account for LocalAccount { // Ensure all paths before sign_in let paths = self.paths().with_account_id(account_id); - paths.ensure().await?; tracing::debug!( account_id = %account_id, From cc704ba4c2bd70a0d9f7b9c1e0f7e2904972d027 Mon Sep 17 00:00:00 2001 From: muji Date: Fri, 14 Mar 2025 11:31:19 +0800 Subject: [PATCH 36/61] Remove PairTargetUrl. --- crates/net/src/pairing/mod.rs | 2 +- crates/net/src/pairing/share_url.rs | 66 ----------------------------- tests/unit/src/tests/pairing_url.rs | 27 +----------- 3 files changed, 2 insertions(+), 93 deletions(-) diff --git a/crates/net/src/pairing/mod.rs b/crates/net/src/pairing/mod.rs index dcd25d3efa..bf27af5ce8 100644 --- a/crates/net/src/pairing/mod.rs +++ b/crates/net/src/pairing/mod.rs @@ -6,7 +6,7 @@ mod websocket; pub use enrollment::DeviceEnrollment; pub use error::Error; -pub use share_url::{PairTargetUrl, ServerPairUrl}; +pub use share_url::ServerPairUrl; pub use websocket::{AcceptPairing, OfferPairing}; /// Result type for the pairing module. diff --git a/crates/net/src/pairing/share_url.rs b/crates/net/src/pairing/share_url.rs index de2a1c6d37..3a40c25642 100644 --- a/crates/net/src/pairing/share_url.rs +++ b/crates/net/src/pairing/share_url.rs @@ -5,72 +5,6 @@ use sos_core::{csprng, AccountId}; use std::str::FromStr; use url::Url; -/// Pair target URL encapsulates a server and account ID. -/// -/// It is used to transfer a server and account identifier -/// from a mobile device to a desktop for the inverted pairing flow. -/// -/// The URL format is `?aid=`. -#[derive(Debug, PartialEq, Eq)] -pub struct PairTargetUrl { - /// Server used to transfer the account data. - server: Url, - /// Account identifier. - account_id: AccountId, -} - -impl PairTargetUrl { - /// Create a new pair target URL. - pub fn new(server: Url, account_id: AccountId) -> Self { - Self { server, account_id } - } - - /// Server URL. - pub fn server(&self) -> &Url { - &self.server - } - - /// Account identifier. - pub fn account_id(&self) -> &AccountId { - &self.account_id - } -} - -impl From for Url { - fn from(value: PairTargetUrl) -> Self { - let mut url = value.server; - url.query_pairs_mut() - .append_pair(AID, &value.account_id.to_string()); - url - } -} - -impl TryFrom for PairTargetUrl { - type Error = Error; - - fn try_from(mut url: Url) -> Result { - let mut pairs = url.query_pairs(); - - let account_id = pairs.find_map(|q| { - if q.0.as_ref() == AID { - Some(q.1) - } else { - None - } - }); - let account_id = account_id.ok_or(Error::InvalidShareUrl)?; - let account_id: AccountId = account_id.as_ref().parse()?; - - url.set_query(None); - url.set_path(""); - - Ok(PairTargetUrl { - server: url, - account_id, - }) - } -} - /// Account identifier. const AID: &str = "aid"; /// Server URL. diff --git a/tests/unit/src/tests/pairing_url.rs b/tests/unit/src/tests/pairing_url.rs index e0a5d93083..3b8f04911e 100644 --- a/tests/unit/src/tests/pairing_url.rs +++ b/tests/unit/src/tests/pairing_url.rs @@ -1,35 +1,10 @@ use anyhow::Result; use sos_core::AccountId; -use sos_net::pairing::{PairTargetUrl, ServerPairUrl}; +use sos_net::pairing::ServerPairUrl; use url::Url; const SERVER: &str = "http://192.168.1.8:5053"; -#[test] -fn pair_target_url() -> Result<()> { - let mock_server: Url = SERVER.parse()?; - let mock_account_id = AccountId::random(); - let mock_url = - Url::parse(&format!("{}/?aid={}", mock_server, mock_account_id))?; - let pairing_target = - PairTargetUrl::new(mock_server.clone(), mock_account_id.clone()); - - assert_eq!(&mock_server, pairing_target.server()); - assert_eq!(&mock_account_id, pairing_target.account_id()); - - let parsed_target: PairTargetUrl = mock_url.try_into()?; - assert_eq!(pairing_target, parsed_target); - - Ok(()) -} - -#[test] -fn pair_target_url_errors() -> Result<()> { - // No account identifier in query string - assert!(PairTargetUrl::try_from(Url::parse(SERVER).unwrap()).is_err()); - Ok(()) -} - #[test] fn pair_server_url() -> Result<()> { let mock_account_id = AccountId::random(); From 020be3c8c354c38a6886bd59db606772c1e211ba Mon Sep 17 00:00:00 2001 From: muji Date: Fri, 14 Mar 2025 11:57:35 +0800 Subject: [PATCH 37/61] Tidy pairing websocket loops. --- crates/net/src/pairing/websocket.rs | 68 +++++++++++++++-------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/crates/net/src/pairing/websocket.rs b/crates/net/src/pairing/websocket.rs index fb9e0cf406..95e2c22fcc 100644 --- a/crates/net/src/pairing/websocket.rs +++ b/crates/net/src/pairing/websocket.rs @@ -2,9 +2,8 @@ use super::{DeviceEnrollment, Error, Result, ServerPairUrl}; use crate::NetworkAccount; use futures::{ - select, stream::{SplitSink, SplitStream}, - FutureExt, SinkExt, StreamExt, + SinkExt, StreamExt, }; use prost::bytes::Bytes; use snow::{Builder, HandshakeState, Keypair, TransportState}; @@ -70,7 +69,7 @@ enum IncomingAction { HandleMessage(PairingMessage), } -/// Listen for incoming messages on the stream. +/// Listen for incoming messages on the websocket stream. async fn listen( mut rx: WsStream, tx: mpsc::Sender, @@ -102,6 +101,7 @@ async fn listen( } } } + tracing::debug!("pairing::websocket::connection_closed"); } /// Offer is the device that is authenticated and can @@ -217,26 +217,29 @@ impl<'a> OfferPairing<'a> { let (close_tx, mut close_rx) = mpsc::channel::<()>(1); tokio::task::spawn(listen(stream, offer_tx, close_tx)); loop { - select! { - event = offer_rx.recv().fuse() => { - if let Some(event) = event { - self.incoming(event).await?; - if self.is_finished() { - break; - } + tokio::select! { + biased; + // Explicit shutdown notification + Some(_) = shutdown_rx.recv() => { + tracing::debug!("pairing::offer::shutdown_received"); + if let Err(error) = self.tx.send(Message::Close(Some(CloseFrame { + code: CloseCode::Normal, + reason: Utf8Bytes::from_static("closed"), + }))).await { + tracing::error!( + error = %error, + "pairing::offer::websocket_close_frame::error"); } + break; } - event = close_rx.recv().fuse() => { - if event.is_some() { - break; - } + // Close signal from the websocket stream + Some(_) = close_rx.recv() => { + break; } - event = shutdown_rx.recv().fuse() => { - if event.is_some() { - let _ = self.tx.send(Message::Close(Some(CloseFrame { - code: CloseCode::Normal, - reason: Utf8Bytes::from_static("closed"), - }))).await; + // Incoming event + Some(event) = offer_rx.recv() => { + self.incoming(event).await?; + if self.is_finished() { break; } } @@ -633,8 +636,18 @@ impl<'a> AcceptPairing<'a> { tokio::task::spawn(listen(stream, offer_tx, close_tx)); loop { - select! { - event = offer_rx.recv().fuse() => { + tokio::select! { + biased; + event = shutdown_rx.recv() => { + if event.is_some() { + let _ = self.tx.send(Message::Close(Some(CloseFrame { + code: CloseCode::Normal, + reason: Utf8Bytes::from_static("closed"), + }))).await; + break; + } + } + event = offer_rx.recv() => { if let Some(event) = event { self.incoming(event).await?; if self.is_finished() { @@ -642,17 +655,8 @@ impl<'a> AcceptPairing<'a> { } } } - event = close_rx.recv().fuse() => { - if event.is_some() { - break; - } - } - event = shutdown_rx.recv().fuse() => { + event = close_rx.recv() => { if event.is_some() { - let _ = self.tx.send(Message::Close(Some(CloseFrame { - code: CloseCode::Normal, - reason: Utf8Bytes::from_static("closed"), - }))).await; break; } } From cad121796e9b3c52a8370e0ff25f0fe92930247a Mon Sep 17 00:00:00 2001 From: muji Date: Fri, 14 Mar 2025 18:44:22 +0800 Subject: [PATCH 38/61] Remove stale files in changes producer. --- crates/changes/src/producer.rs | 72 ++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs index 3fcb1d5a61..516efd4c73 100644 --- a/crates/changes/src/producer.rs +++ b/crates/changes/src/producer.rs @@ -2,8 +2,11 @@ use crate::{Error, Result}; use futures::sink::SinkExt; use interprocess::local_socket::{tokio::prelude::*, GenericNamespaced}; -use sos_core::{events::changes_feed, Paths}; -use std::{sync::Arc, time::Duration}; +use sos_core::{ + events::{changes_feed, LocalChangeEvent}, + Paths, +}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::{ select, sync::{watch, Mutex}, @@ -59,41 +62,26 @@ impl ChangeProducer { loop { let paths = paths.clone(); select! { + // Explicit cancel notification _ = cancel_rx.changed() => { if *cancel_rx.borrow_and_update() { break; } } + // Periodically refresh the list of consumer sockets + // to dispatch change events to _ = interval.tick() => { let active = find_active_sockets(paths).await?; let mut sockets = sockets.lock().await; *sockets = active; } + // Proxy the change events to the consumer sockets event = rx.changed() => { match event { Ok(_) => { let event = rx.borrow_and_update().clone(); let sockets = sockets.lock().await; - for pid in &*sockets { - let ps_name = pid.to_string(); - let name = ps_name.to_ns_name::()?; - match LocalSocketStream::connect(name).await { - Ok(socket) => { - let mut writer = - LengthDelimitedCodec::builder() - .native_endian() - .new_write(socket); - let message = serde_json::to_vec(&event)?; - writer.send(message.into()).await?; - } - Err(e) => { - tracing::warn!( - pid = %pid, - error = %e, - "changes::producer::connect_error"); - } - } - } + dispatch_sockets(event, &*sockets).await?; } Err(_) => {} } @@ -106,8 +94,44 @@ impl ChangeProducer { } } +async fn dispatch_sockets( + event: LocalChangeEvent, + sockets: &[(u32, PathBuf)], +) -> Result<()> { + for (pid, file) in sockets { + let ps_name = pid.to_string(); + let name = ps_name.to_ns_name::()?; + match LocalSocketStream::connect(name).await { + Ok(socket) => { + let mut writer = LengthDelimitedCodec::builder() + .native_endian() + .new_write(socket); + let message = serde_json::to_vec(&event)?; + writer.send(message.into()).await?; + } + Err(e) => { + // If we can't connect to the socket + // then treat the file as stale and + // remove from disc. + // + // This could happen if the consumer + // process aborted abnormally and + // wasn't able to cleanly remove the file. + let _ = std::fs::remove_file(file)?; + tracing::warn!( + pid = %pid, + error = %e, + "changes::producer::connect_error"); + } + } + } + Ok(()) +} + /// Find active socket files for a producer. -async fn find_active_sockets(paths: Arc) -> Result> { +async fn find_active_sockets( + paths: Arc, +) -> Result> { use std::fs::read_dir; let mut sockets = Vec::new(); let socks = paths.documents_dir().join(crate::SOCKS); @@ -126,7 +150,7 @@ async fn find_active_sockets(paths: Arc) -> Result> { sock_file_pid = %pid, "changes::producer::find_active_sockets", ); - sockets.push(pid); + sockets.push((pid, entry.path().to_owned())); } } } From 23d6c9b558e0885d6d99b3d9daa2e3e653800d0a Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 14:40:15 +0800 Subject: [PATCH 39/61] Support updating audit trail providers. --- crates/backend/src/audit.rs | 28 ++++++++++++++++++++-------- crates/backend/src/lib.rs | 1 - 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/backend/src/audit.rs b/crates/backend/src/audit.rs index d45e568141..478e0260e6 100644 --- a/crates/backend/src/audit.rs +++ b/crates/backend/src/audit.rs @@ -10,33 +10,45 @@ pub type AuditProvider = type AuditProviders = Vec; -static PROVIDERS: OnceLock = OnceLock::new(); +static mut PROVIDERS: OnceLock = OnceLock::new(); /// Initialize audit trail providers. pub fn init_providers(providers: AuditProviders) { - PROVIDERS.get_or_init(|| providers); + unsafe { + PROVIDERS.get_or_init(|| providers); + } +} + +/// Update audit trail providers. +/// +/// # Panics +/// +/// If the audit trail providers have not already been set. +pub fn update_providers(providers: AuditProviders) { + unsafe { + let audit_providers = PROVIDERS.get_mut().unwrap(); + *audit_providers = providers; + } } /// Configured audit providers. pub fn providers<'a>() -> Option<&'a AuditProviders> { - PROVIDERS.get() + unsafe { PROVIDERS.get() } } /// Append audit events to all configured providers. pub async fn append_audit_events(events: &[AuditEvent]) -> Result<()> { #[cfg(not(debug_assertions))] { - let providers = PROVIDERS - .get() - .ok_or_else(|| Error::AuditProvidersNotConfigured)?; + let providers = + providers().ok_or_else(|| Error::AuditProvidersNotConfigured)?; for provider in providers { provider.append_audit_events(events).await?; } } #[cfg(debug_assertions)] { - let providers = PROVIDERS.get(); - if let Some(providers) = providers { + if let Some(providers) = providers() { for provider in providers { provider.append_audit_events(events).await?; } diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index 3fc71cddba..fde004f5a6 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -1,5 +1,4 @@ #![deny(missing_docs)] -#![forbid(unsafe_code)] #![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] //! Backend database and file system storage. mod access_point; From 0701090743c42331562d78f9a5687af67bc1204e Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 14:46:39 +0800 Subject: [PATCH 40/61] Update file deletion logic in upgrade accounts. --- crates/backend/src/audit.rs | 2 ++ crates/core/src/paths.rs | 7 ++++++- crates/database_upgrader/src/upgrader/mod.rs | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/backend/src/audit.rs b/crates/backend/src/audit.rs index 478e0260e6..e3e757b45e 100644 --- a/crates/backend/src/audit.rs +++ b/crates/backend/src/audit.rs @@ -46,6 +46,8 @@ pub async fn append_audit_events(events: &[AuditEvent]) -> Result<()> { provider.append_audit_events(events).await?; } } + + // For test specs we don't require an audit trail #[cfg(debug_assertions)] { if let Some(providers) = providers() { diff --git a/crates/core/src/paths.rs b/crates/core/src/paths.rs index 2862ade3c1..d95500c69d 100644 --- a/crates/core/src/paths.rs +++ b/crates/core/src/paths.rs @@ -330,7 +330,7 @@ impl Paths { /// account-level preferences. pub fn preferences_file(&self) -> PathBuf { let mut path = if self.is_global() { - self.documents_dir().join(PREFERENCES_FILE) + self.global_preferences_file() } else { self.user_dir().join(PREFERENCES_FILE) }; @@ -338,6 +338,11 @@ impl Paths { path } + /// Path to the global preferences file. + pub fn global_preferences_file(&self) -> PathBuf { + self.documents_dir().join(PREFERENCES_FILE) + } + /// Path to the file used to store account-level system messages. /// /// # Panics diff --git a/crates/database_upgrader/src/upgrader/mod.rs b/crates/database_upgrader/src/upgrader/mod.rs index 34c4295bb2..3553f03fac 100644 --- a/crates/database_upgrader/src/upgrader/mod.rs +++ b/crates/database_upgrader/src/upgrader/mod.rs @@ -435,6 +435,7 @@ async fn delete_stale_files( let mut files = vec![ options.paths.identity_dir().to_owned(), options.paths.audit_file().to_owned(), + options.paths.global_preferences_file().to_owned(), ]; for account in accounts { @@ -461,5 +462,12 @@ async fn delete_stale_files( } } + // Can error if the local directory is not empty! + if let Err(error) = vfs::remove_dir(options.paths.local_dir()).await { + tracing::error!( + error = %error, + "updgrade_accounts::remove_local_dir"); + } + Ok(files) } From d220a005412b69b240cd3d5b99305789c24e8b90 Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 14:48:49 +0800 Subject: [PATCH 41/61] Fix typo. --- crates/database_upgrader/src/upgrader/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/database_upgrader/src/upgrader/mod.rs b/crates/database_upgrader/src/upgrader/mod.rs index 3553f03fac..906ffa8f38 100644 --- a/crates/database_upgrader/src/upgrader/mod.rs +++ b/crates/database_upgrader/src/upgrader/mod.rs @@ -466,7 +466,7 @@ async fn delete_stale_files( if let Err(error) = vfs::remove_dir(options.paths.local_dir()).await { tracing::error!( error = %error, - "updgrade_accounts::remove_local_dir"); + "upgrade_accounts::remove_local_dir"); } Ok(files) From 8abecad95a196b0c9a3d3f85c857d99703222980 Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 14:50:59 +0800 Subject: [PATCH 42/61] Update default log level. --- crates/database_upgrader/src/upgrader/mod.rs | 2 +- crates/logs/src/logger.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/database_upgrader/src/upgrader/mod.rs b/crates/database_upgrader/src/upgrader/mod.rs index 906ffa8f38..e6d420451e 100644 --- a/crates/database_upgrader/src/upgrader/mod.rs +++ b/crates/database_upgrader/src/upgrader/mod.rs @@ -435,7 +435,7 @@ async fn delete_stale_files( let mut files = vec![ options.paths.identity_dir().to_owned(), options.paths.audit_file().to_owned(), - options.paths.global_preferences_file().to_owned(), + options.paths.global_preferences_file(), ]; for account in accounts { diff --git a/crates/logs/src/logger.rs b/crates/logs/src/logger.rs index 2e2f8981e6..9f87942631 100644 --- a/crates/logs/src/logger.rs +++ b/crates/logs/src/logger.rs @@ -17,7 +17,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub const LOG_FILE_NAME: &str = "saveoursecrets.log"; const DEFAULT_LOG_LEVEL: &str = - "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug,sos_app=debug"; + "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug,sos_app=debug,sos_database_upgrader=debug"; /// State of the log files on disc. pub struct LogFileStatus { From 6c391f5ad7273335ad82bc48d8c7f494e8a6f24c Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 14:58:24 +0800 Subject: [PATCH 43/61] Update logic for db upgrade file deletion. --- crates/core/src/paths.rs | 6 ++++-- crates/database_upgrader/src/upgrader/mod.rs | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/core/src/paths.rs b/crates/core/src/paths.rs index d95500c69d..a0c722175b 100644 --- a/crates/core/src/paths.rs +++ b/crates/core/src/paths.rs @@ -330,7 +330,7 @@ impl Paths { /// account-level preferences. pub fn preferences_file(&self) -> PathBuf { let mut path = if self.is_global() { - self.global_preferences_file() + self.documents_dir().join(PREFERENCES_FILE) } else { self.user_dir().join(PREFERENCES_FILE) }; @@ -340,7 +340,9 @@ impl Paths { /// Path to the global preferences file. pub fn global_preferences_file(&self) -> PathBuf { - self.documents_dir().join(PREFERENCES_FILE) + let mut path = self.documents_dir().join(PREFERENCES_FILE); + path.set_extension(JSON_EXT); + path } /// Path to the file used to store account-level system messages. diff --git a/crates/database_upgrader/src/upgrader/mod.rs b/crates/database_upgrader/src/upgrader/mod.rs index e6d420451e..c32027bb58 100644 --- a/crates/database_upgrader/src/upgrader/mod.rs +++ b/crates/database_upgrader/src/upgrader/mod.rs @@ -436,6 +436,7 @@ async fn delete_stale_files( options.paths.identity_dir().to_owned(), options.paths.audit_file().to_owned(), options.paths.global_preferences_file(), + options.paths.local_dir().join(".DS_Store"), ]; for account in accounts { From 0f3b770c9c13b89c9a8504c38af828712c379e3b Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 16:26:44 +0800 Subject: [PATCH 44/61] Revert changes to audit module. --- crates/backend/src/audit.rs | 20 +++----------------- crates/backend/src/lib.rs | 1 + 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/crates/backend/src/audit.rs b/crates/backend/src/audit.rs index e3e757b45e..103b1ad514 100644 --- a/crates/backend/src/audit.rs +++ b/crates/backend/src/audit.rs @@ -10,30 +10,16 @@ pub type AuditProvider = type AuditProviders = Vec; -static mut PROVIDERS: OnceLock = OnceLock::new(); +static PROVIDERS: OnceLock = OnceLock::new(); /// Initialize audit trail providers. pub fn init_providers(providers: AuditProviders) { - unsafe { - PROVIDERS.get_or_init(|| providers); - } -} - -/// Update audit trail providers. -/// -/// # Panics -/// -/// If the audit trail providers have not already been set. -pub fn update_providers(providers: AuditProviders) { - unsafe { - let audit_providers = PROVIDERS.get_mut().unwrap(); - *audit_providers = providers; - } + PROVIDERS.get_or_init(|| providers); } /// Configured audit providers. pub fn providers<'a>() -> Option<&'a AuditProviders> { - unsafe { PROVIDERS.get() } + PROVIDERS.get() } /// Append audit events to all configured providers. diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index fde004f5a6..3fc71cddba 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -1,4 +1,5 @@ #![deny(missing_docs)] +#![forbid(unsafe_code)] #![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] //! Backend database and file system storage. mod access_point; From 30d7008871936efba8ef421da996a0e63e6026f8 Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 16:38:43 +0800 Subject: [PATCH 45/61] Improve handling of closed channels. --- crates/database/src/audit_provider.rs | 16 +++++++++------- crates/database/src/event_log.rs | 15 ++++++++++----- crates/filesystem/src/audit_provider.rs | 3 +++ crates/filesystem/src/event_log.rs | 3 +++ 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/database/src/audit_provider.rs b/crates/database/src/audit_provider.rs index 193cd1fae9..2cac910e04 100644 --- a/crates/database/src/audit_provider.rs +++ b/crates/database/src/audit_provider.rs @@ -100,23 +100,25 @@ where let mut rows = stmt.query([])?; while let Some(row) = rows.next()? { + if tx.is_closed() { + break; + } let row: AuditRow = row.try_into()?; let record: AuditRecord = row.try_into()?; let inner_tx = tx.clone(); - futures::executor::block_on(async move { - if let Err(e) = - inner_tx.send(Ok(record.event)).await - { - tracing::error!(error = %e); - } + let res = futures::executor::block_on(async move { + inner_tx.send(Ok(record.event)).await }); + if let Err(e) = res { + tracing::error!(error = %e); + break; + } } Ok::<_, Error>(()) }) .await .map_err(Error::from)?; - Ok::<_, Self::Error>(()) }); diff --git a/crates/database/src/event_log.rs b/crates/database/src/event_log.rs index 1898e97ba5..c080782135 100644 --- a/crates/database/src/event_log.rs +++ b/crates/database/src/event_log.rs @@ -393,14 +393,19 @@ where })?; for row in rows { + if tx.is_closed() { + break; + } let row = row?; let record: EventRecord = row.try_into()?; - let sender = tx.clone(); - futures::executor::block_on(async move { - if let Err(err) = sender.send(Ok(record)).await { - tracing::error!(error = %err); - } + let inner_tx = tx.clone(); + let res = futures::executor::block_on(async move { + inner_tx.send(Ok(record)).await }); + if let Err(e) = res { + tracing::error!(error = %e); + break; + } } Ok::<_, Error>(()) diff --git a/crates/filesystem/src/audit_provider.rs b/crates/filesystem/src/audit_provider.rs index f7b5b7f752..4424a09dad 100644 --- a/crates/filesystem/src/audit_provider.rs +++ b/crates/filesystem/src/audit_provider.rs @@ -222,6 +222,9 @@ where let it_file = self.file.clone(); tokio::task::spawn(async move { while let Some(record) = it.next().await? { + if tx.is_closed() { + break; + } let mut inner = it_file.lock().await; let event = inner.read_event(&record).await?; if let Err(e) = tx.send(Ok(event)).await { diff --git a/crates/filesystem/src/event_log.rs b/crates/filesystem/src/event_log.rs index a56352e57a..495ca0ef17 100644 --- a/crates/filesystem/src/event_log.rs +++ b/crates/filesystem/src/event_log.rs @@ -142,6 +142,9 @@ where let file_path = self.data.clone(); tokio::task::spawn(async move { while let Some(record) = it.next().await? { + if tx.is_closed() { + break; + } let event_buffer = read_event_buffer(file_path.clone(), &record).await?; let event_record = record.into_event_record(event_buffer); From 6e04806d193c6c66d635717aa764cdf770eff0ca Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 18:13:31 +0800 Subject: [PATCH 46/61] Remove duplicate migration logs in db upgrade. --- crates/database_upgrader/src/upgrader/mod.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/database_upgrader/src/upgrader/mod.rs b/crates/database_upgrader/src/upgrader/mod.rs index c32027bb58..2a42565181 100644 --- a/crates/database_upgrader/src/upgrader/mod.rs +++ b/crates/database_upgrader/src/upgrader/mod.rs @@ -114,13 +114,7 @@ async fn import_accounts( let mut client = open_file_with_journal_mode(db_file, JournalMode::Memory).await?; - let report = migrate_client(&mut client).await?; - for migration in report.applied_migrations() { - tracing::debug!( - name = %migration.name(), - version = %migration.version(), - "import_accounts::migration",); - } + migrate_client(&mut client).await?; client } else { open_memory().await? From 39625cc88bcdfcb38ec6f62a1617505ed7f935bd Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 18:15:43 +0800 Subject: [PATCH 47/61] Delete files after moving db into place. In the database upgrade logic. --- crates/database_upgrader/src/upgrader/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/database_upgrader/src/upgrader/mod.rs b/crates/database_upgrader/src/upgrader/mod.rs index 2a42565181..c475450faf 100644 --- a/crates/database_upgrader/src/upgrader/mod.rs +++ b/crates/database_upgrader/src/upgrader/mod.rs @@ -215,13 +215,6 @@ pub async fn upgrade_accounts( copy_file_blobs(accounts.as_slice(), &options).await?; } - if !options.keep_stale_files { - tracing::debug!("upgrade_accounts::delete_stale_files"); - - result.deleted_files = - delete_stale_files(accounts.as_slice(), &options).await?; - } - result.accounts = accounts; result.database_file = options.paths.database_file().to_owned(); @@ -235,6 +228,13 @@ pub async fn upgrade_accounts( vfs::rename(db_temp.path(), options.paths.database_file()).await?; } + if !options.keep_stale_files { + tracing::debug!("upgrade_accounts::delete_stale_files"); + + result.deleted_files = + delete_stale_files(result.accounts.as_slice(), &options).await?; + } + Ok(result) } From 27d2c8ac654fac6f3a6fc4c080c10d6d36d50613 Mon Sep 17 00:00:00 2001 From: muji Date: Sat, 15 Mar 2025 18:21:37 +0800 Subject: [PATCH 48/61] Add Debug impl for BackendTarget. --- crates/backend/src/lib.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index 3fc71cddba..1863d2cce7 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -98,6 +98,23 @@ impl fmt::Display for BackendTarget { } } +impl fmt::Debug for BackendTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", { + match self { + Self::FileSystem(paths) => format!( + "file:{}", + paths.documents_dir().to_string_lossy() + ), + Self::Database(paths, _) => format!( + "sqlite:{}", + paths.database_file().to_string_lossy() + ), + } + }) + } +} + impl BackendTarget { /// Infer and initialize a new backend target. /// From d8dc58942bcdaf9e99cc59c8b064986510e86f03 Mon Sep 17 00:00:00 2001 From: muji Date: Fri, 18 Apr 2025 11:30:11 +0800 Subject: [PATCH 49/61] Start refactor of ipc changes handling. So that we remove the file system watching logic and receive events from the changes consumer instead. --- Cargo.lock | 1 + crates/ipc/Cargo.toml | 1 + crates/ipc/src/error.rs | 4 + crates/ipc/src/extension_helper/server.rs | 12 +- crates/ipc/src/web_service/account.rs | 157 +++--- crates/ipc/src/web_service/mod.rs | 6 +- crates/ipc/src/web_service/web_accounts2.rs | 578 ++++++++++++++++++++ 7 files changed, 669 insertions(+), 90 deletions(-) create mode 100644 crates/ipc/src/web_service/web_accounts2.rs diff --git a/Cargo.lock b/Cargo.lock index a0ed1d3d5e..51ccb60fde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5503,6 +5503,7 @@ dependencies = [ "serde_with", "sos-account", "sos-backend", + "sos-changes", "sos-client-storage", "sos-core", "sos-database", diff --git a/crates/ipc/Cargo.toml b/crates/ipc/Cargo.toml index c1432de8c2..032d38a522 100644 --- a/crates/ipc/Cargo.toml +++ b/crates/ipc/Cargo.toml @@ -58,6 +58,7 @@ extension-helper-client = [ [dependencies] sos-account.workspace = true sos-core.workspace = true +sos-changes.workspace = true sos-client-storage.workspace = true sos-backend.workspace = true sos-database.workspace = true diff --git a/crates/ipc/src/error.rs b/crates/ipc/src/error.rs index 598003bf10..6c681c543a 100644 --- a/crates/ipc/src/error.rs +++ b/crates/ipc/src/error.rs @@ -75,6 +75,10 @@ pub enum Error { #[error(transparent)] Network(#[from] sos_protocol::NetworkError), + /// Errors generated by the changes library. + #[error(transparent)] + Changes(#[from] sos_changes::Error), + /// Errors generated on conflict. #[error(transparent)] Conflict(#[from] sos_protocol::ConflictError), diff --git a/crates/ipc/src/extension_helper/server.rs b/crates/ipc/src/extension_helper/server.rs index a5e27e4ca7..5255a870c4 100644 --- a/crates/ipc/src/extension_helper/server.rs +++ b/crates/ipc/src/extension_helper/server.rs @@ -12,7 +12,8 @@ use http::{ StatusCode, }; use sos_account::{Account, AccountSwitcher}; -use sos_core::ErrorExt; +use sos_changes::consumer::ChangeConsumer; +use sos_core::{ErrorExt, Paths}; use sos_login::DelegatedAccess; use sos_logs::Logger; use sos_protocol::{constants::MIME_TYPE_JSON, ErrorReply}; @@ -113,8 +114,15 @@ where } tracing::info!(options = ?options, "extension_helper"); - + let paths = { + let accounts = accounts.read().await; + accounts.paths().unwrap_or_else(|| { + Paths::new_client(Paths::data_dir().unwrap()) + }) + }; let accounts = WebAccounts::new(accounts); + let changes_consumer = ChangeConsumer::listen(paths.clone())?; + accounts.listen_changes(changes_consumer, paths)?; let client = LocalMemoryServer::listen( accounts.clone(), options.service_info.clone(), diff --git a/crates/ipc/src/web_service/account.rs b/crates/ipc/src/web_service/account.rs index 77590b2f28..13f0e6f1ee 100644 --- a/crates/ipc/src/web_service/account.rs +++ b/crates/ipc/src/web_service/account.rs @@ -232,7 +232,7 @@ where /// Sign in to an account pub async fn sign_in_password( - accounts: WebAccounts, + mut accounts: WebAccounts, account_id: AccountId, password: SecretString, save_password: bool, @@ -257,52 +257,43 @@ where + 'static, { use sos_platform_authenticator::keyring_password; + { + let mut user_accounts = accounts.as_ref().write().await; + let Some(account) = user_accounts + .iter_mut() + .find(|a| a.account_id() == &account_id) + else { + return status(StatusCode::NOT_FOUND); + }; - let mut user_accounts = accounts.as_ref().write().await; - let Some(account) = user_accounts - .iter_mut() - .find(|a| a.account_id() == &account_id) - else { - return status(StatusCode::NOT_FOUND); - }; - - let key: AccessKey = password.clone().into(); - - let folder_ids = if let Ok(folders) = account.list_folders().await { - folders.into_iter().map(|f| *f.id()).collect::>() - } else { - vec![] - }; + let key: AccessKey = password.clone().into(); - match account.sign_in(&key).await { - Ok(_) => { - if let Err(e) = - accounts.watch(account_id, account.paths(), folder_ids) - { - tracing::error!(error = ?e); + match account.sign_in(&key).await { + Ok(_) => {} + Err(e) => { + if e.is_permission_denied() { + return status(StatusCode::FORBIDDEN); + } else { + return internal_server_error(e); + } } } - Err(e) => { - if e.is_permission_denied() { - return status(StatusCode::FORBIDDEN); - } else { + + if let Err(e) = account.initialize_search_index().await { + return internal_server_error(e); + } + + if save_password && keyring_password::supported() { + if let Err(e) = keyring_password::save_account_password( + &account_id.to_string(), + password, + ) { return internal_server_error(e); } } } - if let Err(e) = account.initialize_search_index().await { - return internal_server_error(e); - } - - if save_password && keyring_password::supported() { - if let Err(e) = keyring_password::save_account_password( - &account_id.to_string(), - password, - ) { - return internal_server_error(e); - } - } + accounts.watch(account_id); status(StatusCode::OK) } @@ -373,7 +364,7 @@ where /// Sign out of an account pub async fn sign_out( - accounts: WebAccounts, + mut accounts: WebAccounts, account_id: Option, ) -> hyper::Result> where @@ -395,61 +386,55 @@ where + Sync + 'static, { - let mut user_accounts = accounts.as_ref().write().await; if let Some(account_id) = account_id { - let Some(account) = user_accounts - .iter_mut() - .find(|a| a.account_id() == &account_id) - else { - return status(StatusCode::NOT_FOUND); - }; - - let folder_ids = if let Ok(folders) = account.list_folders().await { - folders.into_iter().map(|f| *f.id()).collect::>() - } else { - vec![] - }; + { + let mut user_accounts = accounts.as_ref().write().await; + let Some(account) = user_accounts + .iter_mut() + .find(|a| a.account_id() == &account_id) + else { + return status(StatusCode::NOT_FOUND); + }; - match account.sign_out().await { - Ok(_) => { - if let Err(e) = - accounts.unwatch(&account_id, account.paths(), folder_ids) - { - return internal_server_error(e); - } - status(StatusCode::OK) + match account.sign_out().await { + Ok(_) => {} + Err(e) => return internal_server_error(e), } - Err(e) => internal_server_error(e), } + + accounts.unwatch(&account_id); + status(StatusCode::OK) } else { - let mut account_info = Vec::new(); - for account in user_accounts.iter() { - let folder_ids = if let Ok(folders) = account.list_folders().await - { - folders.into_iter().map(|f| *f.id()).collect::>() - } else { - vec![] - }; + let account_info = { + let mut user_accounts = accounts.as_ref().write().await; + let mut account_info = Vec::new(); + for account in user_accounts.iter() { + let folder_ids = if let Ok(folders) = + account.list_folders().await + { + folders.into_iter().map(|f| *f.id()).collect::>() + } else { + vec![] + }; - account_info.push(( - *account.account_id(), - account.paths(), - folder_ids, - )); - } + account_info.push(( + *account.account_id(), + account.paths(), + folder_ids, + )); + } - match user_accounts.sign_out_all().await { - Ok(_) => { - for (account_id, paths, folder_ids) in account_info { - if let Err(e) = - accounts.unwatch(&account_id, paths, folder_ids) - { - return internal_server_error(e); - } - } - status(StatusCode::OK) + match user_accounts.sign_out_all().await { + Ok(_) => {} + Err(e) => return internal_server_error(e), } - Err(e) => internal_server_error(e), + account_info + }; + + for (account_id, _, _) in account_info { + accounts.unwatch(&account_id); } + + status(StatusCode::OK) } } diff --git a/crates/ipc/src/web_service/mod.rs b/crates/ipc/src/web_service/mod.rs index 695f86be39..7221cc7cde 100644 --- a/crates/ipc/src/web_service/mod.rs +++ b/crates/ipc/src/web_service/mod.rs @@ -28,14 +28,16 @@ mod common; mod helpers; mod search; mod secret; -mod web_accounts; +// mod web_accounts; +mod web_accounts2; use account::*; use common::*; use helpers::*; use search::*; use secret::*; -pub use web_accounts::*; +// pub use web_accounts::*; +pub use web_accounts2::*; async fn index( app_info: Arc, diff --git a/crates/ipc/src/web_service/web_accounts2.rs b/crates/ipc/src/web_service/web_accounts2.rs new file mode 100644 index 0000000000..84fc728a22 --- /dev/null +++ b/crates/ipc/src/web_service/web_accounts2.rs @@ -0,0 +1,578 @@ +use crate::{Error, FileEventError, Result}; +use serde::{Deserialize, Serialize}; +use sos_account::{Account, AccountSwitcher}; +use sos_backend::BackendTarget; +use sos_changes::consumer::ConsumerHandle; +use sos_core::{ + events::{ + AccountEvent, EventLog, EventLogType, LocalChangeEvent, WriteEvent, + }, + AccountId, ErrorExt, Paths, VaultId, +}; +use sos_login::DelegatedAccess; +use sos_sync::SyncStorage; +use sos_vault::SecretAccess; +use std::{collections::HashSet, sync::Arc}; +use tokio::sync::{broadcast, RwLock}; + +/// Event broadcast when an account changes. +#[typeshare::typeshare] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountChangeEvent { + /// Account identifier. + pub account_id: AccountId, + /// Event records with information about the changes. + pub records: ChangeRecords, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ChangeRecords { + /// Account level events. + Account(Vec), + /// Folder level events. + Folder(VaultId, Vec), +} + +impl ChangeRecords { + /// Determine if the records are empty. + pub fn is_empty(&self) -> bool { + match self { + Self::Account(records) => records.is_empty(), + Self::Folder(_, records) => records.is_empty(), + } + } +} + +/// User accounts for the web service. +pub struct WebAccounts +where + A: Account + SyncStorage, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + accounts: Arc>>, + watched_accounts: HashSet, + channel: broadcast::Sender, +} + +impl Clone for WebAccounts +where + A: Account + SyncStorage, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + fn clone(&self) -> Self { + Self { + accounts: self.accounts.clone(), + watched_accounts: self.watched_accounts.clone(), + channel: self.channel.clone(), + } + } +} + +impl WebAccounts +where + A: Account + + SyncStorage + + DelegatedAccess, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + /// Create new accounts. + pub fn new(accounts: Arc>>) -> Self { + let (tx, _) = broadcast::channel::(64); + Self { + accounts, + watched_accounts: HashSet::new(), + channel: tx, + } + } + + /// Create a backend target for the accounts. + pub async fn backend_target(&self) -> Result { + let accounts = self.accounts.read().await; + let paths = if let Some(paths) = accounts.paths() { + paths + } else { + Paths::new_client(Paths::data_dir().unwrap()) + }; + Ok(BackendTarget::from_paths(&paths).await?) + } + + /// Subscribe to change events. + pub fn subscribe(&self) -> broadcast::Receiver { + self.channel.subscribe() + } + + /// Start listening for changes. + pub fn listen_changes( + &self, + mut changes_consumer: ConsumerHandle, + paths: Arc, + ) -> Result<()> { + // Start a background task to listen for change events + let channel = self.channel.clone(); + let task_accounts = self.accounts.clone(); + + tokio::task::spawn(async move { + let receiver = changes_consumer.changes(); + + while let Some(event) = receiver.recv().await { + tracing::debug!( + event = ?event, + "change_consumer::event_received" + ); + + // The account_id is included in the event variants + if let Err(e) = process_change_event( + event, + // We don't need to pass account_id separately as it's in the event + // Passing default here as it's not used in this form + AccountId::default(), + paths.clone(), + task_accounts.clone(), + channel.clone(), + ) + .await + { + tracing::error!(error = %e, "process_change_event"); + } + } + + tracing::debug!("consumer_task_completed"); + }); + + Ok(()) + } + + /// Start watching an account for changes. + pub fn watch(&mut self, account_id: AccountId) { + self.watched_accounts.insert(account_id); + } + + /// Stop watching for changes. + pub fn unwatch(&mut self, account_id: &AccountId) { + self.watched_accounts.remove(account_id); + } +} + +impl AsRef>>> + for WebAccounts +where + A: Account + SyncStorage, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + fn as_ref(&self) -> &Arc>> { + &self.accounts + } +} + +/// Process change events and update the system state accordingly +async fn process_change_event( + event: LocalChangeEvent, + _account_id_unused: AccountId, // Keeping for backward compatibility but not using + paths: Arc, + accounts: Arc>>, + channel: broadcast::Sender, +) -> Result<()> +where + A: Account + + SyncStorage + + DelegatedAccess + + Sync + + Send + + 'static, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + match event { + LocalChangeEvent::Init => { + // Just initialization, nothing to do + Ok(()) + } + LocalChangeEvent::AccountCreated(account_id) => { + // New account created + // NOTE: This would need implementation if we want to handle new accounts + tracing::debug!(account_id = %account_id, "account_created_event"); + // TODO: Implement account creation handling if needed + Ok(()) + } + LocalChangeEvent::AccountDeleted(account_id) => { + // Account deleted + tracing::debug!(account_id = %account_id, "account_deleted_event"); + // TODO: Implement account deletion handling if needed + Ok(()) + } + LocalChangeEvent::AccountModified { + account_id, + log_type, + commit_span: _, + } => { + // Account was modified - process changes based on log_type + tracing::debug!(account_id = %account_id, log_type = ?log_type, "account_modified_event"); + + match log_type { + EventLogType::Identity + | EventLogType::Account + | EventLogType::Device => { + // Account-level changes + let mut accounts_lock = accounts.write().await; + let account = accounts_lock + .iter_mut() + .find(|a| a.account_id() == &account_id) + .ok_or(Error::from(FileEventError::NoAccount( + account_id.clone(), + )))?; + + // Reload the identity folder for account-level changes + account.reload_login_folder().await.map_err(|e| { + Error::from(FileEventError::ReloadIdentityFolder( + e.to_string(), + )) + })?; + + // Load account events + let records = load_account_records(account).await?; + + // Update folders in memory + tracing::debug!("account_change::load_folders"); + if let Err(e) = account.load_folders().await { + tracing::error!(error = %e, "load_folders_error"); + } + + // Update search index + let records_clone = + ChangeRecords::Account(records.clone()); + update_account_search_index(account, &records_clone) + .await + .map_err(|e| { + Error::from(FileEventError::UpdateSearchIndex( + e.to_string(), + )) + })?; + + // Send event if there are records + if !records.is_empty() { + let evt = AccountChangeEvent { + account_id: account_id.clone(), + records: ChangeRecords::Account(records), + }; + if let Err(e) = channel.send(evt) { + tracing::error!(error = ?e, "account_channel::send"); + } + } + Ok(()) + } + EventLogType::Folder(folder_id) => { + // Folder-level changes + let accounts_lock = accounts.read().await; + let account = accounts_lock + .iter() + .find(|a| a.account_id() == &account_id) + .ok_or(Error::from(FileEventError::NoAccount( + account_id.clone(), + )))?; + + let folder = + account.folder(&folder_id).await.ok().ok_or( + Error::from(FileEventError::NoFolder(folder_id)), + )?; + + let event_log = folder.event_log(); + let mut event_log = event_log.write().await; + let commit = event_log.tree().last_commit(); + let patch = + event_log.diff_events(commit.as_ref()).await?; + let records = patch.into_events::().await?; + + event_log.load_tree().await?; + + // Update search index for folder changes + { + let mut accounts_lock = accounts.write().await; + if let Some(account) = accounts_lock + .iter_mut() + .find(|a| a.account_id() == &account_id) + { + let records_clone = ChangeRecords::Folder( + folder_id, + records.clone(), + ); + update_account_search_index( + account, + &records_clone, + ) + .await + .map_err(|e| { + Error::from( + FileEventError::UpdateSearchIndex( + e.to_string(), + ), + ) + })?; + } + } + + // Send event if there are records + if !records.is_empty() { + let evt = AccountChangeEvent { + account_id: account_id.clone(), + records: ChangeRecords::Folder( + folder_id, records, + ), + }; + if let Err(e) = channel.send(evt) { + tracing::error!(error = ?e, "account_channel::send"); + } + } + + Ok(()) + } + #[cfg(feature = "files")] + EventLogType::Files => { + // No need to handle file change events + Ok(()) + } + } + } + } +} + +/// Update the search index for an account. +async fn update_account_search_index( + account: &mut A, + records: &ChangeRecords, +) -> std::result::Result<(), E> +where + A: Account + + SyncStorage + + DelegatedAccess, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + let paths = account.paths(); + let index = account.search_index().await?; + + let folder_ids = match records { + ChangeRecords::Account(events) => { + let mut folder_ids = Vec::new(); + for event in events { + match event { + AccountEvent::CreateFolder(folder_id, _) => { + folder_ids.push(*folder_id); + } + AccountEvent::DeleteFolder(folder_id) => { + folder_ids.push(*folder_id) + } + _ => {} + } + } + folder_ids + } + ChangeRecords::Folder(folder_id, _) => vec![*folder_id], + }; + + for folder_id in folder_ids { + match records { + ChangeRecords::Account(events) => { + for event in events { + match event { + AccountEvent::CreateFolder(_, _) => { + // Find the folder password which should be available + // as the identity folder has been reloaded already + let key = account + .find_folder_password(&folder_id) + .await? + .ok_or( + sos_account::Error::NoFolderPassword( + folder_id, + ), + )?; + // Import the vault into the account + account + .import_folder( + paths.vault_path(&folder_id), + key, + true, + ) + .await?; + + // Now the storage should have the folder so + // we can access the access point and add it to + // the search index + if let Some(folder) = + account.folder(&folder_id).await.ok() + { + let access_point = folder.access_point(); + let access_point = access_point.lock().await; + let mut index = index.write().await; + index.add_folder(&*access_point).await?; + } + } + AccountEvent::DeleteFolder(_) => { + let mut index = index.write().await; + index.remove_vault(&folder_id); + } + _ => {} + } + } + } + ChangeRecords::Folder(folder_id, events) => { + if let Some(folder) = account.folder(&folder_id).await.ok() { + let access_point = folder.access_point(); + let mut access_point = access_point.lock().await; + + // Must reload the vault before updating the + // search index + let path = paths.vault_path(folder_id); + access_point.reload_vault(path).await?; + + for event in events { + match event { + WriteEvent::CreateSecret(secret_id, _) => { + if let Some((meta, secret, _)) = access_point + .read_secret(secret_id) + .await? + { + let mut index = index.write().await; + index.add( + folder_id, secret_id, &meta, &secret, + ); + } + } + WriteEvent::UpdateSecret(secret_id, _) => { + if let Some((meta, secret, _)) = access_point + .read_secret(secret_id) + .await? + { + let mut index = index.write().await; + index.update( + folder_id, secret_id, &meta, &secret, + ); + } + } + WriteEvent::DeleteSecret(secret_id) => { + let mut index = index.write().await; + index.remove(folder_id, secret_id); + } + _ => {} + } + } + } + } + } + } + + Ok(()) +} + +async fn load_account_records( + account: &A, +) -> Result> +where + A: Account + + SyncStorage + + Sync + + Send + + 'static, + R: 'static, + E: std::fmt::Debug + + std::error::Error + + ErrorExt + + From + + From + + From + + From + + From + + From + + From + + Send + + Sync + + 'static, +{ + // FIXME: update the error handling to avoid the unwrap + let account_log = account.account_log().await.unwrap(); + let mut event_log = account_log.write().await; + let commit = event_log.tree().last_commit(); + + let patch = event_log.diff_events(commit.as_ref()).await?; + let records = patch.into_events::().await?; + + event_log.load_tree().await?; + Ok(records) +} From 53f3f899b9edb063dd3f5441523ab11f2ca80b30 Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 22 Apr 2025 13:37:16 +0800 Subject: [PATCH 50/61] Add claude config. --- CLAUDE.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..849ac6a93f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,32 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build and Test Commands +- Build: `cargo make debug` (debug) or `cargo make release` (optimized) +- Format: `cargo make format` or check with `cargo make format-check` +- Lint: `cargo make clippy` +- Test: `cargo make test` (standard) or `cargo make test-all` (all backends) +- Unit tests only: `cargo make unit` +- Single test: `cargo nextest run -p sos-integration-tests test_name` +- CLI tests: `cargo make test-cli` or `cargo make test-shell` +- Documentation: `cargo make doc` + +## Code Style +- Use the Rust 2018 edition style +- Maximum line width: 78 characters +- Prefer `thiserror` for error handling with clear, specific error types +- Use doc comments (`///`) for public APIs +- Add `#![deny(missing_docs)]` and `#![forbid(unsafe_code)]` to crate roots +- Prefer `Result` with custom error types for error handling +- Implement informative Display/Debug traits for public types +- Use bitflags for flag-based enums +- Ensure proper serialization/deserialization for custom types + +## Naming and Structure +- Use snake_case for variables, functions, and modules +- Use PascalCase for types and traits +- Group related functionality in modules +- Re-export important types in crate root +- Use clear, descriptive names that reflect domain concepts +- Follow vault, folder, secret terminology as defined in documentation \ No newline at end of file From bb1f3c3bc0942fb256b8cddd34a79f2d5dd00a5b Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 23 Apr 2025 08:58:49 +0800 Subject: [PATCH 51/61] Update dependencies. --- Cargo.lock | 917 +++++++++++++++++++++++++++++------------------------ 1 file changed, 508 insertions(+), 409 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51ccb60fde..b866762a6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,9 +132,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-build" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a133d38cebf328adaea4bc1891d9568e14a394b50e4f4ba5f63dc14e8beaaee9" +checksum = "29082cbb17a6bd7110eac366e11877ac418a2e47719be68c34050ce03aa63c4c" dependencies = [ "windows-sys 0.52.0", ] @@ -236,22 +236,23 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arboard" -version = "3.4.1" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" dependencies = [ "clipboard-win", "log", - "objc2", + "objc2 0.6.1", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.1", "parking_lot", + "percent-encoding", "x11rb", ] @@ -297,7 +298,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "synstructure", ] @@ -309,14 +310,14 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] name = "async-compression" -version = "0.4.20" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "flate2", "futures-core", @@ -348,7 +349,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -365,13 +366,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.87" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -403,9 +404,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" dependencies = [ "axum-core", "base64 0.22.1", @@ -432,7 +433,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-tungstenite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -440,12 +441,12 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", @@ -460,9 +461,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" dependencies = [ "axum", "axum-core", @@ -474,8 +475,9 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", + "rustversion", "serde", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] @@ -496,22 +498,21 @@ dependencies = [ "metrics-exporter-prometheus", "pin-project", "tokio", - "tower 0.5.2", + "tower", "tower-http", ] [[package]] name = "axum-server" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56bac90848f6a9393ac03c63c640925c4b7c8ca21654de40d53f55964667c7d8" +checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" dependencies = [ "arc-swap", "bytes", - "futures-util", + "fs-err", "http", "http-body", - "http-body-util", "hyper", "hyper-util", "pin-project-lite", @@ -520,7 +521,6 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tower 0.4.13", "tower-service", ] @@ -558,9 +558,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base32" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" @@ -576,9 +576,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "basic-toml" @@ -667,7 +667,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", ] [[package]] @@ -708,9 +708,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" dependencies = [ "shlex", ] @@ -763,7 +763,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -797,7 +797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45a7298287f1443f422d3f46e8ce9f855e75f0e43c06605adb4c52a262faeabd" dependencies = [ "derive_builder 0.10.2", - "getrandom 0.2.15", + "getrandom 0.2.16", "rand 0.8.5", "thiserror 1.0.69", ] @@ -830,9 +830,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.31" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -840,9 +840,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.31" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -853,14 +853,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -927,9 +927,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" -version = "0.2.6" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "cookie-factory" @@ -995,9 +995,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1101,9 +1101,9 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.5" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" dependencies = [ "nix 0.29.0", "windows-sys 0.59.0", @@ -1133,7 +1133,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1148,12 +1148,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.10", - "darling_macro 0.20.10", + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -1172,16 +1172,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1197,34 +1197,20 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.10", + "darling_core 0.20.11", "quote", - "syn 2.0.99", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", + "syn 2.0.100", ] [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbus" @@ -1252,9 +1238,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "zeroize", @@ -1276,9 +1262,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", "serde", @@ -1320,10 +1306,10 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1343,7 +1329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core 0.20.2", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1364,6 +1350,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.0", + "objc2 0.6.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1372,7 +1368,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1468,7 +1464,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1479,9 +1475,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1535,12 +1531,12 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" -version = "4.0.3" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c44818c96aec5cadc9dacfb97bbcbcfc19a0de75b218412d56f57fbaab94e439" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if 1.0.0", - "rustix 0.38.44", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -1555,9 +1551,9 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", "subtle", @@ -1598,9 +1594,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -1658,9 +1654,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" @@ -1671,6 +1667,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1749,7 +1755,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -1805,9 +1811,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1818,14 +1824,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1949,9 +1957,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" dependencies = [ "atomic-waker", "bytes", @@ -1959,7 +1967,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -2074,9 +2082,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -2095,12 +2103,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -2165,9 +2173,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", "futures-channel", @@ -2175,6 +2183,7 @@ dependencies = [ "http", "http-body", "hyper", + "libc", "pin-project-lite", "socket2", "tokio", @@ -2198,9 +2207,9 @@ dependencies = [ [[package]] name = "i18n-embed" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0454970a5853f498e686cbd7bf9391aac2244928194780cb7a0af0f41937db6" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" dependencies = [ "arc-swap", "fluent", @@ -2218,11 +2227,10 @@ dependencies = [ [[package]] name = "i18n-embed-fl" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7578cee2940492a648bd60fb49ca85ee8c821a63790e0ef5b604cfed353b2a" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" dependencies = [ - "dashmap", "find-crate", "fluent", "fluent-syntax", @@ -2232,7 +2240,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.99", + "syn 2.0.100", "unic-langid", ] @@ -2246,21 +2254,22 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.0", ] [[package]] @@ -2313,9 +2322,9 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" [[package]] name = "icu_normalizer" @@ -2337,9 +2346,9 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" @@ -2358,9 +2367,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" @@ -2387,7 +2396,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -2419,9 +2428,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.5" +version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", @@ -2442,9 +2451,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2657,7 +2666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5e25f9b861a88faa9d272ca4376e1a13c9a37d36de623f013c7bbb0ae2baa1" dependencies = [ "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -2729,9 +2738,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.170" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libdbus-sys" @@ -2744,9 +2753,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" [[package]] name = "libredox" @@ -2778,9 +2787,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -2800,9 +2809,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "logos" @@ -2834,7 +2843,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.8.5", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -2850,7 +2859,7 @@ dependencies = [ "quote", "regex-syntax 0.8.5", "rustc_version", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -2915,9 +2924,9 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7deb012b3b2767169ff203fadb4c6b0b82b947512e5eb9e0b78c2e186ad9e3" +checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" dependencies = [ "ahash", "portable-atomic", @@ -2930,7 +2939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ "base64 0.22.1", - "indexmap 2.7.1", + "indexmap 2.9.0", "metrics", "metrics-util", "quanta", @@ -2939,16 +2948,16 @@ dependencies = [ [[package]] name = "metrics-util" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd4884b1dd24f7d6628274a2f5ae22465c337c5ba065ec9b6edccddf8acc673" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" dependencies = [ "crossbeam-epoch", "crossbeam-utils", "hashbrown 0.15.2", "metrics", "quanta", - "rand 0.8.5", + "rand 0.9.1", "rand_xoshiro", "sketches-ddsketch", ] @@ -2977,9 +2986,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", "simd-adler32", @@ -3214,44 +3223,49 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.9.0", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", + "objc2 0.6.1", + "objc2-core-graphics", + "objc2-foundation 0.3.1", ] [[package]] -name = "objc2-core-data" -version = "0.2.2" +name = "objc2-core-foundation" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.9.0", - "block2", - "objc2", - "objc2-foundation", + "dispatch2", + "objc2 0.6.1", ] [[package]] -name = "objc2-core-image" -version = "0.2.2" +name = "objc2-core-graphics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "bitflags 2.9.0", + "dispatch2", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -3269,43 +3283,40 @@ dependencies = [ "bitflags 2.9.0", "block2", "libc", - "objc2", + "objc2 0.5.2", ] [[package]] -name = "objc2-local-authentication" -version = "0.2.2" +name = "objc2-foundation" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430605e43490dc3837b7d50d8daedacb9f7926da3935a8cd09651a6a9d071b71" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.9.0", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-metal" -version = "0.2.2" +name = "objc2-io-surface" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags 2.9.0", - "block2", - "objc2", - "objc2-foundation", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-quartz-core" +name = "objc2-local-authentication" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +checksum = "430605e43490dc3837b7d50d8daedacb9f7926da3935a8cd09651a6a9d071b71" dependencies = [ - "bitflags 2.9.0", "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3328,9 +3339,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" @@ -3370,7 +3381,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -3467,7 +3478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.7.1", + "indexmap 2.9.0", ] [[package]] @@ -3525,7 +3536,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -3558,12 +3569,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64 0.22.1", - "indexmap 2.7.1", + "indexmap 2.9.0", "quick-xml", "serde", "time", @@ -3644,11 +3655,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy 0.8.24", ] [[package]] @@ -3663,12 +3674,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.30" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -3744,14 +3755,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -3764,7 +3775,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "version_check", "yansi", ] @@ -3795,7 +3806,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.99", + "syn 2.0.100", "tempfile", ] @@ -3809,7 +3820,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -3930,11 +3941,12 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" dependencies = [ "bytes", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -3944,17 +3956,18 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" dependencies = [ "bytes", - "getrandom 0.2.15", - "rand 0.8.5", + "getrandom 0.3.2", + "rand 0.9.1", "ring", "rustc-hash 2.1.1", "rustls", @@ -3968,9 +3981,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" dependencies = [ "cfg_aliases 0.2.1", "libc", @@ -3982,13 +3995,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radix_trie" version = "0.2.1" @@ -4012,13 +4031,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.23", ] [[package]] @@ -4047,7 +4065,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -4056,16 +4074,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.2", ] [[package]] name = "rand_xoshiro" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.9.3", ] [[package]] @@ -4104,9 +4122,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags 2.9.0", ] @@ -4152,7 +4170,7 @@ dependencies = [ "quote", "refinery-core", "regex", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4201,9 +4219,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "base64 0.22.1", "bytes", @@ -4233,7 +4251,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower 0.5.2", + "tower", "tower-service", "url", "wasm-bindgen", @@ -4246,11 +4264,11 @@ dependencies = [ [[package]] name = "retry" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9166d72162de3575f950507683fac47e30f6f2c3836b71b7fbc61aa517c9c5f4" +checksum = "a1e211f878258887b3e65dd3c8ff9f530fe109f441a117ee0cdc27f341355032" dependencies = [ - "rand 0.8.5", + "rand 0.9.1", ] [[package]] @@ -4274,13 +4292,13 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.13" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if 1.0.0", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -4308,8 +4326,8 @@ dependencies = [ "gio", "jni", "log", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-local-authentication", "polkit", "retry", @@ -4343,9 +4361,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4354,22 +4372,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.99", + "syn 2.0.100", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21" dependencies = [ "sha2", "walkdir", @@ -4426,22 +4444,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.1" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ "bitflags 2.9.0", "errno", "libc", - "linux-raw-sys 0.9.2", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "once_cell", "ring", @@ -4471,9 +4489,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", @@ -4516,7 +4534,7 @@ checksum = "327e9d075f6df7e25fbf594f1be7ef55cf0d567a6cb5112eeccbbd51ceb48e0d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4635,14 +4653,14 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" dependencies = [ - "self_cell 1.1.0", + "self_cell 1.2.0", ] [[package]] name = "self_cell" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" @@ -4655,9 +4673,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -4676,13 +4694,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4744,7 +4762,7 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4788,7 +4806,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_derive", "serde_json", @@ -4802,10 +4820,10 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -4884,9 +4902,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -4930,9 +4948,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "smawk" @@ -4958,9 +4976,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -5038,7 +5056,7 @@ dependencies = [ "futures", "futures-util", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5121,7 +5139,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "serde", "sos-archive", @@ -5176,7 +5194,7 @@ dependencies = [ "binary-stream", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "parking_lot", "rustc_version", "secrecy", @@ -5259,7 +5277,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "refinery", "rustc_version", "secrecy", @@ -5292,7 +5310,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5354,7 +5372,7 @@ dependencies = [ name = "sos-external-files" version = "0.17.0" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-core", "sos-vault", @@ -5372,7 +5390,7 @@ dependencies = [ "futures", "futures-util", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "parking_lot", "rustc_version", "serde", @@ -5409,7 +5427,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "maplit2", "parking_lot", "pretty_assertions", @@ -5462,7 +5480,7 @@ dependencies = [ "binary-stream", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sha2", "sos-backend", @@ -5518,7 +5536,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-util", - "tower 0.5.2", + "tower", "tracing", "typeshare", "url", @@ -5604,7 +5622,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "prost", "rand 0.8.5", "rs_merkle", @@ -5694,7 +5712,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "prost", "prost-build", "protoc-bin-vendored", @@ -5727,7 +5745,7 @@ name = "sos-reducers" version = "0.17.0" dependencies = [ "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-core", "sos-vault", @@ -5739,7 +5757,7 @@ name = "sos-remote-sync" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-account", "sos-backend", @@ -5817,7 +5835,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "k256", "rustc_version", "rustls", @@ -5860,7 +5878,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5908,7 +5926,7 @@ name = "sos-sync" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "serde", "sos-backend", @@ -5994,7 +6012,7 @@ dependencies = [ "bytes", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "k256", "keychain_parser", "pem", @@ -6047,7 +6065,7 @@ dependencies = [ "ed25519-dalek", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "pem", "rustc_version", "secrecy", @@ -6085,7 +6103,7 @@ name = "sos-web" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "sos-account", @@ -6177,9 +6195,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.99" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -6203,7 +6221,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6227,15 +6245,14 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.18.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if 1.0.0", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.2", "once_cell", - "rustix 1.0.1", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -6252,11 +6269,11 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 0.38.44", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -6298,7 +6315,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6309,7 +6326,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6324,9 +6341,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.39" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -6342,15 +6359,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -6402,9 +6419,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -6425,7 +6442,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6496,9 +6513,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", @@ -6544,7 +6561,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "toml_datetime", "winnow 0.5.40", ] @@ -6555,18 +6572,18 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.7.3", + "winnow 0.7.6", ] [[package]] name = "totp-rs" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90" +checksum = "f124352108f58ef88299e909f6e9470f1cdc8d2a1397963901b4a6366206bf72" dependencies = [ "base32", "constant_time_eq", @@ -6580,21 +6597,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -6671,7 +6673,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6743,7 +6745,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.0", + "rand 0.9.1", "rustls", "rustls-pki-types", "sha1", @@ -6762,9 +6764,9 @@ dependencies = [ [[package]] name = "typed-generational-arena" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3478ec5cc6caaa9ed86791e8970e320841c3362a7a14b81a5c5c3f9e254b8a44" +checksum = "8c27e0b89f359e864283feca64a75f1cb249370ea97bf42a451521b696ca17cc" dependencies = [ "cfg-if 0.1.10", "nonzero_ext", @@ -6796,7 +6798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" dependencies = [ "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -6937,7 +6939,7 @@ version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_json", "utoipa-gen", @@ -6951,7 +6953,7 @@ checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "uuid", ] @@ -6969,11 +6971,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.15.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.2", "serde", ] @@ -6985,9 +6987,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcard4" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f37cbf15f76a5cb6bf6a4d7d6004471cfe0974eac80605fee1cb8c837c9df5d" +checksum = "a7529ce5655c3d5da5e738bb887ae158ed70009baa3591fc58b12358d0adb0bd" dependencies = [ "aho-corasick", "base64 0.22.1", @@ -7048,9 +7050,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -7077,7 +7079,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "wasm-bindgen-shared", ] @@ -7112,7 +7114,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7170,9 +7172,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "winapi" @@ -7226,23 +7228,27 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.56.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings 0.4.0", ] [[package]] @@ -7253,7 +7259,18 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] @@ -7264,24 +7281,35 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.2.0", - "windows-strings", - "windows-targets 0.52.6", + "windows-result 0.3.2", + "windows-strings 0.3.1", + "windows-targets 0.53.0", ] [[package]] @@ -7295,21 +7323,29 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", ] [[package]] @@ -7400,13 +7436,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -7425,6 +7477,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -7449,6 +7507,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -7473,12 +7537,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -7503,6 +7579,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -7527,6 +7609,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -7545,6 +7633,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -7569,6 +7663,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.5.40" @@ -7580,18 +7680,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.0", ] @@ -7668,9 +7768,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "yansi" @@ -7707,7 +7807,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "synstructure", ] @@ -7717,17 +7817,16 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", "zerocopy-derive 0.7.35", ] [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ - "zerocopy-derive 0.8.23", + "zerocopy-derive 0.8.24", ] [[package]] @@ -7738,18 +7837,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -7769,7 +7868,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", "synstructure", ] @@ -7790,7 +7889,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] @@ -7812,7 +7911,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.100", ] [[package]] From 9c6c82f46134ac2ba05badc55a449307d222bc15 Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 23 Apr 2025 09:03:15 +0800 Subject: [PATCH 52/61] Enable files feature on sos-ipc in sos-extension-service. Prevents the gui code from triggering a compiler error due to the files feature being enabled elsewhere. --- crates/extension_service/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/extension_service/Cargo.toml b/crates/extension_service/Cargo.toml index c46073a58a..cd64832f9b 100644 --- a/crates/extension_service/Cargo.toml +++ b/crates/extension_service/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/saveoursecrets/sdk" sos-account = { workspace = true, features = ["clipboard", "search"] } sos-backend.workspace = true sos-core.workspace = true -sos-ipc = { workspace = true, features = ["extension-helper-server", "search", "clipboard"] } +sos-ipc = { workspace = true, features = ["extension-helper-server", "search", "clipboard", "files"] } sos-net = { workspace = true, features = ["clipboard", "search"] } sos-changes = { workspace = true, features = ["changes-consumer"] } tokio.workspace = true From d55f56c2be5d57793346b3089d1cba69b2925c2e Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 23 Apr 2025 13:35:20 +0800 Subject: [PATCH 53/61] Fix duplicate initialization of the ChangeConsumer listener. --- crates/extension_service/src/lib.rs | 19 ++++---------- crates/ipc/src/extension_helper/server.rs | 5 ++-- crates/ipc/src/web_service/web_accounts2.rs | 26 +++++++++---------- .../integration/src/test_extension_helper.rs | 3 ++- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/crates/extension_service/src/lib.rs b/crates/extension_service/src/lib.rs index 0c75256183..d14c014915 100644 --- a/crates/extension_service/src/lib.rs +++ b/crates/extension_service/src/lib.rs @@ -1,6 +1,5 @@ use sos_account::AccountSwitcherOptions; use sos_backend::{BackendTarget, InferOptions}; -use sos_changes::consumer::ChangeConsumer; use sos_core::{events::changes_feed, Paths}; use sos_ipc::{ extension_helper::server::{ @@ -31,18 +30,7 @@ pub async fn run() -> anyhow::Result<()> { let extension_id = args.pop().unwrap_or_else(String::new).to_string(); - // Spawn a task to listen for incoming changes and - // proxy them to the standard changes feed. - tokio::task::spawn(async { - let paths = Paths::new_client(Paths::data_dir()?); - let mut changes_handle = ChangeConsumer::listen(paths)?; - let changes_feed = changes_feed(); - let incoming_changes = changes_handle.changes(); - while let Some(event) = incoming_changes.recv().await { - changes_feed.send_replace(event); - } - Ok::<_, anyhow::Error>(()) - }); + let changes_feed = changes_feed(); let mut accounts = NetworkAccountSwitcher::new_with_options(AccountSwitcherOptions { @@ -78,7 +66,10 @@ pub async fn run() -> anyhow::Result<()> { let accounts = Arc::new(RwLock::new(accounts)); let options = ExtensionHelperOptions::new(extension_id, info); - let server = ExtensionHelperServer::new(options, accounts).await?; + let server = ExtensionHelperServer::new(options, accounts, |event| { + changes_feed.send_replace(event); + }) + .await?; server.listen().await; Ok(()) } diff --git a/crates/ipc/src/extension_helper/server.rs b/crates/ipc/src/extension_helper/server.rs index 5255a870c4..b47a7b1fcd 100644 --- a/crates/ipc/src/extension_helper/server.rs +++ b/crates/ipc/src/extension_helper/server.rs @@ -13,7 +13,7 @@ use http::{ }; use sos_account::{Account, AccountSwitcher}; use sos_changes::consumer::ChangeConsumer; -use sos_core::{ErrorExt, Paths}; +use sos_core::{events::LocalChangeEvent, ErrorExt, Paths}; use sos_login::DelegatedAccess; use sos_logs::Logger; use sos_protocol::{constants::MIME_TYPE_JSON, ErrorReply}; @@ -99,6 +99,7 @@ where pub async fn new( options: ExtensionHelperOptions, accounts: Arc>>, + change_handler: impl Fn(LocalChangeEvent) + Send + Sync + 'static, ) -> Result { let log_level = std::env::var("SOS_NATIVE_BRIDGE_LOG_LEVEL") .map(|s| s.to_string()) @@ -122,7 +123,7 @@ where }; let accounts = WebAccounts::new(accounts); let changes_consumer = ChangeConsumer::listen(paths.clone())?; - accounts.listen_changes(changes_consumer, paths)?; + accounts.listen_changes(changes_consumer, paths, change_handler)?; let client = LocalMemoryServer::listen( accounts.clone(), options.service_info.clone(), diff --git a/crates/ipc/src/web_service/web_accounts2.rs b/crates/ipc/src/web_service/web_accounts2.rs index 84fc728a22..81589031e8 100644 --- a/crates/ipc/src/web_service/web_accounts2.rs +++ b/crates/ipc/src/web_service/web_accounts2.rs @@ -147,6 +147,7 @@ where &self, mut changes_consumer: ConsumerHandle, paths: Arc, + change_handler: impl Fn(LocalChangeEvent) + Send + Sync + 'static, ) -> Result<()> { // Start a background task to listen for change events let channel = self.channel.clone(); @@ -161,11 +162,8 @@ where "change_consumer::event_received" ); - // The account_id is included in the event variants if let Err(e) = process_change_event( - event, - // We don't need to pass account_id separately as it's in the event - // Passing default here as it's not used in this form + &event, AccountId::default(), paths.clone(), task_accounts.clone(), @@ -175,6 +173,8 @@ where { tracing::error!(error = %e, "process_change_event"); } + + change_handler(event); } tracing::debug!("consumer_task_completed"); @@ -220,9 +220,9 @@ where /// Process change events and update the system state accordingly async fn process_change_event( - event: LocalChangeEvent, - _account_id_unused: AccountId, // Keeping for backward compatibility but not using - paths: Arc, + event: &LocalChangeEvent, + _account_id: AccountId, + _paths: Arc, accounts: Arc>>, channel: broadcast::Sender, ) -> Result<()> @@ -282,7 +282,7 @@ where let mut accounts_lock = accounts.write().await; let account = accounts_lock .iter_mut() - .find(|a| a.account_id() == &account_id) + .find(|a| a.account_id() == account_id) .ok_or(Error::from(FileEventError::NoAccount( account_id.clone(), )))?; @@ -331,14 +331,14 @@ where let accounts_lock = accounts.read().await; let account = accounts_lock .iter() - .find(|a| a.account_id() == &account_id) + .find(|a| a.account_id() == account_id) .ok_or(Error::from(FileEventError::NoAccount( account_id.clone(), )))?; let folder = account.folder(&folder_id).await.ok().ok_or( - Error::from(FileEventError::NoFolder(folder_id)), + Error::from(FileEventError::NoFolder(*folder_id)), )?; let event_log = folder.event_log(); @@ -355,10 +355,10 @@ where let mut accounts_lock = accounts.write().await; if let Some(account) = accounts_lock .iter_mut() - .find(|a| a.account_id() == &account_id) + .find(|a| a.account_id() == account_id) { let records_clone = ChangeRecords::Folder( - folder_id, + *folder_id, records.clone(), ); update_account_search_index( @@ -381,7 +381,7 @@ where let evt = AccountChangeEvent { account_id: account_id.clone(), records: ChangeRecords::Folder( - folder_id, records, + *folder_id, records, ), }; if let Err(e) = channel.send(evt) { diff --git a/tests/integration/src/test_extension_helper.rs b/tests/integration/src/test_extension_helper.rs index 1cdb0a143d..7d4d7de97b 100644 --- a/tests/integration/src/test_extension_helper.rs +++ b/tests/integration/src/test_extension_helper.rs @@ -66,7 +66,8 @@ pub async fn main() -> anyhow::Result<()> { }; let accounts = Arc::new(RwLock::new(accounts)); let options = ExtensionHelperOptions::new(extension_id, info); - let server = ExtensionHelperServer::new(options, accounts).await?; + let server = + ExtensionHelperServer::new(options, accounts, |_| {}).await?; server.listen().await; Ok(()) } From a5c0866017d72f7bea690641defa08f4d8db95ad Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 23 Apr 2025 14:22:44 +0800 Subject: [PATCH 54/61] Fix bug in AccountSwitcher::load_accounts(). No longer assumes a file system backend but expects a BackendTarget to be passed. --- crates/account/src/account_switcher.rs | 15 ++++----------- tests/integration/src/test_extension_helper.rs | 6 +++++- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/account/src/account_switcher.rs b/crates/account/src/account_switcher.rs index a6b08dc128..9e754232a8 100644 --- a/crates/account/src/account_switcher.rs +++ b/crates/account/src/account_switcher.rs @@ -1,10 +1,10 @@ use crate::{Account, Error, LocalAccount, Result}; +use sos_backend::BackendTarget; use sos_core::{AccountId, Paths}; use sos_login::PublicIdentity; -use sos_vault::list_accounts; use std::pin::Pin; +use std::sync::Arc; use std::{collections::HashMap, future::Future}; -use std::{path::PathBuf, sync::Arc}; #[cfg(feature = "search")] use sos_search::{ArchiveFilter, Document, DocumentView, QueryFilter}; @@ -101,7 +101,7 @@ where pub async fn load_accounts( &mut self, builder: B, - data_dir: Option<&PathBuf>, + target: BackendTarget, ) -> Result<()> where B: Fn( @@ -109,14 +109,7 @@ where ) -> Pin>>>, { - let paths = if let Some(data_dir) = data_dir { - Paths::new_client(data_dir) - } else { - Paths::new_client(Paths::data_dir()?) - }; - - let identities = list_accounts(Some(&paths)).await?; - + let identities = target.list_accounts().await?; for identity in identities { tracing::info!( account_id = %identity.account_id(), "add_account"); diff --git a/tests/integration/src/test_extension_helper.rs b/tests/integration/src/test_extension_helper.rs index 7d4d7de97b..f4147a0c50 100644 --- a/tests/integration/src/test_extension_helper.rs +++ b/tests/integration/src/test_extension_helper.rs @@ -1,6 +1,7 @@ use sos_account::{ AccountSwitcherOptions, LocalAccount, LocalAccountSwitcher, }; +use sos_backend::BackendTarget; use sos_ipc::{ extension_helper::server::{ ExtensionHelperOptions, ExtensionHelperServer, @@ -40,6 +41,9 @@ pub async fn main() -> anyhow::Result<()> { ..Default::default() }; let mut accounts = LocalAccountSwitcher::new_with_options(options); + let target = BackendTarget::FileSystem(Paths::new_client( + data_dir.as_ref().unwrap(), + )); accounts .load_accounts( |identity| { @@ -55,7 +59,7 @@ pub async fn main() -> anyhow::Result<()> { .await?) }) }, - data_dir.as_ref(), + target, ) .await?; From c386a13e0d699c2aaa029ed37cd65a8bb958e014 Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 23 Apr 2025 14:25:42 +0800 Subject: [PATCH 55/61] Update call to load_accounts(). --- Cargo.lock | 3 ++- crates/extension_service/Cargo.toml | 1 + crates/extension_service/src/lib.rs | 11 ++++++++-- crates/ipc/src/memory_server.rs | 29 --------------------------- crates/ipc/src/web_service/account.rs | 2 ++ 5 files changed, 14 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b866762a6d..a732bc65ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -5365,6 +5365,7 @@ dependencies = [ "sos-ipc", "sos-net", "tokio", + "tracing", "xclipboard", ] diff --git a/crates/extension_service/Cargo.toml b/crates/extension_service/Cargo.toml index cd64832f9b..ff57394ac6 100644 --- a/crates/extension_service/Cargo.toml +++ b/crates/extension_service/Cargo.toml @@ -17,6 +17,7 @@ sos-changes = { workspace = true, features = ["changes-consumer"] } tokio.workspace = true anyhow.workspace = true xclipboard.workspace = true +tracing.workspace = true [[bin]] name = "sos-extension-helper" diff --git a/crates/extension_service/src/lib.rs b/crates/extension_service/src/lib.rs index d14c014915..e32f7e3a28 100644 --- a/crates/extension_service/src/lib.rs +++ b/crates/extension_service/src/lib.rs @@ -29,7 +29,6 @@ pub async fn run() -> anyhow::Result<()> { args.pop(); let extension_id = args.pop().unwrap_or_else(String::new).to_string(); - let changes_feed = changes_feed(); let mut accounts = @@ -38,9 +37,17 @@ pub async fn run() -> anyhow::Result<()> { ..Default::default() }); + let paths = Paths::new_client(Paths::data_dir()?); + let target = BackendTarget::infer(paths, InferOptions::default()).await?; + + tracing::info!(backend_target = %target, "extension_service"); + accounts .load_accounts( |identity| { + tracing::debug!( + account_id = %identity.account_id(), + "extension::load_account"); Box::pin(async move { let paths = Paths::new_client(Paths::data_dir()?) .with_account_id(identity.account_id()); @@ -55,7 +62,7 @@ pub async fn run() -> anyhow::Result<()> { .await?) }) }, - None, + target, ) .await?; diff --git a/crates/ipc/src/memory_server.rs b/crates/ipc/src/memory_server.rs index c5c4032a93..bca2976cf3 100644 --- a/crates/ipc/src/memory_server.rs +++ b/crates/ipc/src/memory_server.rs @@ -72,35 +72,6 @@ impl LocalMemoryClient { let response = Response::from_parts(header, Full::new(bytes)); Ok(response) } - - /* - /// Get application information. - pub async fn info(&mut self) -> Result { - let response = self.send_request(Default::default()).await?; - let status = response.status()?; - if status.is_success() { - let app_info: ServiceAppInfo = - serde_json::from_slice(&response.body)?; - Ok(app_info) - } else { - Err(NetworkError::ResponseCode(status).into()) - } - } - - /// List accounts. - pub async fn list_accounts(&mut self) -> Result> { - let request = LocalRequest::get("/accounts".parse()?); - let response = self.send_request(request).await?; - let status = response.status()?; - if status.is_success() { - let accounts: Vec = - serde_json::from_slice(&response.body)?; - Ok(accounts) - } else { - Err(NetworkError::ResponseCode(status).into()) - } - } - */ } /// Server for in-memory communication. diff --git a/crates/ipc/src/web_service/account.rs b/crates/ipc/src/web_service/account.rs index 13f0e6f1ee..540770308a 100644 --- a/crates/ipc/src/web_service/account.rs +++ b/crates/ipc/src/web_service/account.rs @@ -215,6 +215,8 @@ where keyring_password_supported = %keyring_password::supported(), ); + tracing::info!(account_id = %account_id, "sign_in"); + match find_account_credential(&account_id.to_string()).await { Ok(password) => { sign_in_password(accounts, account_id, password, false).await From f7db41bd1650c5f9be001478a367b77e089ba58e Mon Sep 17 00:00:00 2001 From: muji Date: Wed, 23 Apr 2025 14:26:09 +0800 Subject: [PATCH 56/61] Bump patch version. --- Cargo.lock | 2 +- crates/account/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a732bc65ac..d64688589e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5050,7 +5050,7 @@ dependencies = [ [[package]] name = "sos-account" -version = "0.17.3" +version = "0.17.4" dependencies = [ "async-trait", "futures", diff --git a/crates/account/Cargo.toml b/crates/account/Cargo.toml index b455064155..71a93dc2dd 100644 --- a/crates/account/Cargo.toml +++ b/crates/account/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sos-account" -version = "0.17.3" +version = "0.17.4" edition = "2021" description = "Local accounts for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" From 043a557975661abc6e30cf9019f24b4e588ab5da Mon Sep 17 00:00:00 2001 From: muji Date: Sun, 1 Jun 2025 13:01:25 +0800 Subject: [PATCH 57/61] Update dependencies. --- Cargo.lock | 1308 ++++++++++++++++++++++++++++------------------------ 1 file changed, 697 insertions(+), 611 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d57631b6c..8ec17f8ccd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,14 +99,14 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if 1.0.0", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -132,9 +132,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-build" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a133d38cebf328adaea4bc1891d9568e14a394b50e4f4ba5f63dc14e8beaaee9" +checksum = "9994787facbc3375d2b510024117d11fa98087be537ac878033892193bbb33d2" dependencies = [ "windows-sys 0.52.0", ] @@ -195,12 +195,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -236,22 +236,23 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arboard" -version = "3.4.1" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" dependencies = [ "clipboard-win", "log", - "objc2", + "objc2 0.6.1", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.3.1", "parking_lot", + "percent-encoding", "x11rb", ] @@ -297,7 +298,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", "synstructure", ] @@ -309,14 +310,14 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] name = "async-compression" -version = "0.4.20" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "flate2", "futures-core", @@ -348,7 +349,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -365,13 +366,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.87" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -403,9 +404,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core", "base64 0.22.1", @@ -432,7 +433,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-tungstenite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -440,12 +441,12 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", @@ -460,9 +461,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" dependencies = [ "axum", "axum-core", @@ -474,8 +475,9 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", + "rustversion", "serde", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] @@ -496,22 +498,21 @@ dependencies = [ "metrics-exporter-prometheus", "pin-project", "tokio", - "tower 0.5.2", + "tower", "tower-http", ] [[package]] name = "axum-server" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56bac90848f6a9393ac03c63c640925c4b7c8ca21654de40d53f55964667c7d8" +checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" dependencies = [ "arc-swap", "bytes", - "futures-util", + "fs-err", "http", "http-body", - "http-body-util", "hyper", "hyper-util", "pin-project-lite", @@ -520,15 +521,14 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tower 0.4.13", "tower-service", ] [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if 1.0.0", @@ -558,9 +558,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base32" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" @@ -576,9 +576,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "basic-toml" @@ -636,9 +636,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] @@ -667,7 +667,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", ] [[package]] @@ -684,9 +684,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" [[package]] name = "byteorder" @@ -708,9 +708,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ "shlex", ] @@ -763,7 +763,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -797,16 +797,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45a7298287f1443f422d3f46e8ce9f855e75f0e43c06605adb4c52a262faeabd" dependencies = [ "derive_builder 0.10.2", - "getrandom 0.2.15", + "getrandom 0.2.16", "rand 0.8.5", "thiserror 1.0.69", ] [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -830,9 +830,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.31" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", "clap_derive", @@ -840,9 +840,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.31" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ "anstream", "anstyle", @@ -853,14 +853,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -927,9 +927,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" -version = "0.2.6" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "cookie-factory" @@ -961,9 +961,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -1023,7 +1023,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "crossterm_winapi", "mio", "parking_lot", @@ -1067,9 +1067,9 @@ dependencies = [ [[package]] name = "csv-async" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d37fe5b0d07f4a8260ce1e9a81413e88f459af0f2dfc55c15e96868a2f99c0f0" +checksum = "888dbb0f640d2c4c04e50f933885c7e9c95995d93cec90aba8735b4c610f26f1" dependencies = [ "cfg-if 1.0.0", "csv-core", @@ -1101,11 +1101,11 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.5" +version = "3.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" dependencies = [ - "nix 0.29.0", + "nix 0.30.1", "windows-sys 0.59.0", ] @@ -1133,7 +1133,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -1148,12 +1148,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.10", - "darling_macro 0.20.10", + "darling_core 0.20.11", + "darling_macro 0.20.11", ] [[package]] @@ -1172,16 +1172,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -1197,34 +1197,20 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.10", + "darling_core 0.20.11", "quote", - "syn 2.0.99", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", + "syn 2.0.101", ] [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dbus" @@ -1252,9 +1238,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "zeroize", @@ -1276,9 +1262,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", "serde", @@ -1320,10 +1306,10 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -1343,7 +1329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core 0.20.2", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -1364,6 +1350,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1372,9 +1368,15 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1462,7 +1464,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -1473,9 +1475,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1483,9 +1485,9 @@ dependencies = [ [[package]] name = "error-code" -version = "3.3.1" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "etcetera" @@ -1529,12 +1531,12 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" -version = "4.0.3" +version = "4.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c44818c96aec5cadc9dacfb97bbcbcfc19a0de75b218412d56f57fbaab94e439" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if 1.0.0", - "rustix 0.38.44", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -1549,9 +1551,9 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", "subtle", @@ -1592,9 +1594,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -1652,9 +1654,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" @@ -1665,6 +1667,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1743,7 +1755,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -1799,9 +1811,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1812,14 +1824,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -1943,9 +1957,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", @@ -1953,7 +1967,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -1978,9 +1992,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "foldhash", ] @@ -2068,9 +2082,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -2089,12 +2103,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -2141,11 +2155,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -2154,21 +2167,26 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.0", ] [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -2192,9 +2210,9 @@ dependencies = [ [[package]] name = "i18n-embed" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0454970a5853f498e686cbd7bf9391aac2244928194780cb7a0af0f41937db6" +checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4" dependencies = [ "arc-swap", "fluent", @@ -2212,11 +2230,10 @@ dependencies = [ [[package]] name = "i18n-embed-fl" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7578cee2940492a648bd60fb49ca85ee8c821a63790e0ef5b604cfed353b2a" +checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d" dependencies = [ - "dashmap", "find-crate", "fluent", "fluent-syntax", @@ -2226,7 +2243,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.99", + "syn 2.0.101", "unic-langid", ] @@ -2240,21 +2257,22 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.2", ] [[package]] @@ -2268,21 +2286,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -2291,31 +2310,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -2323,67 +2322,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.99", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -2403,9 +2389,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -2413,9 +2399,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.5" +version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", @@ -2436,12 +2422,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "serde", ] @@ -2483,11 +2469,26 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "intl-memoizer" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" dependencies = [ "type-map", "unic-langid", @@ -2523,6 +2524,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -2636,7 +2647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5e25f9b861a88faa9d272ca4376e1a13c9a37d36de623f013c7bbb0ae2baa1" dependencies = [ "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -2673,9 +2684,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -2708,9 +2719,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.170" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libdbus-sys" @@ -2723,9 +2734,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" @@ -2733,7 +2744,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] @@ -2757,21 +2768,21 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -2779,9 +2790,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "logos" @@ -2813,7 +2824,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.8.5", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -2829,7 +2840,7 @@ dependencies = [ "quote", "regex-syntax 0.8.5", "rustc_version", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -2850,6 +2861,12 @@ dependencies = [ "logos-codegen 0.15.0", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "maplit2" version = "1.0.5" @@ -2894,9 +2911,9 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7deb012b3b2767169ff203fadb4c6b0b82b947512e5eb9e0b78c2e186ad9e3" +checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" dependencies = [ "ahash", "portable-atomic", @@ -2909,7 +2926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ "base64 0.22.1", - "indexmap 2.7.1", + "indexmap 2.9.0", "metrics", "metrics-util", "quanta", @@ -2918,16 +2935,16 @@ dependencies = [ [[package]] name = "metrics-util" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd4884b1dd24f7d6628274a2f5ae22465c337c5ba065ec9b6edccddf8acc673" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "metrics", "quanta", - "rand 0.8.5", + "rand 0.9.1", "rand_xoshiro", "sketches-ddsketch", ] @@ -2956,9 +2973,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", "simd-adler32", @@ -2966,21 +2983,21 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "ndk-context" @@ -3016,7 +3033,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if 1.0.0", "cfg_aliases 0.1.1", "libc", @@ -3024,11 +3041,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if 1.0.0", "cfg_aliases 0.2.1", "libc", @@ -3056,7 +3073,7 @@ version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "filetime", "fsevent-sys", "inotify", @@ -3193,44 +3210,49 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.9.0", - "block2", - "libc", - "objc2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation", - "objc2-quartz-core", + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-core-graphics", + "objc2-foundation 0.3.1", ] [[package]] -name = "objc2-core-data" -version = "0.2.2" +name = "objc2-core-foundation" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.0", - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.1", ] [[package]] -name = "objc2-core-image" -version = "0.2.2" +name = "objc2-core-graphics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -3245,46 +3267,43 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "block2", "libc", - "objc2", + "objc2 0.5.2", ] [[package]] -name = "objc2-local-authentication" -version = "0.2.2" +name = "objc2-foundation" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430605e43490dc3837b7d50d8daedacb9f7926da3935a8cd09651a6a9d071b71" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-metal" -version = "0.2.2" +name = "objc2-io-surface" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ - "bitflags 2.9.0", - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-quartz-core" +name = "objc2-local-authentication" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +checksum = "430605e43490dc3837b7d50d8daedacb9f7926da3935a8cd09651a6a9d071b71" dependencies = [ - "bitflags 2.9.0", "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3307,9 +3326,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opaque-debug" @@ -3349,7 +3374,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -3366,9 +3391,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -3376,9 +3401,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if 1.0.0", "libc", @@ -3446,7 +3471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.7.1", + "indexmap 2.9.0", ] [[package]] @@ -3504,7 +3529,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -3537,12 +3562,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ "base64 0.22.1", - "indexmap 2.7.1", + "indexmap 2.9.0", "quick-xml", "serde", "time", @@ -3615,6 +3640,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3623,11 +3657,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -3642,12 +3676,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.30" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ccf34da56fc294e7d4ccf69a85992b7dfb826b7cf57bac6a70bba3494cc08a" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ "proc-macro2", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -3723,14 +3757,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -3743,7 +3777,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", "version_check", "yansi", ] @@ -3774,7 +3808,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.99", + "syn 2.0.101", "tempfile", ] @@ -3788,7 +3822,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -3909,11 +3943,12 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", + "cfg_aliases 0.2.1", "pin-project-lite", "quinn-proto", "quinn-udp", @@ -3923,17 +3958,19 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.9" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "getrandom 0.2.15", - "rand 0.8.5", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", "ring", "rustc-hash 2.1.1", "rustls", @@ -3947,9 +3984,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.10" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" dependencies = [ "cfg_aliases 0.2.1", "libc", @@ -3961,13 +3998,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radix_trie" version = "0.2.1" @@ -3991,13 +4034,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.23", ] [[package]] @@ -4026,7 +4068,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -4035,16 +4077,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", ] [[package]] name = "rand_xoshiro" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.9.3", ] [[package]] @@ -4059,7 +4101,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -4075,13 +4117,19 @@ dependencies = [ "yasna", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -4109,7 +4157,7 @@ dependencies = [ "siphasher", "thiserror 1.0.69", "time", - "toml 0.8.20", + "toml 0.8.22", "url", "walkdir", ] @@ -4125,7 +4173,7 @@ dependencies = [ "quote", "refinery-core", "regex", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -4174,9 +4222,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" dependencies = [ "base64 0.22.1", "bytes", @@ -4197,7 +4245,6 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -4206,24 +4253,24 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower 0.5.2", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", - "windows-registry", + "webpki-roots 1.0.0", ] [[package]] name = "retry" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9166d72162de3575f950507683fac47e30f6f2c3836b71b7fbc61aa517c9c5f4" +checksum = "a1e211f878258887b3e65dd3c8ff9f530fe109f441a117ee0cdc27f341355032" dependencies = [ - "rand 0.8.5", + "rand 0.9.1", ] [[package]] @@ -4247,13 +4294,13 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.13" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if 1.0.0", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -4281,8 +4328,8 @@ dependencies = [ "gio", "jni", "log", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-local-authentication", "polkit", "retry", @@ -4306,7 +4353,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4316,9 +4363,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.6.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4327,22 +4374,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.6.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.99", + "syn 2.0.101", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.6.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" dependencies = [ "sha2", "walkdir", @@ -4390,7 +4437,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4399,22 +4446,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.1" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys 0.9.2", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "ring", @@ -4435,18 +4482,19 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -4455,9 +4503,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rustyline" @@ -4465,7 +4513,7 @@ version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if 1.0.0", "clipboard-win", "fd-lock", @@ -4483,13 +4531,13 @@ dependencies = [ [[package]] name = "rustyline-derive" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327e9d075f6df7e25fbf594f1be7ef55cf0d567a6cb5112eeccbbd51ceb48e0d" +checksum = "5d66de233f908aebf9cc30ac75ef9103185b4b715c6f2fb7a626aa5e5ede53ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -4572,7 +4620,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4585,8 +4633,8 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.10.0", + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4608,14 +4656,14 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" dependencies = [ - "self_cell 1.1.0", + "self_cell 1.2.0", ] [[package]] name = "self_cell" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" +checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semver" @@ -4628,9 +4676,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -4649,13 +4697,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -4717,7 +4765,7 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -4761,7 +4809,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_derive", "serde_json", @@ -4775,10 +4823,10 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ - "darling 0.20.10", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -4794,9 +4842,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -4836,9 +4884,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -4857,9 +4905,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -4903,9 +4951,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "smawk" @@ -4931,9 +4979,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -4973,6 +5021,7 @@ dependencies = [ "sos-core", "sos-database", "sos-database-upgrader", + "sos-debug-snapshot", "sos-external-files", "sos-integrity", "sos-login", @@ -4993,7 +5042,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-rustls", - "toml 0.8.20", + "toml 0.8.22", "tracing", "tracing-subscriber", "unicode-width 0.2.0", @@ -5004,13 +5053,13 @@ dependencies = [ [[package]] name = "sos-account" -version = "0.17.3" +version = "0.17.4" dependencies = [ "async-trait", "futures", "futures-util", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5077,7 +5126,7 @@ version = "0.17.0" dependencies = [ "async-trait", "binary-stream", - "bitflags 2.9.0", + "bitflags 2.9.1", "futures", "rustc_version", "serde", @@ -5093,7 +5142,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "serde", "sos-archive", @@ -5115,6 +5164,21 @@ dependencies = [ "urn", ] +[[package]] +name = "sos-changes" +version = "0.17.0" +dependencies = [ + "futures", + "interprocess", + "rustc_version", + "serde_json", + "sos-core", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "sos-cli-helpers" version = "0.1.1" @@ -5133,7 +5197,7 @@ dependencies = [ "binary-stream", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "parking_lot", "rustc_version", "secrecy", @@ -5182,7 +5246,7 @@ dependencies = [ "async-trait", "balloon-hash", "binary-stream", - "bitflags 2.9.0", + "bitflags 2.9.1", "bs58", "chacha20poly1305", "etcetera", @@ -5216,7 +5280,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "refinery", "rustc_version", "secrecy", @@ -5249,7 +5313,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5277,6 +5341,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "sos-debug-snapshot" +version = "0.17.0" +dependencies = [ + "futures", + "serde_json", + "sos-archive", + "sos-backend", + "sos-client-storage", + "sos-logs", + "sos-sync", + "sos-vfs", + "thiserror 2.0.12", +] + [[package]] name = "sos-extension-service" version = "0.17.0" @@ -5284,10 +5363,12 @@ dependencies = [ "anyhow", "sos-account", "sos-backend", + "sos-changes", "sos-core", "sos-ipc", "sos-net", "tokio", + "tracing", "xclipboard", ] @@ -5295,7 +5376,7 @@ dependencies = [ name = "sos-external-files" version = "0.17.0" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-core", "sos-vault", @@ -5313,7 +5394,7 @@ dependencies = [ "futures", "futures-util", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "parking_lot", "rustc_version", "serde", @@ -5350,7 +5431,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "maplit2", "parking_lot", "pretty_assertions", @@ -5360,6 +5441,7 @@ dependencies = [ "sos-account", "sos-audit", "sos-backend", + "sos-changes", "sos-client-storage", "sos-core", "sos-database", @@ -5402,7 +5484,7 @@ dependencies = [ "binary-stream", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sha2", "sos-backend", @@ -5443,6 +5525,7 @@ dependencies = [ "serde_with", "sos-account", "sos-backend", + "sos-changes", "sos-client-storage", "sos-core", "sos-database", @@ -5457,7 +5540,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-util", - "tower 0.5.2", + "tower", "tracing", "typeshare", "url", @@ -5543,7 +5626,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "prost", "rand 0.8.5", "rs_merkle", @@ -5633,7 +5716,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "prost", "prost-build", "protoc-bin-vendored", @@ -5666,7 +5749,7 @@ name = "sos-reducers" version = "0.17.0" dependencies = [ "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-core", "sos-vault", @@ -5678,7 +5761,7 @@ name = "sos-remote-sync" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "sos-account", "sos-backend", @@ -5756,7 +5839,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.7.1", + "indexmap 2.9.0", "k256", "rustc_version", "rustls", @@ -5782,7 +5865,7 @@ dependencies = [ "tokio-rustls-acme", "tokio-stream", "tokio-util", - "toml 0.8.20", + "toml 0.8.22", "tower-http", "tracing", "tracing-subscriber", @@ -5799,7 +5882,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "serde", @@ -5847,7 +5930,7 @@ name = "sos-sync" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "serde", "sos-backend", @@ -5933,7 +6016,7 @@ dependencies = [ "bytes", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "k256", "keychain_parser", "pem", @@ -5982,11 +6065,11 @@ dependencies = [ "age", "async-trait", "binary-stream", - "bitflags 2.9.0", + "bitflags 2.9.1", "ed25519-dalek", "futures", "hex", - "indexmap 2.7.1", + "indexmap 2.9.0", "pem", "rustc_version", "secrecy", @@ -6013,7 +6096,7 @@ version = "0.3.0" dependencies = [ "anyhow", "async-recursion", - "bitflags 2.9.0", + "bitflags 2.9.1", "futures", "parking_lot", "tokio", @@ -6024,7 +6107,7 @@ name = "sos-web" version = "0.17.0" dependencies = [ "async-trait", - "indexmap 2.7.1", + "indexmap 2.9.0", "rustc_version", "secrecy", "sos-account", @@ -6069,9 +6152,9 @@ dependencies = [ [[package]] name = "sql_query_builder" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "635f6bb07a64bf4a9a55251b965fd3c7998ecb56f2aaa59f3a6abca6f3f07974" +checksum = "17871783fcf12dbdf8e7d243240dd950f613c486d5e079026ec11495f7b24b06" [[package]] name = "stable_deref_trait" @@ -6116,9 +6199,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.99" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -6136,13 +6219,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -6154,7 +6237,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.20", + "toml 0.8.22", "version-compare", ] @@ -6166,15 +6249,14 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.18.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if 1.0.0", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.1", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -6191,11 +6273,11 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 0.38.44", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -6237,7 +6319,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -6248,7 +6330,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -6263,9 +6345,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.39" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -6281,15 +6363,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -6316,9 +6398,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -6364,7 +6446,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -6402,7 +6484,7 @@ dependencies = [ "time", "tokio", "tokio-rustls", - "webpki-roots", + "webpki-roots 0.26.11", "x509-parser", ] @@ -6430,14 +6512,14 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -6458,21 +6540,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.24", + "toml_edit 0.22.26", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] @@ -6483,29 +6565,36 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.7.3", + "toml_write", + "winnow 0.7.10", ] +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "totp-rs" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90" +checksum = "f124352108f58ef88299e909f6e9470f1cdc8d2a1397963901b4a6366206bf72" dependencies = [ "base32", "constant_time_eq", @@ -6519,21 +6608,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -6552,15 +6626,18 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", + "futures-util", "http", "http-body", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -6610,7 +6687,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -6682,7 +6759,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.0", + "rand 0.9.1", "rustls", "rustls-pki-types", "sha1", @@ -6692,18 +6769,18 @@ dependencies = [ [[package]] name = "type-map" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 1.1.0", + "rustc-hash 2.1.1", ] [[package]] name = "typed-generational-arena" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3478ec5cc6caaa9ed86791e8970e320841c3362a7a14b81a5c5c3f9e254b8a44" +checksum = "23107afb81ff70f7588dca56c1eeab4dad8b3a141b0e85d2a61fb575d1f12d62" dependencies = [ "cfg-if 0.1.10", "nonzero_ext", @@ -6735,23 +6812,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" dependencies = [ "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] name = "unic-langid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" dependencies = [ "unic-langid-impl", ] [[package]] name = "unic-langid-impl" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" dependencies = [ "serde", "tinystr", @@ -6852,12 +6929,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -6876,7 +6947,7 @@ version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.9.0", "serde", "serde_json", "utoipa-gen", @@ -6890,7 +6961,7 @@ checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", "uuid", ] @@ -6908,12 +6979,14 @@ dependencies = [ [[package]] name = "uuid" -version = "1.15.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.3", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] @@ -6924,9 +6997,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcard4" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f37cbf15f76a5cb6bf6a4d7d6004471cfe0974eac80605fee1cb8c837c9df5d" +checksum = "a7529ce5655c3d5da5e738bb887ae158ed70009baa3591fc58b12358d0adb0bd" dependencies = [ "aho-corasick", "base64 0.22.1", @@ -6987,9 +7060,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -7016,7 +7089,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", "wasm-bindgen-shared", ] @@ -7051,7 +7124,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7100,13 +7173,28 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.8" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.0", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" @@ -7159,23 +7247,27 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.52.0" +version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.56.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.4", + "windows-strings", ] [[package]] @@ -7186,7 +7278,18 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -7197,25 +7300,25 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] -name = "windows-link" -version = "0.1.0" +name = "windows-interface" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] [[package]] -name = "windows-registry" -version = "0.2.0" +name = "windows-link" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result 0.2.0", - "windows-strings", - "windows-targets 0.52.6", -] +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" @@ -7228,21 +7331,20 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.2.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -7513,33 +7615,27 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x11rb" @@ -7601,9 +7697,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "yansi" @@ -7622,9 +7718,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -7634,55 +7730,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" -dependencies = [ - "zerocopy-derive 0.8.23", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.99", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] @@ -7702,7 +7777,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", "synstructure", ] @@ -7723,14 +7798,25 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", ] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -7739,13 +7825,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.99", + "syn 2.0.101", ] [[package]] From 2f1c8bc34f08ac5ac4bbdcd182c074f6eba3a0a1 Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 1 Sep 2025 21:23:59 +0800 Subject: [PATCH 58/61] Fixing lint warnings. --- crates/database/src/entity/account.rs | 14 +++-- crates/database/src/entity/event.rs | 12 ++--- crates/database/src/entity/folder.rs | 64 ++++++++++------------- crates/protocol/src/network_client/mod.rs | 20 ++++--- 4 files changed, 48 insertions(+), 62 deletions(-) diff --git a/crates/database/src/entity/account.rs b/crates/database/src/entity/account.rs index b8657a7785..0bf148d07b 100644 --- a/crates/database/src/entity/account.rs +++ b/crates/database/src/entity/account.rs @@ -287,8 +287,8 @@ where .from("accounts") .where_clause("identifier = ?1"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - Ok(stmt - .query_row([account_id.to_string()], |row| Ok(row.try_into()?))?) + stmt + .query_row([account_id.to_string()], |row| row.try_into()) } /// Find an optional account in the database. @@ -301,9 +301,9 @@ where .from("accounts") .where_clause("identifier = ?1"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - Ok(stmt - .query_row([account_id.to_string()], |row| Ok(row.try_into()?)) - .optional()?) + stmt + .query_row([account_id.to_string()], |row| row.try_into()) + .optional() } /// List accounts. @@ -318,9 +318,7 @@ where Ok(row.try_into()?) } - let rows = stmt.query_and_then([], |row| { - Ok::<_, crate::Error>(convert_row(row)?) - })?; + let rows = stmt.query_and_then([], convert_row)?; let mut accounts = Vec::new(); for row in rows { accounts.push(row?); diff --git a/crates/database/src/entity/event.rs b/crates/database/src/entity/event.rs index 01e6150d6d..5e1d867fec 100644 --- a/crates/database/src/entity/event.rs +++ b/crates/database/src/entity/event.rs @@ -202,7 +202,7 @@ where .from(table.as_str()) .where_clause("event_id=?1"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - Ok(stmt.query_row([event_id], |row| Ok(row.try_into()?))?) + stmt.query_row([event_id], |row| row.try_into()) } /// Delete an event from the database table. @@ -298,9 +298,7 @@ where Ok(row.try_into()?) } - let rows = stmt.query_and_then([id], |row| { - Ok::<_, crate::Error>(convert_row(row)?) - })?; + let rows = stmt.query_and_then([id], convert_row)?; let mut events = Vec::new(); for row in rows { @@ -328,9 +326,7 @@ where Ok(row.try_into()?) } - let rows = stmt.query_and_then([account_or_folder_id], |row| { - Ok::<_, crate::Error>(convert_row(row)?) - })?; + let rows = stmt.query_and_then([account_or_folder_id], convert_row)?; let mut commits = Vec::new(); for row in rows { @@ -350,7 +346,7 @@ where .delete_from(table.as_str()) .where_clause(&format!("{}=?1", table.id_column())); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - Ok(stmt.execute([account_or_folder_id])?) + stmt.execute([account_or_folder_id]) } fn create_events( diff --git a/crates/database/src/entity/folder.rs b/crates/database/src/entity/folder.rs index 5e4f6aeb69..c72632d299 100644 --- a/crates/database/src/entity/folder.rs +++ b/crates/database/src/entity/folder.rs @@ -219,7 +219,7 @@ impl FolderRecord { let mut vault: Vault = self.summary.clone().into(); vault.header_mut().set_meta(self.meta.clone()); vault.header_mut().set_salt(self.salt.clone()); - vault.header_mut().set_seed(self.seed.clone()); + vault.header_mut().set_seed(self.seed); Ok(vault) } } @@ -399,7 +399,7 @@ impl<'conn> FolderEntity<'conn, Transaction<'conn>> { secret_rows.push(SecretRow::new(secret_id, commit, entry).await?); } - Ok(client + client .conn_mut_and_then(move |conn| { let tx = conn.transaction()?; let folder_entity = FolderEntity::new(&tx); @@ -421,7 +421,7 @@ impl<'conn> FolderEntity<'conn, Transaction<'conn>> { tx.commit()?; Ok::<_, Error>((folder_id, secret_ids)) }) - .await?) + .await } /// Replace all secrets for a folder using a transaction. @@ -430,7 +430,7 @@ impl<'conn> FolderEntity<'conn, Transaction<'conn>> { folder_id: &VaultId, vault: &Vault, ) -> Result<()> { - let folder_id = folder_id.clone(); + let folder_id = *folder_id; let mut insert_secrets = Vec::new(); for (secret_id, commit) in vault.iter() { let VaultCommit(commit, entry) = commit; @@ -470,10 +470,10 @@ where Self { conn } } - fn select_folder( - &self, + fn select_folder<'a>( + &'a self, use_identifier: bool, - ) -> StdResult { + ) -> StdResult, SqlError> { let query = folder_select_columns(sql::Select::new()).from("folders"); let query = if use_identifier { @@ -481,7 +481,7 @@ where } else { query.where_clause("folder_id = ?1") }; - Ok(self.conn.prepare_cached(&query.as_string())?) + self.conn.prepare_cached(&query.as_string()) } /// Find a folder in the database. @@ -491,8 +491,8 @@ where folder_id: &VaultId, ) -> StdResult { let mut stmt = self.select_folder(true)?; - Ok(stmt - .query_row([folder_id.to_string()], |row| Ok(row.try_into()?))?) + stmt + .query_row([folder_id.to_string()], |row| row.try_into()) } /// Find an optional folder in the database. @@ -502,12 +502,12 @@ where folder_id: &VaultId, ) -> StdResult, SqlError> { let mut stmt = self.select_folder(true)?; - Ok(stmt + stmt .query_row([folder_id.to_string()], |row| { let row: FolderRow = row.try_into()?; Ok(row) }) - .optional()?) + .optional() } /// Find a folder in the database by primary key. @@ -516,14 +516,14 @@ where folder_id: i64, ) -> StdResult { let mut stmt = self.select_folder(false)?; - Ok(stmt.query_row([folder_id], |row| Ok(row.try_into()?))?) + stmt.query_row([folder_id], |row| row.try_into()) } /// Try to find a login folder for an account. pub fn find_login_folder(&self, account_id: i64) -> Result { - Ok(self + self .find_login_folder_optional(account_id)? - .ok_or_else(|| Error::NoLoginFolder(account_id))?) + .ok_or_else(|| Error::NoLoginFolder(account_id)) } /// Try to find an optional login folder for an account. @@ -540,9 +540,9 @@ where .where_and("login.account_id=?1"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - Ok(stmt - .query_row([account_id], |row| Ok(row.try_into()?)) - .optional()?) + stmt + .query_row([account_id], |row| row.try_into()) + .optional() } /// Try to find a device folder for an account. @@ -559,9 +559,9 @@ where .where_and("device.account_id=?1"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - Ok(stmt - .query_row([account_id], |row| Ok(row.try_into()?)) - .optional()?) + stmt + .query_row([account_id], |row| row.try_into()) + .optional() } /// List user folders for an account. @@ -589,9 +589,7 @@ where Ok(row.try_into()?) } - let rows = stmt.query_and_then([account_id], |row| { - Ok::<_, crate::Error>(convert_row(row)?) - })?; + let rows = stmt.query_and_then([account_id], convert_row)?; let mut folders = Vec::new(); for row in rows { folders.push(row?); @@ -742,7 +740,7 @@ where for secret_row in rows { let identifier: SecretId = secret_row.identifier.parse()?; let secret_id = - self.insert_secret_by_row_id(folder_id, &secret_row)?; + self.insert_secret_by_row_id(folder_id, secret_row)?; secret_ids.insert(identifier, secret_id); } Ok(secret_ids) @@ -755,7 +753,7 @@ where secret_row: &SecretRow, ) -> StdResult { let row = self.find_one(folder_id)?; - Ok(self.insert_secret_by_row_id(row.row_id, secret_row)?) + self.insert_secret_by_row_id(row.row_id, secret_row) } /// Insert a secret using the folder row id. @@ -806,12 +804,12 @@ where .where_and("identifier=?2"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - Ok(stmt + stmt .query_row((row.row_id, secret_id.to_string()), |row| { let row: SecretRow = row.try_into()?; Ok(row) }) - .optional()?) + .optional() } /// Update a folder secret. @@ -859,9 +857,7 @@ where Ok(row.try_into()?) } - let rows = stmt.query_and_then([folder_row_id], |row| { - Ok::<_, crate::Error>(convert_row(row)?) - })?; + let rows = stmt.query_and_then([folder_row_id], convert_row)?; let mut secrets = Vec::new(); for row in rows { secrets.push(row?); @@ -886,9 +882,7 @@ where Ok(id.parse()?) } - let rows = stmt.query_and_then([folder.row_id], |row| { - Ok::<_, crate::Error>(convert_row(row)?) - })?; + let rows = stmt.query_and_then([folder.row_id], convert_row)?; let mut secrets = Vec::new(); for row in rows { secrets.push(row?); @@ -936,6 +930,6 @@ where .delete_from("folder_secrets") .where_clause("folder_id = ?1"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - Ok(stmt.execute([folder_id])?) + stmt.execute([folder_id]) } } diff --git a/crates/protocol/src/network_client/mod.rs b/crates/protocol/src/network_client/mod.rs index dc6cc03e02..cee22d89e6 100644 --- a/crates/protocol/src/network_client/mod.rs +++ b/crates/protocol/src/network_client/mod.rs @@ -118,17 +118,15 @@ impl NetworkRetry { "retry", ); - loop { - tokio::select! { - _ = cancel.changed() => { - let reason = cancel.borrow(); - tracing::debug!(id = %id, "retry::canceled"); - return Err(Error::RetryCanceled(reason.clone())); - } - _ = tokio::time::sleep(Duration::from_millis(delay)) => { - return Ok(callback.await) - } - }; + tokio::select! { + _ = cancel.changed() => { + let reason = cancel.borrow(); + tracing::debug!(id = %id, "retry::canceled"); + Err(Error::RetryCanceled(reason.clone())) + } + _ = tokio::time::sleep(Duration::from_millis(delay)) => { + Ok(callback.await) + } } } } From 3e013e32e4890e2bb7e3514fe843bba2c9d6fa9c Mon Sep 17 00:00:00 2001 From: muji Date: Mon, 1 Sep 2025 21:35:15 +0800 Subject: [PATCH 59/61] Fixing clippy warnings. --- clippy.toml | 1 + crates/archive/src/writer.rs | 2 +- crates/audit/src/encoding.rs | 5 ++--- crates/changes/src/producer.rs | 2 +- crates/clipboard/src/desktop.rs | 2 +- crates/core/src/account.rs | 8 +------- crates/keychain_parser/src/parser.rs | 6 +++--- crates/system_messages/src/system_messages.rs | 3 +-- tests/unit/build.rs | 1 + 9 files changed, 12 insertions(+), 18 deletions(-) create mode 100644 clippy.toml diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000000..594fe364e6 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +allow = ["clippy::new_without_default", "clippy::len_without_is_empty"] diff --git a/crates/archive/src/writer.rs b/crates/archive/src/writer.rs index b75133e4ed..6cfc03b233 100644 --- a/crates/archive/src/writer.rs +++ b/crates/archive/src/writer.rs @@ -44,7 +44,7 @@ impl Writer { let month: u8 = now.month().into(); let dt = ZipDateTimeBuilder::new() - .year(now.year().into()) + .year(now.year()) .month(month.into()) .day(now.day().into()) .hour(hours.into()) diff --git a/crates/audit/src/encoding.rs b/crates/audit/src/encoding.rs index 7d680a44be..ca67425431 100644 --- a/crates/audit/src/encoding.rs +++ b/crates/audit/src/encoding.rs @@ -7,7 +7,7 @@ use sos_core::{ encoding::{decode_uuid, encoding_error}, UtcDateTime, }; -use std::io::{Error, ErrorKind, Result}; +use std::io::{Error, Result}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; #[async_trait] @@ -79,8 +79,7 @@ impl Decodable for AuditEvent { } } } else { - return Err(Error::new( - ErrorKind::Other, + return Err(Error::other( "log data flags has bad bits", )); } diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs index 516efd4c73..4a5e4ba3ad 100644 --- a/crates/changes/src/producer.rs +++ b/crates/changes/src/producer.rs @@ -81,7 +81,7 @@ impl ChangeProducer { Ok(_) => { let event = rx.borrow_and_update().clone(); let sockets = sockets.lock().await; - dispatch_sockets(event, &*sockets).await?; + dispatch_sockets(event, &sockets).await?; } Err(_) => {} } diff --git a/crates/clipboard/src/desktop.rs b/crates/clipboard/src/desktop.rs index 771b91f5db..1050df52c2 100644 --- a/crates/clipboard/src/desktop.rs +++ b/crates/clipboard/src/desktop.rs @@ -94,7 +94,7 @@ impl Clipboard { Ok(mut clipboard) => match clipboard.get_text() { Ok(text) => { let mut reader = source_text.lock().await; - if &*reader == &text { + if *reader == text { let source = &mut *reader; source.zeroize(); diff --git a/crates/core/src/account.rs b/crates/core/src/account.rs index cb4cf170f9..3d6a7863ab 100644 --- a/crates/core/src/account.rs +++ b/crates/core/src/account.rs @@ -7,7 +7,7 @@ use std::{fmt, str::FromStr}; /// /// String encoding starts with 0x and is followed with /// 20 bytes hex-encoded. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Hash, Eq, PartialEq)] +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, Hash, Eq, PartialEq)] #[serde(try_from = "String", into = "String")] pub struct AccountId([u8; 20]); @@ -25,12 +25,6 @@ impl fmt::Display for AccountId { } } -impl Default for AccountId { - fn default() -> Self { - Self([0u8; 20]) - } -} - impl AsRef<[u8]> for AccountId { fn as_ref(&self) -> &[u8] { &self.0 diff --git a/crates/keychain_parser/src/parser.rs b/crates/keychain_parser/src/parser.rs index da0b18fc60..1a57b54992 100644 --- a/crates/keychain_parser/src/parser.rs +++ b/crates/keychain_parser/src/parser.rs @@ -54,10 +54,10 @@ pub fn unescape_octal(value: &str) -> Result> { } /// Parse a plist and extract the value for a secure note. -pub fn plist_secure_note( - value: &str, +pub fn plist_secure_note<'a>( + value: &'a str, unescape: bool, -) -> Result>> { +) -> Result>> { let plist = if unescape { unescape_octal(value)? } else { diff --git a/crates/system_messages/src/system_messages.rs b/crates/system_messages/src/system_messages.rs index 7244d24f3b..7343524709 100644 --- a/crates/system_messages/src/system_messages.rs +++ b/crates/system_messages/src/system_messages.rs @@ -307,8 +307,7 @@ where } fn counts(&self) -> SysMessageCount { - let mut counts: SysMessageCount = Default::default(); - counts.total = self.messages.0.len(); + let mut counts = SysMessageCount { total: self.messages.0.len(), ..Default::default() }; for item in self.messages.0.values() { if !item.is_read { counts.unread += 1; diff --git a/tests/unit/build.rs b/tests/unit/build.rs index 21d3b257a4..23246b0881 100644 --- a/tests/unit/build.rs +++ b/tests/unit/build.rs @@ -1,3 +1,4 @@ +#![allow(clippy::print_literal)] fn main() { println!("cargo::rustc-check-cfg=cfg(NOT_CI)"); if option_env!("CI").is_some() { From 2a407f6c2e5a52d388d364671677c795bb934fbc Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 4 Sep 2025 15:59:46 +0800 Subject: [PATCH 60/61] Fix clippy.toml. --- clippy.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/clippy.toml b/clippy.toml index 594fe364e6..e69de29bb2 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +0,0 @@ -allow = ["clippy::new_without_default", "clippy::len_without_is_empty"] From 1f78996279526eb3b0de21a339ef7d419886ff48 Mon Sep 17 00:00:00 2001 From: muji Date: Thu, 4 Sep 2025 16:09:04 +0800 Subject: [PATCH 61/61] Fixing clippy warnings. --- crates/database/src/entity/folder.rs | 6 +----- crates/database/src/entity/server.rs | 14 +++++++------- crates/database/src/event_log.rs | 14 +++++++------- crates/database/src/server_origins.rs | 12 ++++++------ crates/database/src/vault_writer.rs | 20 ++++++++++---------- 5 files changed, 31 insertions(+), 35 deletions(-) diff --git a/crates/database/src/entity/folder.rs b/crates/database/src/entity/folder.rs index c72632d299..5ec838d0a0 100644 --- a/crates/database/src/entity/folder.rs +++ b/crates/database/src/entity/folder.rs @@ -181,11 +181,7 @@ impl FolderRecord { let flags = VaultFlags::from_bits(bits) .ok_or(sos_vault::Error::InvalidVaultFlags)?; - let salt = if let Some(salt) = value.salt { - Some(salt) - } else { - None - }; + let salt = value.salt; let meta = if let Some(meta) = &value.meta { Some(decode(meta).await?) diff --git a/crates/database/src/entity/server.rs b/crates/database/src/entity/server.rs index d3119184f5..1abb6ac4b1 100644 --- a/crates/database/src/entity/server.rs +++ b/crates/database/src/entity/server.rs @@ -96,9 +96,9 @@ where let mut stmt = self .conn .prepare_cached(&self.find_server_select(true).as_string())?; - Ok(stmt.query_row((account_id, url.to_string()), |row| { - Ok(row.try_into()?) - })?) + stmt.query_row((account_id, url.to_string()), |row| { + row.try_into() + }) } /// Find an optional server in the database. @@ -110,11 +110,11 @@ where let mut stmt = self .conn .prepare_cached(&self.find_server_select(true).as_string())?; - Ok(stmt + stmt .query_row((account_id, url.to_string()), |row| { - Ok(row.try_into()?) + row.try_into() }) - .optional()?) + .optional() } /// Load servers for an account. @@ -127,7 +127,7 @@ where } let rows = stmt.query_and_then([account_id], |row| { - Ok::<_, crate::Error>(convert_row(row)?) + convert_row(row) })?; let mut servers = Vec::new(); for row in rows { diff --git a/crates/database/src/event_log.rs b/crates/database/src/event_log.rs index c080782135..3b569e0d05 100644 --- a/crates/database/src/event_log.rs +++ b/crates/database/src/event_log.rs @@ -178,12 +178,12 @@ where after: None, }; - let log_type = self.log_type.clone(); + let log_type = self.log_type; let mut insert_rows = Vec::new(); let mut commits = Vec::new(); for record in records { commits.push(*record.commit()); - insert_rows.push(EventRecordRow::new(&record)?); + insert_rows.push(EventRecordRow::new(record)?); } let id = (&self.owner).into(); @@ -370,7 +370,7 @@ where let (tx, rx) = tokio::sync::mpsc::channel(8); let id: i64 = (&self.owner).into(); - let log_type = self.log_type.clone(); + let log_type = self.log_type; let client = self.client.clone(); tokio::spawn(async move { @@ -389,7 +389,7 @@ where } let rows = stmt.query_and_then([id], |row| { - Ok::<_, crate::Error>(convert_row(row)?) + convert_row(row) })?; for row in rows { @@ -502,7 +502,7 @@ where records.iter().map(|r| *r.commit()).collect::>(); // Delete from the database - let log_type = self.log_type.clone(); + let log_type = self.log_type; self.client .conn_mut(move |conn| { let tx = conn.transaction()?; @@ -523,7 +523,7 @@ where } async fn load_tree(&mut self) -> Result<(), Self::Error> { - let log_type = self.log_type.clone(); + let log_type = self.log_type; let id = (&self.owner).into(); let commits = self .client @@ -544,7 +544,7 @@ where } async fn clear(&mut self) -> Result<(), Self::Error> { - let log_type = self.log_type.clone(); + let log_type = self.log_type; let id = (&self.owner).into(); self.client .conn_mut(move |conn| { diff --git a/crates/database/src/server_origins.rs b/crates/database/src/server_origins.rs index 27576a97a9..a7016a0398 100644 --- a/crates/database/src/server_origins.rs +++ b/crates/database/src/server_origins.rs @@ -41,14 +41,14 @@ where } async fn list_origins(&self) -> Result, E> { - let account_id = self.account_id.clone(); + let account_id = self.account_id; let servers = self .client .conn_and_then(move |conn| { let accounts = AccountEntity::new(&conn); let account_row = accounts.find_one(&account_id)?; let servers = ServerEntity::new(&conn); - Ok(servers.load_servers(account_row.row_id)?) + servers.load_servers(account_row.row_id) }) .await?; let mut set = HashSet::new(); @@ -63,7 +63,7 @@ where origin: Origin, remove: Option<&Origin>, ) -> Result<(), E> { - let account_id = self.account_id.clone(); + let account_id = self.account_id; let remove = remove.cloned(); let server_row: ServerRow = origin.try_into()?; self.client @@ -110,7 +110,7 @@ where &mut self, origin: Origin, ) -> Result<(), Self::Error> { - let account_id = self.account_id.clone(); + let account_id = self.account_id; let url = origin.url().clone(); let server_row = self .client @@ -118,7 +118,7 @@ where let accounts = AccountEntity::new(&conn); let account_row = accounts.find_one(&account_id)?; let servers = ServerEntity::new(&conn); - Ok(servers.find_optional(account_row.row_id, &url)?) + servers.find_optional(account_row.row_id, &url) }) .await .map_err(Error::from)?; @@ -144,7 +144,7 @@ where &mut self, origin: &Origin, ) -> Result<(), Self::Error> { - let account_id = self.account_id.clone(); + let account_id = self.account_id; let origin = origin.clone(); self.client .conn_mut(move |conn| { diff --git a/crates/database/src/vault_writer.rs b/crates/database/src/vault_writer.rs index 2b9a0aa9a2..bb36fce880 100644 --- a/crates/database/src/vault_writer.rs +++ b/crates/database/src/vault_writer.rs @@ -68,7 +68,7 @@ where type Error = E; async fn summary(&self) -> Result { - let folder_id = self.folder_id.clone(); + let folder_id = self.folder_id; let row = self .client .conn(move |conn| { @@ -92,7 +92,7 @@ where &mut self, name: String, ) -> Result { - let folder_id = self.folder_id.clone(); + let folder_id = self.folder_id; let folder_name = name.clone(); self.client .conn_and_then(move |conn| { @@ -107,7 +107,7 @@ where &mut self, flags: VaultFlags, ) -> Result { - let folder_id = self.folder_id.clone(); + let folder_id = self.folder_id; let folder_flags = flags.clone(); self.client .conn_and_then(move |conn| { @@ -122,7 +122,7 @@ where &mut self, meta_data: AeadPack, ) -> Result { - let folder_id = self.folder_id.clone(); + let folder_id = self.folder_id; let folder_meta = encode(&meta_data).await?; self.client .conn_and_then(move |conn| { @@ -147,7 +147,7 @@ where commit: CommitHash, secret: VaultEntry, ) -> Result { - let folder_id = self.folder_id.clone(); + let folder_id = self.folder_id; let secret_row = SecretRow::new(&secret_id, &commit, &secret).await?; self.client .conn(move |conn| { @@ -167,13 +167,13 @@ where &'a self, secret_id: &SecretId, ) -> Result, ReadEvent)>, Self::Error> { - let folder_id = self.folder_id.clone(); + let folder_id = self.folder_id; let folder_secret_id = *secret_id; let secret_row = self .client .conn(move |conn| { let folder = FolderEntity::new(&conn); - Ok(folder.find_secret(&folder_id, &folder_secret_id)?) + folder.find_secret(&folder_id, &folder_secret_id) }) .await .map_err(Error::from)?; @@ -193,7 +193,7 @@ where commit: CommitHash, secret: VaultEntry, ) -> Result, Self::Error> { - let folder_id = self.folder_id.clone(); + let folder_id = self.folder_id; let secret_row = SecretRow::new(secret_id, &commit, &secret).await?; let updated = self .client @@ -212,13 +212,13 @@ where &mut self, secret_id: &SecretId, ) -> Result, Self::Error> { - let folder_id = self.folder_id.clone(); + let folder_id = self.folder_id; let folder_secret_id = *secret_id; let deleted = self .client .conn(move |conn| { let folder = FolderEntity::new(&conn); - Ok(folder.delete_secret(&folder_id, &folder_secret_id)?) + folder.delete_secret(&folder_id, &folder_secret_id) }) .await .map_err(Error::from)?;