diff --git a/src/binary.rs b/src/binary.rs new file mode 100644 index 00000000..e7a8e789 --- /dev/null +++ b/src/binary.rs @@ -0,0 +1,113 @@ +use crate::{spec::BinarySubtype, Document, RawBinaryRef}; +use std::{ + convert::TryFrom, + error, + fmt::{self, Display}, +}; + +/// Represents a BSON binary value. +#[derive(Debug, Clone, PartialEq)] +pub struct Binary { + /// The subtype of the bytes. + pub subtype: BinarySubtype, + + /// The binary bytes. + pub bytes: Vec, +} + +impl Display for Binary { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!( + fmt, + "Binary({:#x}, {})", + u8::from(self.subtype), + base64::encode(&self.bytes) + ) + } +} + +impl Binary { + /// Creates a [`Binary`] from a base64 string and optional [`BinarySubtype`]. If the + /// `subtype` argument is `None`, the [`Binary`] constructed will default to + /// [`BinarySubtype::Generic`]. + /// + /// ```rust + /// # use bson::{Binary, binary::Result}; + /// # fn example() -> Result<()> { + /// let input = base64::encode("hello"); + /// let binary = Binary::from_base64(input, None)?; + /// println!("{:?}", binary); + /// // binary: Binary { subtype: Generic, bytes: [104, 101, 108, 108, 111] } + /// # Ok(()) + /// # } + /// ``` + pub fn from_base64( + input: impl AsRef, + subtype: impl Into>, + ) -> Result { + let bytes = base64::decode(input.as_ref()).map_err(|e| Error::DecodingError { + message: e.to_string(), + })?; + let subtype = match subtype.into() { + Some(s) => s, + None => BinarySubtype::Generic, + }; + Ok(Binary { subtype, bytes }) + } + + pub(crate) fn from_extended_doc(doc: &Document) -> Option { + let binary_doc = doc.get_document("$binary").ok()?; + + if let Ok(bytes) = binary_doc.get_str("base64") { + let bytes = base64::decode(bytes).ok()?; + let subtype = binary_doc.get_str("subType").ok()?; + let subtype = hex::decode(subtype).ok()?; + if subtype.len() == 1 { + Some(Self { + bytes, + subtype: subtype[0].into(), + }) + } else { + None + } + } else { + // in non-human-readable mode, RawBinary will serialize as + // { "$binary": { "bytes": , "subType": } }; + let binary = binary_doc.get_binary_generic("bytes").ok()?; + let subtype = binary_doc.get_i32("subType").ok()?; + + Some(Self { + bytes: binary.clone(), + subtype: u8::try_from(subtype).ok()?.into(), + }) + } + } + + /// Borrow the contents as a `RawBinaryRef`. + pub fn as_raw_binary(&self) -> RawBinaryRef<'_> { + RawBinaryRef { + bytes: self.bytes.as_slice(), + subtype: self.subtype, + } + } +} + +/// Possible errors that can arise during [`Binary`] construction. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Error { + /// While trying to decode from base64, an error was returned. + DecodingError { message: String }, +} + +impl error::Error for Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::DecodingError { message: m } => fmt.write_str(m), + } + } +} + +pub type Result = std::result::Result; diff --git a/src/bson.rs b/src/bson.rs index 83f8b424..bb8af34f 100644 --- a/src/bson.rs +++ b/src/bson.rs @@ -32,8 +32,8 @@ pub use crate::document::Document; use crate::{ oid::{self, ObjectId}, spec::{BinarySubtype, ElementType}, + Binary, Decimal128, - RawBinaryRef, }; /// Possible BSON value types. @@ -1080,65 +1080,6 @@ impl Display for JavaScriptCodeWithScope { } } -/// Represents a BSON binary value. -#[derive(Debug, Clone, PartialEq)] -pub struct Binary { - /// The subtype of the bytes. - pub subtype: BinarySubtype, - - /// The binary bytes. - pub bytes: Vec, -} - -impl Display for Binary { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - write!( - fmt, - "Binary({:#x}, {})", - u8::from(self.subtype), - base64::encode(&self.bytes) - ) - } -} - -impl Binary { - pub(crate) fn from_extended_doc(doc: &Document) -> Option { - let binary_doc = doc.get_document("$binary").ok()?; - - if let Ok(bytes) = binary_doc.get_str("base64") { - let bytes = base64::decode(bytes).ok()?; - let subtype = binary_doc.get_str("subType").ok()?; - let subtype = hex::decode(subtype).ok()?; - if subtype.len() == 1 { - Some(Self { - bytes, - subtype: subtype[0].into(), - }) - } else { - None - } - } else { - // in non-human-readable mode, RawBinary will serialize as - // { "$binary": { "bytes": , "subType": } }; - let binary = binary_doc.get_binary_generic("bytes").ok()?; - let subtype = binary_doc.get_i32("subType").ok()?; - - Some(Self { - bytes: binary.clone(), - subtype: u8::try_from(subtype).ok()?.into(), - }) - } - } - - /// Borrow the contents as a `RawBinaryRef`. - pub fn as_raw_binary(&self) -> RawBinaryRef<'_> { - RawBinaryRef { - bytes: self.bytes.as_slice(), - subtype: self.subtype, - } - } -} - /// Represents a DBPointer. (Deprecated) #[derive(Debug, Clone, PartialEq)] pub struct DbPointer { diff --git a/src/de/mod.rs b/src/de/mod.rs index 5078263e..05b3214b 100644 --- a/src/de/mod.rs +++ b/src/de/mod.rs @@ -33,11 +33,12 @@ pub use self::{ use std::io::Read; use crate::{ - bson::{Array, Binary, Bson, DbPointer, Document, JavaScriptCodeWithScope, Regex, Timestamp}, + bson::{Array, Bson, DbPointer, Document, JavaScriptCodeWithScope, Regex, Timestamp}, oid::{self, ObjectId}, raw::RawBinaryRef, ser::write_i32, spec::{self, BinarySubtype}, + Binary, Decimal128, }; diff --git a/src/de/serde.rs b/src/de/serde.rs index 4598a935..3045f643 100644 --- a/src/de/serde.rs +++ b/src/de/serde.rs @@ -21,13 +21,14 @@ use serde::de::{ use serde_bytes::ByteBuf; use crate::{ - bson::{Binary, Bson, DbPointer, JavaScriptCodeWithScope, Regex, Timestamp}, + bson::{Bson, DbPointer, JavaScriptCodeWithScope, Regex, Timestamp}, datetime::DateTime, document::{Document, IntoIter}, oid::ObjectId, raw::{RawBsonRef, RAW_ARRAY_NEWTYPE, RAW_BSON_NEWTYPE, RAW_DOCUMENT_NEWTYPE}, spec::BinarySubtype, uuid::UUID_NEWTYPE_NAME, + Binary, Decimal128, }; diff --git a/src/document.rs b/src/document.rs index fedc38a1..b8d1600c 100644 --- a/src/document.rs +++ b/src/document.rs @@ -13,11 +13,12 @@ use indexmap::IndexMap; use serde::de::Error; use crate::{ - bson::{Array, Binary, Bson, Timestamp}, + bson::{Array, Bson, Timestamp}, de::{deserialize_bson_kvp, ensure_read_exactly, read_i32, MIN_BSON_DOCUMENT_SIZE}, oid::ObjectId, ser::{serialize_bson, write_i32}, spec::BinarySubtype, + Binary, Decimal128, }; diff --git a/src/lib.rs b/src/lib.rs index ed301c54..9e17cffe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -269,8 +269,9 @@ #[doc(inline)] pub use self::{ - bson::{Array, Binary, Bson, DbPointer, Document, JavaScriptCodeWithScope, Regex, Timestamp}, + bson::{Array, Bson, DbPointer, Document, JavaScriptCodeWithScope, Regex, Timestamp}, datetime::DateTime, + binary::Binary, de::{ from_bson, from_bson_with_options, from_document, from_document_with_options, from_reader, from_reader_utf8_lossy, from_slice, from_slice_utf8_lossy, Deserializer, @@ -291,6 +292,7 @@ pub use self::{ #[macro_use] mod macros; mod bson; +pub mod binary; pub mod datetime; pub mod de; pub mod decimal128; diff --git a/src/tests/modules/binary.rs b/src/tests/modules/binary.rs new file mode 100644 index 00000000..d26c4d65 --- /dev/null +++ b/src/tests/modules/binary.rs @@ -0,0 +1,21 @@ +use crate::{spec::BinarySubtype, tests::LOCK, Binary}; + +#[test] +fn binary_from_base64() { + let _guard = LOCK.run_concurrently(); + + let input = base64::encode("hello"); + let produced = Binary::from_base64(input, None).unwrap(); + let expected = Binary { + bytes: "hello".as_bytes().to_vec(), + subtype: BinarySubtype::Generic, + }; + assert_eq!(produced, expected); + + let produced = Binary::from_base64("", BinarySubtype::Uuid).unwrap(); + let expected = Binary { + bytes: "".as_bytes().to_vec(), + subtype: BinarySubtype::Uuid, + }; + assert_eq!(produced, expected); +} diff --git a/src/tests/modules/mod.rs b/src/tests/modules/mod.rs index 36c1e4fd..d55a287e 100644 --- a/src/tests/modules/mod.rs +++ b/src/tests/modules/mod.rs @@ -1,3 +1,4 @@ +mod binary; mod bson; mod document; mod lock;