diff --git a/src/external/serde_support.rs b/src/external/serde_support.rs index 5228daf1..bb20aac8 100644 --- a/src/external/serde_support.rs +++ b/src/external/serde_support.rs @@ -84,22 +84,22 @@ impl<'de> Deserialize<'de> for Uuid { { #[rustfmt::skip] let bytes = [ - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, - match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(16, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(0, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(1, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(2, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(3, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(4, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(5, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(6, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(7, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(8, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(9, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(10, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(11, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(12, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(13, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(14, &self)) }, + match seq.next_element()? { Some(e) => e, None => return Err(A::Error::invalid_length(15, &self)) }, ]; Ok(Uuid::from_bytes(bytes)) @@ -127,6 +127,33 @@ impl<'de> Deserialize<'de> for Uuid { } } +enum ExpectedFormat { + Simple, + Braced, + Urn, +} + +impl std::fmt::Display for ExpectedFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + ExpectedFormat::Simple => "a simple Uuid string like 67e5504410b1426f9247bb680e5fe0c8", + ExpectedFormat::Braced => { + "a braced Uuid string like {67e55044-10b1-426f-9247-bb680e5fe0c8}" + } + ExpectedFormat::Urn => { + "a URN Uuid string like urn:uuid:67e55044-10b1-426f-9247-bb680e5fe0c8" + } + }; + f.write_str(s) + } +} + +impl de::Expected for ExpectedFormat { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + ::fmt(self, formatter) + } +} + pub mod compact { //! Serialize a [`Uuid`] as a `[u8; 16]`. //! @@ -207,6 +234,401 @@ pub mod compact { } } +/// Serialize from a [`Uuid`] as a `uuid::fmt::Simple` +/// +/// [`Uuid`]: ../../struct.Uuid.html +/// +/// ## Example +/// +/// ```rust +/// #[derive(serde_derive::Serialize, serde_derive::Deserialize)] +/// struct StructA { +/// // This will change both serailization and deserialization +/// #[serde(with = "uuid::serde::simple")] +/// id: uuid::Uuid, +/// } +/// +/// #[derive(serde_derive::Serialize, serde_derive::Deserialize)] +/// struct StructB { +/// // This will be serialized as uuid::fmt::Simple and deserialize from all valid formats +/// #[serde(serialize_with = "uuid::serde::simple::serialize")] +/// id: uuid::Uuid, +/// } +/// ``` +pub mod simple { + use serde::{de, Deserialize}; + + use crate::{parser::parse_simple, Uuid}; + + use super::ExpectedFormat; + + /// Serialize from a [`Uuid`] as a `uuid::fmt::Simple` + /// + /// [`Uuid`]: ../../struct.Uuid.html + /// + /// # Example + /// + /// ```rust + /// #[derive(serde_derive::Serialize)] + /// struct Struct { + /// // This will be serialize as uuid::fmt::Simple + /// #[serde(serialize_with = "uuid::serde::simple::serialize")] + /// id: uuid::Uuid, + /// } + /// + /// ``` + pub fn serialize(u: &crate::Uuid, serializer: S) -> Result + where + S: serde::Serializer, + { + serde::Serialize::serialize(u.as_simple(), serializer) + } + + /// Deserialize a simple Uuid string as a [`Uuid`] + /// + /// [`Uuid`]: ../../struct.Uuid.html + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = <&str as Deserialize>::deserialize(deserializer)?; + let bytes = parse_simple(s.as_bytes()).map_err(|_| { + de::Error::invalid_value(de::Unexpected::Str(s), &ExpectedFormat::Simple) + })?; + Ok(Uuid::from_bytes(bytes)) + } + + #[cfg(test)] + mod tests { + use serde::de::{self, Error}; + use serde_test::{Readable, Token}; + + use crate::{external::serde_support::ExpectedFormat, Uuid}; + + const HYPHENATED_UUID_STR: &'static str = "f9168c5e-ceb2-4faa-b6bf-329bf39fa1e4"; + const SIMPLE_UUID_STR: &'static str = "f9168c5eceb24faab6bf329bf39fa1e4"; + + #[test] + fn test_serialize_as_simple() { + #[derive(serde_derive::Serialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + + let u = Struct(Uuid::parse_str(HYPHENATED_UUID_STR).unwrap()); + serde_test::assert_ser_tokens( + &u, + &[ + Token::NewtypeStruct { name: "Struct" }, + Token::Str(SIMPLE_UUID_STR), + ], + ); + } + + #[test] + fn test_de_from_simple() { + #[derive(PartialEq, Debug, serde_derive::Deserialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + let s = Struct(HYPHENATED_UUID_STR.parse().unwrap()); + serde_test::assert_de_tokens::( + &s, + &[ + Token::TupleStruct { + name: "Struct", + len: 1, + }, + Token::BorrowedStr(SIMPLE_UUID_STR), + Token::TupleStructEnd, + ], + ); + } + + #[test] + fn test_de_reject_hypenated() { + #[derive(PartialEq, Debug, serde_derive::Deserialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + serde_test::assert_de_tokens_error::>( + &[ + Token::TupleStruct { + name: "Struct", + len: 1, + }, + Token::BorrowedStr(HYPHENATED_UUID_STR), + Token::TupleStructEnd, + ], + &format!( + "{}", + de::value::Error::invalid_value( + de::Unexpected::Str(HYPHENATED_UUID_STR), + &ExpectedFormat::Simple, + ) + ), + ); + } + } +} + +/// Serialize from a [`Uuid`] as a `uuid::fmt::Braced` +/// +/// [`Uuid`]: ../../struct.Uuid.html +/// +/// ## Example +/// +/// ```rust +/// #[derive(serde_derive::Serialize, serde_derive::Deserialize)] +/// struct StructA { +/// // This will change both serailization and deserialization +/// #[serde(with = "uuid::serde::braced")] +/// id: uuid::Uuid, +/// } +/// +/// #[derive(serde_derive::Serialize, serde_derive::Deserialize)] +/// struct StructB { +/// // This will be serialized as uuid::fmt::Urn and deserialize from all valid formats +/// #[serde(serialize_with = "uuid::serde::braced::serialize")] +/// id: uuid::Uuid, +/// } +/// ``` +pub mod braced { + use serde::{de, Deserialize}; + + use crate::parser::parse_braced; + + use super::ExpectedFormat; + + /// Serialize from a [`Uuid`] as a `uuid::fmt::Braced` + /// + /// [`Uuid`]: ../../struct.Uuid.html + /// + /// # Example + /// + /// ```rust + /// #[derive(serde_derive::Serialize)] + /// struct Struct { + /// // This will be serialize as uuid::fmt::Braced + /// #[serde(serialize_with = "uuid::serde::braced::serialize")] + /// id: uuid::Uuid, + /// } + /// + /// ``` + pub fn serialize(u: &crate::Uuid, serializer: S) -> Result + where + S: serde::Serializer, + { + serde::Serialize::serialize(u.as_braced(), serializer) + } + + /// Deserialize a braced Uuid string as a [`Uuid`] + /// + /// [`Uuid`]: ../../struct.Uuid.html + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = <&str as Deserialize>::deserialize(deserializer)?; + let bytes = parse_braced(s.as_bytes()).map_err(|_| { + de::Error::invalid_value(de::Unexpected::Str(s), &ExpectedFormat::Braced) + })?; + Ok(crate::Uuid::from_bytes(bytes)) + } + + #[cfg(test)] + mod tests { + use serde::de::{self, Error}; + use serde_test::{Readable, Token}; + + use crate::{external::serde_support::ExpectedFormat, Uuid}; + + const HYPHENATED_UUID_STR: &'static str = "f9168c5e-ceb2-4faa-b6bf-329bf39fa1e4"; + const BRACED_UUID_STR: &'static str = "{f9168c5e-ceb2-4faa-b6bf-329bf39fa1e4}"; + + #[test] + fn test_serialize_as_braced() { + #[derive(serde_derive::Serialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + + let u = Struct(Uuid::parse_str(HYPHENATED_UUID_STR).unwrap()); + serde_test::assert_ser_tokens( + &u, + &[ + Token::NewtypeStruct { name: "Struct" }, + Token::Str(BRACED_UUID_STR), + ], + ); + } + + #[test] + fn test_de_from_braced() { + #[derive(PartialEq, Debug, serde_derive::Deserialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + let s = Struct(HYPHENATED_UUID_STR.parse().unwrap()); + serde_test::assert_de_tokens::( + &s, + &[ + Token::TupleStruct { + name: "Struct", + len: 1, + }, + Token::BorrowedStr(BRACED_UUID_STR), + Token::TupleStructEnd, + ], + ); + } + + #[test] + fn test_de_reject_hypenated() { + #[derive(PartialEq, Debug, serde_derive::Deserialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + serde_test::assert_de_tokens_error::>( + &[ + Token::TupleStruct { + name: "Struct", + len: 1, + }, + Token::BorrowedStr(HYPHENATED_UUID_STR), + Token::TupleStructEnd, + ], + &format!( + "{}", + de::value::Error::invalid_value( + de::Unexpected::Str(HYPHENATED_UUID_STR), + &ExpectedFormat::Braced, + ) + ), + ); + } + } +} + +/// Serialize from a [`Uuid`] as a `uuid::fmt::Urn` +/// +/// [`Uuid`]: ../../struct.Uuid.html +/// +/// ## Example +/// +/// ```rust +/// #[derive(serde_derive::Serialize, serde_derive::Deserialize)] +/// struct StructA { +/// // This will change both serailization and deserialization +/// #[serde(with = "uuid::serde::urn")] +/// id: uuid::Uuid, +/// } +/// +/// #[derive(serde_derive::Serialize, serde_derive::Deserialize)] +/// struct StructB { +/// // This will be serialized as uuid::fmt::Urn and deserialize from all valid formats +/// #[serde(serialize_with = "uuid::serde::urn::serialize")] +/// id: uuid::Uuid, +/// } +/// ``` +pub mod urn { + use serde::{de, Deserialize}; + + use crate::parser::parse_urn; + + use super::ExpectedFormat; + + /// Serialize from a [`Uuid`] as a `uuid::fmt::Urn` + /// + /// [`Uuid`]: ../../struct.Uuid.html + /// + /// # Example + /// + /// ```rust + /// #[derive(serde_derive::Serialize)] + /// struct Struct { + /// // This will be serialize as uuid::fmt::Urn + /// #[serde(serialize_with = "uuid::serde::urn::serialize")] + /// id: uuid::Uuid, + /// } + /// + /// ``` + pub fn serialize(u: &crate::Uuid, serializer: S) -> Result + where + S: serde::Serializer, + { + serde::Serialize::serialize(u.as_urn(), serializer) + } + + /// Deserialize a urn Uuid string as a [`Uuid`] + /// + /// [`Uuid`]: ../../struct.Uuid.html + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = <&str as Deserialize>::deserialize(deserializer)?; + let bytes = parse_urn(s.as_bytes()) + .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(s), &ExpectedFormat::Urn))?; + Ok(crate::Uuid::from_bytes(bytes)) + } + + #[cfg(test)] + mod tests { + use serde::de::{self, Error}; + use serde_test::{Readable, Token}; + + use crate::{external::serde_support::ExpectedFormat, Uuid}; + + const HYPHENATED_UUID_STR: &'static str = "f9168c5e-ceb2-4faa-b6bf-329bf39fa1e4"; + const URN_UUID_STR: &'static str = "urn:uuid:f9168c5e-ceb2-4faa-b6bf-329bf39fa1e4"; + + #[test] + fn test_serialize_as_urn() { + #[derive(serde_derive::Serialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + + let u = Struct(Uuid::parse_str(HYPHENATED_UUID_STR).unwrap()); + serde_test::assert_ser_tokens( + &u, + &[ + Token::NewtypeStruct { name: "Struct" }, + Token::Str(URN_UUID_STR), + ], + ); + } + + #[test] + fn test_de_from_urn() { + #[derive(PartialEq, Debug, serde_derive::Deserialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + let s = Struct(HYPHENATED_UUID_STR.parse().unwrap()); + serde_test::assert_de_tokens::( + &s, + &[ + Token::TupleStruct { + name: "Struct", + len: 1, + }, + Token::BorrowedStr(URN_UUID_STR), + Token::TupleStructEnd, + ], + ); + } + + #[test] + fn test_de_reject_hypenated() { + #[derive(PartialEq, Debug, serde_derive::Deserialize)] + struct Struct(#[serde(with = "super")] crate::Uuid); + serde_test::assert_de_tokens_error::>( + &[ + Token::TupleStruct { + name: "Struct", + len: 1, + }, + Token::BorrowedStr(HYPHENATED_UUID_STR), + Token::TupleStructEnd, + ], + &format!( + "{}", + de::value::Error::invalid_value( + de::Unexpected::Str(HYPHENATED_UUID_STR), + &ExpectedFormat::Urn, + ) + ), + ); + } + } +} + #[cfg(test)] mod serde_tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 4c3c9b99..8e9b866d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -963,7 +963,7 @@ pub mod serde { //! to change the way a [`Uuid`](../struct.Uuid.html) is serialized //! and deserialized. - pub use crate::external::serde_support::compact; + pub use crate::external::serde_support::{braced, compact, simple, urn}; } #[cfg(test)] diff --git a/src/parser.rs b/src/parser.rs index 0eabcfe7..c782e779 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -133,7 +133,7 @@ impl Uuid { } const fn try_parse(input: &[u8]) -> Result<[u8; 16], InvalidUuid> { - let result = match (input.len(), input) { + match (input.len(), input) { // Inputs of 32 bytes must be a non-hyphenated UUID (32, s) => parse_simple(s), // Hyphenated UUIDs may be wrapped in various ways: @@ -146,21 +146,36 @@ const fn try_parse(input: &[u8]) -> Result<[u8; 16], InvalidUuid> { parse_hyphenated(s) } // Any other shaped input is immediately invalid - _ => Err(()), - }; + _ => Err(InvalidUuid(input)), + } +} + +#[inline] +pub(crate) const fn parse_braced(input: &[u8]) -> Result<[u8; 16], InvalidUuid> { + if let (38, [b'{', s @ .., b'}']) = (input.len(), input) { + parse_hyphenated(s) + } else { + Err(InvalidUuid(input)) + } +} - match result { - Ok(b) => Ok(b), - Err(()) => Err(InvalidUuid(input)), +#[inline] +pub(crate) const fn parse_urn(input: &[u8]) -> Result<[u8; 16], InvalidUuid> { + if let (45, [b'u', b'r', b'n', b':', b'u', b'u', b'i', b'd', b':', s @ ..]) = + (input.len(), input) + { + parse_hyphenated(s) + } else { + Err(InvalidUuid(input)) } } #[inline] -const fn parse_simple(s: &[u8]) -> Result<[u8; 16], ()> { +pub(crate) const fn parse_simple(s: &[u8]) -> Result<[u8; 16], InvalidUuid> { // This length check here removes all other bounds // checks in this function if s.len() != 32 { - return Err(()); + return Err(InvalidUuid(s)); } let mut buf: [u8; 16] = [0; 16]; @@ -175,7 +190,7 @@ const fn parse_simple(s: &[u8]) -> Result<[u8; 16], ()> { // We use `0xff` as a sentinel value to indicate // an invalid hex character sequence (like the letter `G`) if h1 | h2 == 0xff { - return Err(()); + return Err(InvalidUuid(s)); } // The upper nibble needs to be shifted into position @@ -188,11 +203,11 @@ const fn parse_simple(s: &[u8]) -> Result<[u8; 16], ()> { } #[inline] -const fn parse_hyphenated(s: &[u8]) -> Result<[u8; 16], ()> { +const fn parse_hyphenated(s: &[u8]) -> Result<[u8; 16], InvalidUuid> { // This length check here removes all other bounds // checks in this function if s.len() != 36 { - return Err(()); + return Err(InvalidUuid(s)); } // We look at two hex-encoded values (4 chars) at a time because @@ -207,7 +222,7 @@ const fn parse_hyphenated(s: &[u8]) -> Result<[u8; 16], ()> { // First, ensure the hyphens appear in the right places match [s[8], s[13], s[18], s[23]] { [b'-', b'-', b'-', b'-'] => {} - _ => return Err(()), + _ => return Err(InvalidUuid(s)), } let positions: [u8; 8] = [0, 4, 9, 14, 19, 24, 28, 32]; @@ -225,7 +240,7 @@ const fn parse_hyphenated(s: &[u8]) -> Result<[u8; 16], ()> { let h4 = HEX_TABLE[s[(i + 3) as usize] as usize]; if h1 | h2 | h3 | h4 == 0xff { - return Err(()); + return Err(InvalidUuid(s)); } buf[j * 2] = SHL4_TABLE[h1 as usize] | h2; @@ -522,6 +537,22 @@ mod tests { assert_eq!(uuid_orig, uuid_out); } + #[test] + fn test_roundtrip_parse_urn() { + let uuid_orig = new(); + let orig_str = uuid_orig.urn().to_string(); + let uuid_out = Uuid::from_bytes(parse_urn(orig_str.as_bytes()).unwrap()); + assert_eq!(uuid_orig, uuid_out); + } + + #[test] + fn test_roundtrip_parse_braced() { + let uuid_orig = new(); + let orig_str = uuid_orig.braced().to_string(); + let uuid_out = Uuid::from_bytes(parse_braced(orig_str.as_bytes()).unwrap()); + assert_eq!(uuid_orig, uuid_out); + } + #[test] fn test_try_parse_ascii_non_utf8() { assert!(Uuid::try_parse_ascii(b"67e55044-10b1-426f-9247-bb680e5\0e0c8").is_err());