diff --git a/codegen/src/v1/mod.rs b/codegen/src/v1/mod.rs
index 8b239abe..1a89f49c 100644
--- a/codegen/src/v1/mod.rs
+++ b/codegen/src/v1/mod.rs
@@ -27,7 +27,7 @@ fn write_file(path: &str, f: impl FnOnce()) {
}
#[derive(Debug, Clone, Copy)]
-enum Patch {
+pub enum Patch {
Minio,
}
@@ -76,7 +76,7 @@ fn inner_run(code_patch: Option) {
{
let path = format!("crates/s3s/src/xml/generated{suffix}.rs");
- write_file(&path, || xml::codegen(&ops, &rust_types));
+ write_file(&path, || xml::codegen(&ops, &rust_types, code_patch));
}
{
@@ -86,7 +86,7 @@ fn inner_run(code_patch: Option) {
{
let path = format!("crates/s3s/src/ops/generated{suffix}.rs");
- write_file(&path, || ops::codegen(&ops, &rust_types));
+ write_file(&path, || ops::codegen(&ops, &rust_types, code_patch));
}
{
diff --git a/codegen/src/v1/ops.rs b/codegen/src/v1/ops.rs
index f8a5c603..2fc6e3f5 100644
--- a/codegen/src/v1/ops.rs
+++ b/codegen/src/v1/ops.rs
@@ -5,6 +5,7 @@ use super::{dto, rust, smithy};
use super::{headers, o};
use crate::declare_codegen;
+use crate::v1::Patch;
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet, HashMap};
@@ -151,7 +152,7 @@ pub fn is_op_output(name: &str, ops: &Operations) -> bool {
name.strip_suffix("Output").is_some_and(|x| ops.contains_key(x))
}
-pub fn codegen(ops: &Operations, rust_types: &RustTypes) {
+pub fn codegen(ops: &Operations, rust_types: &RustTypes, patch: Option) {
declare_codegen!();
for op in ops.values() {
@@ -178,7 +179,7 @@ pub fn codegen(ops: &Operations, rust_types: &RustTypes) {
"",
]);
- codegen_http(ops, rust_types);
+ codegen_http(ops, rust_types, patch);
codegen_router(ops, rust_types);
}
@@ -190,7 +191,7 @@ fn status_code_name(code: u16) -> &'static str {
}
}
-fn codegen_http(ops: &Operations, rust_types: &RustTypes) {
+fn codegen_http(ops: &Operations, rust_types: &RustTypes, patch: Option) {
codegen_header_value(ops, rust_types);
for op in ops.values() {
@@ -202,7 +203,7 @@ fn codegen_http(ops: &Operations, rust_types: &RustTypes) {
g!("impl {} {{", op.name);
- codegen_op_http_de(op, rust_types);
+ codegen_op_http_de(op, rust_types, patch);
codegen_op_http_ser(op, rust_types);
g!("}}");
@@ -563,7 +564,7 @@ fn codegen_op_http_ser(op: &Operation, rust_types: &RustTypes) {
}
#[allow(clippy::too_many_lines)]
-fn codegen_op_http_de(op: &Operation, rust_types: &RustTypes) {
+fn codegen_op_http_de(op: &Operation, rust_types: &RustTypes, patch: Option) {
let input = op.input.as_str();
let rust_type = &rust_types[input];
match rust_type {
@@ -708,9 +709,29 @@ fn codegen_op_http_de(op: &Operation, rust_types: &RustTypes) {
g!(" Err(e) => return Err(e),");
g!("}};");
} else if field.option_type {
- g!("let {}: Option<{}> = http::take_opt_xml_body(req)?;", field.name, field.type_);
+ // MinIO compatibility: literal " Enabled " for legacy config
+ if op.name == "PutObjectLockConfiguration"
+ && field.name == "object_lock_configuration"
+ && matches!(patch, Some(Patch::Minio))
+ {
+ g!(
+ "let {}: Option<{}> = http::take_opt_object_lock_configuration(req)?;",
+ field.name,
+ field.type_
+ );
+ } else {
+ g!("let {}: Option<{}> = http::take_opt_xml_body(req)?;", field.name, field.type_);
+ }
} else {
- g!("let {}: {} = http::take_xml_body(req)?;", field.name, field.type_);
+ // MinIO compatibility: literal " Enabled " for legacy config
+ if op.name == "PutBucketVersioning"
+ && field.name == "versioning_configuration"
+ && matches!(patch, Some(Patch::Minio))
+ {
+ g!("let {}: {} = http::take_versioning_configuration(req)?;", field.name, field.type_);
+ } else {
+ g!("let {}: {} = http::take_xml_body(req)?;", field.name, field.type_);
+ }
}
}
},
diff --git a/codegen/src/v1/xml.rs b/codegen/src/v1/xml.rs
index bbc30cf7..1924f2fb 100644
--- a/codegen/src/v1/xml.rs
+++ b/codegen/src/v1/xml.rs
@@ -4,6 +4,7 @@ use super::rust;
use super::rust::default_value_literal;
use crate::declare_codegen;
+use crate::v1::Patch;
use crate::v1::ops::is_op_output;
use crate::v1::rust::StructField;
@@ -13,7 +14,7 @@ use std::ops::Not;
use scoped_writer::g;
use stdx::default::default;
-pub fn codegen(ops: &Operations, rust_types: &RustTypes) {
+pub fn codegen(ops: &Operations, rust_types: &RustTypes, patch: Option) {
declare_codegen!();
g([
@@ -63,8 +64,8 @@ pub fn codegen(ops: &Operations, rust_types: &RustTypes) {
g!("const XMLNS_S3: &str = \"http://s3.amazonaws.com/doc/2006-03-01/\";");
g!();
- codegen_xml_serde(ops, rust_types, &root_type_names);
- codegen_xml_serde_content(ops, rust_types, &field_type_names);
+ codegen_xml_serde(ops, rust_types, &root_type_names, patch);
+ codegen_xml_serde_content(ops, rust_types, &field_type_names, patch);
}
pub fn is_xml_payload(field: &rust::StructField) -> bool {
@@ -220,7 +221,12 @@ fn s3_unwrapped_xml_output(ops: &Operations, ty_name: &str) -> bool {
ops.iter().any(|(_, op)| op.s3_unwrapped_xml_output && op.output == ty_name)
}
-fn codegen_xml_serde(ops: &Operations, rust_types: &RustTypes, root_type_names: &BTreeMap<&str, Option<&str>>) {
+fn codegen_xml_serde(
+ ops: &Operations,
+ rust_types: &RustTypes,
+ root_type_names: &BTreeMap<&str, Option<&str>>,
+ patch: Option,
+) {
for (rust_type, xml_name) in root_type_names.iter().map(|(&name, xml_name)| (&rust_types[name], xml_name)) {
let rust::Type::Struct(ty) = rust_type else { panic!("{rust_type:#?}") };
@@ -253,7 +259,15 @@ fn codegen_xml_serde(ops: &Operations, rust_types: &RustTypes, root_type_names:
g!("impl<'xml> Deserialize<'xml> for {} {{", ty.name);
g!("fn deserialize(d: &mut Deserializer<'xml>) -> DeResult {{");
- g!("d.named_element(\"{xml_name}\", Deserializer::content)");
+ // MinIO compatibility: accept both LifecycleConfiguration and BucketLifecycleConfiguration
+ if ty.name == "BucketLifecycleConfiguration" && matches!(patch, Some(Patch::Minio)) {
+ g!("d.named_element_any(");
+ g!(" &[\"LifecycleConfiguration\", \"BucketLifecycleConfiguration\"],");
+ g!(" Deserializer::content,");
+ g!(")");
+ } else {
+ g!("d.named_element(\"{xml_name}\", Deserializer::content)");
+ }
g!("}}");
g!("}}");
@@ -262,7 +276,7 @@ fn codegen_xml_serde(ops: &Operations, rust_types: &RustTypes, root_type_names:
}
}
-fn codegen_xml_serde_content(ops: &Operations, rust_types: &RustTypes, field_type_names: &BTreeSet<&str>) {
+fn codegen_xml_serde_content(ops: &Operations, rust_types: &RustTypes, field_type_names: &BTreeSet<&str>, patch: Option) {
for rust_type in field_type_names.iter().map(|&name| &rust_types[name]) {
match rust_type {
rust::Type::Alias(_) => {}
@@ -329,13 +343,13 @@ fn codegen_xml_serde_content(ops: &Operations, rust_types: &RustTypes, field_typ
g!("}}");
}
}
- rust::Type::Struct(ty) => codegen_xml_serde_content_struct(ops, rust_types, ty),
+ rust::Type::Struct(ty) => codegen_xml_serde_content_struct(ops, rust_types, ty, patch),
}
}
}
#[allow(clippy::too_many_lines)]
-fn codegen_xml_serde_content_struct(_ops: &Operations, rust_types: &RustTypes, ty: &rust::Struct) {
+fn codegen_xml_serde_content_struct(_ops: &Operations, rust_types: &RustTypes, ty: &rust::Struct, patch: Option) {
if can_impl_serialize_content(rust_types, &ty.name) {
g!("impl SerializeContent for {} {{", ty.name);
g!(
@@ -537,7 +551,12 @@ fn codegen_xml_serde_content_struct(_ops: &Operations, rust_types: &RustTypes, t
g!("Ok(())");
g!("}}");
}
- g!("_ => Err(DeError::UnexpectedTagName)");
+ // MinIO compatibility: skip unknown elements for BucketLifecycleConfiguration
+ if ty.name == "BucketLifecycleConfiguration" && matches!(patch, Some(Patch::Minio)) {
+ g!("_ => Ok(()),");
+ } else {
+ g!("_ => Err(DeError::UnexpectedTagName)");
+ }
g!("}})?;");
}
diff --git a/crates/s3s-aws/src/conv/generated_minio.rs b/crates/s3s-aws/src/conv/generated_minio.rs
index c39124d3..83ed18c8 100644
--- a/crates/s3s-aws/src/conv/generated_minio.rs
+++ b/crates/s3s-aws/src/conv/generated_minio.rs
@@ -366,6 +366,7 @@ impl AwsConversion for s3s::dto::BucketLifecycleConfiguration {
fn try_from_aws(x: Self::Target) -> S3Result {
Ok(Self {
+ expiry_updated_at: None,
rules: try_from_aws(x.rules)?,
})
}
@@ -4597,6 +4598,7 @@ impl AwsConversion for s3s::dto::LifecycleExpiration {
Ok(Self {
date: try_from_aws(x.date)?,
days: try_from_aws(x.days)?,
+ expired_object_all_versions: None,
expired_object_delete_marker: try_from_aws(x.expired_object_delete_marker)?,
})
}
@@ -4618,6 +4620,7 @@ impl AwsConversion for s3s::dto::LifecycleRule {
fn try_from_aws(x: Self::Target) -> S3Result {
Ok(Self {
abort_incomplete_multipart_upload: try_from_aws(x.abort_incomplete_multipart_upload)?,
+ del_marker_expiration: None,
expiration: try_from_aws(x.expiration)?,
filter: try_from_aws(x.filter)?,
id: try_from_aws(x.id)?,
diff --git a/crates/s3s/src/dto/generated_minio.rs b/crates/s3s/src/dto/generated_minio.rs
index 4879d5ba..a5cf1ea2 100644
--- a/crates/s3s/src/dto/generated_minio.rs
+++ b/crates/s3s/src/dto/generated_minio.rs
@@ -660,6 +660,7 @@ pub type BucketKeyEnabled = bool;
/// in the Amazon S3 User Guide .
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct BucketLifecycleConfiguration {
+ pub expiry_updated_at: Option,
/// A lifecycle rule for individual objects in an Amazon S3 bucket.
pub rules: LifecycleRules,
}
@@ -667,6 +668,9 @@ pub struct BucketLifecycleConfiguration {
impl fmt::Debug for BucketLifecycleConfiguration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut d = f.debug_struct("BucketLifecycleConfiguration");
+ if let Some(ref val) = self.expiry_updated_at {
+ d.field("expiry_updated_at", val);
+ }
d.field("rules", &self.rules);
d.finish_non_exhaustive()
}
@@ -3957,6 +3961,21 @@ impl fmt::Debug for DefaultRetention {
}
}
+#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
+pub struct DelMarkerExpiration {
+ pub days: Option,
+}
+
+impl fmt::Debug for DelMarkerExpiration {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut d = f.debug_struct("DelMarkerExpiration");
+ if let Some(ref val) = self.days {
+ d.field("days", val);
+ }
+ d.finish_non_exhaustive()
+ }
+}
+
/// Container for the objects to delete.
#[derive(Clone, Default, PartialEq)]
pub struct Delete {
@@ -7539,6 +7558,8 @@ impl FromStr for ExpirationStatus {
}
}
+pub type ExpiredObjectAllVersions = bool;
+
pub type ExpiredObjectDeleteMarker = bool;
pub type Expires = Timestamp;
@@ -11653,6 +11674,7 @@ pub struct LifecycleExpiration {
/// Indicates the lifetime, in days, of the objects that are subject to the rule. The value
/// must be a non-zero positive integer.
pub days: Option,
+ pub expired_object_all_versions: Option,
/// Indicates whether Amazon S3 will remove a delete marker with no noncurrent versions. If set
/// to true, the delete marker will be expired; if set to false the policy takes no action.
/// This cannot be specified with Days or Date in a Lifecycle Expiration Policy.
@@ -11672,6 +11694,9 @@ impl fmt::Debug for LifecycleExpiration {
if let Some(ref val) = self.days {
d.field("days", val);
}
+ if let Some(ref val) = self.expired_object_all_versions {
+ d.field("expired_object_all_versions", val);
+ }
if let Some(ref val) = self.expired_object_delete_marker {
d.field("expired_object_delete_marker", val);
}
@@ -11685,6 +11710,7 @@ impl fmt::Debug for LifecycleExpiration {
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct LifecycleRule {
pub abort_incomplete_multipart_upload: Option,
+ pub del_marker_expiration: Option,
/// Specifies the expiration for the lifecycle of the object in the form of date, days and,
/// whether the object has a delete marker.
pub expiration: Option,
@@ -11735,6 +11761,9 @@ impl fmt::Debug for LifecycleRule {
if let Some(ref val) = self.abort_incomplete_multipart_upload {
d.field("abort_incomplete_multipart_upload", val);
}
+ if let Some(ref val) = self.del_marker_expiration {
+ d.field("del_marker_expiration", val);
+ }
if let Some(ref val) = self.expiration {
d.field("expiration", val);
}
@@ -34677,6 +34706,9 @@ impl DtoExt for DefaultRetention {
}
}
}
+impl DtoExt for DelMarkerExpiration {
+ fn ignore_empty_strings(&mut self) {}
+}
impl DtoExt for Delete {
fn ignore_empty_strings(&mut self) {}
}
@@ -35972,6 +36004,9 @@ impl DtoExt for LifecycleRule {
if let Some(ref mut val) = self.abort_incomplete_multipart_upload {
val.ignore_empty_strings();
}
+ if let Some(ref mut val) = self.del_marker_expiration {
+ val.ignore_empty_strings();
+ }
if let Some(ref mut val) = self.expiration {
val.ignore_empty_strings();
}
diff --git a/crates/s3s/src/http/de.rs b/crates/s3s/src/http/de.rs
index ec9156a5..11d71f72 100644
--- a/crates/s3s/src/http/de.rs
+++ b/crates/s3s/src/http/de.rs
@@ -258,6 +258,57 @@ where
result
}
+/// `MinIO` compatibility: literal `" Enabled "` (with spaces) for legacy object lock/versioning config.
+#[cfg(feature = "minio")]
+fn is_minio_enabled_literal(bytes: &[u8]) -> bool {
+ bytes.trim_ascii() == b"Enabled"
+}
+
+/// `MinIO` compatibility: take `ObjectLockConfiguration`, accepting literal `" Enabled "` as enabled.
+#[cfg(feature = "minio")]
+pub fn take_opt_object_lock_configuration(req: &mut Request) -> S3Result> {
+ use crate::dto::{ObjectLockConfiguration, ObjectLockEnabled};
+
+ let bytes = req.body.take_bytes().expect("full body not found");
+ if bytes.is_empty() {
+ return Ok(None);
+ }
+ if is_minio_enabled_literal(&bytes) {
+ return Ok(Some(ObjectLockConfiguration {
+ object_lock_enabled: Some(ObjectLockEnabled::from("Enabled".to_owned())),
+ rule: None,
+ }));
+ }
+ let result = deserialize_xml::(&bytes).map(Some);
+ if result.is_err() {
+ error!(?bytes, "malformed xml body");
+ }
+ result
+}
+
+/// `MinIO` compatibility: take `VersioningConfiguration`, accepting literal `" Enabled "` as enabled.
+#[cfg(feature = "minio")]
+pub fn take_versioning_configuration(req: &mut Request) -> S3Result {
+ use crate::dto::{BucketVersioningStatus, VersioningConfiguration};
+ use stdx::default::default;
+
+ let bytes = req.body.take_bytes().expect("full body not found");
+ if bytes.is_empty() {
+ return Err(S3ErrorCode::MissingRequestBodyError.into());
+ }
+ if is_minio_enabled_literal(&bytes) {
+ return Ok(VersioningConfiguration {
+ status: Some(BucketVersioningStatus::from("Enabled".to_owned())),
+ ..default()
+ });
+ }
+ let result = deserialize_xml::(&bytes);
+ if result.is_err() {
+ error!(?bytes, "malformed xml body");
+ }
+ result
+}
+
pub fn take_string_body(req: &mut Request) -> S3Result {
let bytes = req.body.take_bytes().expect("full body not found");
match String::from_utf8_simd(bytes.into()) {
diff --git a/crates/s3s/src/ops/generated_minio.rs b/crates/s3s/src/ops/generated_minio.rs
index 6d045216..e0f6ab1f 100644
--- a/crates/s3s/src/ops/generated_minio.rs
+++ b/crates/s3s/src/ops/generated_minio.rs
@@ -5345,7 +5345,7 @@ impl PutBucketVersioning {
let mfa: Option = http::parse_opt_header(req, &X_AMZ_MFA)?;
- let versioning_configuration: VersioningConfiguration = http::take_xml_body(req)?;
+ let versioning_configuration: VersioningConfiguration = http::take_versioning_configuration(req)?;
Ok(PutBucketVersioningInput {
bucket,
@@ -5943,7 +5943,7 @@ impl PutObjectLockConfiguration {
let expected_bucket_owner: Option = http::parse_opt_header(req, &X_AMZ_EXPECTED_BUCKET_OWNER)?;
- let object_lock_configuration: Option = http::take_opt_xml_body(req)?;
+ let object_lock_configuration: Option = http::take_opt_object_lock_configuration(req)?;
let request_payer: Option = http::parse_opt_header(req, &X_AMZ_REQUEST_PAYER)?;
diff --git a/crates/s3s/src/xml/de.rs b/crates/s3s/src/xml/de.rs
index e0402953..a0a82cca 100644
--- a/crates/s3s/src/xml/de.rs
+++ b/crates/s3s/src/xml/de.rs
@@ -175,8 +175,28 @@ impl<'xml> Deserializer<'xml> {
}
}
- /// Expects an end event
- fn expect_end(&mut self, name: &[u8]) -> DeResult {
+ /// Expects a start event with any of the given names. Returns the matched name (owned).
+ fn expect_start_any(&mut self, names: &[&str]) -> DeResult> {
+ let names_bytes: Vec> = names.iter().map(|s| s.as_bytes().to_vec()).collect();
+ loop {
+ match self.next_event()? {
+ DeEvent::Start(x) => {
+ let name = x.name().as_ref().to_vec();
+ if names_bytes.contains(&name) {
+ return Ok(name);
+ }
+ return Err(unexpected_tag_name());
+ }
+ DeEvent::End(_) => return Err(unexpected_end()),
+ DeEvent::Text(_) => continue,
+ DeEvent::Eof => return Err(unexpected_eof()),
+ }
+ }
+ }
+
+ /// Expects an end event (accepts both `&[u8]` and `AsRef<[u8]>` for flexibility)
+ fn expect_end(&mut self, name: impl AsRef<[u8]>) -> DeResult {
+ let name = name.as_ref();
loop {
match self.next_event()? {
DeEvent::Start(_) => return Err(unexpected_start()),
@@ -215,6 +235,17 @@ impl<'xml> Deserializer<'xml> {
Ok(ans)
}
+ /// Deserializes an element with any of the given root names (`MinIO` compatibility).
+ ///
+ /// # Errors
+ /// Returns an error if the deserialization fails.
+ pub fn named_element_any(&mut self, names: &[&str], f: impl FnOnce(&mut Self) -> DeResult) -> DeResult {
+ let name = self.expect_start_any(names)?;
+ let ans = f(self)?;
+ self.expect_end(name)?;
+ Ok(ans)
+ }
+
pub fn element(&mut self, f: impl FnOnce(&mut Self, &[u8]) -> DeResult) -> DeResult {
loop {
match self.peek_event()? {
diff --git a/crates/s3s/src/xml/generated_minio.rs b/crates/s3s/src/xml/generated_minio.rs
index f6b8d23d..8d73d5b5 100644
--- a/crates/s3s/src/xml/generated_minio.rs
+++ b/crates/s3s/src/xml/generated_minio.rs
@@ -260,6 +260,8 @@ use std::io::Write;
// DeserializeContent: DaysAfterInitiation
// SerializeContent: DefaultRetention
// DeserializeContent: DefaultRetention
+// SerializeContent: DelMarkerExpiration
+// DeserializeContent: DelMarkerExpiration
// SerializeContent: Delete
// DeserializeContent: Delete
// SerializeContent: DeleteMarker
@@ -327,6 +329,8 @@ use std::io::Write;
// DeserializeContent: ExistingObjectReplicationStatus
// SerializeContent: ExpirationStatus
// DeserializeContent: ExpirationStatus
+// SerializeContent: ExpiredObjectAllVersions
+// DeserializeContent: ExpiredObjectAllVersions
// SerializeContent: ExpiredObjectDeleteMarker
// DeserializeContent: ExpiredObjectDeleteMarker
// SerializeContent: ExposeHeader
@@ -875,7 +879,7 @@ impl Serialize for BucketLifecycleConfiguration {
impl<'xml> Deserialize<'xml> for BucketLifecycleConfiguration {
fn deserialize(d: &mut Deserializer<'xml>) -> DeResult {
- d.named_element("LifecycleConfiguration", Deserializer::content)
+ d.named_element_any(&["LifecycleConfiguration", "BucketLifecycleConfiguration"], Deserializer::content)
}
}
@@ -1994,6 +1998,9 @@ impl<'xml> DeserializeContent<'xml> for BucketInfo {
}
impl SerializeContent for BucketLifecycleConfiguration {
fn serialize_content(&self, s: &mut Serializer) -> SerResult {
+ if let Some(ref val) = self.expiry_updated_at {
+ s.timestamp("ExpiryUpdatedAt", val, TimestampFormat::DateTime)?;
+ }
{
let iter = &self.rules;
s.flattened_list("Rule", iter)?;
@@ -2004,16 +2011,25 @@ impl SerializeContent for BucketLifecycleConfiguration {
impl<'xml> DeserializeContent<'xml> for BucketLifecycleConfiguration {
fn deserialize_content(d: &mut Deserializer<'xml>) -> DeResult {
+ let mut expiry_updated_at: Option = None;
let mut rules: Option = None;
d.for_each_element(|d, x| match x {
+ b"ExpiryUpdatedAt" => {
+ if expiry_updated_at.is_some() {
+ return Err(DeError::DuplicateField);
+ }
+ expiry_updated_at = Some(d.timestamp(TimestampFormat::DateTime)?);
+ Ok(())
+ }
b"Rule" => {
let ans: LifecycleRule = d.content()?;
rules.get_or_insert_with(List::new).push(ans);
Ok(())
}
- _ => Err(DeError::UnexpectedTagName),
+ _ => Ok(()),
})?;
Ok(Self {
+ expiry_updated_at,
rules: rules.ok_or(DeError::MissingField)?,
})
}
@@ -3189,6 +3205,31 @@ impl<'xml> DeserializeContent<'xml> for DefaultRetention {
Ok(Self { days, mode, years })
}
}
+impl SerializeContent for DelMarkerExpiration {
+ fn serialize_content(&self, s: &mut Serializer) -> SerResult {
+ if let Some(ref val) = self.days {
+ s.content("Days", val)?;
+ }
+ Ok(())
+ }
+}
+
+impl<'xml> DeserializeContent<'xml> for DelMarkerExpiration {
+ fn deserialize_content(d: &mut Deserializer<'xml>) -> DeResult {
+ let mut days: Option = None;
+ d.for_each_element(|d, x| match x {
+ b"Days" => {
+ if days.is_some() {
+ return Err(DeError::DuplicateField);
+ }
+ days = Some(d.content()?);
+ Ok(())
+ }
+ _ => Err(DeError::UnexpectedTagName),
+ })?;
+ Ok(Self { days })
+ }
+}
impl SerializeContent for Delete {
fn serialize_content(&self, s: &mut Serializer) -> SerResult {
{
@@ -5384,6 +5425,9 @@ impl SerializeContent for LifecycleExpiration {
if let Some(ref val) = self.days {
s.content("Days", val)?;
}
+ if let Some(ref val) = self.expired_object_all_versions {
+ s.content("ExpiredObjectAllVersions", val)?;
+ }
if let Some(ref val) = self.expired_object_delete_marker {
s.content("ExpiredObjectDeleteMarker", val)?;
}
@@ -5395,6 +5439,7 @@ impl<'xml> DeserializeContent<'xml> for LifecycleExpiration {
fn deserialize_content(d: &mut Deserializer<'xml>) -> DeResult {
let mut date: Option = None;
let mut days: Option = None;
+ let mut expired_object_all_versions: Option = None;
let mut expired_object_delete_marker: Option = None;
d.for_each_element(|d, x| match x {
b"Date" => {
@@ -5411,6 +5456,13 @@ impl<'xml> DeserializeContent<'xml> for LifecycleExpiration {
days = Some(d.content()?);
Ok(())
}
+ b"ExpiredObjectAllVersions" => {
+ if expired_object_all_versions.is_some() {
+ return Err(DeError::DuplicateField);
+ }
+ expired_object_all_versions = Some(d.content()?);
+ Ok(())
+ }
b"ExpiredObjectDeleteMarker" => {
if expired_object_delete_marker.is_some() {
return Err(DeError::DuplicateField);
@@ -5423,6 +5475,7 @@ impl<'xml> DeserializeContent<'xml> for LifecycleExpiration {
Ok(Self {
date,
days,
+ expired_object_all_versions,
expired_object_delete_marker,
})
}
@@ -5432,6 +5485,9 @@ impl SerializeContent for LifecycleRule {
if let Some(ref val) = self.abort_incomplete_multipart_upload {
s.content("AbortIncompleteMultipartUpload", val)?;
}
+ if let Some(ref val) = self.del_marker_expiration {
+ s.content("DelMarkerExpiration", val)?;
+ }
if let Some(ref val) = self.expiration {
s.content("Expiration", val)?;
}
@@ -5461,6 +5517,7 @@ impl SerializeContent for LifecycleRule {
impl<'xml> DeserializeContent<'xml> for LifecycleRule {
fn deserialize_content(d: &mut Deserializer<'xml>) -> DeResult {
let mut abort_incomplete_multipart_upload: Option = None;
+ let mut del_marker_expiration: Option = None;
let mut expiration: Option = None;
let mut filter: Option = None;
let mut id: Option = None;
@@ -5477,6 +5534,13 @@ impl<'xml> DeserializeContent<'xml> for LifecycleRule {
abort_incomplete_multipart_upload = Some(d.content()?);
Ok(())
}
+ b"DelMarkerExpiration" => {
+ if del_marker_expiration.is_some() {
+ return Err(DeError::DuplicateField);
+ }
+ del_marker_expiration = Some(d.content()?);
+ Ok(())
+ }
b"Expiration" => {
if expiration.is_some() {
return Err(DeError::DuplicateField);
@@ -5533,6 +5597,7 @@ impl<'xml> DeserializeContent<'xml> for LifecycleRule {
})?;
Ok(Self {
abort_incomplete_multipart_upload,
+ del_marker_expiration,
expiration,
filter,
id,
diff --git a/crates/s3s/tests/xml.rs b/crates/s3s/tests/xml.rs
index 20077b2b..e7a2b11f 100644
--- a/crates/s3s/tests/xml.rs
+++ b/crates/s3s/tests/xml.rs
@@ -199,9 +199,8 @@ fn tagging() {
#[test]
fn lifecycle_expiration() {
let val = s3s::dto::LifecycleExpiration {
- date: None,
days: Some(365),
- expired_object_delete_marker: None,
+ ..Default::default()
};
let ans = serialize_content(&val).unwrap();
@@ -285,6 +284,37 @@ fn assume_role_output() {
test_serde(&val);
}
+#[cfg(feature = "minio")]
+#[test]
+fn minio_bucket_lifecycle_configuration_root() {
+ // MinIO compatibility: accept both LifecycleConfiguration and BucketLifecycleConfiguration
+ let xml_bucket = r"
+
+
+ r1
+ Enabled
+ 30
+
+
+ ";
+ let val = deserialize::(xml_bucket.as_bytes()).unwrap();
+ assert_eq!(val.rules.len(), 1);
+ assert_eq!(val.rules[0].id.as_deref(), Some("r1"));
+
+ let xml_std = r"
+
+
+ r2
+ Enabled
+ 30
+
+
+ ";
+ let val2 = deserialize::(xml_std.as_bytes()).unwrap();
+ assert_eq!(val2.rules.len(), 1);
+ assert_eq!(val2.rules[0].id.as_deref(), Some("r2"));
+}
+
#[cfg(feature = "minio")]
#[test]
fn minio_versioning_configuration() {
diff --git a/data/minio-patches.json b/data/minio-patches.json
index 1de7eee3..813dab89 100644
--- a/data/minio-patches.json
+++ b/data/minio-patches.json
@@ -173,6 +173,59 @@
}
}
}
+ },
+ "com.amazonaws.s3#DelMarkerExpiration": {
+ "type": "structure",
+ "members": {
+ "Days": {
+ "target": "com.amazonaws.s3#Days"
+ }
+ },
+ "traits": {
+ "s3s#minio": ""
+ }
+ },
+ "com.amazonaws.s3#BucketLifecycleConfiguration": {
+ "type": "structure",
+ "members": {
+ "ExpiryUpdatedAt": {
+ "target": "com.amazonaws.s3#Date",
+ "traits": {
+ "smithy.api#xmlName": "ExpiryUpdatedAt",
+ "s3s#minio": ""
+ }
+ }
+ }
+ },
+ "com.amazonaws.s3#LifecycleRule": {
+ "type": "structure",
+ "members": {
+ "DelMarkerExpiration": {
+ "target": "com.amazonaws.s3#DelMarkerExpiration",
+ "traits": {
+ "smithy.api#xmlName": "DelMarkerExpiration",
+ "s3s#minio": ""
+ }
+ }
+ }
+ },
+ "com.amazonaws.s3#ExpiredObjectAllVersions": {
+ "type": "boolean",
+ "traits": {
+ "s3s#minio": ""
+ }
+ },
+ "com.amazonaws.s3#LifecycleExpiration": {
+ "type": "structure",
+ "members": {
+ "ExpiredObjectAllVersions": {
+ "target": "com.amazonaws.s3#ExpiredObjectAllVersions",
+ "traits": {
+ "smithy.api#xmlName": "ExpiredObjectAllVersions",
+ "s3s#minio": ""
+ }
+ }
+ }
}
}
}
\ No newline at end of file