diff --git a/Cargo.toml b/Cargo.toml index 37eb9dd..d461381 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ reflink-copy = "0.1.9" serde = "1.0.130" serde_derive = "1.0.130" serde_json = "1.0.68" +base64 = "0.22.0" +base64-serde = "0.7.0" sha1 = "0.10.5" sha2 = "0.10.6" ssri = "9.0.0" diff --git a/src/index.rs b/src/index.rs index b9f5257..f8e4aa1 100644 --- a/src/index.rs +++ b/src/index.rs @@ -18,6 +18,8 @@ use sha2::Sha256; use ssri::Integrity; use walkdir::WalkDir; +use base64_serde::base64_serde_type; + #[cfg(any(feature = "async-std", feature = "tokio"))] use crate::async_lib::{AsyncBufReadExt, AsyncWriteExt}; use crate::content::path::content_path; @@ -50,9 +52,39 @@ struct SerializableMetadata { time: u128, size: usize, metadata: Value, + #[serde(with = "option_base64")] raw_metadata: Option>, } +base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD); + +mod option_base64 { + use super::Base64Standard; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(data: &Option>, serializer: S) -> Result + where + S: Serializer, + { + match data { + Some(data) => Base64Standard::serialize(data, serializer), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + // Create a wrapper type to reuse existing "with" attribute easily + #[derive(Deserialize)] + struct WrappedVecU8(#[serde(with = "Base64Standard")] Vec); + + Option::::deserialize(deserializer) + .map(|it| it.map(|wrapped_value| wrapped_value.0)) + } +} + impl PartialEq for SerializableMetadata { fn eq(&self, other: &Self) -> bool { self.key == other.key @@ -586,6 +618,64 @@ mod tests { assert!(!content.exists()); } + #[test] + fn serde_json_raw_metadata() { + let meta = SerializableMetadata { + key: "hello".to_string(), + integrity: Some("sha1-deadbeef".to_string()), + time: 0, + size: 0, + metadata: json!(null), + raw_metadata: Some(vec![b'1', b'2', b'3', b'4']), + }; + + assert_eq!( + serde_json::to_string(&meta).unwrap(), + "{\"key\":\"hello\",\"integrity\":\"sha1-deadbeef\",\"time\":0,\"size\":0,\"metadata\":null,\"raw_metadata\":\"MTIzNA==\"}" + ); + + let value = json!( + { + "key": "hello", + "integrity": "sha1-deadbeef", + "time": 0, + "size": 0, + "metadata": null, + "raw_metadata": "MTIzNA==" + } + ); + + let de_meta: SerializableMetadata = serde_json::from_value(value).unwrap(); + + assert_eq!(de_meta, meta); + } + + #[test] + fn raw_metadata() { + let tmp = tempfile::tempdir().unwrap(); + let dir = tmp.path().to_owned(); + let sri: Integrity = "sha1-deadbeef".parse().unwrap(); + let time = 1_234_567; + let raw_metadata = vec![1, 2, 3, 4]; + let opts = WriteOpts::new() + .integrity(sri.clone()) + .time(time) + .raw_metadata(raw_metadata.clone()); + insert(&dir, "hello", opts).unwrap(); + let entry = find(&dir, "hello").unwrap().unwrap(); + assert_eq!( + entry, + Metadata { + key: String::from("hello"), + integrity: sri, + time: time, + size: 0, + metadata: json!(null), + raw_metadata: Some(raw_metadata), + } + ); + } + #[test] fn round_trip() { let tmp = tempfile::tempdir().unwrap();