use {Error, base64, serde, std};
use mime::Mime;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::str::FromStr;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Attachment {
content_type: Mime,
inner: Inner,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Inner {
ServerOrigin {
content: Content,
digest: Digest,
encoding: Option<Encoding>,
revpos: u64,
},
ClientOrigin { content: Vec<u8> },
Follows { content_length: u64 },
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Content {
WithBytes(Vec<u8>),
WithLength(u64),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Digest {
#[doc(hidden)]
Md5 { value: Vec<u8> },
#[doc(hidden)]
Other { name: String, value: Vec<u8> },
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum EncodingCodec {
Gzip,
Other(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Encoding {
length: u64,
codec: EncodingCodec,
}
impl Attachment {
pub fn new(content_type: Mime, content: Vec<u8>) -> Self {
Attachment {
content_type: content_type,
inner: Inner::ClientOrigin { content: content },
}
}
pub fn is_server_origin(&self) -> bool {
match self.inner {
Inner::ServerOrigin { .. } => true,
Inner::ClientOrigin { .. } => false,
Inner::Follows { .. } => false,
}
}
pub fn is_client_origin(&self) -> bool {
match self.inner {
Inner::ServerOrigin { .. } => false,
Inner::ClientOrigin { .. } => true,
Inner::Follows { .. } => false,
}
}
pub fn content_type(&self) -> &Mime {
&self.content_type
}
pub fn content(&self) -> Option<&[u8]> {
match self.inner {
Inner::ServerOrigin { content: Content::WithBytes(ref bytes), .. } => Some(bytes),
Inner::ServerOrigin { content: Content::WithLength(_), .. } => None,
Inner::ClientOrigin { ref content } => Some(content),
Inner::Follows { .. } => None,
}
}
pub fn content_length(&self) -> u64 {
match self.inner {
Inner::ServerOrigin { content: Content::WithBytes(ref bytes), .. } => bytes.len() as u64,
Inner::ServerOrigin { content: Content::WithLength(length), .. } => length,
Inner::ClientOrigin { ref content } => content.len() as u64,
Inner::Follows { content_length } => content_length,
}
}
pub fn to_stub(&self) -> Option<Attachment> {
match self.inner {
Inner::ServerOrigin {
ref content,
ref digest,
ref encoding,
ref revpos,
} => {
Some(Attachment {
content_type: self.content_type.clone(),
inner: Inner::ServerOrigin {
content: content.to_length_only(),
digest: digest.clone(),
encoding: encoding.clone(),
revpos: revpos.clone(),
},
})
}
_ => None,
}
}
pub fn to_multipart_stub(&self) -> Attachment {
Attachment {
content_type: self.content_type.clone(),
inner: Inner::Follows { content_length: self.content_length() },
}
}
pub fn digest(&self) -> Option<&Digest> {
match self.inner {
Inner::ServerOrigin { ref digest, .. } => Some(digest),
Inner::ClientOrigin { .. } => None,
Inner::Follows { .. } => None,
}
}
pub fn encoding(&self) -> Option<&Encoding> {
match self.inner {
Inner::ServerOrigin { ref encoding, .. } => encoding.as_ref().clone(),
Inner::ClientOrigin { .. } => None,
Inner::Follows { .. } => None,
}
}
pub fn revision_sequence(&self) -> Option<u64> {
match self.inner {
Inner::ServerOrigin { revpos, .. } => Some(revpos),
Inner::ClientOrigin { .. } => None,
Inner::Follows { .. } => None,
}
}
}
impl<'a> Deserialize<'a> for Attachment {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'a>,
{
#[derive(Deserialize)]
struct T {
content_type: SerializableMime,
data: Option<SerializableBase64>,
digest: Digest,
encoded_length: Option<u64>,
encoding: Option<String>,
length: Option<u64>,
revpos: u64,
}
let x = T::deserialize(deserializer)?;
let encoding = match (x.encoding, x.encoded_length) {
(Some(codec), Some(length)) => Some(Encoding {
codec: EncodingCodec::from(codec),
length: length,
}),
(None, None) => None,
_ => return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Map,
&Expectation(
"a JSON object with complete CouchDB attachment encoding info OR no such info",
),
)),
};
let inner = if let Some(SerializableBase64(bytes)) = x.data {
Inner::ServerOrigin {
content: Content::WithBytes(bytes),
digest: x.digest,
encoding: encoding,
revpos: x.revpos,
}
} else if let Some(content_length) = x.length {
Inner::ServerOrigin {
content: Content::WithLength(content_length),
digest: x.digest,
encoding: encoding,
revpos: x.revpos,
}
} else {
return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Map,
&Expectation(
"a JSON object with CouchDB attachment content OR content length",
),
));
};
Ok(Attachment {
content_type: x.content_type.0,
inner: inner,
})
}
}
impl Serialize for Attachment {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Debug, Default, Deserialize, Serialize)]
struct T {
content_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
stub: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
follows: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
length: Option<u64>,
}
let mut x = T::default();
x.content_type = self.content_type.to_string();
match self.inner {
Inner::ServerOrigin { .. } => {
x.stub = Some(true);
}
Inner::ClientOrigin { ref content } => {
x.data = Some(base64::encode(content));
}
Inner::Follows { content_length } => {
x.follows = Some(true);
x.length = Some(content_length);
}
};
x.serialize(serializer)
}
}
struct Expectation(&'static str);
impl serde::de::Expected for Expectation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
f.write_str(self.0)
}
}
struct SerializableBase64(Vec<u8>);
impl<'a> Deserialize<'a> for SerializableBase64 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'a>,
{
let s = String::deserialize(deserializer)?;
let v = base64::decode(&s).map_err(|_| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&s),
&Expectation("a base64-encoded string containing CouchDB attachment data"),
)
})?;
Ok(SerializableBase64(v))
}
}
impl<'a> Deserialize<'a> for Digest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'a>,
{
let s = String::deserialize(deserializer)?;
let v = Digest::from_str(&s).map_err(|_| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&s),
&Expectation("a CouchDB attachment digest"),
)
})?;
Ok(v)
}
}
impl Content {
pub fn to_length_only(&self) -> Content {
match *self {
Content::WithBytes(ref bytes) => Content::WithLength(bytes.len() as u64),
Content::WithLength(length) => Content::WithLength(length),
}
}
}
impl Digest {
pub fn bytes(&self) -> &[u8] {
match *self {
Digest::Md5 { ref value } => value,
Digest::Other { ref value, .. } => value,
}
}
pub fn is_md5(&self) -> bool {
match *self {
Digest::Md5 { .. } => true,
_ => false,
}
}
}
impl FromStr for Digest {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
let mut iter = s.splitn(2, '-');
let name = iter.next().unwrap();
let value = iter.next().ok_or(Error::BadDigest)?;
let value = base64::decode(&value).map_err(|_| Error::BadDigest)?;
Ok(match name {
"md5" => Digest::Md5 { value: value },
_ => Digest::Other {
name: String::from(name),
value: value,
},
})
}
}
impl Encoding {
pub fn length(&self) -> u64 {
self.length
}
pub fn is_gzip(&self) -> bool {
self.codec == EncodingCodec::Gzip
}
}
impl From<String> for EncodingCodec {
fn from(s: String) -> Self {
match s.as_str() {
"gzip" => EncodingCodec::Gzip,
_ => EncodingCodec::Other(s),
}
}
}
struct SerializableMime(Mime);
impl<'a> Deserialize<'a> for SerializableMime {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'a>,
{
let s = String::deserialize(deserializer)?;
let v = Mime::from_str(&s).map_err(|_| {
serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&s),
&Expectation("a string specifying a MIME type"),
)
})?;
Ok(SerializableMime(v))
}
}
#[cfg(test)]
mod tests {
use super::*;
use {mime, serde_json};
#[test]
fn attachment_deserializes_as_stub() {
let source = r#"{
"content_type": "text/plain",
"digest": "md5-Ids41vtv725jyrN7iUvMcQ==",
"length": 1872,
"revpos": 4,
"stub": true
}"#;
let expected = Attachment {
content_type: mime::TEXT_PLAIN,
inner: Inner::ServerOrigin {
content: Content::WithLength(1872),
digest: Digest::Md5 {
value: Vec::from(
b"\x21\xdb\x38\xd6\
\xfb\x6f\xef\x6e\
\x63\xca\xb3\x7b\
\x89\x4b\xcc\x71"
.as_ref(),
),
},
encoding: None,
revpos: 4,
},
};
let got: Attachment = serde_json::from_str(source).unwrap();
assert_eq!(got, expected);
}
#[test]
fn attachment_deserializes_with_data() {
let source = r#"{
"content_type": "image/gif",
"data": "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
"digest": "md5-2JdGiI2i2VELZKnwMers1Q==",
"revpos": 2
}"#;
let expected = Attachment {
content_type: mime::IMAGE_GIF,
inner: Inner::ServerOrigin {
content: Content::WithBytes(Vec::from(
b"\x47\x49\x46\x38\
\x39\x61\x01\x00\
\x01\x00\x80\x00\
\x00\x00\x00\x00\
\xff\xff\xff\x21\
\xf9\x04\x01\x00\
\x00\x00\x00\x2c\
\x00\x00\x00\x00\
\x01\x00\x01\x00\
\x00\x02\x01\x44\
\x00\x3b"
.as_ref(),
)),
digest: Digest::Md5 {
value: Vec::from(
b"\xd8\x97\x46\x88\
\x8d\xa2\xd9\x51\
\x0b\x64\xa9\xf0\
\x31\xea\xec\xd5"
.as_ref(),
),
},
encoding: None,
revpos: 2,
},
};
let got: Attachment = serde_json::from_str(source).unwrap();
assert_eq!(got, expected);
}
#[test]
fn attachment_deserializes_with_encoding_info() {
let source = r#"{
"content_type": "text/plain",
"digest": "md5-Ids41vtv725jyrN7iUvMcQ==",
"encoded_length": 693,
"encoding": "gzip",
"length": 1872,
"revpos": 4,
"stub": true
}"#;
let expected = Attachment {
content_type: mime::TEXT_PLAIN,
inner: Inner::ServerOrigin {
content: Content::WithLength(1872),
digest: Digest::Md5 {
value: Vec::from(
b"\x21\xdb\x38\xd6\
\xfb\x6f\xef\x6e\
\x63\xca\xb3\x7b\
\x89\x4b\xcc\x71"
.as_ref(),
),
},
encoding: Some(Encoding {
length: 693,
codec: EncodingCodec::Gzip,
}),
revpos: 4,
},
};
let got: Attachment = serde_json::from_str(source).unwrap();
assert_eq!(got, expected);
}
#[test]
fn client_origin_attachment_serializes_with_content() {
let source = Attachment::new(
mime::TEXT_PLAIN,
Vec::from(b"Lorem ipsum dolor sit amet".as_ref()),
);
let encoded = serde_json::to_vec(&source).unwrap();
let decoded: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
let expected = json!({
"content_type": "text/plain",
"data": "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ=",
});
assert_eq!(decoded, expected);
}
#[test]
fn server_origin_attachment_serializes_as_stub() {
let source = Attachment {
content_type: mime::TEXT_PLAIN,
inner: Inner::ServerOrigin {
content: Content::WithLength(1872),
digest: Digest::Md5 {
value: Vec::from(
b"\x21\xdb\x38\xd6\
\xfb\x6f\xef\x6e\
\x63\xca\xb3\x7b\
\x89\x4b\xcc\x71"
.as_ref(),
),
},
encoding: Some(Encoding {
length: 693,
codec: EncodingCodec::Gzip,
}),
revpos: 4,
},
};
let encoded = serde_json::to_vec(&source).unwrap();
let decoded: serde_json::Value = serde_json::from_slice(&encoded).unwrap();
let expected = json!({
"content_type": "text/plain",
"stub": true,
});
assert_eq!(decoded, expected);
}
}