From ee99f119f055aba4263310955e5f9d290e4e3acd Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Wed, 2 Jul 2025 10:48:44 -0700 Subject: [PATCH] Atproto, add bound string --- atproto/Cargo.toml | 1 + atproto/src/error.rs | 2 ++ atproto/src/types.rs | 2 ++ atproto/src/types/bound_string.rs | 57 +++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 atproto/src/types/bound_string.rs diff --git a/atproto/Cargo.toml b/atproto/Cargo.toml index 3f5445b..0881493 100644 --- a/atproto/Cargo.toml +++ b/atproto/Cargo.toml @@ -13,6 +13,7 @@ time = { version = "0.3.41", features = ["parsing", "formatting"] } tracing-subscriber.workspace = true tracing.workspace = true thiserror.workspace = true +unicode-segmentation = "1.9.0" [features] default = [] diff --git a/atproto/src/error.rs b/atproto/src/error.rs index 5b52dfb..e320a61 100644 --- a/atproto/src/error.rs +++ b/atproto/src/error.rs @@ -24,6 +24,8 @@ pub enum ParseError { Serde(#[from] serde_json::error::Error), #[error("Length of parsed object too long, max: {max:?}, got: {got:?}.")] Length { max: usize, got: usize }, + #[error("Length of parsed object too short, min: {min:?}, got: {got:?}.")] + MinLength { min: usize, got: usize }, #[error("Currently Did is enforced, cannot use handle, {handle:?}")] ForceDid { handle: String }, #[error("Incorrectly formatted")] diff --git a/atproto/src/types.rs b/atproto/src/types.rs index 5baa02d..2e1c387 100644 --- a/atproto/src/types.rs +++ b/atproto/src/types.rs @@ -56,6 +56,7 @@ macro_rules! basic_string_type { } mod authority; +mod bound_string; mod cid; mod datetime; mod did; @@ -63,6 +64,7 @@ mod record_key; mod strong_ref; mod uri; pub use authority::Authority; +pub use bound_string::BoundString; pub use cid::Cid; pub use datetime::Datetime; pub use did::Did; diff --git a/atproto/src/types/bound_string.rs b/atproto/src/types/bound_string.rs new file mode 100644 index 0000000..db81bcf --- /dev/null +++ b/atproto/src/types/bound_string.rs @@ -0,0 +1,57 @@ +use unicode_segmentation::UnicodeSegmentation; +use crate::error::{Error, ParseError}; +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + str::FromStr, +}; + +pub struct BoundString< + const MIN: usize, const MAX: usize> +{ + value: String, +} + +impl BoundString { + fn check_length(s: &str) -> Result<(), Error> { + let grapheme_count: usize = s.graphemes(true).take(MAX + 1).count(); + if grapheme_count > MAX { + return Err(Error::Parse { + err: ParseError::Length { max: MAX, got: grapheme_count }, + object: "String".to_string(), + }); + } + if grapheme_count < MIN { + return Err(Error::Parse { + err: ParseError::MinLength { min: MIN, got: grapheme_count }, + object: "String".to_string(), + }); + } + Ok(()) + } +} + +impl Display for BoundString { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{}", self.value) + } +} + +impl FromStr for BoundString { + type Err = Error; + fn from_str(s: &str) -> Result { + Self::check_length(s)?; + + Ok(BoundString { value: s.to_string() }) + } +} + +impl<'de, const MIN: usize, const MAX: usize> serde::de::Deserialize<'de> + for BoundString { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let value: String = serde::de::Deserialize::deserialize(deserializer)?; + value.parse::>().map_err(::custom) + } +}