Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions codegen/src/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn write_file(path: &str, f: impl FnOnce()) {
}

#[derive(Debug, Clone, Copy)]
enum Patch {
pub enum Patch {
Minio,
}

Expand Down Expand Up @@ -76,7 +76,7 @@ fn inner_run(code_patch: Option<Patch>) {

{
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));
}

{
Expand All @@ -86,7 +86,7 @@ fn inner_run(code_patch: Option<Patch>) {

{
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));
}

{
Expand Down
35 changes: 28 additions & 7 deletions codegen/src/v1/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Patch>) {
declare_codegen!();

for op in ops.values() {
Expand All @@ -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);
}

Expand All @@ -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<Patch>) {
codegen_header_value(ops, rust_types);

for op in ops.values() {
Expand All @@ -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!("}}");
Expand Down Expand Up @@ -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<Patch>) {
let input = op.input.as_str();
let rust_type = &rust_types[input];
match rust_type {
Expand Down Expand Up @@ -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_);
}
}
}
},
Expand Down
37 changes: 28 additions & 9 deletions codegen/src/v1/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<Patch>) {
declare_codegen!();

g([
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Patch>,
) {
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:#?}") };

Expand Down Expand Up @@ -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<Self> {{");

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!("}}");
Expand All @@ -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<Patch>) {
for rust_type in field_type_names.iter().map(|&name| &rust_types[name]) {
match rust_type {
rust::Type::Alias(_) => {}
Expand Down Expand Up @@ -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<Patch>) {
if can_impl_serialize_content(rust_types, &ty.name) {
g!("impl SerializeContent for {} {{", ty.name);
g!(
Expand Down Expand Up @@ -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!("}})?;");
}

Expand Down
3 changes: 3 additions & 0 deletions crates/s3s-aws/src/conv/generated_minio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ impl AwsConversion for s3s::dto::BucketLifecycleConfiguration {

fn try_from_aws(x: Self::Target) -> S3Result<Self> {
Ok(Self {
expiry_updated_at: None,
rules: try_from_aws(x.rules)?,
})
}
Expand Down Expand Up @@ -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)?,
})
}
Expand All @@ -4618,6 +4620,7 @@ impl AwsConversion for s3s::dto::LifecycleRule {
fn try_from_aws(x: Self::Target) -> S3Result<Self> {
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)?,
Expand Down
35 changes: 35 additions & 0 deletions crates/s3s/src/dto/generated_minio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,13 +660,17 @@ pub type BucketKeyEnabled = bool;
/// in the <i>Amazon S3 User Guide</i>.</p>
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct BucketLifecycleConfiguration {
pub expiry_updated_at: Option<Date>,
/// <p>A lifecycle rule for individual objects in an Amazon S3 bucket.</p>
pub rules: LifecycleRules,
}

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()
}
Expand Down Expand Up @@ -3957,6 +3961,21 @@ impl fmt::Debug for DefaultRetention {
}
}

#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DelMarkerExpiration {
pub days: Option<Days>,
}

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()
}
}

/// <p>Container for the objects to delete.</p>
#[derive(Clone, Default, PartialEq)]
pub struct Delete {
Expand Down Expand Up @@ -7539,6 +7558,8 @@ impl FromStr for ExpirationStatus {
}
}

pub type ExpiredObjectAllVersions = bool;

pub type ExpiredObjectDeleteMarker = bool;

pub type Expires = Timestamp;
Expand Down Expand Up @@ -11653,6 +11674,7 @@ pub struct LifecycleExpiration {
/// <p>Indicates the lifetime, in days, of the objects that are subject to the rule. The value
/// must be a non-zero positive integer.</p>
pub days: Option<Days>,
pub expired_object_all_versions: Option<ExpiredObjectAllVersions>,
/// <p>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.</p>
Expand All @@ -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);
}
Expand All @@ -11685,6 +11710,7 @@ impl fmt::Debug for LifecycleExpiration {
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct LifecycleRule {
pub abort_incomplete_multipart_upload: Option<AbortIncompleteMultipartUpload>,
pub del_marker_expiration: Option<DelMarkerExpiration>,
/// <p>Specifies the expiration for the lifecycle of the object in the form of date, days and,
/// whether the object has a delete marker.</p>
pub expiration: Option<LifecycleExpiration>,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) {}
}
Expand Down Expand Up @@ -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();
}
Expand Down
51 changes: 51 additions & 0 deletions crates/s3s/src/http/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<crate::dto::ObjectLockConfiguration>> {
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::<ObjectLockConfiguration>(&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<crate::dto::VersioningConfiguration> {
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::<VersioningConfiguration>(&bytes);
if result.is_err() {
error!(?bytes, "malformed xml body");
}
result
}

pub fn take_string_body(req: &mut Request) -> S3Result<String> {
let bytes = req.body.take_bytes().expect("full body not found");
match String::from_utf8_simd(bytes.into()) {
Expand Down
Loading
Loading