Skip to content

RUST-465 Introduce a bson::Uuid wrapper type #314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,14 @@ indexmap = "1.6.2"
hex = "0.4.2"
base64 = "0.13.0"
lazy_static = "1.4.0"
uuid = "0.8.1"
uuid = { version = "0.8.1", features = ["serde", "v4"] }
serde_bytes = "0.11.5"

[dev-dependencies]
assert_matches = "1.2"
serde_bytes = "0.11"
pretty_assertions = "0.6.1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "0.8.1", features = ["serde", "v4"] }

[package.metadata.docs.rs]
all-features = true
Expand Down
35 changes: 1 addition & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,40 +246,7 @@ let query = doc! {

### Working with UUIDs

The BSON format does not contain a dedicated UUID type, though it does have a "binary" type which
has UUID subtypes. Accordingly, this crate provides a
[`Binary`](https://docs.rs/bson/latest/bson/struct.Binary.html) struct which models this binary type
and serializes to and deserializes from it. The popular [`uuid`](https://docs.rs/uuid) crate does
provide a UUID type (`Uuid`), though its `Serialize` and `Deserialize` implementations operate on
strings, so when using it with BSON, the BSON binary type will not be used. To facilitate the
conversion between `Uuid` values and BSON binary values, the `uuid-0_8` feature flag can be
enabled. This flag exposes a number of convenient conversions from `Uuid`, including the
[`uuid_as_binary`](https://docs.rs/bson/latest/bson/serde_helpers/uuid_as_binary/index.html)
serde helper, which can be used to (de)serialize `Uuid`s to/from BSON binaries with the UUID
subtype, and the `From<Uuid>` implementation for `Bson`, which allows `Uuid` values to be used in
the `doc!` and `bson!` macros.

e.g.

``` rust
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Foo {
// serializes as a String.
uuid: Uuid,

// serializes as a BSON binary with subtype 4.
// this requires the "uuid-0_8" feature flag
#[serde(with = "bson::serde_helpers::uuid_as_binary")]
uuid_as_bson: uuid::Uuid,
}

// this automatic conversion also requires the "uuid-0_8" feature flag
let query = doc! {
"uuid": uuid::Uuid::new_v4(),
};
```
See the module-level documentation for the [`bson::uuid` module](https://docs.rs/bson/latest/bson/uuid).

## Minimum supported Rust version (MSRV)

Expand Down
147 changes: 1 addition & 146 deletions src/bson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ use crate::{
Decimal128,
};

#[cfg(feature = "uuid-0_8")]
use serde::de::Error;

/// Possible BSON value types.
#[derive(Clone, PartialEq)]
pub enum Bson {
Expand Down Expand Up @@ -1058,7 +1055,7 @@ impl Display for Binary {
}

impl Binary {
fn from_extended_doc(doc: &Document) -> Option<Self> {
pub(crate) fn from_extended_doc(doc: &Document) -> Option<Self> {
let binary = doc.get_document("$binary").ok()?;
let bytes = binary.get_str("base64").ok()?;
let bytes = base64::decode(bytes).ok()?;
Expand All @@ -1076,148 +1073,6 @@ impl Binary {
}
}

/// Enum of the possible representations to use when converting between [Uuid](https://docs.rs/uuid/0.8.2/uuid/) and [`Binary`].
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was all moved to the uuid module. The main difference is that these methods now work with bson::Uuid instead of uuid::Uuid, and that they also can be used without a feature flag.

/// This enum is necessary because the different drivers used to have different ways of encoding
/// UUIDs, with the BSON subtype: 0x03 (UUID old).
/// If a UUID has been serialized with a particular representation, it MUST
/// be deserialized with the same representation.
///
/// Example:
/// ```
/// use crate::{bson::UuidRepresentation, bson::Binary};
/// use uuid::Uuid;
/// let uuid = Uuid::parse_str("00112233445566778899AABBCCDDEEFF").unwrap();
/// let bin = Binary::from_uuid_with_representation(uuid, UuidRepresentation::PythonLegacy);
/// let new_uuid = bin.to_uuid();
/// assert!(new_uuid.is_err());
/// let new_uuid = bin.to_uuid_with_representation(UuidRepresentation::PythonLegacy);
/// assert!(new_uuid.is_ok());
/// assert_eq!(new_uuid.unwrap(), uuid);
/// ```
#[cfg(feature = "uuid-0_8")]
#[cfg_attr(docsrs, doc(cfg(feature = "uuid-0_8")))]
#[non_exhaustive]
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum UuidRepresentation {
/// The canonical representation of UUIDs in BSON (binary with subtype 0x04)
Standard,
/// The legacy representation of UUIDs in BSON used by the C# driver (binary subtype 0x03)
CSharpLegacy,
/// The legacy representation of UUIDs in BSON used by the Java driver (binary subtype 0x03)
JavaLegacy,
/// The legacy representation of UUIDs in BSON used by the Python driver, which is the same
/// format as STANDARD, but has binary subtype 0x03
PythonLegacy,
}

#[cfg(feature = "uuid-0_8")]
#[cfg_attr(docsrs, doc(cfg(feature = "uuid-0_8")))]
impl Binary {
/// Serializes a [Uuid](https://docs.rs/uuid/0.8.2/uuid/) into BSON [`Binary`] type
pub fn from_uuid(uuid: uuid::Uuid) -> Self {
Binary::from(uuid)
}
/// Serializes a UUID into BSON binary type and takes the desired representation as a parameter.
/// `Binary::from_uuid_with_representation(uuid, UuidRepresentation::Standard)` is equivalent
/// to `Binary::from_uuid(uuid)`.
///
/// See the documentation for [`UuidRepresentation`] for more information on the possible
/// representations.
pub fn from_uuid_with_representation(uuid: uuid::Uuid, rep: UuidRepresentation) -> Self {
match rep {
UuidRepresentation::Standard => Binary::from_uuid(uuid),
UuidRepresentation::CSharpLegacy => {
let mut bytes = uuid.as_bytes().to_vec();
bytes[0..4].reverse();
bytes[4..6].reverse();
bytes[6..8].reverse();
Binary {
subtype: BinarySubtype::UuidOld,
bytes,
}
}
UuidRepresentation::PythonLegacy => Binary {
subtype: BinarySubtype::UuidOld,
bytes: uuid.as_bytes().to_vec(),
},
UuidRepresentation::JavaLegacy => {
let mut bytes = uuid.as_bytes().to_vec();
bytes[0..8].reverse();
bytes[8..16].reverse();
Binary {
subtype: BinarySubtype::UuidOld,
bytes,
}
}
}
}
/// Deserializes a BSON [`Binary`] type into a [Uuid](https://docs.rs/uuid/0.8.2/uuid/), takes the representation with which the [`Binary`]
/// was serialized.
///
/// See the documentation for [`UuidRepresentation`] for more information on the possible
/// representations.
pub fn to_uuid_with_representation(
&self,
rep: UuidRepresentation,
) -> Result<uuid::Uuid, crate::de::Error> {
// If representation is non-standard, then its subtype must be UuidOld
if rep != UuidRepresentation::Standard && self.subtype != BinarySubtype::UuidOld {
return Err(Error::custom(format!(
"expected binary subtype 3 when converting to UUID with a non-standard \
representation, instead got {:#04x}",
u8::from(self.subtype)
)));
}
// If representation is standard, then its subtype must be Uuid
if rep == UuidRepresentation::Standard && self.subtype != BinarySubtype::Uuid {
return Err(Error::custom(format!(
"expected binary subtype 4 when converting to UUID with the standard \
representation, instead got {:#04x}",
u8::from(self.subtype)
)));
}
// Must be 16 bytes long
if self.bytes.len() != 16 {
return Err(Error::custom(format!(
"expected UUID to contain 16 bytes, instead got {}",
self.bytes.len()
)));
}
let mut buf = [0u8; 16];
buf.copy_from_slice(&self.bytes);
match rep {
UuidRepresentation::Standard => Ok(uuid::Uuid::from_bytes(buf)),
UuidRepresentation::CSharpLegacy => {
buf[0..4].reverse();
buf[4..6].reverse();
buf[6..8].reverse();
Ok(uuid::Uuid::from_bytes(buf))
}
UuidRepresentation::PythonLegacy => Ok(uuid::Uuid::from_bytes(buf)),
UuidRepresentation::JavaLegacy => {
buf[0..8].reverse();
buf[8..16].reverse();
Ok(uuid::Uuid::from_bytes(buf))
}
}
}
/// Deserializes a BSON [`Binary`] type into a [Uuid](https://docs.rs/uuid/0.8.2/uuid/) using the standard representation.
pub fn to_uuid(&self) -> Result<uuid::Uuid, crate::de::Error> {
self.to_uuid_with_representation(UuidRepresentation::Standard)
}
}

#[cfg(feature = "uuid-0_8")]
#[cfg_attr(docsrs, doc(cfg(feature = "uuid-0_8")))]
impl From<uuid::Uuid> for Binary {
fn from(uuid: uuid::Uuid) -> Self {
Binary {
subtype: BinarySubtype::Uuid,
bytes: uuid.as_bytes().to_vec(),
}
}
}

/// Represents a DBPointer. (Deprecated)
#[derive(Debug, Clone, PartialEq)]
pub struct DbPointer {
Expand Down
2 changes: 2 additions & 0 deletions src/de/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ use ::serde::{
Deserialize,
};

pub(crate) use self::serde::BsonVisitor;

pub(crate) const MAX_BSON_SIZE: i32 = 16 * 1024 * 1024;
pub(crate) const MIN_BSON_DOCUMENT_SIZE: i32 = 4 + 1; // 4 bytes for length, one byte for null terminator
pub(crate) const MIN_BSON_STRING_SIZE: i32 = 4 + 1; // 4 bytes for length, one byte for null terminator
Expand Down
55 changes: 49 additions & 6 deletions src/de/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use serde::{
use crate::{
oid::ObjectId,
spec::{BinarySubtype, ElementType},
uuid::UUID_NEWTYPE_NAME,
Binary,
Bson,
DateTime,
Expand Down Expand Up @@ -130,15 +131,24 @@ impl<'de> Deserializer<'de> {
self.current_type = element_type;
Ok(Some(element_type))
}
}

impl<'de, 'a> serde::de::Deserializer<'de> for &'a mut Deserializer<'de> {
type Error = Error;

fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
fn deserialize_next<V>(
&mut self,
visitor: V,
binary_subtype_hint: Option<BinarySubtype>,
) -> Result<V::Value>
where
V: serde::de::Visitor<'de>,
{
if let Some(expected_st) = binary_subtype_hint {
if self.current_type != ElementType::Binary {
return Err(Error::custom(format!(
"expected Binary with subtype {:?}, instead got {:?}",
expected_st, self.current_type
)));
}
}

match self.current_type {
ElementType::Int32 => visitor.visit_i32(read_i32(&mut self.bytes)?),
ElementType::Int64 => visitor.visit_i64(read_i64(&mut self.bytes)?),
Expand Down Expand Up @@ -166,6 +176,16 @@ impl<'de, 'a> serde::de::Deserializer<'de> for &'a mut Deserializer<'de> {
));
}
let subtype = BinarySubtype::from(read_u8(&mut self.bytes)?);

if let Some(expected_subtype) = binary_subtype_hint {
if subtype != expected_subtype {
return Err(Error::custom(format!(
"expected binary subtype {:?} instead got {:?}",
expected_subtype, subtype
)));
}
}

match subtype {
BinarySubtype::Generic => {
visitor.visit_borrowed_bytes(self.bytes.read_slice(len as usize)?)
Expand Down Expand Up @@ -245,6 +265,18 @@ impl<'de, 'a> serde::de::Deserializer<'de> for &'a mut Deserializer<'de> {
}
}
}
}

impl<'de, 'a> serde::de::Deserializer<'de> for &'a mut Deserializer<'de> {
type Error = Error;

#[inline]
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
where
V: serde::de::Visitor<'de>,
{
self.deserialize_next(visitor, None)
}

#[inline]
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
Expand Down Expand Up @@ -285,13 +317,24 @@ impl<'de, 'a> serde::de::Deserializer<'de> for &'a mut Deserializer<'de> {
}
}

fn deserialize_newtype_struct<V>(self, name: &'static str, visitor: V) -> Result<V::Value>
where
V: serde::de::Visitor<'de>,
{
if name == UUID_NEWTYPE_NAME {
self.deserialize_next(visitor, Some(BinarySubtype::Uuid))
} else {
visitor.visit_newtype_struct(self)
}
}

fn is_human_readable(&self) -> bool {
false
}

forward_to_deserialize_any! {
bool char str byte_buf unit unit_struct string
identifier newtype_struct seq tuple tuple_struct struct
identifier seq tuple tuple_struct struct
map ignored_any i8 i16 i32 i64 u8 u16 u32 u64 f32 f64
}
}
Expand Down
28 changes: 25 additions & 3 deletions src/de/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{
document::{Document, IntoIter},
oid::ObjectId,
spec::BinarySubtype,
uuid::UUID_NEWTYPE_NAME,
Decimal128,
};

Expand Down Expand Up @@ -127,7 +128,7 @@ impl<'de> Visitor<'de> for BsonVisitor {
type Value = Bson;

fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("expecting a Bson")
f.write_str("a Bson")
}

#[inline]
Expand Down Expand Up @@ -460,6 +461,14 @@ impl<'de> Visitor<'de> for BsonVisitor {
bytes: v,
}))
}

#[inline]
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(self)
}
}

fn convert_unsigned_to_signed<E>(value: u64) -> Result<Bson, E>
Expand Down Expand Up @@ -652,13 +661,26 @@ impl<'de> de::Deserializer<'de> for Deserializer {
#[inline]
fn deserialize_newtype_struct<V>(
self,
_name: &'static str,
name: &'static str,
visitor: V,
) -> crate::de::Result<V::Value>
where
V: Visitor<'de>,
{
visitor.visit_newtype_struct(self)
// if this is a UUID, ensure that value is a subtype 4 binary
if name == UUID_NEWTYPE_NAME {
match self.value {
Some(Bson::Binary(ref b)) if b.subtype == BinarySubtype::Uuid => {
self.deserialize_any(visitor)
}
b => Err(Error::custom(format!(
"expected Binary with subtype 4, instead got {:?}",
b
))),
}
} else {
visitor.visit_newtype_struct(self)
}
}

forward_to_deserialize! {
Expand Down
Loading