aboutsummaryrefslogtreecommitdiff
path: root/src/api/s3_xml.rs
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2021-05-03 22:45:42 +0200
committerAlex Auvolat <alex@adnab.me>2021-05-06 22:37:15 +0200
commit6ccffc316228bb056fa135cc5fceb2ed75f545f5 (patch)
tree109e9334aaee5b7865a5d29fcfaea23010da0cc4 /src/api/s3_xml.rs
parente4b9e4e24d006373a20cfcbdac9dba5e399ee182 (diff)
downloadgarage-better_xml.tar.gz
garage-better_xml.zip
Improved XML serializationbetter_xml
- Use quick_xml and serde for all XML response returned by the S3 API. - Include tests for all structs used to generate XML - Remove old manual XML escaping function which was unsafe
Diffstat (limited to 'src/api/s3_xml.rs')
-rw-r--r--src/api/s3_xml.rs597
1 files changed, 597 insertions, 0 deletions
diff --git a/src/api/s3_xml.rs b/src/api/s3_xml.rs
new file mode 100644
index 00000000..ba9b9083
--- /dev/null
+++ b/src/api/s3_xml.rs
@@ -0,0 +1,597 @@
+use quick_xml::se::to_string;
+use serde::{Serialize, Serializer};
+
+use crate::Error as ApiError;
+
+pub fn to_xml_with_header<T: Serialize>(x: &T) -> Result<String, ApiError> {
+ let mut xml = r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string();
+ xml.push_str(&to_string(x)?);
+ Ok(xml)
+}
+
+fn xmlns_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
+ s.serialize_str("http://s3.amazonaws.com/doc/2006-03-01/")
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct Value(#[serde(rename = "$value")] pub String);
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct IntValue(#[serde(rename = "$value")] pub i64);
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct Bucket {
+ #[serde(rename = "CreationDate")]
+ pub creation_date: Value,
+ #[serde(rename = "Name")]
+ pub name: Value,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct DisplayName(#[serde(rename = "$value")] pub String);
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct Id(#[serde(rename = "$value")] pub String);
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct Owner {
+ #[serde(rename = "DisplayName")]
+ pub display_name: Value,
+ #[serde(rename = "ID")]
+ pub id: Value,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct BucketList {
+ #[serde(rename = "Bucket")]
+ pub entries: Vec<Bucket>,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct ListAllMyBucketsResult {
+ #[serde(rename = "Buckets")]
+ pub buckets: BucketList,
+ #[serde(rename = "Owner")]
+ pub owner: Owner,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct LocationConstraint {
+ #[serde(serialize_with = "xmlns_tag")]
+ pub xmlns: (),
+ #[serde(rename = "$value")]
+ pub region: String,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct Deleted {
+ #[serde(rename = "Key")]
+ pub key: Value,
+ #[serde(rename = "VersionId")]
+ pub version_id: Value,
+ #[serde(rename = "DeleteMarkerVersionId")]
+ pub delete_marker_version_id: Value,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct Error {
+ #[serde(rename = "Code")]
+ pub code: Value,
+ #[serde(rename = "Message")]
+ pub message: Value,
+ #[serde(rename = "Resource")]
+ pub resource: Option<Value>,
+ #[serde(rename = "Region")]
+ pub region: Option<Value>,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct DeleteError {
+ #[serde(rename = "Code")]
+ pub code: Value,
+ #[serde(rename = "Key")]
+ pub key: Option<Value>,
+ #[serde(rename = "Message")]
+ pub message: Value,
+ #[serde(rename = "VersionId")]
+ pub version_id: Option<Value>,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct DeleteResult {
+ #[serde(serialize_with = "xmlns_tag")]
+ pub xmlns: (),
+ #[serde(rename = "Deleted")]
+ pub deleted: Vec<Deleted>,
+ #[serde(rename = "Error")]
+ pub errors: Vec<DeleteError>,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct CopyObjectResult {
+ #[serde(rename = "LastModified")]
+ pub last_modified: Value,
+ #[serde(rename = "ETag")]
+ pub etag: Value,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct InitiateMultipartUploadResult {
+ #[serde(serialize_with = "xmlns_tag")]
+ pub xmlns: (),
+ #[serde(rename = "Bucket")]
+ pub bucket: Value,
+ #[serde(rename = "Key")]
+ pub key: Value,
+ #[serde(rename = "UploadId")]
+ pub upload_id: Value,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct CompleteMultipartUploadResult {
+ #[serde(serialize_with = "xmlns_tag")]
+ pub xmlns: (),
+ #[serde(rename = "Location")]
+ pub location: Option<Value>,
+ #[serde(rename = "Bucket")]
+ pub bucket: Value,
+ #[serde(rename = "Key")]
+ pub key: Value,
+ #[serde(rename = "ETag")]
+ pub etag: Value,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct ListBucketItem {
+ #[serde(rename = "Key")]
+ pub key: Value,
+ #[serde(rename = "LastModified")]
+ pub last_modified: Value,
+ #[serde(rename = "ETag")]
+ pub etag: Value,
+ #[serde(rename = "Size")]
+ pub size: IntValue,
+ #[serde(rename = "StorageClass")]
+ pub storage_class: Value,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct CommonPrefix {
+ #[serde(rename = "Prefix")]
+ pub prefix: Value,
+}
+
+#[derive(Debug, Serialize, PartialEq)]
+pub struct ListBucketResult {
+ #[serde(serialize_with = "xmlns_tag")]
+ pub xmlns: (),
+ #[serde(rename = "Name")]
+ pub name: Value,
+ #[serde(rename = "Prefix")]
+ pub prefix: Value,
+ #[serde(rename = "Marker")]
+ pub marker: Option<Value>,
+ #[serde(rename = "NextMarker")]
+ pub next_marker: Option<Value>,
+ #[serde(rename = "StartAfter")]
+ pub start_after: Option<Value>,
+ #[serde(rename = "ContinuationToken")]
+ pub continuation_token: Option<Value>,
+ #[serde(rename = "NextContinuationToken")]
+ pub next_continuation_token: Option<Value>,
+ #[serde(rename = "KeyCount")]
+ pub key_count: Option<IntValue>,
+ #[serde(rename = "MaxKeys")]
+ pub max_keys: IntValue,
+ #[serde(rename = "Delimiter")]
+ pub delimiter: Option<Value>,
+ #[serde(rename = "EncodingType")]
+ pub encoding_type: Option<Value>,
+ #[serde(rename = "IsTruncated")]
+ pub is_truncated: Value,
+ #[serde(rename = "Contents")]
+ pub contents: Vec<ListBucketItem>,
+ #[serde(rename = "CommonPrefixes")]
+ pub common_prefixes: Vec<CommonPrefix>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use garage_util::time::*;
+
+ #[test]
+ fn error_message() -> Result<(), ApiError> {
+ let error = Error {
+ code: Value("TestError".to_string()),
+ message: Value("A dummy error message".to_string()),
+ resource: Some(Value("/bucket/a/plop".to_string())),
+ region: Some(Value("garage".to_string())),
+ };
+ assert_eq!(
+ to_xml_with_header(&error)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<Error>\
+ <Code>TestError</Code>\
+ <Message>A dummy error message</Message>\
+ <Resource>/bucket/a/plop</Resource>\
+ <Region>garage</Region>\
+</Error>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn list_all_my_buckets_result() -> Result<(), ApiError> {
+ let list_buckets = ListAllMyBucketsResult {
+ owner: Owner {
+ display_name: Value("owner_name".to_string()),
+ id: Value("qsdfjklm".to_string()),
+ },
+ buckets: BucketList {
+ entries: vec![
+ Bucket {
+ creation_date: Value(msec_to_rfc3339(0)),
+ name: Value("bucket_A".to_string()),
+ },
+ Bucket {
+ creation_date: Value(msec_to_rfc3339(3600 * 24 * 1000)),
+ name: Value("bucket_B".to_string()),
+ },
+ ],
+ },
+ };
+ assert_eq!(
+ to_xml_with_header(&list_buckets)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<ListAllMyBucketsResult>\
+ <Buckets>\
+ <Bucket>\
+ <CreationDate>1970-01-01T00:00:00.000Z</CreationDate>\
+ <Name>bucket_A</Name>\
+ </Bucket>\
+ <Bucket>\
+ <CreationDate>1970-01-02T00:00:00.000Z</CreationDate>\
+ <Name>bucket_B</Name>\
+ </Bucket>\
+ </Buckets>\
+ <Owner>\
+ <DisplayName>owner_name</DisplayName>\
+ <ID>qsdfjklm</ID>\
+ </Owner>\
+</ListAllMyBucketsResult>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn get_bucket_location_result() -> Result<(), ApiError> {
+ let get_bucket_location = LocationConstraint {
+ xmlns: (),
+ region: "garage".to_string(),
+ };
+ assert_eq!(
+ to_xml_with_header(&get_bucket_location)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">garage</LocationConstraint>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn delete_result() -> Result<(), ApiError> {
+ let delete_result = DeleteResult {
+ xmlns: (),
+ deleted: vec![
+ Deleted {
+ key: Value("a/plop".to_string()),
+ version_id: Value("qsdfjklm".to_string()),
+ delete_marker_version_id: Value("wxcvbn".to_string()),
+ },
+ Deleted {
+ key: Value("b/plip".to_string()),
+ version_id: Value("1234".to_string()),
+ delete_marker_version_id: Value("4321".to_string()),
+ },
+ ],
+ errors: vec![
+ DeleteError {
+ code: Value("NotFound".to_string()),
+ key: Some(Value("c/plap".to_string())),
+ message: Value("Object c/plap not found".to_string()),
+ version_id: None,
+ },
+ DeleteError {
+ code: Value("Forbidden".to_string()),
+ key: Some(Value("d/plep".to_string())),
+ message: Value("Not authorized".to_string()),
+ version_id: Some(Value("789".to_string())),
+ },
+ ],
+ };
+ assert_eq!(
+ to_xml_with_header(&delete_result)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<DeleteResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
+ <Deleted>\
+ <Key>a/plop</Key>\
+ <VersionId>qsdfjklm</VersionId>\
+ <DeleteMarkerVersionId>wxcvbn</DeleteMarkerVersionId>\
+ </Deleted>\
+ <Deleted>\
+ <Key>b/plip</Key>\
+ <VersionId>1234</VersionId>\
+ <DeleteMarkerVersionId>4321</DeleteMarkerVersionId>\
+ </Deleted>\
+ <Error>\
+ <Code>NotFound</Code>\
+ <Key>c/plap</Key>\
+ <Message>Object c/plap not found</Message>\
+ </Error>\
+ <Error>\
+ <Code>Forbidden</Code>\
+ <Key>d/plep</Key>\
+ <Message>Not authorized</Message>\
+ <VersionId>789</VersionId>\
+ </Error>\
+</DeleteResult>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn copy_object_result() -> Result<(), ApiError> {
+ let copy_result = CopyObjectResult {
+ last_modified: Value(msec_to_rfc3339(0)),
+ etag: Value("9b2cf535f27731c974343645a3985328".to_string()),
+ };
+ assert_eq!(
+ to_xml_with_header(&copy_result)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<CopyObjectResult>\
+ <LastModified>1970-01-01T00:00:00.000Z</LastModified>\
+ <ETag>9b2cf535f27731c974343645a3985328</ETag>\
+</CopyObjectResult>\
+ "
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn initiate_multipart_upload_result() -> Result<(), ApiError> {
+ let result = InitiateMultipartUploadResult {
+ xmlns: (),
+ bucket: Value("mybucket".to_string()),
+ key: Value("a/plop".to_string()),
+ upload_id: Value("azerty".to_string()),
+ };
+ assert_eq!(
+ to_xml_with_header(&result)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
+ <Bucket>mybucket</Bucket>\
+ <Key>a/plop</Key>\
+ <UploadId>azerty</UploadId>\
+</InitiateMultipartUploadResult>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn complete_multipart_upload_result() -> Result<(), ApiError> {
+ let result = CompleteMultipartUploadResult {
+ xmlns: (),
+ location: Some(Value("https://garage.tld/mybucket/a/plop".to_string())),
+ bucket: Value("mybucket".to_string()),
+ key: Value("a/plop".to_string()),
+ etag: Value("3858f62230ac3c915f300c664312c11f-9".to_string()),
+ };
+ assert_eq!(
+ to_xml_with_header(&result)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
+ <Location>https://garage.tld/mybucket/a/plop</Location>\
+ <Bucket>mybucket</Bucket>\
+ <Key>a/plop</Key>\
+ <ETag>3858f62230ac3c915f300c664312c11f-9</ETag>\
+</CompleteMultipartUploadResult>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn list_objects_v1_1() -> Result<(), ApiError> {
+ let result = ListBucketResult {
+ xmlns: (),
+ name: Value("example-bucket".to_string()),
+ prefix: Value("".to_string()),
+ marker: Some(Value("".to_string())),
+ next_marker: None,
+ start_after: None,
+ continuation_token: None,
+ next_continuation_token: None,
+ key_count: None,
+ max_keys: IntValue(1000),
+ encoding_type: None,
+ delimiter: Some(Value("/".to_string())),
+ is_truncated: Value("false".to_string()),
+ contents: vec![ListBucketItem {
+ key: Value("sample.jpg".to_string()),
+ last_modified: Value(msec_to_rfc3339(0)),
+ etag: Value("bf1d737a4d46a19f3bced6905cc8b902".to_string()),
+ size: IntValue(142863),
+ storage_class: Value("STANDARD".to_string()),
+ }],
+ common_prefixes: vec![CommonPrefix {
+ prefix: Value("photos/".to_string()),
+ }],
+ };
+ assert_eq!(
+ to_xml_with_header(&result)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
+ <Name>example-bucket</Name>\
+ <Prefix></Prefix>\
+ <Marker></Marker>\
+ <MaxKeys>1000</MaxKeys>\
+ <Delimiter>/</Delimiter>\
+ <IsTruncated>false</IsTruncated>\
+ <Contents>\
+ <Key>sample.jpg</Key>\
+ <LastModified>1970-01-01T00:00:00.000Z</LastModified>\
+ <ETag>bf1d737a4d46a19f3bced6905cc8b902</ETag>\
+ <Size>142863</Size>\
+ <StorageClass>STANDARD</StorageClass>\
+ </Contents>\
+ <CommonPrefixes>\
+ <Prefix>photos/</Prefix>\
+ </CommonPrefixes>\
+</ListBucketResult>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn list_objects_v1_2() -> Result<(), ApiError> {
+ let result = ListBucketResult {
+ xmlns: (),
+ name: Value("example-bucket".to_string()),
+ prefix: Value("photos/2006/".to_string()),
+ marker: Some(Value("".to_string())),
+ next_marker: None,
+ start_after: None,
+ continuation_token: None,
+ next_continuation_token: None,
+ key_count: None,
+ max_keys: IntValue(1000),
+ delimiter: Some(Value("/".to_string())),
+ encoding_type: None,
+ is_truncated: Value("false".to_string()),
+ contents: vec![],
+ common_prefixes: vec![
+ CommonPrefix {
+ prefix: Value("photos/2006/February/".to_string()),
+ },
+ CommonPrefix {
+ prefix: Value("photos/2006/January/".to_string()),
+ },
+ ],
+ };
+ assert_eq!(
+ to_xml_with_header(&result)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
+ <Name>example-bucket</Name>\
+ <Prefix>photos/2006/</Prefix>\
+ <Marker></Marker>\
+ <MaxKeys>1000</MaxKeys>\
+ <Delimiter>/</Delimiter>\
+ <IsTruncated>false</IsTruncated>\
+ <CommonPrefixes>\
+ <Prefix>photos/2006/February/</Prefix>\
+ </CommonPrefixes>\
+ <CommonPrefixes>\
+ <Prefix>photos/2006/January/</Prefix>\
+ </CommonPrefixes>\
+</ListBucketResult>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn list_objects_v2_1() -> Result<(), ApiError> {
+ let result = ListBucketResult {
+ xmlns: (),
+ name: Value("quotes".to_string()),
+ prefix: Value("E".to_string()),
+ marker: None,
+ next_marker: None,
+ start_after: Some(Value("ExampleGuide.pdf".to_string())),
+ continuation_token: None,
+ next_continuation_token: None,
+ key_count: None,
+ max_keys: IntValue(3),
+ delimiter: None,
+ encoding_type: None,
+ is_truncated: Value("false".to_string()),
+ contents: vec![ListBucketItem {
+ key: Value("ExampleObject.txt".to_string()),
+ last_modified: Value(msec_to_rfc3339(0)),
+ etag: Value("599bab3ed2c697f1d26842727561fd94".to_string()),
+ size: IntValue(857),
+ storage_class: Value("REDUCED_REDUNDANCY".to_string()),
+ }],
+ common_prefixes: vec![],
+ };
+ assert_eq!(
+ to_xml_with_header(&result)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
+ <Name>quotes</Name>\
+ <Prefix>E</Prefix>\
+ <StartAfter>ExampleGuide.pdf</StartAfter>\
+ <MaxKeys>3</MaxKeys>\
+ <IsTruncated>false</IsTruncated>\
+ <Contents>\
+ <Key>ExampleObject.txt</Key>\
+ <LastModified>1970-01-01T00:00:00.000Z</LastModified>\
+ <ETag>599bab3ed2c697f1d26842727561fd94</ETag>\
+ <Size>857</Size>\
+ <StorageClass>REDUCED_REDUNDANCY</StorageClass>\
+ </Contents>\
+</ListBucketResult>"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn list_objects_v2_2() -> Result<(), ApiError> {
+ let result = ListBucketResult {
+ xmlns: (),
+ name: Value("bucket".to_string()),
+ prefix: Value("".to_string()),
+ marker: None,
+ next_marker: None,
+ start_after: None,
+ continuation_token: Some(Value(
+ "1ueGcxLPRx1Tr/XYExHnhbYLgveDs2J/wm36Hy4vbOwM=".to_string(),
+ )),
+ next_continuation_token: Some(Value("qsdfjklm".to_string())),
+ key_count: Some(IntValue(112)),
+ max_keys: IntValue(1000),
+ delimiter: None,
+ encoding_type: None,
+ is_truncated: Value("false".to_string()),
+ contents: vec![ListBucketItem {
+ key: Value("happyfacex.jpg".to_string()),
+ last_modified: Value(msec_to_rfc3339(0)),
+ etag: Value("70ee1738b6b21e2c8a43f3a5ab0eee71".to_string()),
+ size: IntValue(1111),
+ storage_class: Value("STANDARD".to_string()),
+ }],
+ common_prefixes: vec![],
+ };
+ assert_eq!(
+ to_xml_with_header(&result)?,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
+<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
+ <Name>bucket</Name>\
+ <Prefix></Prefix>\
+ <ContinuationToken>1ueGcxLPRx1Tr/XYExHnhbYLgveDs2J/wm36Hy4vbOwM=</ContinuationToken>\
+ <NextContinuationToken>qsdfjklm</NextContinuationToken>\
+ <KeyCount>112</KeyCount>\
+ <MaxKeys>1000</MaxKeys>\
+ <IsTruncated>false</IsTruncated>\
+ <Contents>\
+ <Key>happyfacex.jpg</Key>\
+ <LastModified>1970-01-01T00:00:00.000Z</LastModified>\
+ <ETag>70ee1738b6b21e2c8a43f3a5ab0eee71</ETag>\
+ <Size>1111</Size>\
+ <StorageClass>STANDARD</StorageClass>\
+ </Contents>\
+</ListBucketResult>"
+ );
+ Ok(())
+ }
+}