diff --git a/crates/ecstore/src/bucket/metadata_sys.rs b/crates/ecstore/src/bucket/metadata_sys.rs
index 3e0e6ded6..52edc4350 100644
--- a/crates/ecstore/src/bucket/metadata_sys.rs
+++ b/crates/ecstore/src/bucket/metadata_sys.rs
@@ -2512,11 +2512,169 @@ pub(crate) mod test_support {
mod tests {
use super::test_support::isolated_store_over_temp_disks;
use super::*;
+ use crate::bucket::metadata::{
+ BUCKET_ACCELERATE_CONFIG, BUCKET_CORS_CONFIG, BUCKET_LIFECYCLE_CONFIG, BUCKET_LOGGING_CONFIG, BUCKET_NOTIFICATION_CONFIG,
+ BUCKET_POLICY_CONFIG, BUCKET_PUBLIC_ACCESS_BLOCK_CONFIG, BUCKET_REPLICATION_CONFIG, BUCKET_REQUEST_PAYMENT_CONFIG,
+ BUCKET_SSECONFIG, BUCKET_TAGGING_CONFIG, BUCKET_VERSIONING_CONFIG, BUCKET_WEBSITE_CONFIG, OBJECT_LOCK_CONFIG,
+ };
use crate::bucket::target::{BucketTarget, BucketTargetType, Credentials};
+ use crate::config::com::read_config;
use crate::storage_api_contracts::bucket::{BucketOperations as _, DeleteBucketOptions, MakeBucketOptions};
+ use byteorder::{ByteOrder as _, LittleEndian};
use serial_test::serial;
use tokio::time::timeout;
+ const NEW_WRITER_REPLICATION_XML: &[u8] = br#"arn:aws:iam::111122223333:role/replication-rolerollback1documents/Enabledarn:aws:s3:::replica-bucketDisabled"#;
+
+ const NEW_WRITER_CONFIGS: [(&str, &[u8]); 14] = [
+ (BUCKET_POLICY_CONFIG, br#"{"Version":"2012-10-17","Statement":[]}"#),
+ (BUCKET_NOTIFICATION_CONFIG, br#""#),
+ (
+ BUCKET_LIFECYCLE_CONFIG,
+ br#"expireEnabledlogs/30"#,
+ ),
+ (
+ OBJECT_LOCK_CONFIG,
+ br#"EnabledGOVERNANCE7"#,
+ ),
+ (
+ BUCKET_VERSIONING_CONFIG,
+ br#"Enabled"#,
+ ),
+ (
+ BUCKET_SSECONFIG,
+ br#"AES256"#,
+ ),
+ (
+ BUCKET_TAGGING_CONFIG,
+ r#"environment测试-🦀"#.as_bytes(),
+ ),
+ (BUCKET_REPLICATION_CONFIG, NEW_WRITER_REPLICATION_XML),
+ (
+ BUCKET_CORS_CONFIG,
+ br#"GEThttps://example.test"#,
+ ),
+ (BUCKET_LOGGING_CONFIG, br#""#),
+ (
+ BUCKET_WEBSITE_CONFIG,
+ br#"index.html"#,
+ ),
+ (
+ BUCKET_ACCELERATE_CONFIG,
+ br#"Enabled"#,
+ ),
+ (
+ BUCKET_REQUEST_PAYMENT_CONFIG,
+ br#"Requester"#,
+ ),
+ (
+ BUCKET_PUBLIC_ACCESS_BLOCK_CONFIG,
+ br#"truetruetruefalse"#,
+ ),
+ ];
+
+ #[tokio::test]
+ async fn g_d3_003_new_writer_replication_loads_without_fail_closed_state() {
+ let (dirs, store) = isolated_store_over_temp_disks().await;
+ let bucket = "rollback-new-replication";
+ for dir in &dirs {
+ std::fs::create_dir_all(dir.path().join(bucket)).expect("rollback fixture bucket should be created");
+ }
+
+ let writer = BucketMetadataSys::new(store.clone());
+ let mut metadata = BucketMetadata::new(bucket);
+ metadata
+ .update_config(BUCKET_REPLICATION_CONFIG, NEW_WRITER_REPLICATION_XML.to_vec())
+ .expect("new-writer replication XML should be accepted before persistence");
+ writer
+ .persist_new_and_set(metadata)
+ .await
+ .expect("new-writer replication metadata should persist");
+
+ let old_reader = BucketMetadataSys::new(store);
+ let (loaded, _) = old_reader
+ .get_replication_config(bucket)
+ .await
+ .expect("old metadata_sys must not classify new-writer replication XML as invalid");
+ assert_eq!(loaded.role, "arn:aws:iam::111122223333:role/replication-role");
+ assert_eq!(loaded.rules.len(), 1);
+ assert_eq!(loaded.rules[0].id.as_deref(), Some("rollback"));
+ }
+
+ #[tokio::test]
+ async fn g_d3_004_new_writer_metadata_blob_keeps_legacy_header_and_configs() {
+ let (dirs, store) = isolated_store_over_temp_disks().await;
+ let bucket = "rollback-new-metadata";
+ for dir in &dirs {
+ std::fs::create_dir_all(dir.path().join(bucket)).expect("rollback fixture bucket should be created");
+ }
+
+ let writer = BucketMetadataSys::new(store.clone());
+ let mut metadata = BucketMetadata::new(bucket);
+ for (config_file, bytes) in NEW_WRITER_CONFIGS {
+ metadata
+ .update_config(config_file, bytes.to_vec())
+ .unwrap_or_else(|err| panic!("new-writer {config_file} fixture must be valid: {err}"));
+ }
+ writer
+ .persist_new_and_set(metadata)
+ .await
+ .expect("new-writer metadata should persist");
+
+ let path = BucketMetadata::new(bucket).save_file_path();
+ let blob = read_config(store.clone(), &path)
+ .await
+ .expect("persisted .metadata.bin should be readable");
+ assert_eq!(
+ LittleEndian::read_u16(&blob[0..2]),
+ 1,
+ "bucket metadata format must stay rollback-readable"
+ );
+ assert_eq!(
+ LittleEndian::read_u16(&blob[2..4]),
+ 1,
+ "bucket metadata version must stay rollback-readable"
+ );
+
+ let loaded = load_bucket_metadata(store, bucket)
+ .await
+ .expect("old read_bucket_metadata path must load the new-writer blob");
+ let loaded_configs: [(&str, &[u8]); 14] = [
+ (BUCKET_POLICY_CONFIG, &loaded.policy_config_json),
+ (BUCKET_NOTIFICATION_CONFIG, &loaded.notification_config_xml),
+ (BUCKET_LIFECYCLE_CONFIG, &loaded.lifecycle_config_xml),
+ (OBJECT_LOCK_CONFIG, &loaded.object_lock_config_xml),
+ (BUCKET_VERSIONING_CONFIG, &loaded.versioning_config_xml),
+ (BUCKET_SSECONFIG, &loaded.encryption_config_xml),
+ (BUCKET_TAGGING_CONFIG, &loaded.tagging_config_xml),
+ (BUCKET_REPLICATION_CONFIG, &loaded.replication_config_xml),
+ (BUCKET_CORS_CONFIG, &loaded.cors_config_xml),
+ (BUCKET_LOGGING_CONFIG, &loaded.logging_config_xml),
+ (BUCKET_WEBSITE_CONFIG, &loaded.website_config_xml),
+ (BUCKET_ACCELERATE_CONFIG, &loaded.accelerate_config_xml),
+ (BUCKET_REQUEST_PAYMENT_CONFIG, &loaded.request_payment_config_xml),
+ (BUCKET_PUBLIC_ACCESS_BLOCK_CONFIG, &loaded.public_access_block_config_xml),
+ ];
+ for ((expected_name, expected), (loaded_name, actual)) in NEW_WRITER_CONFIGS.into_iter().zip(loaded_configs) {
+ assert_eq!(loaded_name, expected_name);
+ assert_eq!(actual, expected, "old read_bucket_metadata changed {expected_name} bytes");
+ }
+ assert!(loaded.policy_config.is_some());
+ assert!(loaded.notification_config.is_some());
+ assert!(loaded.lifecycle_config.is_some());
+ assert!(loaded.object_lock_config.is_some());
+ assert!(loaded.versioning_config.is_some());
+ assert!(loaded.sse_config.is_some());
+ assert!(loaded.tagging_config.is_some());
+ assert!(loaded.replication_config.is_some());
+ assert!(loaded.cors_config.is_some());
+ assert!(loaded.logging_config.is_some());
+ assert!(loaded.website_config.is_some());
+ assert!(loaded.accelerate_config.is_some());
+ assert!(loaded.request_payment_config.is_some());
+ assert!(loaded.public_access_block_config.is_some());
+ }
+
#[tokio::test]
async fn malformed_delete_configs_are_not_treated_as_absent() {
let (_dirs, ecstore) = isolated_store_over_temp_disks().await;