From b65086985f9f7a0d069d6d611c2ef2d3242f39cc Mon Sep 17 00:00:00 2001 From: Tait Hoyem Date: Sun, 21 Apr 2024 13:56:42 -0600 Subject: [PATCH] Update to atspi v0.21.0 --- Cargo.lock | 72 +++-------------- Cargo.toml | 9 +-- cache/Cargo.toml | 1 - cache/src/convertable.rs | 162 +++++++++++++++++++++++++++++++++++++++ cache/src/lib.rs | 154 ++++++++++++++++++------------------- odilia/Cargo.toml | 1 - 6 files changed, 248 insertions(+), 151 deletions(-) create mode 100644 cache/src/convertable.rs diff --git a/Cargo.lock b/Cargo.lock index 79044f9e..aa781d8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,34 +351,20 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" -version = "0.16.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9212da2086f77502f3f384c2c802705f0681cc4937be2417978f8e6243099b3" +checksum = "0b0e6aa3f7b617b3fd296defcbd186159e8182f968805ce4b509f8aa2bddaa30" dependencies = [ - "atspi-client", "atspi-common", "atspi-connection", "atspi-proxies", ] -[[package]] -name = "atspi-client" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e464e7a0e29d619091668329fe3b0de0351227f9a7bf36ce3c79e2c417f744c5" -dependencies = [ - "async-trait", - "atspi-common", - "atspi-proxies", - "static_assertions", - "zbus", -] - [[package]] name = "atspi-common" -version = "0.1.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03467fb79afd9e86e63aa908d542df63171abb02e66cb4e28978ec1e1da7870" +checksum = "67f7c733947d5eb53e7b6c740b8822428813c9f6fd8745c02aa890fde87b5dd6" dependencies = [ "enumflags2", "serde", @@ -390,42 +376,23 @@ dependencies = [ [[package]] name = "atspi-connection" -version = "0.1.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8531aef8ca768b5ecc601b2838dba66b7142f24c5dde587be0900aef96eaf43f" +checksum = "2980b9b690d2a76554cc3e3a984ff69c79ba7cc100f919c39f443780437b81bc" dependencies = [ "atspi-common", "atspi-proxies", - "futures-lite 1.13.0", - "zbus", -] - -[[package]] -name = "atspi-macros" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a1ad0c51b69e83d6f551707598e0b53d14336b16867d0372df31210556c0b0" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "serde", - "syn 1.0.109", + "futures-lite 2.2.0", "zbus", - "zvariant", ] [[package]] name = "atspi-proxies" -version = "0.1.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c539f2c42b6f9c513902c55cf1c48c78140a186b63feb77df1845ef8464275a0" +checksum = "0619da7b6b282cea5a94b8536526659fccab4a0677cee1d05c6783b37ae5a2e8" dependencies = [ - "async-trait", "atspi-common", - "atspi-macros", - "enumflags2", "serde", "zbus", ] @@ -1506,7 +1473,6 @@ name = "odilia" version = "0.1.4" dependencies = [ "atspi", - "atspi-client", "atspi-common", "atspi-connection", "atspi-proxies", @@ -1541,7 +1507,6 @@ version = "0.3.0" dependencies = [ "async-trait", "atspi", - "atspi-client", "atspi-common", "atspi-connection", "atspi-proxies", @@ -1992,18 +1957,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-xml-rs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" -dependencies = [ - "log", - "serde", - "thiserror", - "xml-rs", -] - [[package]] name = "serde_derive" version = "1.0.197" @@ -2828,12 +2781,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "xml-rs" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" - [[package]] name = "zbus" version = "3.15.2" @@ -2863,7 +2810,6 @@ dependencies = [ "ordered-stream", "rand", "serde", - "serde-xml-rs", "serde_repr", "sha1", "static_assertions", diff --git a/Cargo.toml b/Cargo.toml index 5c657972..7fb6fcc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,11 +31,10 @@ pre-release-hook = ["cargo", "fmt"] dependent-version = "upgrade" [workspace.dependencies] -atspi = { version = "0.16.0", default-features = false, features = ["tokio", "unstable-proxy-traits"] } -atspi-client = { version = "0.1.0", default-features = false } -atspi-proxies = { version = "0.1.0", default-features = false } -atspi-common = { version = "0.1.0", default-features = false } -atspi-connection = { version = "0.1.0", default-features = false } +atspi = { version = "0.21.0", default-features = false, features = ["tokio"] } +atspi-proxies = { version = "0.5.0", default-features = false, features = ["tokio"] } +atspi-common = { version = "0.5.0", default-features = false, features = ["tokio"] } +atspi-connection = { version = "0.5.0", default-features = false, features = ["tokio"] } odilia-common = { version = "0.3.0", path = "./common" } odilia-cache = { version = "0.3.0", path = "./cache" } eyre = "0.6.8" diff --git a/cache/Cargo.toml b/cache/Cargo.toml index dceedd42..24324c4b 100644 --- a/cache/Cargo.toml +++ b/cache/Cargo.toml @@ -15,7 +15,6 @@ categories = ["accessibility"] atspi.workspace = true atspi-proxies.workspace = true atspi-common.workspace = true -atspi-client.workspace = true odilia-common.workspace = true dashmap = "5.4.0" serde = "1.0.147" diff --git a/cache/src/convertable.rs b/cache/src/convertable.rs new file mode 100644 index 00000000..eb73c8a4 --- /dev/null +++ b/cache/src/convertable.rs @@ -0,0 +1,162 @@ +use async_trait::async_trait; +use atspi::Interface; +use atspi_proxies::{ + accessible::AccessibleProxy, + action::ActionProxy, + application::ApplicationProxy, + collection::CollectionProxy, + component::ComponentProxy, + document::DocumentProxy, + editable_text::EditableTextProxy, + hyperlink::HyperlinkProxy, + hypertext::HypertextProxy, + image::ImageProxy, + selection::SelectionProxy, + table::TableProxy, + table_cell::TableCellProxy, + text::TextProxy, + value::ValueProxy, + AtspiProxy, +}; +use std::ops::Deref; +use zbus::{ + blocking::Proxy as ProxyBlocking, blocking::ProxyBuilder as ProxyBuilderBlocking, + CacheProperties, Error, Proxy, ProxyBuilder, ProxyDefault, +}; + +#[allow(clippy::module_name_repetitions)] +#[async_trait] +pub trait Convertable { + type Error: std::error::Error; + + /// Creates an [`Self::Accessible`] from the existing accessible item. + /// # Errors + /// + /// This may fail based on the implementation of. + /// Generally, it fails if the accessible item does not implement to accessible interface. + /// This shouldn't be possible, but this function may fail for other reasons. + /// For example, to convert a [`zbus::Proxy`] into a [`Self::Accessible`], it may fail to create the new [`atspi_proxies::accessible::AccessibleProxy`]. + async fn to_accessible(&self) -> Result; + /// Creates an [`Self::Action`] from the existing accessible item. + /// # Errors + /// + /// This may fail based on the implementation. + /// Generally, it fails if the accessible item does not implement to action interface. + async fn to_action(&self) -> Result; + /// Creates an [`Self::Application`] from the existing accessible item. + /// # Errors + /// + /// This may fail based on the implementation. + /// Generally, it fails if the accessible item does not implement to application interface. + async fn to_application(&self) -> Result; + /// Creates an [`Collection`] from the existing accessible item. + /// # Errors + /// + /// This may fail based on the implementation. + /// GenerallyProxy, it fails if the accessible item does not implement to collection interface. + async fn to_collection(&self) -> Result; + /// Creates an [`Component`] from the existing accessible item. + /// # Errors + /// + /// This may fail based on the implementation. + /// GenerallyProxy, it fails if the accessible item does not implement to component interface. + async fn to_component(&self) -> Result; + async fn to_document(&self) -> Result; + async fn to_hypertext(&self) -> Result; + async fn to_hyperlink(&self) -> Result; + async fn to_image(&self) -> Result; + async fn to_selection(&self) -> Result; + async fn to_table(&self) -> Result; + async fn to_table_cell(&self) -> Result; + async fn to_text(&self) -> Result; + async fn to_editable_text(&self) -> Result; + async fn to_value(&self) -> Result; +} + +#[inline] +async fn convert_to_new_type< + 'a, + 'b, + T: From> + ProxyDefault, + U: Deref> + ProxyDefault, +>( + from: &U, +) -> zbus::Result { + // first thing is first, we need to creat an accessible to query the interfaces. + let accessible = AccessibleProxy::builder(from.connection()) + .destination(from.destination())? + .cache_properties(CacheProperties::No) + .path(from.path())? + .build() + .await?; + // if the interface we're trying to convert to is not available as an interface; this can be problematic because the interface we're passing in could potentially be different from what we're converting to. + let new_interface_name = Interface::try_from(::INTERFACE).map_err(|_| Error::InterfaceNotFound)?; + if !accessible + .get_interfaces() + .await? + .contains(new_interface_name) + { + return Err(Error::InterfaceNotFound); + } + // otherwise, make a new Proxy with the related type. + let path = from.path().to_owned(); + let dest = from.destination().to_owned(); + ProxyBuilder::<'b, T>::new_bare(from.connection()) + .interface(::INTERFACE)? + .destination(dest)? + .cache_properties(CacheProperties::No) + .path(path)? + .build() + .await +} + +#[async_trait] +impl<'a, T: Deref> + ProxyDefault + Sync> Convertable for T { + type Error = zbus::Error; + /* no guard due to assumption it is always possible */ + async fn to_accessible(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_action(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_application(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_collection(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_component(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_document(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_hypertext(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_hyperlink(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_image(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_selection(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_table(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_table_cell(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_text(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_editable_text(&self) -> zbus::Result { + convert_to_new_type(self).await + } + async fn to_value(&self) -> zbus::Result { + convert_to_new_type(self).await + } +} diff --git a/cache/src/lib.rs b/cache/src/lib.rs index 06a23f2b..4415e2aa 100644 --- a/cache/src/lib.rs +++ b/cache/src/lib.rs @@ -8,19 +8,21 @@ )] #![allow(clippy::multiple_crate_versions)] +mod convertable; +use convertable::Convertable; + use std::{ collections::HashMap, sync::{Arc, RwLock, Weak}, }; use async_trait::async_trait; -use atspi_client::{convertable::Convertable, text_ext::TextExt}; use atspi_common::{ - ClipType, CoordType, GenericEvent, Granularity, InterfaceSet, RelationType, Role, StateSet, + ClipType, CoordType, GenericEvent, Granularity, InterfaceSet, RelationType, Role, StateSet, object_ref::ObjectRef, }; use atspi_proxies::{ - accessible::{Accessible, AccessibleProxy}, - text::{Text, TextProxy}, + accessible::AccessibleProxy, + text::TextProxy, }; use dashmap::DashMap; use fxhash::FxBuildHasher; @@ -35,6 +37,16 @@ use zbus::{ CacheProperties, ProxyBuilder, }; +trait AllText { + async fn get_all_text(&self) -> Result; +} +impl AllText for TextProxy<'_> { + async fn get_all_text(&self) -> Result { + let length_of_string = self.character_count().await?; + Ok(self.get_text(0, length_of_string).await?) + } +} + type CacheKey = AccessiblePrimitive; type InnerCache = DashMap>, FxBuildHasher>; type ThreadSafeCache = Arc; @@ -105,29 +117,23 @@ impl AccessiblePrimitive { Ok(Self { id, sender: sender.as_str().into() }) } } -impl TryFrom for AccessiblePrimitive { - type Error = AccessiblePrimitiveConversionError; - - #[tracing::instrument(level = "trace", ret, err)] - fn try_from( - atspi_accessible: atspi::events::Accessible, - ) -> Result { +impl From for AccessiblePrimitive { + fn from( + atspi_accessible: ObjectRef, + ) -> AccessiblePrimitive { let tuple_converter = (atspi_accessible.name, atspi_accessible.path); - tuple_converter.try_into() + tuple_converter.into() } } -impl TryFrom<(OwnedUniqueName, OwnedObjectPath)> for AccessiblePrimitive { - type Error = AccessiblePrimitiveConversionError; - - #[tracing::instrument(level = "trace", ret, err)] - fn try_from( +impl From<(OwnedUniqueName, OwnedObjectPath)> for AccessiblePrimitive { + fn from( so: (OwnedUniqueName, OwnedObjectPath), - ) -> Result { + ) -> AccessiblePrimitive { let accessible_id = so.1; - Ok(AccessiblePrimitive { + AccessiblePrimitive { id: accessible_id.to_string(), sender: so.0.as_str().into(), - }) + } } } impl From<(String, OwnedObjectPath)> for AccessiblePrimitive { @@ -338,48 +344,31 @@ fn strong_cache(weak_cache: &Weak) -> OdiliaResult> { Weak::upgrade(weak_cache).ok_or(OdiliaError::Cache(CacheError::NotAvailable)) } -#[async_trait] -impl Accessible for CacheItem { - type Error = OdiliaError; - async fn get_application(&self) -> Result { +impl CacheItem { + async fn get_application(&self) -> Result { let derefed_cache: Arc = strong_cache(&self.cache)?; derefed_cache.get(&self.app).ok_or(CacheError::NoItem.into()) } - async fn parent(&self) -> Result { + async fn parent(&self) -> Result { let parent_item = self .parent .clone_inner() .or_else(|| self.cache.upgrade()?.get(&self.parent.key)); parent_item.ok_or(CacheError::NoItem.into()) } - async fn get_children(&self) -> Result, Self::Error> { - self.get_children() - } - async fn child_count(&self) -> Result { - Ok(self.children_num) - } - async fn get_index_in_parent(&self) -> Result { - Ok(self.index) - } - async fn get_role(&self) -> Result { - Ok(self.role) - } - async fn get_interfaces(&self) -> Result { - Ok(self.interfaces) - } - async fn get_attributes(&self) -> Result, Self::Error> { + async fn get_attributes(&self) -> Result, OdiliaError> { Ok(as_accessible(self).await?.get_attributes().await?) } - async fn name(&self) -> Result { + async fn name(&self) -> Result { Ok(as_accessible(self).await?.name().await?) } - async fn locale(&self) -> Result { + async fn locale(&self) -> Result { Ok(as_accessible(self).await?.locale().await?) } - async fn description(&self) -> Result { + async fn description(&self) -> Result { Ok(as_accessible(self).await?.description().await?) } - async fn get_relation_set(&self) -> Result)>, Self::Error> { + async fn get_relation_set(&self) -> Result)>, OdiliaError> { let cache = strong_cache(&self.cache)?; as_accessible(self) .await? @@ -404,42 +393,39 @@ impl Accessible for CacheItem { .map(|(relation, result_selfs)| Ok((relation, result_selfs?))) .collect::)>, OdiliaError>>() } - async fn get_role_name(&self) -> Result { + async fn get_role_name(&self) -> Result { Ok(as_accessible(self).await?.get_role_name().await?) } - async fn get_state(&self) -> Result { + async fn get_state(&self) -> Result { Ok(self.states) } - async fn get_child_at_index(&self, idx: i32) -> Result { - ::get_children(self) - .await? + async fn get_child_at_index(&self, idx: i32) -> Result { + self.get_children()? .get(usize::try_from(idx)?) .ok_or(CacheError::NoItem.into()) .cloned() } - async fn get_localized_role_name(&self) -> Result { + async fn get_localized_role_name(&self) -> Result { Ok(as_accessible(self).await?.get_localized_role_name().await?) } - async fn accessible_id(&self) -> Result { + async fn accessible_id(&self) -> Result { Ok(self.object.id.to_string()) } } -#[async_trait] -impl Text for CacheItem { - type Error = OdiliaError; +impl CacheItem { async fn add_selection( &self, start_offset: i32, end_offset: i32, - ) -> Result { + ) -> Result { Ok(as_text(self).await?.add_selection(start_offset, end_offset).await?) } async fn get_attribute_run( &self, offset: i32, include_defaults: bool, - ) -> Result<(std::collections::HashMap, i32, i32), Self::Error> { + ) -> Result<(std::collections::HashMap, i32, i32), OdiliaError> { Ok(as_text(self) .await? .get_attribute_run(offset, include_defaults) @@ -449,16 +435,16 @@ impl Text for CacheItem { &self, offset: i32, attribute_name: &str, - ) -> Result { + ) -> Result { Ok(as_text(self) .await? .get_attribute_value(offset, attribute_name) .await?) } - async fn get_attributes( + async fn get_text_attributes( &self, offset: i32, - ) -> Result<(std::collections::HashMap, i32, i32), Self::Error> { + ) -> Result<(std::collections::HashMap, i32, i32), OdiliaError> { Ok(as_text(self).await?.get_attributes(offset).await?) } async fn get_bounded_ranges( @@ -470,7 +456,7 @@ impl Text for CacheItem { coord_type: CoordType, x_clip_type: ClipType, y_clip_type: ClipType, - ) -> Result, Self::Error> { + ) -> Result, OdiliaError> { Ok(as_text(self) .await? .get_bounded_ranges( @@ -484,27 +470,27 @@ impl Text for CacheItem { ) .await?) } - async fn get_character_at_offset(&self, offset: i32) -> Result { + async fn get_character_at_offset(&self, offset: i32) -> Result { Ok(as_text(self).await?.get_character_at_offset(offset).await?) } async fn get_character_extents( &self, offset: i32, coord_type: CoordType, - ) -> Result<(i32, i32, i32, i32), Self::Error> { + ) -> Result<(i32, i32, i32, i32), OdiliaError> { Ok(as_text(self).await?.get_character_extents(offset, coord_type).await?) } async fn get_default_attribute_set( &self, - ) -> Result, Self::Error> { + ) -> Result, OdiliaError> { Ok(as_text(self).await?.get_default_attribute_set().await?) } async fn get_default_attributes( &self, - ) -> Result, Self::Error> { + ) -> Result, OdiliaError> { Ok(as_text(self).await?.get_default_attributes().await?) } - async fn get_nselections(&self) -> Result { + async fn get_nselections(&self) -> Result { Ok(as_text(self).await?.get_nselections().await?) } async fn get_offset_at_point( @@ -512,7 +498,7 @@ impl Text for CacheItem { x: i32, y: i32, coord_type: CoordType, - ) -> Result { + ) -> Result { Ok(as_text(self).await?.get_offset_at_point(x, y, coord_type).await?) } async fn get_range_extents( @@ -520,20 +506,20 @@ impl Text for CacheItem { start_offset: i32, end_offset: i32, coord_type: CoordType, - ) -> Result<(i32, i32, i32, i32), Self::Error> { + ) -> Result<(i32, i32, i32, i32), OdiliaError> { Ok(as_text(self) .await? .get_range_extents(start_offset, end_offset, coord_type) .await?) } - async fn get_selection(&self, selection_num: i32) -> Result<(i32, i32), Self::Error> { + async fn get_selection(&self, selection_num: i32) -> Result<(i32, i32), OdiliaError> { Ok(as_text(self).await?.get_selection(selection_num).await?) } async fn get_string_at_offset( &self, offset: i32, granularity: Granularity, - ) -> Result<(String, i32, i32), Self::Error> { + ) -> Result<(String, i32, i32), OdiliaError> { let uoffset = usize::try_from(offset)?; // optimisations that don't call out to DBus. if granularity == Granularity::Paragraph { @@ -595,34 +581,40 @@ impl Text for CacheItem { &self, start_offset: i32, end_offset: i32, - ) -> Result { + ) -> Result { self.text .get(usize::try_from(start_offset)?..usize::try_from(end_offset)?) .map(std::borrow::ToOwned::to_owned) .ok_or(OdiliaError::Generic("Type is None, not Some".to_string())) } + async fn get_all_text( + &self + ) -> Result { + let length_of_string = self.character_count().await?; + Ok(self.get_text(0, length_of_string).await?) + } async fn get_text_after_offset( &self, offset: i32, type_: u32, - ) -> Result<(String, i32, i32), Self::Error> { + ) -> Result<(String, i32, i32), OdiliaError> { Ok(as_text(self).await?.get_text_after_offset(offset, type_).await?) } async fn get_text_at_offset( &self, offset: i32, type_: u32, - ) -> Result<(String, i32, i32), Self::Error> { + ) -> Result<(String, i32, i32), OdiliaError> { Ok(as_text(self).await?.get_text_at_offset(offset, type_).await?) } async fn get_text_before_offset( &self, offset: i32, type_: u32, - ) -> Result<(String, i32, i32), Self::Error> { + ) -> Result<(String, i32, i32), OdiliaError> { Ok(as_text(self).await?.get_text_before_offset(offset, type_).await?) } - async fn remove_selection(&self, selection_num: i32) -> Result { + async fn remove_selection(&self, selection_num: i32) -> Result { Ok(as_text(self).await?.remove_selection(selection_num).await?) } async fn scroll_substring_to( @@ -630,7 +622,7 @@ impl Text for CacheItem { start_offset: i32, end_offset: i32, type_: u32, - ) -> Result { + ) -> Result { Ok(as_text(self) .await? .scroll_substring_to(start_offset, end_offset, type_) @@ -643,13 +635,13 @@ impl Text for CacheItem { type_: u32, x: i32, y: i32, - ) -> Result { + ) -> Result { Ok(as_text(self) .await? .scroll_substring_to_point(start_offset, end_offset, type_, x, y) .await?) } - async fn set_caret_offset(&self, offset: i32) -> Result { + async fn set_caret_offset(&self, offset: i32) -> Result { Ok(as_text(self).await?.set_caret_offset(offset).await?) } async fn set_selection( @@ -657,16 +649,16 @@ impl Text for CacheItem { selection_num: i32, start_offset: i32, end_offset: i32, - ) -> Result { + ) -> Result { Ok(as_text(self) .await? .set_selection(selection_num, start_offset, end_offset) .await?) } - async fn caret_offset(&self) -> Result { + async fn caret_offset(&self) -> Result { Ok(as_text(self).await?.caret_offset().await?) } - async fn character_count(&self) -> Result { + async fn character_count(&self) -> Result { Ok(i32::try_from(self.text.len())?) } } diff --git a/odilia/Cargo.toml b/odilia/Cargo.toml index ef339b56..25a27c97 100644 --- a/odilia/Cargo.toml +++ b/odilia/Cargo.toml @@ -34,7 +34,6 @@ atspi.workspace = true atspi-proxies.workspace = true atspi-common.workspace = true atspi-connection.workspace = true -atspi-client.workspace = true circular-queue = "^0.2.6" eyre.workspace = true futures = { version = "^0.3.25", default-features = false }