use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::types::{
BucketInfo, CompletePart, ListObjectsParams, ListObjectsResult, MultipartUpload, S3ObjectMeta,
S3ObjectVersion, UploadPart, xml_escape,
};
pub fn list_buckets_xml(owner_id: &str, owner_name: &str, buckets: &[BucketInfo]) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">");
xml.push_str("<Owner>");
xml.push_str(&alloc::format!("<ID>{}</ID>", xml_escape(owner_id)));
xml.push_str(&alloc::format!(
"<DisplayName>{}</DisplayName>",
xml_escape(owner_name)
));
xml.push_str("</Owner>");
xml.push_str("<Buckets>");
for bucket in buckets {
xml.push_str("<Bucket>");
xml.push_str(&alloc::format!("<Name>{}</Name>", xml_escape(&bucket.name)));
xml.push_str(&alloc::format!(
"<CreationDate>{}</CreationDate>",
xml_escape(&bucket.creation_date)
));
xml.push_str("</Bucket>");
}
xml.push_str("</Buckets>");
xml.push_str("</ListAllMyBucketsResult>");
xml
}
pub fn list_objects_v2_xml(
bucket: &str,
params: &ListObjectsParams,
result: &ListObjectsResult,
) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">");
xml.push_str(&alloc::format!("<Name>{}</Name>", xml_escape(bucket)));
xml.push_str(&alloc::format!(
"<Prefix>{}</Prefix>",
xml_escape(params.prefix.as_deref().unwrap_or(""))
));
xml.push_str(&alloc::format!("<MaxKeys>{}</MaxKeys>", params.max_keys));
xml.push_str(&alloc::format!("<KeyCount>{}</KeyCount>", result.key_count));
xml.push_str(&alloc::format!(
"<IsTruncated>{}</IsTruncated>",
result.is_truncated
));
if let Some(ref delimiter) = params.delimiter {
xml.push_str(&alloc::format!(
"<Delimiter>{}</Delimiter>",
xml_escape(delimiter)
));
}
if let Some(ref token) = params.continuation_token {
xml.push_str(&alloc::format!(
"<ContinuationToken>{}</ContinuationToken>",
xml_escape(token)
));
}
if let Some(ref token) = result.next_continuation_token {
xml.push_str(&alloc::format!(
"<NextContinuationToken>{}</NextContinuationToken>",
xml_escape(token)
));
}
if let Some(ref start_after) = params.start_after {
xml.push_str(&alloc::format!(
"<StartAfter>{}</StartAfter>",
xml_escape(start_after)
));
}
for obj in &result.contents {
xml.push_str("<Contents>");
xml.push_str(&alloc::format!("<Key>{}</Key>", xml_escape(&obj.key)));
xml.push_str(&alloc::format!(
"<LastModified>{}</LastModified>",
xml_escape(&obj.last_modified)
));
xml.push_str(&alloc::format!("<ETag>{}</ETag>", xml_escape(&obj.etag)));
xml.push_str(&alloc::format!("<Size>{}</Size>", obj.size));
xml.push_str(&alloc::format!(
"<StorageClass>{}</StorageClass>",
xml_escape(&obj.storage_class)
));
xml.push_str("</Contents>");
}
for prefix in &result.common_prefixes {
xml.push_str("<CommonPrefixes>");
xml.push_str(&alloc::format!("<Prefix>{}</Prefix>", xml_escape(prefix)));
xml.push_str("</CommonPrefixes>");
}
xml.push_str("</ListBucketResult>");
xml
}
pub fn copy_object_xml(etag: &str, last_modified: &str) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<CopyObjectResult>");
xml.push_str(&alloc::format!("<ETag>{}</ETag>", xml_escape(etag)));
xml.push_str(&alloc::format!(
"<LastModified>{}</LastModified>",
xml_escape(last_modified)
));
xml.push_str("</CopyObjectResult>");
xml
}
pub fn delete_objects_xml(deleted: &[String], errors: &[(String, String, String)]) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<DeleteResult>");
for key in deleted {
xml.push_str("<Deleted>");
xml.push_str(&alloc::format!("<Key>{}</Key>", xml_escape(key)));
xml.push_str("</Deleted>");
}
for (key, code, message) in errors {
xml.push_str("<Error>");
xml.push_str(&alloc::format!("<Key>{}</Key>", xml_escape(key)));
xml.push_str(&alloc::format!("<Code>{}</Code>", xml_escape(code)));
xml.push_str(&alloc::format!(
"<Message>{}</Message>",
xml_escape(message)
));
xml.push_str("</Error>");
}
xml.push_str("</DeleteResult>");
xml
}
pub fn initiate_multipart_xml(bucket: &str, key: &str, upload_id: &str) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<InitiateMultipartUploadResult>");
xml.push_str(&alloc::format!("<Bucket>{}</Bucket>", xml_escape(bucket)));
xml.push_str(&alloc::format!("<Key>{}</Key>", xml_escape(key)));
xml.push_str(&alloc::format!(
"<UploadId>{}</UploadId>",
xml_escape(upload_id)
));
xml.push_str("</InitiateMultipartUploadResult>");
xml
}
pub fn complete_multipart_xml(location: &str, bucket: &str, key: &str, etag: &str) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<CompleteMultipartUploadResult>");
xml.push_str(&alloc::format!(
"<Location>{}</Location>",
xml_escape(location)
));
xml.push_str(&alloc::format!("<Bucket>{}</Bucket>", xml_escape(bucket)));
xml.push_str(&alloc::format!("<Key>{}</Key>", xml_escape(key)));
xml.push_str(&alloc::format!("<ETag>{}</ETag>", xml_escape(etag)));
xml.push_str("</CompleteMultipartUploadResult>");
xml
}
pub fn list_parts_xml(
bucket: &str,
key: &str,
upload_id: &str,
parts: &[UploadPart],
is_truncated: bool,
next_part_number: Option<u32>,
) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<ListPartsResult>");
xml.push_str(&alloc::format!("<Bucket>{}</Bucket>", xml_escape(bucket)));
xml.push_str(&alloc::format!("<Key>{}</Key>", xml_escape(key)));
xml.push_str(&alloc::format!(
"<UploadId>{}</UploadId>",
xml_escape(upload_id)
));
xml.push_str(&alloc::format!(
"<IsTruncated>{}</IsTruncated>",
is_truncated
));
if let Some(next) = next_part_number {
xml.push_str(&alloc::format!(
"<NextPartNumberMarker>{}</NextPartNumberMarker>",
next
));
}
for part in parts {
xml.push_str("<Part>");
xml.push_str(&alloc::format!(
"<PartNumber>{}</PartNumber>",
part.part_number
));
xml.push_str(&alloc::format!("<ETag>{}</ETag>", xml_escape(&part.etag)));
xml.push_str(&alloc::format!("<Size>{}</Size>", part.size));
xml.push_str(&alloc::format!(
"<LastModified>{}</LastModified>",
format_timestamp(part.last_modified)
));
xml.push_str("</Part>");
}
xml.push_str("</ListPartsResult>");
xml
}
pub fn list_multipart_uploads_xml(
bucket: &str,
uploads: &[MultipartUpload],
is_truncated: bool,
) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<ListMultipartUploadsResult>");
xml.push_str(&alloc::format!("<Bucket>{}</Bucket>", xml_escape(bucket)));
xml.push_str(&alloc::format!(
"<IsTruncated>{}</IsTruncated>",
is_truncated
));
for upload in uploads {
xml.push_str("<Upload>");
xml.push_str(&alloc::format!("<Key>{}</Key>", xml_escape(&upload.key)));
xml.push_str(&alloc::format!(
"<UploadId>{}</UploadId>",
xml_escape(&upload.upload_id)
));
xml.push_str(&alloc::format!(
"<Initiated>{}</Initiated>",
format_timestamp(upload.initiated)
));
xml.push_str("</Upload>");
}
xml.push_str("</ListMultipartUploadsResult>");
xml
}
pub fn bucket_versioning_xml(status: &str) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<VersioningConfiguration>");
if !status.is_empty() {
xml.push_str(&alloc::format!("<Status>{}</Status>", xml_escape(status)));
}
xml.push_str("</VersioningConfiguration>");
xml
}
pub fn list_object_versions_xml(
bucket: &str,
prefix: Option<&str>,
versions: &[S3ObjectVersion],
is_truncated: bool,
) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str("<ListVersionsResult>");
xml.push_str(&alloc::format!("<Name>{}</Name>", xml_escape(bucket)));
if let Some(p) = prefix {
xml.push_str(&alloc::format!("<Prefix>{}</Prefix>", xml_escape(p)));
}
xml.push_str(&alloc::format!(
"<IsTruncated>{}</IsTruncated>",
is_truncated
));
for version in versions {
xml.push_str("<Version>");
xml.push_str(&alloc::format!("<Key>{}</Key>", xml_escape(&version.key)));
xml.push_str(&alloc::format!(
"<VersionId>{}</VersionId>",
xml_escape(&version.version_id)
));
xml.push_str(&alloc::format!(
"<IsLatest>{}</IsLatest>",
version.is_latest
));
xml.push_str(&alloc::format!(
"<LastModified>{}</LastModified>",
xml_escape(&version.last_modified)
));
xml.push_str(&alloc::format!(
"<ETag>{}</ETag>",
xml_escape(&version.etag)
));
xml.push_str(&alloc::format!("<Size>{}</Size>", version.size));
xml.push_str(&alloc::format!(
"<StorageClass>{}</StorageClass>",
xml_escape(&version.storage_class)
));
xml.push_str("</Version>");
}
xml.push_str("</ListVersionsResult>");
xml
}
pub fn bucket_location_xml(region: &str) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
if region == "us-east-1" {
xml.push_str("<LocationConstraint/>");
} else {
xml.push_str(&alloc::format!(
"<LocationConstraint>{}</LocationConstraint>",
xml_escape(region)
));
}
xml
}
pub fn parse_complete_multipart_parts(xml: &str) -> Result<Vec<CompletePart>, String> {
let mut parts = Vec::new();
let mut current_part_number: Option<u32> = None;
let mut current_etag: Option<String> = None;
let mut in_part = false;
let mut in_part_number = false;
let mut in_etag = false;
let mut current_text = String::new();
for c in xml.chars() {
if c == '<' {
if in_part_number && !current_text.is_empty() {
current_part_number = current_text.trim().parse().ok();
} else if in_etag && !current_text.is_empty() {
current_etag = Some(current_text.trim().to_string());
}
current_text.clear();
} else if c == '>' {
let tag = current_text.trim();
if tag == "Part" {
in_part = true;
} else if tag == "/Part" {
if let (Some(part_number), Some(etag)) =
(current_part_number.take(), current_etag.take())
{
parts.push(CompletePart { part_number, etag });
}
in_part = false;
} else if tag == "PartNumber" && in_part {
in_part_number = true;
} else if tag == "/PartNumber" {
in_part_number = false;
} else if tag == "ETag" && in_part {
in_etag = true;
} else if tag == "/ETag" {
in_etag = false;
}
current_text.clear();
} else {
current_text.push(c);
}
}
Ok(parts)
}
pub fn parse_delete_objects(xml: &str) -> Result<Vec<String>, String> {
let mut keys = Vec::new();
let mut in_object = false;
let mut in_key = false;
let mut current_text = String::new();
for c in xml.chars() {
if c == '<' {
if in_key && !current_text.is_empty() {
keys.push(current_text.trim().to_string());
}
current_text.clear();
} else if c == '>' {
let tag = current_text.trim();
if tag == "Object" {
in_object = true;
} else if tag == "/Object" {
in_object = false;
} else if tag == "Key" && in_object {
in_key = true;
} else if tag == "/Key" {
in_key = false;
}
current_text.clear();
} else {
current_text.push(c);
}
}
Ok(keys)
}
pub fn format_timestamp(ts: u64) -> String {
let days = ts / 86400;
let time = ts % 86400;
let hours = time / 3600;
let minutes = (time % 3600) / 60;
let seconds = time % 60;
let year = 1970 + (days / 365);
let day_of_year = days % 365;
let month = (day_of_year / 30) + 1;
let day = (day_of_year % 30) + 1;
alloc::format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.000Z",
year,
month.min(12),
day.min(31),
hours,
minutes,
seconds
)
}
pub fn compute_etag(data: &[u8]) -> String {
let mut hash = 0u64;
for (i, &byte) in data.iter().enumerate() {
hash = hash.wrapping_add((byte as u64).wrapping_mul((i as u64).wrapping_add(1)));
hash = hash.rotate_left(7);
}
alloc::format!("\"{:016x}\"", hash)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_list_buckets_xml() {
let buckets = vec![
BucketInfo {
name: "bucket1".into(),
creation_date: "2024-01-01T00:00:00Z".into(),
},
BucketInfo {
name: "bucket2".into(),
creation_date: "2024-01-02T00:00:00Z".into(),
},
];
let xml = list_buckets_xml("owner-id", "owner-name", &buckets);
assert!(xml.contains("<Name>bucket1</Name>"));
assert!(xml.contains("<Name>bucket2</Name>"));
assert!(xml.contains("<ID>owner-id</ID>"));
}
#[test]
fn test_list_objects_v2_xml() {
let params = ListObjectsParams {
prefix: Some("folder/".into()),
delimiter: Some("/".into()),
max_keys: 100,
..Default::default()
};
let result = ListObjectsResult {
contents: vec![S3ObjectMeta::new(
"folder/file.txt".into(),
100,
"\"abc123\"".into(),
"2024-01-01T00:00:00Z".into(),
)],
common_prefixes: vec!["folder/subfolder/".into()],
is_truncated: false,
next_continuation_token: None,
key_count: 1,
};
let xml = list_objects_v2_xml("mybucket", ¶ms, &result);
assert!(xml.contains("<Name>mybucket</Name>"));
assert!(xml.contains("<Key>folder/file.txt</Key>"));
assert!(xml.contains("<Prefix>folder/subfolder/</Prefix>"));
}
#[test]
fn test_copy_object_xml() {
let xml = copy_object_xml("\"abc123\"", "2024-01-01T00:00:00Z");
assert!(xml.contains("<ETag>"abc123"</ETag>"));
assert!(xml.contains("<LastModified>2024-01-01T00:00:00Z</LastModified>"));
}
#[test]
fn test_initiate_multipart_xml() {
let xml = initiate_multipart_xml("mybucket", "mykey", "upload-id-123");
assert!(xml.contains("<Bucket>mybucket</Bucket>"));
assert!(xml.contains("<Key>mykey</Key>"));
assert!(xml.contains("<UploadId>upload-id-123</UploadId>"));
}
#[test]
fn test_parse_complete_multipart_parts() {
let xml = r#"
<CompleteMultipartUpload>
<Part>
<PartNumber>1</PartNumber>
<ETag>"abc"</ETag>
</Part>
<Part>
<PartNumber>2</PartNumber>
<ETag>"def"</ETag>
</Part>
</CompleteMultipartUpload>
"#;
let parts = parse_complete_multipart_parts(xml).unwrap();
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].part_number, 1);
assert_eq!(parts[0].etag, "\"abc\"");
assert_eq!(parts[1].part_number, 2);
}
#[test]
fn test_parse_delete_objects() {
let xml = r#"
<Delete>
<Object>
<Key>file1.txt</Key>
</Object>
<Object>
<Key>file2.txt</Key>
</Object>
</Delete>
"#;
let keys = parse_delete_objects(xml).unwrap();
assert_eq!(keys, vec!["file1.txt", "file2.txt"]);
}
#[test]
fn test_format_timestamp() {
let ts = format_timestamp(0);
assert!(ts.starts_with("1970-01-01T00:00:00"));
}
#[test]
fn test_compute_etag() {
let etag = compute_etag(b"test data");
assert!(etag.starts_with('"'));
assert!(etag.ends_with('"'));
}
#[test]
fn test_bucket_versioning_xml() {
let xml = bucket_versioning_xml("Enabled");
assert!(xml.contains("<Status>Enabled</Status>"));
let xml_empty = bucket_versioning_xml("");
assert!(!xml_empty.contains("<Status>"));
}
#[test]
fn test_bucket_location_xml() {
let xml = bucket_location_xml("us-west-2");
assert!(xml.contains("<LocationConstraint>us-west-2</LocationConstraint>"));
let xml_east = bucket_location_xml("us-east-1");
assert!(xml_east.contains("<LocationConstraint/>"));
}
}