1use std::sync::Arc;
2
3use async_trait::async_trait;
4use bytes::Bytes;
5use chrono::{DateTime, Timelike, Utc};
6use http::{HeaderMap, Method, StatusCode};
7use md5::{Digest, Md5};
8
9use fakecloud_core::delivery::DeliveryBus;
10use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
11use fakecloud_kms::state::SharedKmsState;
12use fakecloud_persistence::{MemoryS3Store, S3Store, StoreError};
13
14use base64::engine::general_purpose::STANDARD as BASE64;
15use base64::Engine as _;
16
17use crate::logging;
18use crate::state::{AclGrant, S3Bucket, S3Object, SharedS3State};
19
20mod acl;
21mod buckets;
22mod config;
23mod lock;
24mod multipart;
25mod notifications;
26mod objects;
27mod tags;
28
29#[cfg(test)]
31use notifications::replicate_object;
32pub(super) use notifications::{
33 deliver_notifications, normalize_notification_ids, normalize_replication_xml,
34 replicate_through_store,
35};
36
37use notifications::extract_all_xml_values;
39
40#[cfg(test)]
42use notifications::{
43 event_matches, key_matches_filters, parse_notification_config, parse_replication_rules,
44 NotificationTargetType,
45};
46
47pub struct S3Service {
48 state: SharedS3State,
49 delivery: Arc<DeliveryBus>,
50 kms_state: Option<SharedKmsState>,
51 pub(crate) kms_hook: Option<Arc<dyn fakecloud_core::delivery::KmsHook>>,
52 #[allow(dead_code)]
53 store: Arc<dyn S3Store>,
54}
55
56pub(crate) fn persistence_error(err: StoreError) -> AwsServiceError {
61 AwsServiceError::aws_error(
62 StatusCode::INTERNAL_SERVER_ERROR,
63 "InternalError",
64 format!("persistence store error: {err}"),
65 )
66}
67
68pub(crate) fn io_to_aws(err: std::io::Error) -> AwsServiceError {
71 AwsServiceError::aws_error(
72 StatusCode::INTERNAL_SERVER_ERROR,
73 "InternalError",
74 format!("failed to read object body from disk: {err}"),
75 )
76}
77
78impl S3Service {
79 pub fn new(state: SharedS3State, delivery: Arc<DeliveryBus>) -> Self {
80 Self::with_store(state, delivery, Arc::new(MemoryS3Store::new()))
81 }
82
83 pub fn with_store(
84 state: SharedS3State,
85 delivery: Arc<DeliveryBus>,
86 store: Arc<dyn S3Store>,
87 ) -> Self {
88 Self {
89 state,
90 delivery,
91 kms_state: None,
92 kms_hook: None,
93 store,
94 }
95 }
96
97 pub fn with_kms(mut self, kms_state: SharedKmsState) -> Self {
98 self.kms_state = Some(kms_state);
99 self
100 }
101
102 pub fn with_kms_hook(mut self, hook: Arc<dyn fakecloud_core::delivery::KmsHook>) -> Self {
103 self.kms_hook = Some(hook);
104 self
105 }
106
107 pub(crate) fn encrypt_object_body(
118 &self,
119 account_id: &str,
120 region: &str,
121 bucket: &str,
122 plaintext: &[u8],
123 kms_key_id: Option<&str>,
124 ) -> Result<bytes::Bytes, AwsServiceError> {
125 let Some(hook) = &self.kms_hook else {
126 return Ok(bytes::Bytes::copy_from_slice(plaintext));
127 };
128 let key = kms_key_id.filter(|k| !k.is_empty()).unwrap_or("aws/s3");
129 let bucket_arn = format!("arn:aws:s3:::{bucket}");
130 let mut ctx = std::collections::HashMap::new();
131 ctx.insert("aws:s3:arn".to_string(), bucket_arn);
132 match hook.encrypt(account_id, region, key, plaintext, "s3.amazonaws.com", ctx) {
133 Ok(envelope) => Ok(bytes::Bytes::from(envelope.into_bytes())),
134 Err(err) => {
135 tracing::warn!(bucket = %bucket, error = %err, "SSE-KMS encrypt failed");
136 Err(AwsServiceError::aws_error(
137 StatusCode::INTERNAL_SERVER_ERROR,
138 "KMS.InternalFailureException",
139 format!("Failed to encrypt object via KMS: {err}"),
140 ))
141 }
142 }
143 }
144
145 pub(crate) fn decrypt_object_body(
155 &self,
156 account_id: &str,
157 bucket: &str,
158 ciphertext: &[u8],
159 ) -> Result<bytes::Bytes, AwsServiceError> {
160 let Some(hook) = &self.kms_hook else {
161 return Ok(bytes::Bytes::copy_from_slice(ciphertext));
162 };
163 let envelope = match std::str::from_utf8(ciphertext) {
166 Ok(s) => s,
167 Err(_) => return Ok(bytes::Bytes::copy_from_slice(ciphertext)),
168 };
169 let bucket_arn = format!("arn:aws:s3:::{bucket}");
170 let mut ctx = std::collections::HashMap::new();
171 ctx.insert("aws:s3:arn".to_string(), bucket_arn);
172 match hook.decrypt(account_id, envelope, "s3.amazonaws.com", ctx) {
173 Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
174 Err(err) => {
175 tracing::warn!(bucket = %bucket, error = %err, "SSE-KMS decrypt failed");
176 Err(AwsServiceError::aws_error(
177 StatusCode::INTERNAL_SERVER_ERROR,
178 "KMS.InternalFailureException",
179 format!("Failed to decrypt object via KMS: {err}"),
180 ))
181 }
182 }
183 }
184}
185
186#[async_trait]
187impl AwsService for S3Service {
188 fn service_name(&self) -> &str {
189 "s3"
190 }
191
192 async fn handle(&self, mut req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
193 let is_put_to_key = req.method == Method::PUT
203 && req.path_segments.len() >= 2
204 && req
205 .path_segments
206 .first()
207 .map(|s| !s.is_empty())
208 .unwrap_or(false);
209 let q = &req.query_params;
210 let is_put_object = is_put_to_key
211 && !q.contains_key("tagging")
212 && !q.contains_key("acl")
213 && !q.contains_key("retention")
214 && !q.contains_key("legal-hold")
215 && !q.contains_key("renameObject")
216 && !q.contains_key("encryption")
217 && !req.headers.contains_key("x-amz-copy-source");
218 let is_upload_part =
223 is_put_to_key && q.contains_key("partNumber") && q.contains_key("uploadId");
224 if !is_put_object && !is_upload_part {
225 if let Some(stream) = req.take_body_stream() {
226 req.body = fakecloud_core::service::drain_request_stream(stream).await?;
227 }
228 }
229
230 let bucket = req.path_segments.first().map(|s| s.as_str());
232 let key = if let Some(b) = bucket {
235 let prefix = format!("/{b}/");
236 if req.raw_path.starts_with(&prefix) && req.raw_path.len() > prefix.len() {
237 let raw_key = &req.raw_path[prefix.len()..];
238 Some(
239 percent_encoding::percent_decode_str(raw_key)
240 .decode_utf8_lossy()
241 .into_owned(),
242 )
243 } else if req.path_segments.len() > 1 {
244 let raw = req.path_segments[1..].join("/");
245 Some(
246 percent_encoding::percent_decode_str(&raw)
247 .decode_utf8_lossy()
248 .into_owned(),
249 )
250 } else {
251 None
252 }
253 } else {
254 None
255 };
256
257 let account_id = req.account_id.as_str();
258
259 if let Some(b) = bucket {
261 if req.method == Method::POST
263 && key.is_some()
264 && req.query_params.contains_key("uploads")
265 {
266 return self.create_multipart_upload(account_id, &req, b, key.as_deref().unwrap());
267 }
268
269 if req.method == Method::POST
271 && key.is_some()
272 && req.query_params.contains_key("restore")
273 {
274 return self.restore_object(account_id, &req, b, key.as_deref().unwrap());
275 }
276
277 if req.method == Method::POST && key.is_some() {
279 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
280 return self.complete_multipart_upload(
281 account_id,
282 &req,
283 b,
284 key.as_deref().unwrap(),
285 &upload_id,
286 );
287 }
288 }
289
290 if req.method == Method::PUT && key.is_some() {
292 if let (Some(part_num_str), Some(upload_id)) = (
293 req.query_params.get("partNumber").cloned(),
294 req.query_params.get("uploadId").cloned(),
295 ) {
296 if let Ok(part_number) = part_num_str.parse::<i64>() {
297 if req.headers.contains_key("x-amz-copy-source") {
298 return self.upload_part_copy(
299 account_id,
300 &req,
301 b,
302 key.as_deref().unwrap(),
303 &upload_id,
304 part_number,
305 );
306 }
307 return self
308 .upload_part(
309 account_id,
310 &req,
311 b,
312 key.as_deref().unwrap(),
313 &upload_id,
314 part_number,
315 )
316 .await;
317 }
318 }
319 }
320
321 if req.method == Method::DELETE && key.is_some() {
323 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
324 return self.abort_multipart_upload(
325 account_id,
326 b,
327 key.as_deref().unwrap(),
328 &upload_id,
329 );
330 }
331 }
332
333 if req.method == Method::GET
335 && key.is_none()
336 && req.query_params.contains_key("uploads")
337 {
338 return self.list_multipart_uploads(account_id, b);
339 }
340
341 if req.method == Method::GET && key.is_some() {
343 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
344 return self.list_parts(
345 account_id,
346 &req,
347 b,
348 key.as_deref().unwrap(),
349 &upload_id,
350 );
351 }
352 }
353 }
354
355 if req.method == Method::OPTIONS {
357 if let Some(b_name) = bucket {
358 let cors_config = {
359 let accounts = self.state.read();
360 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
361 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
362 state
363 .buckets
364 .get(b_name)
365 .and_then(|b| b.cors_config.clone())
366 };
367 if let Some(ref config) = cors_config {
368 let origin = req
369 .headers
370 .get("origin")
371 .and_then(|v| v.to_str().ok())
372 .unwrap_or("");
373 let request_method = req
374 .headers
375 .get("access-control-request-method")
376 .and_then(|v| v.to_str().ok())
377 .unwrap_or("");
378 let rules = parse_cors_config(config);
379 if let Some(rule) = find_cors_rule(&rules, origin, Some(request_method)) {
380 let mut headers = HeaderMap::new();
381 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
382 "*"
383 } else {
384 origin
385 };
386 headers.insert(
387 "access-control-allow-origin",
388 matched_origin
389 .parse()
390 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
391 );
392 headers.insert(
393 "access-control-allow-methods",
394 rule.allowed_methods
395 .join(", ")
396 .parse()
397 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
398 );
399 if !rule.allowed_headers.is_empty() {
400 let ah = if rule.allowed_headers.contains(&"*".to_string()) {
401 req.headers
402 .get("access-control-request-headers")
403 .and_then(|v| v.to_str().ok())
404 .unwrap_or("*")
405 .to_string()
406 } else {
407 rule.allowed_headers.join(", ")
408 };
409 headers.insert(
410 "access-control-allow-headers",
411 ah.parse()
412 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
413 );
414 }
415 if let Some(max_age) = rule.max_age_seconds {
416 headers.insert(
417 "access-control-max-age",
418 max_age
419 .to_string()
420 .parse()
421 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
422 );
423 }
424 return Ok(AwsResponse {
425 status: StatusCode::OK,
426 content_type: String::new(),
427 body: Bytes::new().into(),
428 headers,
429 });
430 }
431 }
432 return Err(AwsServiceError::aws_error(
433 StatusCode::FORBIDDEN,
434 "CORSResponse",
435 "CORS is not enabled for this bucket",
436 ));
437 }
438 }
439
440 let origin_header = req
442 .headers
443 .get("origin")
444 .and_then(|v| v.to_str().ok())
445 .map(|s| s.to_string());
446
447 let mut result = match (&req.method, bucket, key.as_deref()) {
448 (&Method::GET, None, None) => {
450 if req.query_params.get("x-id").map(|s| s.as_str()) == Some("ListDirectoryBuckets")
451 {
452 self.list_directory_buckets(account_id, &req)
453 } else {
454 self.list_buckets(account_id, &req)
455 }
456 }
457
458 (&Method::PUT, Some(b), None) => {
460 if req.query_params.contains_key("tagging") {
461 self.put_bucket_tagging(account_id, &req, b)
462 } else if req.query_params.contains_key("acl") {
463 self.put_bucket_acl(account_id, &req, b)
464 } else if req.query_params.contains_key("versioning") {
465 self.put_bucket_versioning(account_id, &req, b)
466 } else if req.query_params.contains_key("cors") {
467 self.put_bucket_cors(account_id, &req, b)
468 } else if req.query_params.contains_key("notification") {
469 self.put_bucket_notification(account_id, &req, b)
470 } else if req.query_params.contains_key("website") {
471 self.put_bucket_website(account_id, &req, b)
472 } else if req.query_params.contains_key("accelerate") {
473 self.put_bucket_accelerate(account_id, &req, b)
474 } else if req.query_params.contains_key("publicAccessBlock") {
475 self.put_public_access_block(account_id, &req, b)
476 } else if req.query_params.contains_key("encryption") {
477 self.put_bucket_encryption(account_id, &req, b)
478 } else if req.query_params.contains_key("lifecycle") {
479 self.put_bucket_lifecycle(account_id, &req, b)
480 } else if req.query_params.contains_key("logging") {
481 self.put_bucket_logging(account_id, &req, b)
482 } else if req.query_params.contains_key("policy") {
483 self.put_bucket_policy(account_id, &req, b)
484 } else if req.query_params.contains_key("object-lock") {
485 self.put_object_lock_config(account_id, &req, b)
486 } else if req.query_params.contains_key("replication") {
487 self.put_bucket_replication(account_id, &req, b)
488 } else if req.query_params.contains_key("ownershipControls") {
489 self.put_bucket_ownership_controls(account_id, &req, b)
490 } else if req.query_params.contains_key("inventory") {
491 self.put_bucket_inventory(account_id, &req, b)
492 } else if req.query_params.contains_key("analytics") {
493 self.put_bucket_analytics_config(account_id, &req, b)
494 } else if req.query_params.contains_key("intelligent-tiering") {
495 self.put_bucket_intelligent_tiering_config(account_id, &req, b)
496 } else if req.query_params.contains_key("metrics") {
497 self.put_bucket_metrics_config(account_id, &req, b)
498 } else if req.query_params.contains_key("requestPayment") {
499 self.put_bucket_request_payment(account_id, &req, b)
500 } else if req.query_params.contains_key("abac") {
501 self.put_bucket_abac(account_id, &req, b)
502 } else if req.query_params.contains_key("metadataInventoryTable") {
503 self.update_bucket_metadata_inventory_table(account_id, &req, b)
504 } else if req.query_params.contains_key("metadataJournalTable") {
505 self.update_bucket_metadata_journal_table(account_id, &req, b)
506 } else {
507 self.create_bucket(account_id, &req, b)
508 }
509 }
510 (&Method::DELETE, Some(b), None) => {
511 if req.query_params.contains_key("tagging") {
512 self.delete_bucket_tagging(account_id, &req, b)
513 } else if req.query_params.contains_key("cors") {
514 self.delete_bucket_cors(account_id, b)
515 } else if req.query_params.contains_key("website") {
516 self.delete_bucket_website(account_id, b)
517 } else if req.query_params.contains_key("publicAccessBlock") {
518 self.delete_public_access_block(account_id, b)
519 } else if req.query_params.contains_key("encryption") {
520 self.delete_bucket_encryption(account_id, b)
521 } else if req.query_params.contains_key("lifecycle") {
522 self.delete_bucket_lifecycle(account_id, b)
523 } else if req.query_params.contains_key("policy") {
524 self.delete_bucket_policy(account_id, b)
525 } else if req.query_params.contains_key("replication") {
526 self.delete_bucket_replication(account_id, b)
527 } else if req.query_params.contains_key("ownershipControls") {
528 self.delete_bucket_ownership_controls(account_id, b)
529 } else if req.query_params.contains_key("inventory") {
530 self.delete_bucket_inventory(account_id, &req, b)
531 } else if req.query_params.contains_key("analytics") {
532 self.delete_bucket_analytics_config(account_id, &req, b)
533 } else if req.query_params.contains_key("intelligent-tiering") {
534 self.delete_bucket_intelligent_tiering_config(account_id, &req, b)
535 } else if req.query_params.contains_key("metrics") {
536 self.delete_bucket_metrics_config(account_id, &req, b)
537 } else if req.query_params.contains_key("metadataConfiguration") {
538 self.delete_bucket_metadata_config(account_id, b)
539 } else if req.query_params.contains_key("metadataTable") {
540 self.delete_bucket_metadata_table_config(account_id, b)
541 } else {
542 self.delete_bucket(account_id, &req, b)
543 }
544 }
545 (&Method::HEAD, Some(b), None) => self.head_bucket(account_id, b),
546 (&Method::GET, Some(b), None) => {
547 if req.query_params.contains_key("tagging") {
548 self.get_bucket_tagging(account_id, &req, b)
549 } else if req.query_params.contains_key("location") {
550 self.get_bucket_location(account_id, b)
551 } else if req.query_params.contains_key("acl") {
552 self.get_bucket_acl(account_id, &req, b)
553 } else if req.query_params.contains_key("versioning") {
554 self.get_bucket_versioning(account_id, b)
555 } else if req.query_params.contains_key("versions") {
556 self.list_object_versions(account_id, &req, b)
557 } else if req.query_params.contains_key("object-lock") {
558 self.get_object_lock_configuration(account_id, b)
559 } else if req.query_params.contains_key("cors") {
560 self.get_bucket_cors(account_id, b)
561 } else if req.query_params.contains_key("notification") {
562 self.get_bucket_notification(account_id, b)
563 } else if req.query_params.contains_key("website") {
564 self.get_bucket_website(account_id, b)
565 } else if req.query_params.contains_key("accelerate") {
566 self.get_bucket_accelerate(account_id, b)
567 } else if req.query_params.contains_key("publicAccessBlock") {
568 self.get_public_access_block(account_id, b)
569 } else if req.query_params.contains_key("encryption") {
570 self.get_bucket_encryption(account_id, b)
571 } else if req.query_params.contains_key("lifecycle") {
572 self.get_bucket_lifecycle(account_id, b)
573 } else if req.query_params.contains_key("logging") {
574 self.get_bucket_logging(account_id, b)
575 } else if req.query_params.contains_key("policy") {
576 self.get_bucket_policy(account_id, b)
577 } else if req.query_params.contains_key("replication") {
578 self.get_bucket_replication(account_id, b)
579 } else if req.query_params.contains_key("ownershipControls") {
580 self.get_bucket_ownership_controls(account_id, b)
581 } else if req.query_params.contains_key("inventory") {
582 if req.query_params.contains_key("id") {
583 self.get_bucket_inventory(account_id, &req, b)
584 } else {
585 self.list_bucket_inventory_configurations(account_id, b)
586 }
587 } else if req.query_params.contains_key("analytics") {
588 if req.query_params.contains_key("id") {
589 self.get_bucket_analytics_config(account_id, &req, b)
590 } else {
591 self.list_bucket_analytics_configurations(account_id, b)
592 }
593 } else if req.query_params.contains_key("intelligent-tiering") {
594 if req.query_params.contains_key("id") {
595 self.get_bucket_intelligent_tiering_config(account_id, &req, b)
596 } else {
597 self.list_bucket_intelligent_tiering_configurations(account_id, b)
598 }
599 } else if req.query_params.contains_key("metrics") {
600 if req.query_params.contains_key("id") {
601 self.get_bucket_metrics_config(account_id, &req, b)
602 } else {
603 self.list_bucket_metrics_configurations(account_id, b)
604 }
605 } else if req.query_params.contains_key("requestPayment") {
606 self.get_bucket_request_payment(account_id, b)
607 } else if req.query_params.contains_key("abac") {
608 self.get_bucket_abac(account_id, b)
609 } else if req.query_params.contains_key("policyStatus") {
610 self.get_bucket_policy_status(account_id, b)
611 } else if req.query_params.contains_key("metadataConfiguration") {
612 self.get_bucket_metadata_config(account_id, b)
613 } else if req.query_params.contains_key("metadataTable") {
614 self.get_bucket_metadata_table_config(account_id, b)
615 } else if req.query_params.contains_key("session") {
616 self.create_session(account_id, &req, b)
617 } else if req.query_params.get("list-type").map(|s| s.as_str()) == Some("2") {
618 self.list_objects_v2(account_id, &req, b)
619 } else if req.query_params.is_empty() {
620 let website_config = {
622 let accounts = self.state.read();
623 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
624 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
625 state
626 .buckets
627 .get(b)
628 .and_then(|bkt| bkt.website_config.clone())
629 };
630 if let Some(ref config) = website_config {
631 if let Some(index_doc) = extract_xml_value(config, "Suffix").or_else(|| {
632 extract_xml_value(config, "IndexDocument").and_then(|inner| {
633 let open = "<Suffix>";
634 let close = "</Suffix>";
635 let s = inner.find(open)? + open.len();
636 let e = inner.find(close)?;
637 Some(inner[s..e].trim().to_string())
638 })
639 }) {
640 self.serve_website_object(account_id, &req, b, &index_doc, config)
641 } else {
642 self.list_objects_v1(account_id, &req, b)
643 }
644 } else {
645 self.list_objects_v1(account_id, &req, b)
646 }
647 } else {
648 self.list_objects_v1(account_id, &req, b)
649 }
650 }
651
652 (&Method::PUT, Some(b), Some(k)) => {
654 if req.query_params.contains_key("tagging") {
655 self.put_object_tagging(account_id, &req, b, k)
656 } else if req.query_params.contains_key("acl") {
657 self.put_object_acl(account_id, &req, b, k)
658 } else if req.query_params.contains_key("retention") {
659 self.put_object_retention(account_id, &req, b, k)
660 } else if req.query_params.contains_key("legal-hold") {
661 self.put_object_legal_hold(account_id, &req, b, k)
662 } else if req.query_params.contains_key("renameObject") {
663 self.rename_object(account_id, &req, b, k)
664 } else if req.query_params.contains_key("encryption") {
665 self.update_object_encryption(account_id, &req, b, k)
666 } else if req.headers.contains_key("x-amz-copy-source") {
667 self.copy_object(account_id, &req, b, k)
668 } else {
669 self.put_object(account_id, &req, b, k).await
670 }
671 }
672 (&Method::GET, Some(b), Some(k)) => {
673 if req.query_params.contains_key("tagging") {
674 self.get_object_tagging(account_id, &req, b, k)
675 } else if req.query_params.contains_key("acl") {
676 self.get_object_acl(account_id, &req, b, k)
677 } else if req.query_params.contains_key("retention") {
678 self.get_object_retention(account_id, &req, b, k)
679 } else if req.query_params.contains_key("legal-hold") {
680 self.get_object_legal_hold(account_id, &req, b, k)
681 } else if req.query_params.contains_key("attributes") {
682 self.get_object_attributes(account_id, &req, b, k)
683 } else if req.query_params.contains_key("torrent") {
684 self.get_object_torrent(account_id, &req, b, k)
685 } else {
686 let result = self.get_object(account_id, &req, b, k);
687 let is_not_found = matches!(
689 &result,
690 Err(e) if e.code() == "NoSuchKey"
691 );
692 if is_not_found {
693 let website_config = {
694 let accounts = self.state.read();
695 let _empty_s3 =
696 crate::state::S3State::new(&req.account_id, &req.region);
697 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
698 state
699 .buckets
700 .get(b)
701 .and_then(|bkt| bkt.website_config.clone())
702 };
703 if let Some(ref config) = website_config {
704 if let Some(error_key) = extract_xml_value(config, "ErrorDocument")
705 .and_then(|inner| {
706 let open = "<Key>";
707 let close = "</Key>";
708 let s = inner.find(open)? + open.len();
709 let e = inner.find(close)?;
710 Some(inner[s..e].trim().to_string())
711 })
712 .or_else(|| extract_xml_value(config, "Key"))
713 {
714 return self.serve_website_error(account_id, &req, b, &error_key);
715 }
716 }
717 }
718 result
719 }
720 }
721 (&Method::DELETE, Some(b), Some(k)) => {
722 if req.query_params.contains_key("tagging") {
723 self.delete_object_tagging(account_id, b, k)
724 } else {
725 self.delete_object(account_id, &req, b, k)
726 }
727 }
728 (&Method::HEAD, Some(b), Some(k)) => self.head_object(account_id, &req, b, k),
729
730 (&Method::POST, Some(b), None) if req.query_params.contains_key("delete") => {
732 self.delete_objects(account_id, &req, b)
733 }
734 (&Method::POST, Some(b), None)
735 if req.query_params.contains_key("metadataConfiguration") =>
736 {
737 self.create_bucket_metadata_config(account_id, &req, b)
738 }
739 (&Method::POST, Some(b), None) if req.query_params.contains_key("metadataTable") => {
740 self.create_bucket_metadata_table_config(account_id, &req, b)
741 }
742 (&Method::POST, Some(b), Some(k))
743 if req.query_params.get("select-type").map(|s| s.as_str()) == Some("2") =>
744 {
745 self.select_object_content(account_id, &req, b, k)
746 }
747 (&Method::POST, Some("WriteGetObjectResponse"), None) => {
748 self.write_get_object_response(account_id, &req)
749 }
750
751 _ => Err(AwsServiceError::aws_error(
752 StatusCode::METHOD_NOT_ALLOWED,
753 "MethodNotAllowed",
754 "The specified method is not allowed against this resource",
755 )),
756 };
757
758 if let (Some(ref origin), Some(b_name)) = (&origin_header, bucket) {
760 let cors_config = {
761 let accounts = self.state.read();
762 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
763 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
764 state
765 .buckets
766 .get(b_name)
767 .and_then(|b| b.cors_config.clone())
768 };
769 if let Some(ref config) = cors_config {
770 let rules = parse_cors_config(config);
771 if let Some(rule) = find_cors_rule(&rules, origin, None) {
772 if let Ok(ref mut resp) = result {
773 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
774 "*"
775 } else {
776 origin
777 };
778 resp.headers.insert(
779 "access-control-allow-origin",
780 matched_origin
781 .parse()
782 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
783 );
784 if !rule.expose_headers.is_empty() {
785 resp.headers.insert(
786 "access-control-expose-headers",
787 rule.expose_headers
788 .join(", ")
789 .parse()
790 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
791 );
792 }
793 }
794 }
795 }
796 }
797
798 if let Some(b_name) = bucket {
800 let status_code = match &result {
801 Ok(resp) => resp.status.as_u16(),
802 Err(e) => e.status().as_u16(),
803 };
804 let op = logging::operation_name(&req.method, key.as_deref());
805 logging::maybe_write_access_log(
806 &self.state,
807 &self.store,
808 b_name,
809 &logging::AccessLogRequest {
810 operation: op,
811 key: key.as_deref(),
812 status: status_code,
813 request_id: &req.request_id,
814 method: req.method.as_str(),
815 path: &req.raw_path,
816 },
817 );
818 }
819
820 result
821 }
822
823 fn supported_actions(&self) -> &[&str] {
824 &[
825 "ListBuckets",
827 "CreateBucket",
828 "DeleteBucket",
829 "HeadBucket",
830 "GetBucketLocation",
831 "PutObject",
833 "GetObject",
834 "DeleteObject",
835 "HeadObject",
836 "CopyObject",
837 "DeleteObjects",
838 "ListObjectsV2",
839 "ListObjects",
840 "ListObjectVersions",
841 "GetObjectAttributes",
842 "RestoreObject",
843 "PutObjectTagging",
845 "GetObjectTagging",
846 "DeleteObjectTagging",
847 "PutObjectAcl",
848 "GetObjectAcl",
849 "PutObjectRetention",
850 "GetObjectRetention",
851 "PutObjectLegalHold",
852 "GetObjectLegalHold",
853 "PutBucketTagging",
855 "GetBucketTagging",
856 "DeleteBucketTagging",
857 "PutBucketAcl",
858 "GetBucketAcl",
859 "PutBucketVersioning",
860 "GetBucketVersioning",
861 "PutBucketCors",
862 "GetBucketCors",
863 "DeleteBucketCors",
864 "PutBucketNotificationConfiguration",
865 "GetBucketNotificationConfiguration",
866 "PutBucketWebsite",
867 "GetBucketWebsite",
868 "DeleteBucketWebsite",
869 "PutBucketAccelerateConfiguration",
870 "GetBucketAccelerateConfiguration",
871 "PutPublicAccessBlock",
872 "GetPublicAccessBlock",
873 "DeletePublicAccessBlock",
874 "PutBucketEncryption",
875 "GetBucketEncryption",
876 "DeleteBucketEncryption",
877 "PutBucketLifecycleConfiguration",
878 "GetBucketLifecycleConfiguration",
879 "DeleteBucketLifecycle",
880 "PutBucketLogging",
881 "GetBucketLogging",
882 "PutBucketPolicy",
883 "GetBucketPolicy",
884 "DeleteBucketPolicy",
885 "PutObjectLockConfiguration",
886 "GetObjectLockConfiguration",
887 "PutBucketReplication",
888 "GetBucketReplication",
889 "DeleteBucketReplication",
890 "PutBucketOwnershipControls",
891 "GetBucketOwnershipControls",
892 "DeleteBucketOwnershipControls",
893 "PutBucketInventoryConfiguration",
894 "GetBucketInventoryConfiguration",
895 "DeleteBucketInventoryConfiguration",
896 "ListBucketInventoryConfigurations",
897 "PutBucketAnalyticsConfiguration",
898 "GetBucketAnalyticsConfiguration",
899 "DeleteBucketAnalyticsConfiguration",
900 "ListBucketAnalyticsConfigurations",
901 "PutBucketIntelligentTieringConfiguration",
902 "GetBucketIntelligentTieringConfiguration",
903 "DeleteBucketIntelligentTieringConfiguration",
904 "ListBucketIntelligentTieringConfigurations",
905 "PutBucketMetricsConfiguration",
906 "GetBucketMetricsConfiguration",
907 "DeleteBucketMetricsConfiguration",
908 "ListBucketMetricsConfigurations",
909 "PutBucketRequestPayment",
910 "GetBucketRequestPayment",
911 "PutBucketAbac",
912 "GetBucketAbac",
913 "GetBucketPolicyStatus",
914 "CreateBucketMetadataConfiguration",
915 "GetBucketMetadataConfiguration",
916 "DeleteBucketMetadataConfiguration",
917 "CreateBucketMetadataTableConfiguration",
918 "GetBucketMetadataTableConfiguration",
919 "DeleteBucketMetadataTableConfiguration",
920 "UpdateBucketMetadataInventoryTableConfiguration",
921 "UpdateBucketMetadataJournalTableConfiguration",
922 "GetObjectTorrent",
923 "RenameObject",
924 "SelectObjectContent",
925 "UpdateObjectEncryption",
926 "WriteGetObjectResponse",
927 "ListDirectoryBuckets",
928 "CreateSession",
929 "CreateMultipartUpload",
931 "UploadPart",
932 "UploadPartCopy",
933 "CompleteMultipartUpload",
934 "AbortMultipartUpload",
935 "ListParts",
936 "ListMultipartUploads",
937 ]
938 }
939
940 fn iam_enforceable(&self) -> bool {
941 true
942 }
943
944 fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
954 let bucket = request.path_segments.first().map(|s| s.as_str());
959 let key = if request.path_segments.len() > 1 {
960 Some(request.path_segments[1..].join("/"))
961 } else {
962 None
963 };
964 let action = s3_detect_action(
965 request.method.as_str(),
966 bucket,
967 key.as_deref(),
968 &request.query_params,
969 )?;
970 let resource = s3_resource_for(action, bucket, key.as_deref());
971 Some(fakecloud_core::auth::IamAction {
972 service: "s3",
973 action,
974 resource,
975 })
976 }
977
978 fn iam_condition_keys_for(
979 &self,
980 request: &AwsRequest,
981 action: &fakecloud_core::auth::IamAction,
982 ) -> std::collections::BTreeMap<String, Vec<String>> {
983 s3_condition_keys(action.action, &request.query_params)
984 }
985
986 fn resource_tags_for(
987 &self,
988 resource_arn: &str,
989 ) -> Option<std::collections::HashMap<String, String>> {
990 s3_resource_tags(&self.state, resource_arn)
991 }
992
993 fn request_tags_from(
994 &self,
995 request: &AwsRequest,
996 action: &str,
997 ) -> Option<std::collections::HashMap<String, String>> {
998 s3_request_tags(request, action)
999 }
1000}
1001
1002fn s3_condition_keys(
1009 action: &str,
1010 query: &std::collections::HashMap<String, String>,
1011) -> std::collections::BTreeMap<String, Vec<String>> {
1012 let mut out = std::collections::BTreeMap::new();
1013 if matches!(action, "ListObjects" | "ListObjectsV2") {
1014 if let Some(prefix) = query.get("prefix") {
1016 out.insert("s3:prefix".to_string(), vec![prefix.clone()]);
1017 }
1018 if let Some(delimiter) = query.get("delimiter") {
1019 out.insert("s3:delimiter".to_string(), vec![delimiter.clone()]);
1020 }
1021 if let Some(max_keys) = query.get("max-keys") {
1022 out.insert("s3:max-keys".to_string(), vec![max_keys.clone()]);
1023 }
1024 }
1025 out
1026}
1027
1028fn s3_resource_tags(
1034 state: &SharedS3State,
1035 resource_arn: &str,
1036) -> Option<std::collections::HashMap<String, String>> {
1037 if resource_arn == "*" {
1038 return Some(std::collections::HashMap::new());
1039 }
1040 let after_prefix = resource_arn.strip_prefix("arn:aws:s3:::")?;
1042 let mas = state.read();
1043 let bucket_name = after_prefix.split('/').next().unwrap_or(after_prefix);
1045 let state = mas
1046 .find_account(|s| s.buckets.contains_key(bucket_name))
1047 .and_then(|id| mas.get(id))
1048 .or_else(|| Some(mas.default_ref()))?;
1049 if let Some(slash_pos) = after_prefix.find('/') {
1050 let bucket_name = &after_prefix[..slash_pos];
1052 let key = &after_prefix[slash_pos + 1..];
1053 let bucket = state.buckets.get(bucket_name)?;
1054 if let Some(obj) = bucket.objects.get(key) {
1056 Some(obj.tags.clone())
1057 } else if let Some(versions) = bucket.object_versions.get(key) {
1058 versions.last().map(|v| v.tags.clone())
1059 } else {
1060 Some(std::collections::HashMap::new())
1062 }
1063 } else {
1064 let bucket = state.buckets.get(after_prefix)?;
1066 Some(bucket.tags.clone())
1067 }
1068}
1069
1070fn s3_request_tags(
1076 request: &AwsRequest,
1077 action: &str,
1078) -> Option<std::collections::HashMap<String, String>> {
1079 match action {
1080 "PutObject" | "CopyObject" | "CreateMultipartUpload" => {
1081 if let Some(tagging) = request.headers.get("x-amz-tagging") {
1083 let tags = parse_url_encoded_tags(tagging.to_str().unwrap_or(""));
1084 Some(tags.into_iter().collect())
1085 } else {
1086 Some(std::collections::HashMap::new())
1087 }
1088 }
1089 "PutBucketTagging" | "PutObjectTagging" => {
1090 let body = std::str::from_utf8(&request.body).unwrap_or("");
1092 let tags = parse_tagging_xml(body);
1093 Some(tags.into_iter().collect())
1094 }
1095 _ => Some(std::collections::HashMap::new()),
1096 }
1097}
1098
1099fn s3_detect_action(
1112 method: &str,
1113 bucket: Option<&str>,
1114 key: Option<&str>,
1115 query: &std::collections::HashMap<String, String>,
1116) -> Option<&'static str> {
1117 let has = |q: &str| query.contains_key(q);
1118 let is_get = method == "GET";
1119 let is_put = method == "PUT";
1120 let is_post = method == "POST";
1121 let is_delete = method == "DELETE";
1122
1123 if bucket.is_none() {
1125 return match method {
1126 "GET" => Some("ListBuckets"),
1127 _ => None,
1128 };
1129 }
1130 let has_key = key.is_some();
1131
1132 if has_key && is_post && has("uploads") {
1134 return Some("CreateMultipartUpload");
1135 }
1136 if has_key && is_post && has("uploadId") {
1137 return Some("CompleteMultipartUpload");
1138 }
1139 if has_key && is_put && has("partNumber") && has("uploadId") {
1140 return Some("UploadPart");
1141 }
1142 if has_key && is_delete && has("uploadId") {
1143 return Some("AbortMultipartUpload");
1144 }
1145 if has_key && is_get && has("uploadId") {
1146 return Some("ListParts");
1147 }
1148 if !has_key && is_get && has("uploads") {
1149 return Some("ListMultipartUploads");
1150 }
1151
1152 if has_key {
1156 if has("tagging") {
1157 return Some(match method {
1158 "GET" => "GetObjectTagging",
1159 "PUT" => "PutObjectTagging",
1160 "DELETE" => "DeleteObjectTagging",
1161 _ => return None,
1162 });
1163 }
1164 if has("acl") {
1165 return Some(match method {
1166 "GET" => "GetObjectAcl",
1167 "PUT" => "PutObjectAcl",
1168 _ => return None,
1169 });
1170 }
1171 if has("retention") {
1172 return Some(match method {
1173 "GET" => "GetObjectRetention",
1174 "PUT" => "PutObjectRetention",
1175 _ => return None,
1176 });
1177 }
1178 if has("legal-hold") {
1179 return Some(match method {
1180 "GET" => "GetObjectLegalHold",
1181 "PUT" => "PutObjectLegalHold",
1182 _ => return None,
1183 });
1184 }
1185 if has("attributes") && is_get {
1190 return Some("GetObjectAttributes");
1191 }
1192 if has("restore") && is_post {
1193 return Some("RestoreObject");
1194 }
1195 }
1196
1197 if !has_key {
1199 if has("tagging") {
1200 return Some(match method {
1201 "GET" => "GetBucketTagging",
1202 "PUT" => "PutBucketTagging",
1203 "DELETE" => "DeleteBucketTagging",
1204 _ => return None,
1205 });
1206 }
1207 if has("acl") {
1208 return Some(match method {
1209 "GET" => "GetBucketAcl",
1210 "PUT" => "PutBucketAcl",
1211 _ => return None,
1212 });
1213 }
1214 if has("versioning") {
1215 return Some(match method {
1216 "GET" => "GetBucketVersioning",
1217 "PUT" => "PutBucketVersioning",
1218 _ => return None,
1219 });
1220 }
1221 if has("cors") {
1222 return Some(match method {
1223 "GET" => "GetBucketCors",
1224 "PUT" => "PutBucketCors",
1225 "DELETE" => "DeleteBucketCors",
1226 _ => return None,
1227 });
1228 }
1229 if has("policy") {
1230 return Some(match method {
1231 "GET" => "GetBucketPolicy",
1232 "PUT" => "PutBucketPolicy",
1233 "DELETE" => "DeleteBucketPolicy",
1234 _ => return None,
1235 });
1236 }
1237 if has("website") {
1238 return Some(match method {
1239 "GET" => "GetBucketWebsite",
1240 "PUT" => "PutBucketWebsite",
1241 "DELETE" => "DeleteBucketWebsite",
1242 _ => return None,
1243 });
1244 }
1245 if has("lifecycle") {
1246 return Some(match method {
1247 "GET" => "GetBucketLifecycleConfiguration",
1248 "PUT" => "PutBucketLifecycleConfiguration",
1249 "DELETE" => "DeleteBucketLifecycle",
1250 _ => return None,
1251 });
1252 }
1253 if has("encryption") {
1254 return Some(match method {
1255 "GET" => "GetBucketEncryption",
1256 "PUT" => "PutBucketEncryption",
1257 "DELETE" => "DeleteBucketEncryption",
1258 _ => return None,
1259 });
1260 }
1261 if has("logging") {
1262 return Some(match method {
1263 "GET" => "GetBucketLogging",
1264 "PUT" => "PutBucketLogging",
1265 _ => return None,
1266 });
1267 }
1268 if has("notification") {
1269 return Some(match method {
1270 "GET" => "GetBucketNotificationConfiguration",
1271 "PUT" => "PutBucketNotificationConfiguration",
1272 _ => return None,
1273 });
1274 }
1275 if has("replication") {
1276 return Some(match method {
1277 "GET" => "GetBucketReplication",
1278 "PUT" => "PutBucketReplication",
1279 "DELETE" => "DeleteBucketReplication",
1280 _ => return None,
1281 });
1282 }
1283 if has("ownershipControls") {
1284 return Some(match method {
1285 "GET" => "GetBucketOwnershipControls",
1286 "PUT" => "PutBucketOwnershipControls",
1287 "DELETE" => "DeleteBucketOwnershipControls",
1288 _ => return None,
1289 });
1290 }
1291 if has("publicAccessBlock") {
1292 return Some(match method {
1293 "GET" => "GetPublicAccessBlock",
1294 "PUT" => "PutPublicAccessBlock",
1295 "DELETE" => "DeletePublicAccessBlock",
1296 _ => return None,
1297 });
1298 }
1299 if has("accelerate") {
1300 return Some(match method {
1301 "GET" => "GetBucketAccelerateConfiguration",
1302 "PUT" => "PutBucketAccelerateConfiguration",
1303 _ => return None,
1304 });
1305 }
1306 if has("inventory") {
1307 return Some(match method {
1308 "GET" => "GetBucketInventoryConfiguration",
1309 "PUT" => "PutBucketInventoryConfiguration",
1310 "DELETE" => "DeleteBucketInventoryConfiguration",
1311 _ => return None,
1312 });
1313 }
1314 if has("analytics") {
1315 return Some(match method {
1316 "GET" if has("id") => "GetBucketAnalyticsConfiguration",
1317 "GET" => "ListBucketAnalyticsConfigurations",
1318 "PUT" => "PutBucketAnalyticsConfiguration",
1319 "DELETE" => "DeleteBucketAnalyticsConfiguration",
1320 _ => return None,
1321 });
1322 }
1323 if has("intelligent-tiering") {
1324 return Some(match method {
1325 "GET" if has("id") => "GetBucketIntelligentTieringConfiguration",
1326 "GET" => "ListBucketIntelligentTieringConfigurations",
1327 "PUT" => "PutBucketIntelligentTieringConfiguration",
1328 "DELETE" => "DeleteBucketIntelligentTieringConfiguration",
1329 _ => return None,
1330 });
1331 }
1332 if has("metrics") {
1333 return Some(match method {
1334 "GET" if has("id") => "GetBucketMetricsConfiguration",
1335 "GET" => "ListBucketMetricsConfigurations",
1336 "PUT" => "PutBucketMetricsConfiguration",
1337 "DELETE" => "DeleteBucketMetricsConfiguration",
1338 _ => return None,
1339 });
1340 }
1341 if has("requestPayment") {
1342 return Some(match method {
1343 "GET" => "GetBucketRequestPayment",
1344 "PUT" => "PutBucketRequestPayment",
1345 _ => return None,
1346 });
1347 }
1348 if has("policyStatus") && is_get {
1349 return Some("GetBucketPolicyStatus");
1350 }
1351 if has("metadataConfiguration") {
1352 return Some(match method {
1353 "GET" => "GetBucketMetadataConfiguration",
1354 "POST" => "CreateBucketMetadataConfiguration",
1355 "DELETE" => "DeleteBucketMetadataConfiguration",
1356 _ => return None,
1357 });
1358 }
1359 if has("metadataTable") {
1360 return Some(match method {
1361 "GET" => "GetBucketMetadataTableConfiguration",
1362 "POST" => "CreateBucketMetadataTableConfiguration",
1363 "DELETE" => "DeleteBucketMetadataTableConfiguration",
1364 _ => return None,
1365 });
1366 }
1367 if has("metadataInventoryTable") && is_put {
1368 return Some("UpdateBucketMetadataInventoryTableConfiguration");
1369 }
1370 if has("metadataJournalTable") && is_put {
1371 return Some("UpdateBucketMetadataJournalTableConfiguration");
1372 }
1373 if has("abac") && is_put {
1374 return Some("PutBucketAbacConfiguration");
1375 }
1376 if has("renameObject") && is_put {
1377 return Some("RenameObject");
1378 }
1379 if has("object-lock") {
1380 return Some(match method {
1381 "GET" => "GetObjectLockConfiguration",
1382 "PUT" => "PutObjectLockConfiguration",
1383 _ => return None,
1384 });
1385 }
1386 if has("location") {
1387 return Some("GetBucketLocation");
1388 }
1389 if is_post && has("delete") {
1390 return Some("DeleteObjects");
1391 }
1392 if is_get && has("versions") {
1393 return Some("ListObjectVersions");
1394 }
1395 }
1396
1397 match (method, has_key) {
1399 ("GET", true) => Some("GetObject"),
1400 ("PUT", true) => {
1401 Some("PutObject")
1407 }
1408 ("DELETE", true) => Some("DeleteObject"),
1409 ("HEAD", true) => Some("HeadObject"),
1410 ("GET", false) => {
1411 if query.contains_key("list-type") {
1412 Some("ListObjectsV2")
1413 } else {
1414 Some("ListObjects")
1415 }
1416 }
1417 ("PUT", false) => Some("CreateBucket"),
1418 ("DELETE", false) => Some("DeleteBucket"),
1419 ("HEAD", false) => Some("HeadBucket"),
1420 _ => None,
1421 }
1422}
1423
1424#[allow(dead_code)]
1430const S3_SUPPORTED_ACTIONS: &[&str] = &[
1431 "ListBuckets",
1432 "CreateBucket",
1433 "DeleteBucket",
1434 "HeadBucket",
1435 "GetBucketLocation",
1436 "PutObject",
1437 "GetObject",
1438 "DeleteObject",
1439 "HeadObject",
1440 "CopyObject",
1441 "DeleteObjects",
1442 "ListObjectsV2",
1443 "ListObjects",
1444 "ListObjectVersions",
1445 "GetObjectAttributes",
1446 "RestoreObject",
1447 "PutObjectTagging",
1448 "GetObjectTagging",
1449 "DeleteObjectTagging",
1450 "PutObjectAcl",
1451 "GetObjectAcl",
1452 "PutObjectRetention",
1453 "GetObjectRetention",
1454 "PutObjectLegalHold",
1455 "GetObjectLegalHold",
1456 "PutBucketTagging",
1457 "GetBucketTagging",
1458 "DeleteBucketTagging",
1459 "PutBucketAcl",
1460 "GetBucketAcl",
1461 "PutBucketVersioning",
1462 "GetBucketVersioning",
1463 "PutBucketCors",
1464 "GetBucketCors",
1465 "DeleteBucketCors",
1466 "PutBucketNotificationConfiguration",
1467 "GetBucketNotificationConfiguration",
1468 "PutBucketWebsite",
1469 "GetBucketWebsite",
1470 "DeleteBucketWebsite",
1471 "PutBucketAccelerateConfiguration",
1472 "GetBucketAccelerateConfiguration",
1473 "PutPublicAccessBlock",
1474 "GetPublicAccessBlock",
1475 "DeletePublicAccessBlock",
1476 "PutBucketEncryption",
1477 "GetBucketEncryption",
1478 "DeleteBucketEncryption",
1479 "PutBucketLifecycleConfiguration",
1480 "GetBucketLifecycleConfiguration",
1481 "DeleteBucketLifecycle",
1482 "PutBucketLogging",
1483 "GetBucketLogging",
1484 "PutBucketPolicy",
1485 "GetBucketPolicy",
1486 "DeleteBucketPolicy",
1487 "PutObjectLockConfiguration",
1488 "GetObjectLockConfiguration",
1489 "PutBucketReplication",
1490 "GetBucketReplication",
1491 "DeleteBucketReplication",
1492 "PutBucketOwnershipControls",
1493 "GetBucketOwnershipControls",
1494 "DeleteBucketOwnershipControls",
1495 "PutBucketInventoryConfiguration",
1496 "GetBucketInventoryConfiguration",
1497 "DeleteBucketInventoryConfiguration",
1498 "CreateMultipartUpload",
1499 "UploadPart",
1500 "UploadPartCopy",
1501 "CompleteMultipartUpload",
1502 "AbortMultipartUpload",
1503 "ListParts",
1504 "ListMultipartUploads",
1505];
1506
1507fn s3_resource_for(action: &'static str, bucket: Option<&str>, key: Option<&str>) -> String {
1511 const OBJECT_ACTIONS: &[&str] = &[
1513 "PutObject",
1514 "GetObject",
1515 "DeleteObject",
1516 "HeadObject",
1517 "CopyObject",
1518 "GetObjectAttributes",
1519 "RestoreObject",
1520 "PutObjectTagging",
1521 "GetObjectTagging",
1522 "DeleteObjectTagging",
1523 "PutObjectAcl",
1524 "GetObjectAcl",
1525 "PutObjectRetention",
1526 "GetObjectRetention",
1527 "PutObjectLegalHold",
1528 "GetObjectLegalHold",
1529 "CreateMultipartUpload",
1530 "UploadPart",
1531 "UploadPartCopy",
1532 "CompleteMultipartUpload",
1533 "AbortMultipartUpload",
1534 "ListParts",
1535 ];
1536 if action == "ListBuckets" {
1537 return "*".to_string();
1538 }
1539 let Some(bucket) = bucket else {
1540 return "*".to_string();
1541 };
1542 if OBJECT_ACTIONS.contains(&action) {
1543 match key {
1544 Some(k) if !k.is_empty() => format!("arn:aws:s3:::{}/{}", bucket, k),
1545 _ => format!("arn:aws:s3:::{}/*", bucket),
1546 }
1547 } else {
1548 format!("arn:aws:s3:::{}", bucket)
1550 }
1551}
1552
1553pub(crate) fn truncate_to_seconds(dt: DateTime<Utc>) -> DateTime<Utc> {
1557 dt.with_nanosecond(0).unwrap_or(dt)
1558}
1559
1560pub(crate) fn check_get_conditionals(
1561 req: &AwsRequest,
1562 obj: &S3Object,
1563) -> Result<(), AwsServiceError> {
1564 let obj_etag = format!("\"{}\"", obj.etag);
1565 let obj_time = truncate_to_seconds(obj.last_modified);
1566
1567 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
1569 if !etag_matches(if_match, &obj_etag) {
1570 return Err(precondition_failed("If-Match"));
1571 }
1572 }
1573
1574 if let Some(if_none_match) = req
1576 .headers
1577 .get("if-none-match")
1578 .and_then(|v| v.to_str().ok())
1579 {
1580 if etag_matches(if_none_match, &obj_etag) {
1581 return Err(not_modified_with_etag(&obj_etag));
1582 }
1583 }
1584
1585 if let Some(since) = req
1587 .headers
1588 .get("if-unmodified-since")
1589 .and_then(|v| v.to_str().ok())
1590 {
1591 if let Some(dt) = parse_http_date(since) {
1592 if obj_time > dt {
1593 return Err(precondition_failed("If-Unmodified-Since"));
1594 }
1595 }
1596 }
1597
1598 if let Some(since) = req
1600 .headers
1601 .get("if-modified-since")
1602 .and_then(|v| v.to_str().ok())
1603 {
1604 if let Some(dt) = parse_http_date(since) {
1605 if obj_time <= dt {
1606 return Err(not_modified());
1607 }
1608 }
1609 }
1610
1611 Ok(())
1612}
1613
1614pub(crate) fn check_head_conditionals(
1615 req: &AwsRequest,
1616 obj: &S3Object,
1617) -> Result<(), AwsServiceError> {
1618 let obj_etag = format!("\"{}\"", obj.etag);
1619 let obj_time = truncate_to_seconds(obj.last_modified);
1620
1621 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
1623 if !etag_matches(if_match, &obj_etag) {
1624 return Err(AwsServiceError::aws_error(
1625 StatusCode::PRECONDITION_FAILED,
1626 "412",
1627 "Precondition Failed",
1628 ));
1629 }
1630 }
1631
1632 if let Some(if_none_match) = req
1634 .headers
1635 .get("if-none-match")
1636 .and_then(|v| v.to_str().ok())
1637 {
1638 if etag_matches(if_none_match, &obj_etag) {
1639 return Err(not_modified_with_etag(&obj_etag));
1640 }
1641 }
1642
1643 if let Some(since) = req
1645 .headers
1646 .get("if-unmodified-since")
1647 .and_then(|v| v.to_str().ok())
1648 {
1649 if let Some(dt) = parse_http_date(since) {
1650 if obj_time > dt {
1651 return Err(AwsServiceError::aws_error(
1652 StatusCode::PRECONDITION_FAILED,
1653 "412",
1654 "Precondition Failed",
1655 ));
1656 }
1657 }
1658 }
1659
1660 if let Some(since) = req
1662 .headers
1663 .get("if-modified-since")
1664 .and_then(|v| v.to_str().ok())
1665 {
1666 if let Some(dt) = parse_http_date(since) {
1667 if obj_time <= dt {
1668 return Err(not_modified());
1669 }
1670 }
1671 }
1672
1673 Ok(())
1674}
1675
1676pub(crate) fn etag_matches(condition: &str, obj_etag: &str) -> bool {
1677 let condition = condition.trim();
1678 if condition == "*" {
1679 return true;
1680 }
1681 let clean_etag = obj_etag.replace('"', "");
1682 for part in condition.split(',') {
1684 let part = part.trim().replace('"', "");
1685 if part == clean_etag {
1686 return true;
1687 }
1688 }
1689 false
1690}
1691
1692pub(crate) fn parse_http_date(s: &str) -> Option<DateTime<Utc>> {
1693 if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
1695 return Some(dt.with_timezone(&Utc));
1696 }
1697 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
1699 return Some(dt.with_timezone(&Utc));
1700 }
1701 if let Ok(dt) =
1703 chrono::NaiveDateTime::parse_from_str(s.trim_end_matches(" GMT"), "%a, %d %b %Y %H:%M:%S")
1704 {
1705 return Some(dt.and_utc());
1706 }
1707 if let Ok(dt) = s.parse::<DateTime<Utc>>() {
1709 return Some(dt);
1710 }
1711 None
1712}
1713
1714pub(crate) fn not_modified() -> AwsServiceError {
1715 AwsServiceError::aws_error(StatusCode::NOT_MODIFIED, "304", "Not Modified")
1716}
1717
1718pub(crate) fn not_modified_with_etag(etag: &str) -> AwsServiceError {
1719 AwsServiceError::aws_error_with_headers(
1720 StatusCode::NOT_MODIFIED,
1721 "304",
1722 "Not Modified",
1723 vec![("etag".to_string(), etag.to_string())],
1724 )
1725}
1726
1727pub(crate) fn precondition_failed(condition: &str) -> AwsServiceError {
1728 AwsServiceError::aws_error_with_fields(
1729 StatusCode::PRECONDITION_FAILED,
1730 "PreconditionFailed",
1731 "At least one of the pre-conditions you specified did not hold",
1732 vec![("Condition".to_string(), condition.to_string())],
1733 )
1734}
1735
1736pub(crate) fn build_acl_xml(owner_id: &str, grants: &[AclGrant], _account_id: &str) -> String {
1739 let mut grants_xml = String::new();
1740 for g in grants {
1741 let grantee_xml = if g.grantee_type == "Group" {
1742 let uri = g.grantee_uri.as_deref().unwrap_or("");
1743 format!(
1744 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\">\
1745 <URI>{}</URI></Grantee>",
1746 xml_escape(uri),
1747 )
1748 } else {
1749 let id = g.grantee_id.as_deref().unwrap_or("");
1750 format!(
1751 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">\
1752 <ID>{}</ID></Grantee>",
1753 xml_escape(id),
1754 )
1755 };
1756 grants_xml.push_str(&format!(
1757 "<Grant>{grantee_xml}<Permission>{}</Permission></Grant>",
1758 xml_escape(&g.permission),
1759 ));
1760 }
1761
1762 format!(
1763 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
1764 <AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
1765 <Owner><ID>{owner_id}</ID><DisplayName>{owner_id}</DisplayName></Owner>\
1766 <AccessControlList>{grants_xml}</AccessControlList>\
1767 </AccessControlPolicy>",
1768 owner_id = xml_escape(owner_id),
1769 )
1770}
1771
1772pub(crate) fn canned_acl_grants(acl: &str, owner_id: &str) -> Vec<AclGrant> {
1773 let owner_grant = AclGrant {
1774 grantee_type: "CanonicalUser".to_string(),
1775 grantee_id: Some(owner_id.to_string()),
1776 grantee_display_name: Some(owner_id.to_string()),
1777 grantee_uri: None,
1778 permission: "FULL_CONTROL".to_string(),
1779 };
1780 match acl {
1781 "private" => vec![owner_grant],
1782 "public-read" => vec![
1783 owner_grant,
1784 AclGrant {
1785 grantee_type: "Group".to_string(),
1786 grantee_id: None,
1787 grantee_display_name: None,
1788 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1789 permission: "READ".to_string(),
1790 },
1791 ],
1792 "public-read-write" => vec![
1793 owner_grant,
1794 AclGrant {
1795 grantee_type: "Group".to_string(),
1796 grantee_id: None,
1797 grantee_display_name: None,
1798 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1799 permission: "READ".to_string(),
1800 },
1801 AclGrant {
1802 grantee_type: "Group".to_string(),
1803 grantee_id: None,
1804 grantee_display_name: None,
1805 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1806 permission: "WRITE".to_string(),
1807 },
1808 ],
1809 "authenticated-read" => vec![
1810 owner_grant,
1811 AclGrant {
1812 grantee_type: "Group".to_string(),
1813 grantee_id: None,
1814 grantee_display_name: None,
1815 grantee_uri: Some(
1816 "http://acs.amazonaws.com/groups/global/AuthenticatedUsers".to_string(),
1817 ),
1818 permission: "READ".to_string(),
1819 },
1820 ],
1821 "bucket-owner-full-control" => vec![owner_grant],
1822 _ => vec![owner_grant],
1823 }
1824}
1825
1826pub(crate) fn canned_acl_grants_for_object(acl: &str, owner_id: &str) -> Vec<AclGrant> {
1827 canned_acl_grants(acl, owner_id)
1829}
1830
1831pub(crate) fn parse_grant_headers(headers: &HeaderMap) -> Vec<AclGrant> {
1832 let mut grants = Vec::new();
1833 let header_permission_map = [
1834 ("x-amz-grant-read", "READ"),
1835 ("x-amz-grant-write", "WRITE"),
1836 ("x-amz-grant-read-acp", "READ_ACP"),
1837 ("x-amz-grant-write-acp", "WRITE_ACP"),
1838 ("x-amz-grant-full-control", "FULL_CONTROL"),
1839 ];
1840
1841 for (header, permission) in &header_permission_map {
1842 if let Some(value) = headers.get(*header).and_then(|v| v.to_str().ok()) {
1843 for part in value.split(',') {
1845 let part = part.trim();
1846 if let Some((key, val)) = part.split_once('=') {
1847 let val = val.trim().trim_matches('"');
1848 let key = key.trim().to_lowercase();
1849 match key.as_str() {
1850 "id" => {
1851 grants.push(AclGrant {
1852 grantee_type: "CanonicalUser".to_string(),
1853 grantee_id: Some(val.to_string()),
1854 grantee_display_name: Some(val.to_string()),
1855 grantee_uri: None,
1856 permission: permission.to_string(),
1857 });
1858 }
1859 "uri" | "url" => {
1860 grants.push(AclGrant {
1861 grantee_type: "Group".to_string(),
1862 grantee_id: None,
1863 grantee_display_name: None,
1864 grantee_uri: Some(val.to_string()),
1865 permission: permission.to_string(),
1866 });
1867 }
1868 _ => {}
1869 }
1870 }
1871 }
1872 }
1873 }
1874 grants
1875}
1876
1877pub(crate) fn parse_acl_xml(xml: &str) -> Result<Vec<AclGrant>, AwsServiceError> {
1878 if xml.contains("<AccessControlPolicy") && !xml.contains("<Owner>") {
1880 return Err(AwsServiceError::aws_error(
1881 StatusCode::BAD_REQUEST,
1882 "MalformedACLError",
1883 "The XML you provided was not well-formed or did not validate against our published schema",
1884 ));
1885 }
1886
1887 let valid_permissions = ["READ", "WRITE", "READ_ACP", "WRITE_ACP", "FULL_CONTROL"];
1888
1889 let mut grants = Vec::new();
1890 let mut remaining = xml;
1891 while let Some(start) = remaining.find("<Grant>") {
1892 let after = &remaining[start + 7..];
1893 if let Some(end) = after.find("</Grant>") {
1894 let grant_body = &after[..end];
1895
1896 let permission = extract_xml_value(grant_body, "Permission").unwrap_or_default();
1898 if !valid_permissions.contains(&permission.as_str()) {
1899 return Err(AwsServiceError::aws_error(
1900 StatusCode::BAD_REQUEST,
1901 "MalformedACLError",
1902 "The XML you provided was not well-formed or did not validate against our published schema",
1903 ));
1904 }
1905
1906 if grant_body.contains("xsi:type=\"Group\"") || grant_body.contains("<URI>") {
1908 let uri = extract_xml_value(grant_body, "URI").unwrap_or_default();
1909 grants.push(AclGrant {
1910 grantee_type: "Group".to_string(),
1911 grantee_id: None,
1912 grantee_display_name: None,
1913 grantee_uri: Some(uri),
1914 permission,
1915 });
1916 } else {
1917 let id = extract_xml_value(grant_body, "ID").unwrap_or_default();
1918 let display =
1919 extract_xml_value(grant_body, "DisplayName").unwrap_or_else(|| id.clone());
1920 grants.push(AclGrant {
1921 grantee_type: "CanonicalUser".to_string(),
1922 grantee_id: Some(id),
1923 grantee_display_name: Some(display),
1924 grantee_uri: None,
1925 permission,
1926 });
1927 }
1928
1929 remaining = &after[end + 8..];
1930 } else {
1931 break;
1932 }
1933 }
1934 Ok(grants)
1935}
1936
1937pub(crate) enum RangeResult {
1940 Satisfiable { start: usize, end: usize },
1941 NotSatisfiable,
1942 Ignored,
1943}
1944
1945pub(crate) fn parse_range_header(range_str: &str, total_size: usize) -> Option<RangeResult> {
1946 let range_str = range_str.strip_prefix("bytes=")?;
1947 let (start_str, end_str) = range_str.split_once('-')?;
1948 if start_str.is_empty() {
1949 let suffix_len: usize = end_str.parse().ok()?;
1950 if suffix_len == 0 || total_size == 0 {
1951 return Some(RangeResult::NotSatisfiable);
1952 }
1953 let start = total_size.saturating_sub(suffix_len);
1954 Some(RangeResult::Satisfiable {
1955 start,
1956 end: total_size - 1,
1957 })
1958 } else {
1959 let start: usize = start_str.parse().ok()?;
1960 if start >= total_size {
1961 return Some(RangeResult::NotSatisfiable);
1962 }
1963 let end = if end_str.is_empty() {
1964 total_size - 1
1965 } else {
1966 let e: usize = end_str.parse().ok()?;
1967 if e < start {
1968 return Some(RangeResult::Ignored);
1969 }
1970 std::cmp::min(e, total_size - 1)
1971 };
1972 Some(RangeResult::Satisfiable { start, end })
1973 }
1974}
1975
1976pub(crate) fn s3_xml(status: StatusCode, body: impl Into<Bytes>) -> AwsResponse {
1980 AwsResponse {
1981 status,
1982 content_type: "application/xml".to_string(),
1983 body: body.into().into(),
1984 headers: HeaderMap::new(),
1985 }
1986}
1987
1988pub(crate) fn empty_response(status: StatusCode) -> AwsResponse {
1989 AwsResponse {
1990 status,
1991 content_type: "application/xml".to_string(),
1992 body: Bytes::new().into(),
1993 headers: HeaderMap::new(),
1994 }
1995}
1996
1997pub(crate) fn is_frozen(obj: &S3Object) -> bool {
2000 matches!(obj.storage_class.as_str(), "GLACIER" | "DEEP_ARCHIVE")
2001 && obj.restore_ongoing != Some(false)
2002}
2003
2004pub(crate) fn no_such_bucket(bucket: &str) -> AwsServiceError {
2005 AwsServiceError::aws_error_with_fields(
2006 StatusCode::NOT_FOUND,
2007 "NoSuchBucket",
2008 "The specified bucket does not exist",
2009 vec![("BucketName".to_string(), bucket.to_string())],
2010 )
2011}
2012
2013pub(crate) fn no_such_key(key: &str) -> AwsServiceError {
2014 AwsServiceError::aws_error_with_fields(
2015 StatusCode::NOT_FOUND,
2016 "NoSuchKey",
2017 "The specified key does not exist.",
2018 vec![("Key".to_string(), key.to_string())],
2019 )
2020}
2021
2022pub(crate) fn no_such_upload(upload_id: &str) -> AwsServiceError {
2023 AwsServiceError::aws_error_with_fields(
2024 StatusCode::NOT_FOUND,
2025 "NoSuchUpload",
2026 "The specified upload does not exist. The upload ID may be invalid, \
2027 or the upload may have been aborted or completed.",
2028 vec![("UploadId".to_string(), upload_id.to_string())],
2029 )
2030}
2031
2032pub(crate) fn no_such_key_with_detail(key: &str) -> AwsServiceError {
2033 AwsServiceError::aws_error_with_fields(
2034 StatusCode::NOT_FOUND,
2035 "NoSuchKey",
2036 "The specified key does not exist.",
2037 vec![("Key".to_string(), key.to_string())],
2038 )
2039}
2040
2041pub(crate) fn compute_md5(data: &[u8]) -> String {
2042 let digest = Md5::digest(data);
2043 format!("{:x}", digest)
2044}
2045
2046pub(crate) fn compute_checksum(algorithm: &str, data: &[u8]) -> String {
2047 match algorithm {
2048 "CRC32" => {
2049 let crc = crc32fast::hash(data);
2050 BASE64.encode(crc.to_be_bytes())
2051 }
2052 "SHA1" => {
2053 use sha1::Digest as _;
2054 let hash = sha1::Sha1::digest(data);
2055 BASE64.encode(hash)
2056 }
2057 "SHA256" => {
2058 use sha2::Digest as _;
2059 let hash = sha2::Sha256::digest(data);
2060 BASE64.encode(hash)
2061 }
2062 _ => String::new(),
2063 }
2064}
2065
2066pub(crate) async fn compute_checksum_streaming(
2072 algorithm: &str,
2073 path: &std::path::Path,
2074) -> Result<String, std::io::Error> {
2075 use tokio::io::AsyncReadExt;
2076 let mut file = tokio::fs::File::open(path).await?;
2077 let mut buf = vec![0u8; 1024 * 1024];
2078 match algorithm {
2079 "CRC32" => {
2080 let mut hasher = crc32fast::Hasher::new();
2081 loop {
2082 let n = file.read(&mut buf).await?;
2083 if n == 0 {
2084 break;
2085 }
2086 hasher.update(&buf[..n]);
2087 }
2088 Ok(BASE64.encode(hasher.finalize().to_be_bytes()))
2089 }
2090 "SHA1" => {
2091 use sha1::Digest as _;
2092 let mut hasher = sha1::Sha1::new();
2093 loop {
2094 let n = file.read(&mut buf).await?;
2095 if n == 0 {
2096 break;
2097 }
2098 hasher.update(&buf[..n]);
2099 }
2100 Ok(BASE64.encode(hasher.finalize()))
2101 }
2102 "SHA256" => {
2103 use sha2::Digest as _;
2104 let mut hasher = sha2::Sha256::new();
2105 loop {
2106 let n = file.read(&mut buf).await?;
2107 if n == 0 {
2108 break;
2109 }
2110 hasher.update(&buf[..n]);
2111 }
2112 Ok(BASE64.encode(hasher.finalize()))
2113 }
2114 _ => Ok(String::new()),
2115 }
2116}
2117
2118pub(crate) fn url_encode_s3_key(s: &str) -> String {
2119 let mut out = String::with_capacity(s.len() * 2);
2120 for byte in s.bytes() {
2121 match byte {
2122 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
2123 out.push(byte as char);
2124 }
2125 _ => {
2126 out.push_str(&format!("%{:02X}", byte));
2127 }
2128 }
2129 }
2130 out
2131}
2132
2133pub(crate) use fakecloud_aws::xml::xml_escape;
2134
2135pub(crate) fn extract_user_metadata(
2136 headers: &HeaderMap,
2137) -> std::collections::HashMap<String, String> {
2138 let mut meta = std::collections::HashMap::new();
2139 for (name, value) in headers {
2140 if let Some(key) = name.as_str().strip_prefix("x-amz-meta-") {
2141 if let Ok(v) = value.to_str() {
2142 meta.insert(key.to_string(), v.to_string());
2143 }
2144 }
2145 }
2146 meta
2147}
2148
2149pub(crate) fn is_valid_storage_class(class: &str) -> bool {
2150 matches!(
2151 class,
2152 "STANDARD"
2153 | "REDUCED_REDUNDANCY"
2154 | "STANDARD_IA"
2155 | "ONEZONE_IA"
2156 | "INTELLIGENT_TIERING"
2157 | "GLACIER"
2158 | "DEEP_ARCHIVE"
2159 | "GLACIER_IR"
2160 | "OUTPOSTS"
2161 | "SNOW"
2162 | "EXPRESS_ONEZONE"
2163 )
2164}
2165
2166pub(crate) fn is_valid_bucket_name(name: &str) -> bool {
2167 if name.len() < 3 || name.len() > 63 {
2168 return false;
2169 }
2170 let bytes = name.as_bytes();
2172 if !bytes[0].is_ascii_alphanumeric() || !bytes[bytes.len() - 1].is_ascii_alphanumeric() {
2173 return false;
2174 }
2175 name.chars()
2177 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.' || c == '_')
2178}
2179
2180pub(crate) fn is_valid_region(region: &str) -> bool {
2181 let valid_regions = [
2183 "us-east-1",
2184 "us-east-2",
2185 "us-west-1",
2186 "us-west-2",
2187 "af-south-1",
2188 "ap-east-1",
2189 "ap-south-1",
2190 "ap-south-2",
2191 "ap-southeast-1",
2192 "ap-southeast-2",
2193 "ap-southeast-3",
2194 "ap-southeast-4",
2195 "ap-northeast-1",
2196 "ap-northeast-2",
2197 "ap-northeast-3",
2198 "ca-central-1",
2199 "ca-west-1",
2200 "eu-central-1",
2201 "eu-central-2",
2202 "eu-west-1",
2203 "eu-west-2",
2204 "eu-west-3",
2205 "eu-south-1",
2206 "eu-south-2",
2207 "eu-north-1",
2208 "il-central-1",
2209 "me-south-1",
2210 "me-central-1",
2211 "sa-east-1",
2212 "cn-north-1",
2213 "cn-northwest-1",
2214 "us-gov-east-1",
2215 "us-gov-east-2",
2216 "us-gov-west-1",
2217 "us-iso-east-1",
2218 "us-iso-west-1",
2219 "us-isob-east-1",
2220 "us-isof-south-1",
2221 ];
2222 valid_regions.contains(®ion)
2223}
2224
2225pub(crate) fn resolve_object<'a>(
2226 b: &'a S3Bucket,
2227 key: &str,
2228 version_id: Option<&String>,
2229) -> Result<&'a S3Object, AwsServiceError> {
2230 if let Some(vid) = version_id {
2231 if vid == "null" {
2233 if let Some(versions) = b.object_versions.get(key) {
2235 if let Some(obj) = versions
2236 .iter()
2237 .find(|o| o.version_id.is_none() || o.version_id.as_deref() == Some("null"))
2238 {
2239 return Ok(obj);
2240 }
2241 }
2242 if let Some(obj) = b.objects.get(key) {
2244 if obj.version_id.is_none() || obj.version_id.as_deref() == Some("null") {
2245 return Ok(obj);
2246 }
2247 }
2248 } else {
2249 if let Some(versions) = b.object_versions.get(key) {
2251 if let Some(obj) = versions
2252 .iter()
2253 .find(|o| o.version_id.as_deref() == Some(vid.as_str()))
2254 {
2255 return Ok(obj);
2256 }
2257 }
2258 if let Some(obj) = b.objects.get(key) {
2260 if obj.version_id.as_deref() == Some(vid.as_str()) {
2261 return Ok(obj);
2262 }
2263 }
2264 }
2265 if b.versioning.is_some() {
2267 Err(AwsServiceError::aws_error_with_fields(
2268 StatusCode::NOT_FOUND,
2269 "NoSuchVersion",
2270 "The specified version does not exist.",
2271 vec![
2272 ("Key".to_string(), key.to_string()),
2273 ("VersionId".to_string(), vid.to_string()),
2274 ],
2275 ))
2276 } else {
2277 Err(AwsServiceError::aws_error(
2278 StatusCode::BAD_REQUEST,
2279 "InvalidArgument",
2280 "Invalid version id specified",
2281 ))
2282 }
2283 } else {
2284 b.objects.get(key).ok_or_else(|| no_such_key(key))
2285 }
2286}
2287
2288pub(crate) fn make_delete_marker(key: &str, dm_id: &str) -> S3Object {
2289 S3Object {
2290 key: key.to_string(),
2291 last_modified: Utc::now(),
2292 storage_class: "STANDARD".to_string(),
2293 version_id: Some(dm_id.to_string()),
2294 is_delete_marker: true,
2295 ..Default::default()
2296 }
2297}
2298
2299pub(crate) struct DeleteObjectEntry {
2301 key: String,
2302 version_id: Option<String>,
2303}
2304
2305pub(crate) fn parse_delete_objects_xml(xml: &str) -> Vec<DeleteObjectEntry> {
2306 let mut entries = Vec::new();
2307 let mut remaining = xml;
2308 while let Some(obj_start) = remaining.find("<Object>") {
2309 let after = &remaining[obj_start + 8..];
2310 if let Some(obj_end) = after.find("</Object>") {
2311 let obj_body = &after[..obj_end];
2312 let key = extract_xml_value(obj_body, "Key");
2313 let version_id = extract_xml_value(obj_body, "VersionId");
2314 if let Some(k) = key {
2315 entries.push(DeleteObjectEntry { key: k, version_id });
2316 }
2317 remaining = &after[obj_end + 9..];
2318 } else {
2319 break;
2320 }
2321 }
2322 entries
2323}
2324
2325pub(crate) fn parse_tagging_xml(xml: &str) -> Vec<(String, String)> {
2328 let mut tags = Vec::new();
2329 let mut remaining = xml;
2330 while let Some(tag_start) = remaining.find("<Tag>") {
2331 let after = &remaining[tag_start + 5..];
2332 if let Some(tag_end) = after.find("</Tag>") {
2333 let tag_body = &after[..tag_end];
2334 let key = extract_xml_value(tag_body, "Key");
2335 let value = extract_xml_value(tag_body, "Value");
2336 if let (Some(k), Some(v)) = (key, value) {
2337 tags.push((k, v));
2338 }
2339 remaining = &after[tag_end + 6..];
2340 } else {
2341 break;
2342 }
2343 }
2344 tags
2345}
2346
2347pub(crate) fn validate_tags(tags: &[(String, String)]) -> Result<(), AwsServiceError> {
2348 let mut seen = std::collections::HashSet::new();
2350 for (k, _) in tags {
2351 if !seen.insert(k.as_str()) {
2352 return Err(AwsServiceError::aws_error(
2353 StatusCode::BAD_REQUEST,
2354 "InvalidTag",
2355 "Cannot provide multiple Tags with the same key",
2356 ));
2357 }
2358 if k.starts_with("aws:") {
2360 return Err(AwsServiceError::aws_error(
2361 StatusCode::BAD_REQUEST,
2362 "InvalidTag",
2363 "System tags cannot be added/updated by requester",
2364 ));
2365 }
2366 }
2367 Ok(())
2368}
2369
2370pub(crate) fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
2371 let self_closing1 = format!("<{tag} />");
2373 let self_closing2 = format!("<{tag}/>");
2374 if xml.contains(&self_closing1) || xml.contains(&self_closing2) {
2375 let self_pos = xml
2377 .find(&self_closing1)
2378 .or_else(|| xml.find(&self_closing2));
2379 let open = format!("<{tag}>");
2380 let open_pos = xml.find(&open);
2381 match (self_pos, open_pos) {
2382 (Some(sp), Some(op)) if sp < op => return Some(String::new()),
2383 (Some(_), None) => return Some(String::new()),
2384 _ => {}
2385 }
2386 }
2387
2388 let open = format!("<{tag}>");
2389 let close = format!("</{tag}>");
2390 let start = xml.find(&open)? + open.len();
2391 let end = xml.find(&close)?;
2392 Some(xml[start..end].to_string())
2393}
2394
2395pub(crate) fn parse_complete_multipart_xml(xml: &str) -> Vec<(u32, String)> {
2397 let mut parts = Vec::new();
2398 let mut remaining = xml;
2399 while let Some(part_start) = remaining.find("<Part>") {
2400 let after = &remaining[part_start + 6..];
2401 if let Some(part_end) = after.find("</Part>") {
2402 let part_body = &after[..part_end];
2403 let part_num =
2404 extract_xml_value(part_body, "PartNumber").and_then(|s| s.parse::<u32>().ok());
2405 let etag = extract_xml_value(part_body, "ETag")
2406 .map(|s| s.replace(""", "").replace('"', ""));
2407 if let (Some(num), Some(e)) = (part_num, etag) {
2408 parts.push((num, e));
2409 }
2410 remaining = &after[part_end + 7..];
2411 } else {
2412 break;
2413 }
2414 }
2415 parts
2416}
2417
2418pub(crate) fn parse_url_encoded_tags(s: &str) -> Vec<(String, String)> {
2419 let mut tags = Vec::new();
2420 for pair in s.split('&') {
2421 if pair.is_empty() {
2422 continue;
2423 }
2424 let (key, value) = match pair.find('=') {
2425 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
2426 None => (pair, ""),
2427 };
2428 tags.push((
2429 percent_encoding::percent_decode_str(key)
2430 .decode_utf8_lossy()
2431 .to_string(),
2432 percent_encoding::percent_decode_str(value)
2433 .decode_utf8_lossy()
2434 .to_string(),
2435 ));
2436 }
2437 tags
2438}
2439
2440pub(crate) fn validate_lifecycle_xml(xml: &str) -> Result<(), AwsServiceError> {
2442 let malformed = || {
2443 AwsServiceError::aws_error(
2444 StatusCode::BAD_REQUEST,
2445 "MalformedXML",
2446 "The XML you provided was not well-formed or did not validate against our published schema",
2447 )
2448 };
2449
2450 let mut remaining = xml;
2451 while let Some(rule_start) = remaining.find("<Rule>") {
2452 let after = &remaining[rule_start + 6..];
2453 if let Some(rule_end) = after.find("</Rule>") {
2454 let rule_body = &after[..rule_end];
2455
2456 let has_filter = rule_body.contains("<Filter>")
2458 || rule_body.contains("<Filter/>")
2459 || rule_body.contains("<Filter />");
2460
2461 let has_prefix_outside_filter = {
2463 if !rule_body.contains("<Prefix") {
2464 false
2465 } else if !has_filter {
2466 true } else {
2468 let mut stripped = rule_body.to_string();
2470 if let Some(fs) = stripped.find("<Filter") {
2472 if let Some(fe) = stripped.find("</Filter>") {
2473 stripped = format!("{}{}", &stripped[..fs], &stripped[fe + 9..]);
2474 }
2475 }
2476 stripped.contains("<Prefix")
2477 }
2478 };
2479
2480 if !has_filter && !has_prefix_outside_filter {
2481 return Err(malformed());
2482 }
2483 if has_filter && has_prefix_outside_filter {
2485 return Err(malformed());
2486 }
2487
2488 if let Some(exp_start) = rule_body.find("<Expiration>") {
2491 if let Some(exp_end) = rule_body[exp_start..].find("</Expiration>") {
2492 let exp_body = &rule_body[exp_start..exp_start + exp_end];
2493 if exp_body.contains("<ExpiredObjectDeleteMarker>")
2494 && (exp_body.contains("<Days>") || exp_body.contains("<Date>"))
2495 {
2496 return Err(malformed());
2497 }
2498 }
2499 }
2500
2501 if has_filter {
2503 if let Some(fs) = rule_body.find("<Filter>") {
2504 if let Some(fe) = rule_body.find("</Filter>") {
2505 let filter_body = &rule_body[fs + 8..fe];
2506 let has_prefix_in_filter = filter_body.contains("<Prefix");
2507 let has_tag_in_filter = filter_body.contains("<Tag>");
2508 let has_and_in_filter = filter_body.contains("<And>");
2509 if has_prefix_in_filter && has_tag_in_filter && !has_and_in_filter {
2511 return Err(malformed());
2512 }
2513 if has_tag_in_filter && has_and_in_filter {
2515 let and_start = filter_body.find("<And>").unwrap_or(0);
2517 let tag_pos = filter_body.find("<Tag>").unwrap_or(0);
2518 if tag_pos < and_start {
2519 return Err(malformed());
2520 }
2521 }
2522 }
2523 }
2524 }
2525
2526 if rule_body.contains("<NoncurrentVersionTransition>") {
2528 let mut nvt_remaining = rule_body;
2529 while let Some(nvt_start) = nvt_remaining.find("<NoncurrentVersionTransition>") {
2530 let nvt_after = &nvt_remaining[nvt_start + 29..];
2531 if let Some(nvt_end) = nvt_after.find("</NoncurrentVersionTransition>") {
2532 let nvt_body = &nvt_after[..nvt_end];
2533 if !nvt_body.contains("<NoncurrentDays>") {
2534 return Err(malformed());
2535 }
2536 if !nvt_body.contains("<StorageClass>") {
2537 return Err(malformed());
2538 }
2539 nvt_remaining = &nvt_after[nvt_end + 30..];
2540 } else {
2541 break;
2542 }
2543 }
2544 }
2545
2546 remaining = &after[rule_end + 7..];
2547 } else {
2548 break;
2549 }
2550 }
2551
2552 Ok(())
2553}
2554
2555pub(crate) struct CorsRule {
2557 allowed_origins: Vec<String>,
2558 allowed_methods: Vec<String>,
2559 allowed_headers: Vec<String>,
2560 expose_headers: Vec<String>,
2561 max_age_seconds: Option<u32>,
2562}
2563
2564pub(crate) fn parse_cors_config(xml: &str) -> Vec<CorsRule> {
2566 let mut rules = Vec::new();
2567 let mut remaining = xml;
2568 while let Some(start) = remaining.find("<CORSRule>") {
2569 let after = &remaining[start + 10..];
2570 if let Some(end) = after.find("</CORSRule>") {
2571 let block = &after[..end];
2572 let allowed_origins = extract_all_xml_values(block, "AllowedOrigin");
2573 let allowed_methods = extract_all_xml_values(block, "AllowedMethod");
2574 let allowed_headers = extract_all_xml_values(block, "AllowedHeader");
2575 let expose_headers = extract_all_xml_values(block, "ExposeHeader");
2576 let max_age_seconds =
2577 extract_xml_value(block, "MaxAgeSeconds").and_then(|s| s.parse().ok());
2578 rules.push(CorsRule {
2579 allowed_origins,
2580 allowed_methods,
2581 allowed_headers,
2582 expose_headers,
2583 max_age_seconds,
2584 });
2585 remaining = &after[end + 11..];
2586 } else {
2587 break;
2588 }
2589 }
2590 rules
2591}
2592
2593pub(crate) fn origin_matches(origin: &str, pattern: &str) -> bool {
2595 if pattern == "*" {
2596 return true;
2597 }
2598 if let Some(suffix) = pattern.strip_prefix('*') {
2600 return origin.ends_with(suffix);
2601 }
2602 origin == pattern
2603}
2604
2605pub(crate) fn find_cors_rule<'a>(
2607 rules: &'a [CorsRule],
2608 origin: &str,
2609 method: Option<&str>,
2610) -> Option<&'a CorsRule> {
2611 rules.iter().find(|rule| {
2612 let origin_ok = rule
2613 .allowed_origins
2614 .iter()
2615 .any(|o| origin_matches(origin, o));
2616 let method_ok = match method {
2617 Some(m) => rule.allowed_methods.iter().any(|am| am == m),
2618 None => true,
2619 };
2620 origin_ok && method_ok
2621 })
2622}
2623
2624pub(crate) fn check_object_lock_for_overwrite(
2627 obj: &S3Object,
2628 req: &AwsRequest,
2629) -> Option<&'static str> {
2630 if obj.lock_legal_hold.as_deref() == Some("ON") {
2632 return Some("AccessDenied");
2633 }
2634 if let (Some(mode), Some(until)) = (&obj.lock_mode, &obj.lock_retain_until) {
2636 if *until > Utc::now() {
2637 if mode == "COMPLIANCE" {
2638 return Some("AccessDenied");
2639 }
2640 if mode == "GOVERNANCE" {
2641 let bypass = req
2642 .headers
2643 .get("x-amz-bypass-governance-retention")
2644 .and_then(|v| v.to_str().ok())
2645 .map(|s| s.eq_ignore_ascii_case("true"))
2646 .unwrap_or(false);
2647 if !bypass {
2648 return Some("AccessDenied");
2649 }
2650 }
2651 }
2652 }
2653 None
2654}
2655
2656#[cfg(test)]
2657mod tests {
2658 use super::*;
2659
2660 #[test]
2661 fn s3_condition_keys_emits_list_params() {
2662 let mut q = std::collections::HashMap::new();
2663 q.insert("prefix".to_string(), "logs/".to_string());
2664 q.insert("delimiter".to_string(), "/".to_string());
2665 q.insert("max-keys".to_string(), "100".to_string());
2666 let keys = s3_condition_keys("ListObjectsV2", &q);
2667 assert_eq!(keys.get("s3:prefix"), Some(&vec!["logs/".to_string()]));
2668 assert_eq!(keys.get("s3:delimiter"), Some(&vec!["/".to_string()]));
2669 assert_eq!(keys.get("s3:max-keys"), Some(&vec!["100".to_string()]));
2670 }
2671
2672 #[test]
2673 fn s3_condition_keys_omits_absent_params() {
2674 let q = std::collections::HashMap::new();
2675 let keys = s3_condition_keys("ListObjectsV2", &q);
2676 assert!(keys.is_empty());
2677 }
2678
2679 #[test]
2680 fn s3_condition_keys_partial_params() {
2681 let mut q = std::collections::HashMap::new();
2682 q.insert("prefix".to_string(), "archive/".to_string());
2683 let keys = s3_condition_keys("ListObjects", &q);
2684 assert_eq!(keys.len(), 1);
2685 assert_eq!(keys.get("s3:prefix"), Some(&vec!["archive/".to_string()]));
2686 }
2687
2688 #[test]
2689 fn s3_condition_keys_empty_for_non_list_actions() {
2690 let mut q = std::collections::HashMap::new();
2691 q.insert("prefix".to_string(), "logs/".to_string());
2692 assert!(s3_condition_keys("GetObject", &q).is_empty());
2693 assert!(s3_condition_keys("PutObject", &q).is_empty());
2694 assert!(s3_condition_keys("ListBuckets", &q).is_empty());
2695 }
2696
2697 #[test]
2698 fn valid_bucket_names() {
2699 assert!(is_valid_bucket_name("my-bucket"));
2700 assert!(is_valid_bucket_name("my.bucket.name"));
2701 assert!(is_valid_bucket_name("abc"));
2702 assert!(!is_valid_bucket_name("ab"));
2703 assert!(!is_valid_bucket_name("-bucket"));
2704 assert!(!is_valid_bucket_name("Bucket"));
2705 assert!(!is_valid_bucket_name("bucket-"));
2706 }
2707
2708 #[test]
2709 fn parse_delete_xml() {
2710 let xml = r#"<Delete><Object><Key>a.txt</Key></Object><Object><Key>b/c.txt</Key></Object></Delete>"#;
2711 let entries = parse_delete_objects_xml(xml);
2712 assert_eq!(entries.len(), 2);
2713 assert_eq!(entries[0].key, "a.txt");
2714 assert!(entries[0].version_id.is_none());
2715 assert_eq!(entries[1].key, "b/c.txt");
2716 }
2717
2718 #[test]
2719 fn parse_delete_xml_with_version() {
2720 let xml = r#"<Delete><Object><Key>a.txt</Key><VersionId>v1</VersionId></Object></Delete>"#;
2721 let entries = parse_delete_objects_xml(xml);
2722 assert_eq!(entries.len(), 1);
2723 assert_eq!(entries[0].key, "a.txt");
2724 assert_eq!(entries[0].version_id.as_deref(), Some("v1"));
2725 }
2726
2727 #[test]
2728 fn parse_tags_xml() {
2729 let xml =
2730 r#"<Tagging><TagSet><Tag><Key>env</Key><Value>prod</Value></Tag></TagSet></Tagging>"#;
2731 let tags = parse_tagging_xml(xml);
2732 assert_eq!(tags, vec![("env".to_string(), "prod".to_string())]);
2733 }
2734
2735 #[test]
2736 fn md5_hash() {
2737 let hash = compute_md5(b"hello");
2738 assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
2739 }
2740
2741 #[test]
2742 fn test_etag_matches() {
2743 assert!(etag_matches("\"abc\"", "\"abc\""));
2744 assert!(etag_matches("abc", "\"abc\""));
2745 assert!(etag_matches("*", "\"abc\""));
2746 assert!(!etag_matches("\"xyz\"", "\"abc\""));
2747 }
2748
2749 #[test]
2750 fn test_event_matches() {
2751 assert!(event_matches("s3:ObjectCreated:Put", "s3:ObjectCreated:*"));
2752 assert!(event_matches("s3:ObjectCreated:Copy", "s3:ObjectCreated:*"));
2753 assert!(event_matches(
2754 "s3:ObjectRemoved:Delete",
2755 "s3:ObjectRemoved:*"
2756 ));
2757 assert!(!event_matches(
2758 "s3:ObjectRemoved:Delete",
2759 "s3:ObjectCreated:*"
2760 ));
2761 assert!(event_matches(
2762 "s3:ObjectCreated:Put",
2763 "s3:ObjectCreated:Put"
2764 ));
2765 assert!(event_matches("s3:ObjectCreated:Put", "s3:*"));
2766 }
2767
2768 #[test]
2769 fn test_parse_notification_config() {
2770 let xml = r#"<NotificationConfiguration>
2771 <QueueConfiguration>
2772 <Queue>arn:aws:sqs:us-east-1:123456789012:my-queue</Queue>
2773 <Event>s3:ObjectCreated:*</Event>
2774 </QueueConfiguration>
2775 <TopicConfiguration>
2776 <Topic>arn:aws:sns:us-east-1:123456789012:my-topic</Topic>
2777 <Event>s3:ObjectRemoved:*</Event>
2778 </TopicConfiguration>
2779 </NotificationConfiguration>"#;
2780 let targets = parse_notification_config(xml);
2781 assert_eq!(targets.len(), 2);
2782 assert_eq!(
2783 targets[0].arn,
2784 "arn:aws:sqs:us-east-1:123456789012:my-queue"
2785 );
2786 assert_eq!(targets[0].events, vec!["s3:ObjectCreated:*"]);
2787 assert_eq!(
2788 targets[1].arn,
2789 "arn:aws:sns:us-east-1:123456789012:my-topic"
2790 );
2791 assert_eq!(targets[1].events, vec!["s3:ObjectRemoved:*"]);
2792 }
2793
2794 #[test]
2795 fn test_parse_notification_config_lambda() {
2796 let xml = r#"<NotificationConfiguration>
2798 <CloudFunctionConfiguration>
2799 <CloudFunction>arn:aws:lambda:us-east-1:123456789012:function:my-func</CloudFunction>
2800 <Event>s3:ObjectCreated:*</Event>
2801 </CloudFunctionConfiguration>
2802 </NotificationConfiguration>"#;
2803 let targets = parse_notification_config(xml);
2804 assert_eq!(targets.len(), 1);
2805 assert!(matches!(
2806 targets[0].target_type,
2807 NotificationTargetType::Lambda
2808 ));
2809 assert_eq!(
2810 targets[0].arn,
2811 "arn:aws:lambda:us-east-1:123456789012:function:my-func"
2812 );
2813 assert_eq!(targets[0].events, vec!["s3:ObjectCreated:*"]);
2814 }
2815
2816 #[test]
2817 fn test_parse_notification_config_lambda_new_format() {
2818 let xml = r#"<NotificationConfiguration>
2820 <LambdaFunctionConfiguration>
2821 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
2822 <Event>s3:ObjectCreated:Put</Event>
2823 <Event>s3:ObjectRemoved:*</Event>
2824 </LambdaFunctionConfiguration>
2825 </NotificationConfiguration>"#;
2826 let targets = parse_notification_config(xml);
2827 assert_eq!(targets.len(), 1);
2828 assert!(matches!(
2829 targets[0].target_type,
2830 NotificationTargetType::Lambda
2831 ));
2832 assert_eq!(
2833 targets[0].arn,
2834 "arn:aws:lambda:us-east-1:123456789012:function:my-func"
2835 );
2836 assert_eq!(
2837 targets[0].events,
2838 vec!["s3:ObjectCreated:Put", "s3:ObjectRemoved:*"]
2839 );
2840 }
2841
2842 #[test]
2843 fn test_parse_notification_config_all_types() {
2844 let xml = r#"<NotificationConfiguration>
2845 <QueueConfiguration>
2846 <Queue>arn:aws:sqs:us-east-1:123456789012:q</Queue>
2847 <Event>s3:ObjectCreated:*</Event>
2848 </QueueConfiguration>
2849 <TopicConfiguration>
2850 <Topic>arn:aws:sns:us-east-1:123456789012:t</Topic>
2851 <Event>s3:ObjectRemoved:*</Event>
2852 </TopicConfiguration>
2853 <LambdaFunctionConfiguration>
2854 <Function>arn:aws:lambda:us-east-1:123456789012:function:f</Function>
2855 <Event>s3:ObjectCreated:Put</Event>
2856 </LambdaFunctionConfiguration>
2857 </NotificationConfiguration>"#;
2858 let targets = parse_notification_config(xml);
2859 assert_eq!(targets.len(), 3);
2860 assert!(matches!(
2861 targets[0].target_type,
2862 NotificationTargetType::Sqs
2863 ));
2864 assert!(matches!(
2865 targets[1].target_type,
2866 NotificationTargetType::Sns
2867 ));
2868 assert!(matches!(
2869 targets[2].target_type,
2870 NotificationTargetType::Lambda
2871 ));
2872 }
2873
2874 #[test]
2875 fn test_parse_notification_config_with_filters() {
2876 let xml = r#"<NotificationConfiguration>
2877 <LambdaFunctionConfiguration>
2878 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
2879 <Event>s3:ObjectCreated:*</Event>
2880 <Filter>
2881 <S3Key>
2882 <FilterRule>
2883 <Name>prefix</Name>
2884 <Value>images/</Value>
2885 </FilterRule>
2886 <FilterRule>
2887 <Name>suffix</Name>
2888 <Value>.jpg</Value>
2889 </FilterRule>
2890 </S3Key>
2891 </Filter>
2892 </LambdaFunctionConfiguration>
2893 </NotificationConfiguration>"#;
2894 let targets = parse_notification_config(xml);
2895 assert_eq!(targets.len(), 1);
2896 assert_eq!(targets[0].prefix_filter, Some("images/".to_string()));
2897 assert_eq!(targets[0].suffix_filter, Some(".jpg".to_string()));
2898 }
2899
2900 #[test]
2901 fn test_parse_notification_config_no_filters() {
2902 let xml = r#"<NotificationConfiguration>
2903 <LambdaFunctionConfiguration>
2904 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
2905 <Event>s3:ObjectCreated:*</Event>
2906 </LambdaFunctionConfiguration>
2907 </NotificationConfiguration>"#;
2908 let targets = parse_notification_config(xml);
2909 assert_eq!(targets.len(), 1);
2910 assert_eq!(targets[0].prefix_filter, None);
2911 assert_eq!(targets[0].suffix_filter, None);
2912 }
2913
2914 #[test]
2915 fn test_key_matches_filters() {
2916 assert!(key_matches_filters("anything", &None, &None));
2918
2919 assert!(key_matches_filters(
2921 "images/photo.jpg",
2922 &Some("images/".to_string()),
2923 &None
2924 ));
2925 assert!(!key_matches_filters(
2926 "docs/file.txt",
2927 &Some("images/".to_string()),
2928 &None
2929 ));
2930
2931 assert!(key_matches_filters(
2933 "images/photo.jpg",
2934 &None,
2935 &Some(".jpg".to_string())
2936 ));
2937 assert!(!key_matches_filters(
2938 "images/photo.png",
2939 &None,
2940 &Some(".jpg".to_string())
2941 ));
2942
2943 assert!(key_matches_filters(
2945 "images/photo.jpg",
2946 &Some("images/".to_string()),
2947 &Some(".jpg".to_string())
2948 ));
2949 assert!(!key_matches_filters(
2950 "images/photo.png",
2951 &Some("images/".to_string()),
2952 &Some(".jpg".to_string())
2953 ));
2954 assert!(!key_matches_filters(
2955 "docs/photo.jpg",
2956 &Some("images/".to_string()),
2957 &Some(".jpg".to_string())
2958 ));
2959 }
2960
2961 #[test]
2962 fn test_parse_cors_config() {
2963 let xml = r#"<CORSConfiguration>
2964 <CORSRule>
2965 <AllowedOrigin>https://example.com</AllowedOrigin>
2966 <AllowedMethod>GET</AllowedMethod>
2967 <AllowedMethod>PUT</AllowedMethod>
2968 <AllowedHeader>*</AllowedHeader>
2969 <ExposeHeader>x-amz-request-id</ExposeHeader>
2970 <MaxAgeSeconds>3600</MaxAgeSeconds>
2971 </CORSRule>
2972 </CORSConfiguration>"#;
2973 let rules = parse_cors_config(xml);
2974 assert_eq!(rules.len(), 1);
2975 assert_eq!(rules[0].allowed_origins, vec!["https://example.com"]);
2976 assert_eq!(rules[0].allowed_methods, vec!["GET", "PUT"]);
2977 assert_eq!(rules[0].allowed_headers, vec!["*"]);
2978 assert_eq!(rules[0].expose_headers, vec!["x-amz-request-id"]);
2979 assert_eq!(rules[0].max_age_seconds, Some(3600));
2980 }
2981
2982 #[test]
2983 fn test_origin_matches() {
2984 assert!(origin_matches("https://example.com", "https://example.com"));
2985 assert!(origin_matches("https://example.com", "*"));
2986 assert!(origin_matches("https://foo.example.com", "*.example.com"));
2987 assert!(!origin_matches("https://evil.com", "https://example.com"));
2988 }
2989
2990 #[test]
2993 fn resolve_null_version_matches_both_none_and_null_string() {
2994 use crate::state::S3Bucket;
2995 use bytes::Bytes;
2996 use chrono::Utc;
2997
2998 let mut b = S3Bucket::new("test", "us-east-1", "owner");
2999
3000 let make_obj = |key: &str, vid: Option<&str>| crate::state::S3Object {
3002 key: key.to_string(),
3003 body: crate::state::memory_body(Bytes::from_static(b"x")),
3004 content_type: "text/plain".to_string(),
3005 etag: "\"abc\"".to_string(),
3006 size: 1,
3007 last_modified: Utc::now(),
3008 storage_class: "STANDARD".to_string(),
3009 version_id: vid.map(|s| s.to_string()),
3010 ..Default::default()
3011 };
3012
3013 let obj = make_obj("file.txt", Some("null"));
3015 b.objects.insert("file.txt".to_string(), obj.clone());
3016 b.object_versions.insert("file.txt".to_string(), vec![obj]);
3017
3018 let null_str = "null".to_string();
3019 let result = resolve_object(&b, "file.txt", Some(&null_str));
3020 assert!(
3021 result.is_ok(),
3022 "versionId=null should match version_id=Some(\"null\")"
3023 );
3024
3025 let obj2 = make_obj("file2.txt", None);
3027 b.objects.insert("file2.txt".to_string(), obj2.clone());
3028 b.object_versions
3029 .insert("file2.txt".to_string(), vec![obj2]);
3030
3031 let result2 = resolve_object(&b, "file2.txt", Some(&null_str));
3032 assert!(
3033 result2.is_ok(),
3034 "versionId=null should match version_id=None"
3035 );
3036 }
3037
3038 #[test]
3039 fn test_parse_replication_rules() {
3040 let xml = r#"<ReplicationConfiguration>
3041 <Role>arn:aws:iam::role/replication</Role>
3042 <Rule>
3043 <Status>Enabled</Status>
3044 <Filter><Prefix>logs/</Prefix></Filter>
3045 <Destination><Bucket>arn:aws:s3:::dest-bucket</Bucket></Destination>
3046 </Rule>
3047 <Rule>
3048 <Status>Disabled</Status>
3049 <Filter><Prefix></Prefix></Filter>
3050 <Destination><Bucket>arn:aws:s3:::other-bucket</Bucket></Destination>
3051 </Rule>
3052 </ReplicationConfiguration>"#;
3053
3054 let rules = parse_replication_rules(xml);
3055 assert_eq!(rules.len(), 2);
3056 assert_eq!(rules[0].status, "Enabled");
3057 assert_eq!(rules[0].prefix, "logs/");
3058 assert_eq!(rules[0].dest_bucket, "dest-bucket");
3059 assert_eq!(rules[1].status, "Disabled");
3060 assert_eq!(rules[1].prefix, "");
3061 assert_eq!(rules[1].dest_bucket, "other-bucket");
3062 }
3063
3064 #[test]
3065 fn test_parse_normalized_replication_rules() {
3066 let input_xml = r#"<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Role>arn:aws:iam::123456789012:role/replication-role</Role><Rule><ID>replicate-all</ID><Status>Enabled</Status><Filter><Prefix></Prefix></Filter><Destination><Bucket>arn:aws:s3:::repl-dest</Bucket></Destination></Rule></ReplicationConfiguration>"#;
3068 let normalized = normalize_replication_xml(input_xml);
3069 eprintln!("Normalized XML: {normalized}");
3070 let rules = parse_replication_rules(&normalized);
3071 assert_eq!(rules.len(), 1, "Expected 1 rule, got {}", rules.len());
3072 assert_eq!(rules[0].status, "Enabled");
3073 assert_eq!(rules[0].dest_bucket, "repl-dest");
3074 }
3075
3076 #[test]
3077 fn test_replicate_object() {
3078 use crate::state::{S3Bucket, S3State};
3079
3080 let mut state = S3State::new("123456789012", "us-east-1");
3081
3082 let mut src = S3Bucket::new("source", "us-east-1", "owner");
3084 src.versioning = Some("Enabled".to_string());
3085 src.replication_config = Some(
3086 "<ReplicationConfiguration>\
3087 <Rule><Status>Enabled</Status>\
3088 <Filter><Prefix></Prefix></Filter>\
3089 <Destination><Bucket>arn:aws:s3:::destination</Bucket></Destination>\
3090 </Rule></ReplicationConfiguration>"
3091 .to_string(),
3092 );
3093 let obj = S3Object {
3094 key: "test-key".to_string(),
3095 body: crate::state::memory_body(Bytes::from_static(b"hello")),
3096 content_type: "text/plain".to_string(),
3097 etag: "abc".to_string(),
3098 size: 5,
3099 last_modified: Utc::now(),
3100 storage_class: "STANDARD".to_string(),
3101 version_id: Some("v1".to_string()),
3102 ..Default::default()
3103 };
3104 src.objects.insert("test-key".to_string(), obj);
3105 state.buckets.insert("source".to_string(), src);
3106
3107 let dest = S3Bucket::new("destination", "us-east-1", "owner");
3108 state.buckets.insert("destination".to_string(), dest);
3109
3110 replicate_object(&mut state, "source", "test-key");
3111
3112 let dest_obj = state
3114 .buckets
3115 .get("destination")
3116 .unwrap()
3117 .objects
3118 .get("test-key");
3119 assert!(dest_obj.is_some());
3120 assert_eq!(
3121 state.read_body(&dest_obj.unwrap().body).unwrap(),
3122 Bytes::from_static(b"hello")
3123 );
3124 }
3125
3126 #[test]
3127 fn cors_header_value_does_not_panic_on_unusual_input() {
3128 let valid_origin = "https://example.com";
3132 let result: Result<http::HeaderValue, _> = valid_origin.parse();
3133 assert!(result.is_ok());
3134
3135 let bad_origin = "https://ex\x01ample.com";
3137 let result: Result<http::HeaderValue, _> = bad_origin.parse();
3138 assert!(result.is_err());
3139 let fallback = bad_origin
3141 .parse()
3142 .unwrap_or_else(|_| http::HeaderValue::from_static(""));
3143 assert_eq!(fallback, "");
3144 }
3145
3146 use crate::state::{S3Bucket, S3Object};
3155 use bytes::Bytes;
3156 use fakecloud_core::delivery::DeliveryBus;
3157 use fakecloud_core::service::{AwsRequest, AwsServiceError};
3158 use http::{HeaderMap, Method, StatusCode};
3159 use parking_lot::RwLock;
3160 use std::collections::HashMap;
3161 use std::sync::Arc;
3162
3163 fn make_service() -> S3Service {
3164 let state: SharedS3State = Arc::new(RwLock::new(
3165 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3166 ));
3167 S3Service::new(state, Arc::new(DeliveryBus::new()))
3168 }
3169
3170 fn seed_bucket(svc: &S3Service, name: &str) {
3171 let mut mas = svc.state.write();
3172 let state = mas.default_mut();
3173 state
3174 .buckets
3175 .insert(name.to_string(), S3Bucket::new(name, "us-east-1", "owner"));
3176 }
3177
3178 fn seed_object(svc: &S3Service, bucket: &str, key: &str, body: &[u8]) {
3179 let mut mas = svc.state.write();
3180 let state = mas.default_mut();
3181 let b = state.buckets.get_mut(bucket).expect("bucket seeded");
3182 let mut obj = S3Object {
3183 key: key.to_string(),
3184 body: fakecloud_persistence::BodyRef::Memory(Bytes::copy_from_slice(body)),
3185 content_type: "application/octet-stream".to_string(),
3186 etag: format!("\"{}\"", compute_md5(body)),
3187 size: body.len() as u64,
3188 last_modified: chrono::Utc::now(),
3189 ..Default::default()
3190 };
3191 obj.metadata.insert("version".to_string(), "1".to_string());
3192 b.objects.insert(key.to_string(), obj);
3193 }
3194
3195 fn make_request(method: Method, path: &str, query: &[(&str, &str)], body: &[u8]) -> AwsRequest {
3196 let segments: Vec<String> = path
3197 .trim_start_matches('/')
3198 .split('/')
3199 .filter(|s| !s.is_empty())
3200 .map(|s| s.to_string())
3201 .collect();
3202 let query_params: HashMap<String, String> = query
3203 .iter()
3204 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
3205 .collect();
3206 let raw_query = query
3207 .iter()
3208 .map(|(k, v)| format!("{k}={v}"))
3209 .collect::<Vec<_>>()
3210 .join("&");
3211 let stream_body =
3216 fakecloud_core::service::RequestBodyStream::from(Bytes::copy_from_slice(body));
3217 AwsRequest {
3218 service: "s3".to_string(),
3219 action: String::new(),
3220 region: "us-east-1".to_string(),
3221 account_id: "123456789012".to_string(),
3222 request_id: "test-req".to_string(),
3223 headers: HeaderMap::new(),
3224 query_params,
3225 body: Bytes::copy_from_slice(body),
3226 body_stream: parking_lot::Mutex::new(Some(stream_body)),
3227 path_segments: segments,
3228 raw_path: path.to_string(),
3229 raw_query,
3230 method,
3231 is_query_protocol: false,
3232 access_key_id: None,
3233 principal: None,
3234 }
3235 }
3236
3237 fn assert_aws_err(
3238 result: Result<AwsResponse, AwsServiceError>,
3239 expect_code: &str,
3240 ) -> AwsServiceError {
3241 let err = match result {
3242 Ok(_) => panic!("expected error, got Ok response"),
3243 Err(e) => e,
3244 };
3245 match &err {
3246 AwsServiceError::AwsError { code, .. } => {
3247 assert_eq!(code, expect_code, "wrong error code");
3248 }
3249 other => panic!("expected AwsError, got {other:?}"),
3250 }
3251 err
3252 }
3253
3254 #[test]
3257 fn get_object_tagging_on_object_returns_xml_tagset() {
3258 let svc = make_service();
3259 seed_bucket(&svc, "b");
3260 seed_object(&svc, "b", "k", b"hello");
3261 {
3262 let mut mas = svc.state.write();
3263 let obj = mas
3264 .default_mut()
3265 .buckets
3266 .get_mut("b")
3267 .unwrap()
3268 .objects
3269 .get_mut("k")
3270 .unwrap();
3271 obj.tags.insert("env".to_string(), "prod".to_string());
3272 obj.tags.insert("team".to_string(), "plat".to_string());
3273 }
3274
3275 let req = make_request(Method::GET, "/b/k", &[("tagging", "")], b"");
3276 let resp = svc
3277 .get_object_tagging("123456789012", &req, "b", "k")
3278 .unwrap();
3279 assert_eq!(resp.status, StatusCode::OK);
3280 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
3281 assert!(body.contains("<Tag><Key>env</Key><Value>prod</Value></Tag>"));
3282 assert!(body.contains("<Tag><Key>team</Key><Value>plat</Value></Tag>"));
3283 }
3284
3285 #[test]
3286 fn get_object_tagging_missing_bucket_errors() {
3287 let svc = make_service();
3288 let req = make_request(Method::GET, "/nope/k", &[("tagging", "")], b"");
3289 assert_aws_err(
3290 svc.get_object_tagging("123456789012", &req, "nope", "k"),
3291 "NoSuchBucket",
3292 );
3293 }
3294
3295 #[test]
3296 fn put_object_tagging_rejects_aws_prefixed_key() {
3297 let svc = make_service();
3298 seed_bucket(&svc, "b");
3299 seed_object(&svc, "b", "k", b"x");
3300
3301 let xml = r#"<Tagging><TagSet><Tag><Key>aws:internal</Key><Value>v</Value></Tag></TagSet></Tagging>"#;
3302 let req = make_request(Method::PUT, "/b/k", &[("tagging", "")], xml.as_bytes());
3303 assert_aws_err(
3304 svc.put_object_tagging("123456789012", &req, "b", "k"),
3305 "InvalidTag",
3306 );
3307 }
3308
3309 #[test]
3310 fn put_object_tagging_rejects_too_many_tags() {
3311 let svc = make_service();
3312 seed_bucket(&svc, "b");
3313 seed_object(&svc, "b", "k", b"x");
3314
3315 let mut xml = String::from("<Tagging><TagSet>");
3316 for i in 0..11 {
3317 xml.push_str(&format!("<Tag><Key>k{i}</Key><Value>v</Value></Tag>"));
3318 }
3319 xml.push_str("</TagSet></Tagging>");
3320 let req = make_request(Method::PUT, "/b/k", &[("tagging", "")], xml.as_bytes());
3321 assert_aws_err(
3322 svc.put_object_tagging("123456789012", &req, "b", "k"),
3323 "BadRequest",
3324 );
3325 }
3326
3327 #[test]
3328 fn put_object_tagging_on_missing_object_errors() {
3329 let svc = make_service();
3330 seed_bucket(&svc, "b");
3331 let xml =
3332 r#"<Tagging><TagSet><Tag><Key>env</Key><Value>prod</Value></Tag></TagSet></Tagging>"#;
3333 let req = make_request(
3334 Method::PUT,
3335 "/b/missing",
3336 &[("tagging", "")],
3337 xml.as_bytes(),
3338 );
3339 assert_aws_err(
3340 svc.put_object_tagging("123456789012", &req, "b", "missing"),
3341 "NoSuchKey",
3342 );
3343 }
3344
3345 #[test]
3346 fn put_object_tagging_replaces_existing_tags() {
3347 let svc = make_service();
3348 seed_bucket(&svc, "b");
3349 seed_object(&svc, "b", "k", b"x");
3350 {
3351 let mut mas = svc.state.write();
3352 let obj = mas
3353 .default_mut()
3354 .buckets
3355 .get_mut("b")
3356 .unwrap()
3357 .objects
3358 .get_mut("k")
3359 .unwrap();
3360 obj.tags.insert("old".to_string(), "gone".to_string());
3361 }
3362
3363 let xml =
3364 r#"<Tagging><TagSet><Tag><Key>new</Key><Value>here</Value></Tag></TagSet></Tagging>"#;
3365 let req = make_request(Method::PUT, "/b/k", &[("tagging", "")], xml.as_bytes());
3366 let resp = svc
3367 .put_object_tagging("123456789012", &req, "b", "k")
3368 .unwrap();
3369 assert_eq!(resp.status, StatusCode::OK);
3370
3371 let __mas = svc.state.read();
3372 let state = __mas.default_ref();
3373 let tags = &state
3374 .buckets
3375 .get("b")
3376 .unwrap()
3377 .objects
3378 .get("k")
3379 .unwrap()
3380 .tags;
3381 assert_eq!(tags.get("new").map(String::as_str), Some("here"));
3382 assert!(!tags.contains_key("old"));
3383 }
3384
3385 #[test]
3386 fn delete_object_tagging_clears_tags() {
3387 let svc = make_service();
3388 seed_bucket(&svc, "b");
3389 seed_object(&svc, "b", "k", b"x");
3390 {
3391 let mut mas = svc.state.write();
3392 let obj = mas
3393 .default_mut()
3394 .buckets
3395 .get_mut("b")
3396 .unwrap()
3397 .objects
3398 .get_mut("k")
3399 .unwrap();
3400 obj.tags.insert("env".to_string(), "prod".to_string());
3401 }
3402
3403 let resp = svc.delete_object_tagging("123456789012", "b", "k").unwrap();
3404 assert_eq!(resp.status, StatusCode::NO_CONTENT);
3405 let __mas = svc.state.read();
3406 let state = __mas.default_ref();
3407 assert!(state
3408 .buckets
3409 .get("b")
3410 .unwrap()
3411 .objects
3412 .get("k")
3413 .unwrap()
3414 .tags
3415 .is_empty());
3416 }
3417
3418 #[test]
3419 fn delete_object_tagging_missing_key_errors() {
3420 let svc = make_service();
3421 seed_bucket(&svc, "b");
3422 assert_aws_err(
3423 svc.delete_object_tagging("123456789012", "b", "gone"),
3424 "NoSuchKey",
3425 );
3426 }
3427
3428 fn initiate_mpu(svc: &S3Service, bucket: &str, key: &str) -> String {
3431 let req = make_request(
3432 Method::POST,
3433 &format!("/{bucket}/{key}"),
3434 &[("uploads", "")],
3435 b"",
3436 );
3437 let resp = svc
3438 .create_multipart_upload("123456789012", &req, bucket, key)
3439 .unwrap();
3440 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
3441 let start = body.find("<UploadId>").unwrap() + "<UploadId>".len();
3442 let end = body.find("</UploadId>").unwrap();
3443 body[start..end].to_string()
3444 }
3445
3446 #[test]
3447 fn create_multipart_upload_records_upload_in_state() {
3448 let svc = make_service();
3449 seed_bucket(&svc, "b");
3450 let upload_id = initiate_mpu(&svc, "b", "big.bin");
3451 let __mas = svc.state.read();
3452 let state = __mas.default_ref();
3453 assert!(state
3454 .buckets
3455 .get("b")
3456 .unwrap()
3457 .multipart_uploads
3458 .contains_key(&upload_id));
3459 }
3460
3461 #[test]
3462 fn create_multipart_upload_rejects_acl_and_grants_combo() {
3463 let svc = make_service();
3464 seed_bucket(&svc, "b");
3465 let mut req = make_request(Method::POST, "/b/k", &[("uploads", "")], b"");
3466 req.headers.insert("x-amz-acl", "private".parse().unwrap());
3467 req.headers
3468 .insert("x-amz-grant-read", "id=owner".parse().unwrap());
3469 assert_aws_err(
3470 svc.create_multipart_upload("123456789012", &req, "b", "k"),
3471 "InvalidRequest",
3472 );
3473 }
3474
3475 #[test]
3476 fn create_multipart_upload_missing_bucket_errors() {
3477 let svc = make_service();
3478 let req = make_request(Method::POST, "/ghost/k", &[("uploads", "")], b"");
3479 assert_aws_err(
3480 svc.create_multipart_upload("123456789012", &req, "ghost", "k"),
3481 "NoSuchBucket",
3482 );
3483 }
3484
3485 #[tokio::test]
3486 async fn upload_part_rejects_invalid_part_number() {
3487 let svc = make_service();
3488 seed_bucket(&svc, "b");
3489 let upload_id = initiate_mpu(&svc, "b", "k");
3490
3491 let req = make_request(Method::PUT, "/b/k", &[("partNumber", "0")], b"body");
3493 assert_aws_err(
3494 svc.upload_part("123456789012", &req, "b", "k", &upload_id, 0)
3495 .await,
3496 "NoSuchUpload",
3497 );
3498
3499 let req2 = make_request(Method::PUT, "/b/k", &[("partNumber", "10001")], b"body");
3501 assert_aws_err(
3502 svc.upload_part("123456789012", &req2, "b", "k", &upload_id, 10_001)
3503 .await,
3504 "InvalidArgument",
3505 );
3506 }
3507
3508 #[tokio::test]
3509 async fn upload_part_missing_upload_errors() {
3510 let svc = make_service();
3511 seed_bucket(&svc, "b");
3512 let req = make_request(Method::PUT, "/b/k", &[("partNumber", "1")], b"body");
3513 assert_aws_err(
3514 svc.upload_part("123456789012", &req, "b", "k", "not-an-upload", 1)
3515 .await,
3516 "NoSuchUpload",
3517 );
3518 }
3519
3520 #[tokio::test]
3521 async fn mpu_full_lifecycle_creates_object() {
3522 let svc = make_service();
3523 seed_bucket(&svc, "b");
3524 let upload_id = initiate_mpu(&svc, "b", "k");
3525
3526 let part_body = b"hello";
3529 let req = make_request(Method::PUT, "/b/k", &[("partNumber", "1")], part_body);
3530 let resp = svc
3531 .upload_part("123456789012", &req, "b", "k", &upload_id, 1)
3532 .await
3533 .unwrap();
3534 let etag = resp
3535 .headers
3536 .get("etag")
3537 .unwrap()
3538 .to_str()
3539 .unwrap()
3540 .to_string();
3541
3542 let complete_xml = format!(
3543 r#"<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>{etag}</ETag></Part></CompleteMultipartUpload>"#,
3544 );
3545 let complete_req = make_request(
3546 Method::POST,
3547 "/b/k",
3548 &[("uploadId", &upload_id)],
3549 complete_xml.as_bytes(),
3550 );
3551 let resp = svc
3552 .complete_multipart_upload("123456789012", &complete_req, "b", "k", &upload_id)
3553 .unwrap();
3554 assert_eq!(resp.status, StatusCode::OK);
3555
3556 let __mas = svc.state.read();
3557 let state = __mas.default_ref();
3558 let bucket = state.buckets.get("b").unwrap();
3559 let obj = bucket.objects.get("k").expect("object materialized");
3560 assert_eq!(obj.size, part_body.len() as u64);
3561 assert!(!bucket.multipart_uploads.contains_key(&upload_id));
3562 }
3563
3564 #[tokio::test]
3565 async fn mpu_complete_rejects_small_non_last_part() {
3566 let svc = make_service();
3567 seed_bucket(&svc, "b");
3568 let upload_id = initiate_mpu(&svc, "b", "k");
3569
3570 for n in 1..=2 {
3571 let body = format!("part{n}");
3572 let req = make_request(
3573 Method::PUT,
3574 "/b/k",
3575 &[("partNumber", &n.to_string())],
3576 body.as_bytes(),
3577 );
3578 svc.upload_part("123456789012", &req, "b", "k", &upload_id, n)
3579 .await
3580 .unwrap();
3581 }
3582
3583 let (etag1, etag2) = {
3585 let __mas = svc.state.read();
3586 let state = __mas.default_ref();
3587 let parts = &state
3588 .buckets
3589 .get("b")
3590 .unwrap()
3591 .multipart_uploads
3592 .get(&upload_id)
3593 .unwrap()
3594 .parts;
3595 (
3596 parts.get(&1).unwrap().etag.clone(),
3597 parts.get(&2).unwrap().etag.clone(),
3598 )
3599 };
3600
3601 let complete_xml = format!(
3602 r#"<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>{etag1}</ETag></Part><Part><PartNumber>2</PartNumber><ETag>{etag2}</ETag></Part></CompleteMultipartUpload>"#,
3603 );
3604 let complete_req = make_request(
3605 Method::POST,
3606 "/b/k",
3607 &[("uploadId", &upload_id)],
3608 complete_xml.as_bytes(),
3609 );
3610 assert_aws_err(
3611 svc.complete_multipart_upload("123456789012", &complete_req, "b", "k", &upload_id),
3612 "EntityTooSmall",
3613 );
3614 }
3615
3616 #[test]
3617 fn abort_multipart_upload_removes_upload() {
3618 let svc = make_service();
3619 seed_bucket(&svc, "b");
3620 let upload_id = initiate_mpu(&svc, "b", "k");
3621 let resp = svc
3622 .abort_multipart_upload("123456789012", "b", "k", &upload_id)
3623 .unwrap();
3624 assert_eq!(resp.status, StatusCode::NO_CONTENT);
3625 let __mas = svc.state.read();
3626 let state = __mas.default_ref();
3627 assert!(!state
3628 .buckets
3629 .get("b")
3630 .unwrap()
3631 .multipart_uploads
3632 .contains_key(&upload_id));
3633 }
3634
3635 #[test]
3636 fn abort_multipart_upload_unknown_id_errors() {
3637 let svc = make_service();
3638 seed_bucket(&svc, "b");
3639 assert_aws_err(
3640 svc.abort_multipart_upload("123456789012", "b", "k", "no-such"),
3641 "NoSuchUpload",
3642 );
3643 }
3644
3645 #[test]
3646 fn list_multipart_uploads_includes_all_in_flight() {
3647 let svc = make_service();
3648 seed_bucket(&svc, "b");
3649 let u1 = initiate_mpu(&svc, "b", "a");
3650 let u2 = initiate_mpu(&svc, "b", "b");
3651 let resp = svc.list_multipart_uploads("123456789012", "b").unwrap();
3652 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
3653 assert!(body.contains(&u1));
3654 assert!(body.contains(&u2));
3655 }
3656
3657 #[tokio::test]
3658 async fn list_parts_after_upload_returns_parts() {
3659 let svc = make_service();
3660 seed_bucket(&svc, "b");
3661 let upload_id = initiate_mpu(&svc, "b", "k");
3662 let req = make_request(Method::PUT, "/b/k", &[("partNumber", "1")], b"data");
3663 svc.upload_part("123456789012", &req, "b", "k", &upload_id, 1)
3664 .await
3665 .unwrap();
3666
3667 let list_req = make_request(Method::GET, "/b/k", &[("uploadId", &upload_id)], b"");
3668 let resp = svc
3669 .list_parts("123456789012", &list_req, "b", "k", &upload_id)
3670 .unwrap();
3671 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
3672 assert!(body.contains("<PartNumber>1</PartNumber>"));
3673 }
3674
3675 #[test]
3678 fn bucket_encryption_put_get_delete_round_trip() {
3679 let svc = make_service();
3680 seed_bucket(&svc, "b");
3681
3682 let xml = r#"<ServerSideEncryptionConfiguration><Rule><ApplyServerSideEncryptionByDefault><SSEAlgorithm>AES256</SSEAlgorithm></ApplyServerSideEncryptionByDefault></Rule></ServerSideEncryptionConfiguration>"#;
3683 let req = make_request(Method::PUT, "/b", &[("encryption", "")], xml.as_bytes());
3684 let resp = svc
3685 .put_bucket_encryption("123456789012", &req, "b")
3686 .unwrap();
3687 assert_eq!(resp.status, StatusCode::OK);
3688
3689 let get = svc.get_bucket_encryption("123456789012", "b").unwrap();
3691 let body = std::str::from_utf8(get.body.expect_bytes()).unwrap();
3692 assert!(body.contains("AES256"));
3693 assert!(body.contains("<BucketKeyEnabled>false</BucketKeyEnabled>"));
3694
3695 let del = svc.delete_bucket_encryption("123456789012", "b").unwrap();
3696 assert_eq!(del.status, StatusCode::NO_CONTENT);
3697 assert_aws_err(
3698 svc.get_bucket_encryption("123456789012", "b"),
3699 "ServerSideEncryptionConfigurationNotFoundError",
3700 );
3701 }
3702
3703 #[test]
3704 fn bucket_policy_rejects_malformed_json() {
3705 let svc = make_service();
3706 seed_bucket(&svc, "b");
3707 let req = make_request(Method::PUT, "/b", &[("policy", "")], b"not-json");
3708 assert_aws_err(
3709 svc.put_bucket_policy("123456789012", &req, "b"),
3710 "MalformedPolicy",
3711 );
3712 }
3713
3714 #[test]
3715 fn bucket_policy_put_get_delete_round_trip() {
3716 let svc = make_service();
3717 seed_bucket(&svc, "b");
3718
3719 let body = br#"{"Version":"2012-10-17","Statement":[]}"#;
3720 let put_req = make_request(Method::PUT, "/b", &[("policy", "")], body);
3721 let resp = svc
3722 .put_bucket_policy("123456789012", &put_req, "b")
3723 .unwrap();
3724 assert_eq!(resp.status, StatusCode::NO_CONTENT);
3725
3726 let get = svc.get_bucket_policy("123456789012", "b").unwrap();
3727 assert_eq!(get.body.expect_bytes(), body);
3728
3729 let del = svc.delete_bucket_policy("123456789012", "b").unwrap();
3730 assert_eq!(del.status, StatusCode::NO_CONTENT);
3731 assert_aws_err(
3732 svc.get_bucket_policy("123456789012", "b"),
3733 "NoSuchBucketPolicy",
3734 );
3735 }
3736
3737 #[test]
3738 fn bucket_lifecycle_empty_rules_clears_config() {
3739 let svc = make_service();
3740 seed_bucket(&svc, "b");
3741 {
3742 let mut __mas = svc.state.write();
3743 let state = __mas.default_mut();
3744 state.buckets.get_mut("b").unwrap().lifecycle_config = Some("placeholder".to_string());
3745 }
3746 let req = make_request(
3747 Method::PUT,
3748 "/b",
3749 &[("lifecycle", "")],
3750 b"<LifecycleConfiguration></LifecycleConfiguration>",
3751 );
3752 svc.put_bucket_lifecycle("123456789012", &req, "b").unwrap();
3753 let __mas = svc.state.read();
3754 let state = __mas.default_ref();
3755 assert!(state.buckets.get("b").unwrap().lifecycle_config.is_none());
3756 }
3757
3758 #[test]
3759 fn bucket_cors_put_get_delete_round_trip() {
3760 let svc = make_service();
3761 seed_bucket(&svc, "b");
3762 let xml = br#"<CORSConfiguration><CORSRule><AllowedMethod>GET</AllowedMethod><AllowedOrigin>*</AllowedOrigin></CORSRule></CORSConfiguration>"#;
3763 let req = make_request(Method::PUT, "/b", &[("cors", "")], xml);
3764 svc.put_bucket_cors("123456789012", &req, "b").unwrap();
3765 let got = svc.get_bucket_cors("123456789012", "b").unwrap();
3766 assert!(std::str::from_utf8(got.body.expect_bytes())
3767 .unwrap()
3768 .contains("CORSConfiguration"));
3769 svc.delete_bucket_cors("123456789012", "b").unwrap();
3770 assert_aws_err(
3771 svc.get_bucket_cors("123456789012", "b"),
3772 "NoSuchCORSConfiguration",
3773 );
3774 }
3775
3776 #[test]
3777 fn bucket_versioning_put_and_get() {
3778 let svc = make_service();
3779 seed_bucket(&svc, "b");
3780 let req = make_request(
3781 Method::PUT,
3782 "/b",
3783 &[("versioning", "")],
3784 b"<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>",
3785 );
3786 svc.put_bucket_versioning("123456789012", &req, "b")
3787 .unwrap();
3788
3789 let __mas = svc.state.read();
3790 let state = __mas.default_ref();
3791 assert_eq!(
3792 state.buckets.get("b").unwrap().versioning.as_deref(),
3793 Some("Enabled")
3794 );
3795 drop(__mas);
3796
3797 let resp = svc.get_bucket_versioning("123456789012", "b").unwrap();
3798 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
3799 assert!(body.contains("<Status>Enabled</Status>"));
3800 }
3801
3802 #[test]
3803 fn bucket_tagging_put_get_delete_round_trip() {
3804 let svc = make_service();
3805 seed_bucket(&svc, "b");
3806 let req = make_request(
3807 Method::PUT,
3808 "/b",
3809 &[("tagging", "")],
3810 br#"<Tagging><TagSet><Tag><Key>env</Key><Value>prod</Value></Tag></TagSet></Tagging>"#,
3811 );
3812 svc.put_bucket_tagging("123456789012", &req, "b").unwrap();
3813 let get_req = make_request(Method::GET, "/b", &[("tagging", "")], b"");
3814 let got = svc
3815 .get_bucket_tagging("123456789012", &get_req, "b")
3816 .unwrap();
3817 assert!(std::str::from_utf8(got.body.expect_bytes())
3818 .unwrap()
3819 .contains("<Key>env</Key>"));
3820 let del_req = make_request(Method::DELETE, "/b", &[("tagging", "")], b"");
3821 svc.delete_bucket_tagging("123456789012", &del_req, "b")
3822 .unwrap();
3823 assert_aws_err(
3824 svc.get_bucket_tagging("123456789012", &get_req, "b"),
3825 "NoSuchTagSet",
3826 );
3827 }
3828
3829 #[test]
3830 fn bucket_accelerate_rejects_invalid_status() {
3831 let svc = make_service();
3832 seed_bucket(&svc, "b");
3833 let req = make_request(
3834 Method::PUT,
3835 "/b",
3836 &[("accelerate", "")],
3837 b"<AccelerateConfiguration><Status>Bogus</Status></AccelerateConfiguration>",
3838 );
3839 assert_aws_err(
3840 svc.put_bucket_accelerate("123456789012", &req, "b"),
3841 "MalformedXML",
3842 );
3843 }
3844
3845 #[test]
3846 fn bucket_accelerate_enabled_is_persisted() {
3847 let svc = make_service();
3848 seed_bucket(&svc, "b");
3849 let req = make_request(
3850 Method::PUT,
3851 "/b",
3852 &[("accelerate", "")],
3853 b"<AccelerateConfiguration><Status>Enabled</Status></AccelerateConfiguration>",
3854 );
3855 svc.put_bucket_accelerate("123456789012", &req, "b")
3856 .unwrap();
3857 let got = svc.get_bucket_accelerate("123456789012", "b").unwrap();
3858 assert!(std::str::from_utf8(got.body.expect_bytes())
3859 .unwrap()
3860 .contains("<Status>Enabled</Status>"));
3861 }
3862
3863 #[test]
3864 fn public_access_block_put_get_delete_round_trip() {
3865 let svc = make_service();
3866 seed_bucket(&svc, "b");
3867 let body = br#"<PublicAccessBlockConfiguration><BlockPublicAcls>true</BlockPublicAcls><IgnorePublicAcls>true</IgnorePublicAcls><BlockPublicPolicy>true</BlockPublicPolicy><RestrictPublicBuckets>true</RestrictPublicBuckets></PublicAccessBlockConfiguration>"#;
3868 let req = make_request(Method::PUT, "/b", &[("publicAccessBlock", "")], body);
3869 svc.put_public_access_block("123456789012", &req, "b")
3870 .unwrap();
3871 let got = svc.get_public_access_block("123456789012", "b").unwrap();
3872 assert!(std::str::from_utf8(got.body.expect_bytes())
3873 .unwrap()
3874 .contains("<BlockPublicAcls>true</BlockPublicAcls>"));
3875 svc.delete_public_access_block("123456789012", "b").unwrap();
3876 assert_aws_err(
3877 svc.get_public_access_block("123456789012", "b"),
3878 "NoSuchPublicAccessBlockConfiguration",
3879 );
3880 }
3881
3882 #[test]
3883 fn bucket_website_put_get_delete_round_trip() {
3884 let svc = make_service();
3885 seed_bucket(&svc, "b");
3886 let req = make_request(
3887 Method::PUT,
3888 "/b",
3889 &[("website", "")],
3890 b"<WebsiteConfiguration><IndexDocument><Suffix>index.html</Suffix></IndexDocument></WebsiteConfiguration>",
3891 );
3892 svc.put_bucket_website("123456789012", &req, "b").unwrap();
3893 let got = svc.get_bucket_website("123456789012", "b").unwrap();
3894 assert!(std::str::from_utf8(got.body.expect_bytes())
3895 .unwrap()
3896 .contains("<Suffix>index.html</Suffix>"));
3897 svc.delete_bucket_website("123456789012", "b").unwrap();
3898 assert_aws_err(
3899 svc.get_bucket_website("123456789012", "b"),
3900 "NoSuchWebsiteConfiguration",
3901 );
3902 }
3903
3904 #[test]
3905 fn bucket_replication_requires_existing_bucket() {
3906 let svc = make_service();
3907 let req = make_request(Method::PUT, "/nope", &[("replication", "")], b"<x/>");
3908 assert_aws_err(
3909 svc.put_bucket_replication("123456789012", &req, "nope"),
3910 "NoSuchBucket",
3911 );
3912 }
3913
3914 #[test]
3915 fn bucket_ownership_controls_put_get_delete_round_trip() {
3916 let svc = make_service();
3917 seed_bucket(&svc, "b");
3918 let req = make_request(
3919 Method::PUT,
3920 "/b",
3921 &[("ownershipControls", "")],
3922 b"<OwnershipControls><Rule><ObjectOwnership>BucketOwnerEnforced</ObjectOwnership></Rule></OwnershipControls>",
3923 );
3924 svc.put_bucket_ownership_controls("123456789012", &req, "b")
3925 .unwrap();
3926 let got = svc
3927 .get_bucket_ownership_controls("123456789012", "b")
3928 .unwrap();
3929 assert!(std::str::from_utf8(got.body.expect_bytes())
3930 .unwrap()
3931 .contains("BucketOwnerEnforced"));
3932 svc.delete_bucket_ownership_controls("123456789012", "b")
3933 .unwrap();
3934 assert_aws_err(
3935 svc.get_bucket_ownership_controls("123456789012", "b"),
3936 "OwnershipControlsNotFoundError",
3937 );
3938 }
3939
3940 #[test]
3943 fn get_object_nonexistent_bucket() {
3944 let svc = make_service();
3945 let req = make_request(Method::GET, "/no-bucket/key", &[], b"");
3946 assert_aws_err(
3947 svc.get_object("123456789012", &req, "no-bucket", "key"),
3948 "NoSuchBucket",
3949 );
3950 }
3951
3952 #[test]
3953 fn get_object_nonexistent_key() {
3954 let svc = make_service();
3955 seed_bucket(&svc, "b");
3956 let req = make_request(Method::GET, "/b/missing", &[], b"");
3957 assert_aws_err(
3958 svc.get_object("123456789012", &req, "b", "missing"),
3959 "NoSuchKey",
3960 );
3961 }
3962
3963 #[tokio::test]
3964 async fn put_object_key_too_long() {
3965 let svc = make_service();
3966 seed_bucket(&svc, "b");
3967 let long_key = "x".repeat(1025);
3968 let req = make_request(Method::PUT, &format!("/b/{long_key}"), &[], b"data");
3969 assert_aws_err(
3970 svc.put_object("123456789012", &req, "b", &long_key).await,
3971 "KeyTooLongError",
3972 );
3973 }
3974
3975 #[tokio::test]
3976 async fn put_object_with_aws_tag_prefix() {
3977 let svc = make_service();
3978 seed_bucket(&svc, "b");
3979 let mut req = make_request(Method::PUT, "/b/tagged", &[], b"data");
3980 req.headers
3981 .insert("x-amz-tagging", "aws:reserved=nope".parse().unwrap());
3982 assert_aws_err(
3983 svc.put_object("123456789012", &req, "b", "tagged").await,
3984 "InvalidTag",
3985 );
3986 }
3987
3988 #[tokio::test]
3989 async fn put_object_acl_and_grant_conflict() {
3990 let svc = make_service();
3991 seed_bucket(&svc, "b");
3992 let mut req = make_request(Method::PUT, "/b/conflict", &[], b"data");
3993 req.headers
3994 .insert("x-amz-acl", "public-read".parse().unwrap());
3995 req.headers
3996 .insert("x-amz-grant-read", "id=abc123".parse().unwrap());
3997 assert_aws_err(
3998 svc.put_object("123456789012", &req, "b", "conflict").await,
3999 "InvalidRequest",
4000 );
4001 }
4002
4003 #[test]
4004 fn head_object_nonexistent_key() {
4005 let svc = make_service();
4006 seed_bucket(&svc, "b");
4007 let req = make_request(Method::HEAD, "/b/missing", &[], b"");
4008 assert_aws_err(
4009 svc.head_object("123456789012", &req, "b", "missing"),
4010 "NoSuchKey",
4011 );
4012 }
4013
4014 #[test]
4015 fn head_object_nonexistent_bucket() {
4016 let svc = make_service();
4017 let req = make_request(Method::HEAD, "/nope/key", &[], b"");
4018 assert_aws_err(
4019 svc.head_object("123456789012", &req, "nope", "key"),
4020 "NoSuchBucket",
4021 );
4022 }
4023
4024 #[test]
4025 fn delete_object_nonexistent_bucket() {
4026 let svc = make_service();
4027 let req = make_request(Method::DELETE, "/nope/key", &[], b"");
4028 assert_aws_err(
4029 svc.delete_object("123456789012", &req, "nope", "key"),
4030 "NoSuchBucket",
4031 );
4032 }
4033
4034 #[test]
4035 fn copy_object_source_not_found() {
4036 let svc = make_service();
4037 seed_bucket(&svc, "src-b");
4038 seed_bucket(&svc, "dst-b");
4039 let mut req = make_request(Method::PUT, "/dst-b/copied", &[], b"");
4040 req.headers
4041 .insert("x-amz-copy-source", "src-b/nonexistent".parse().unwrap());
4042 assert_aws_err(
4043 svc.copy_object("123456789012", &req, "dst-b", "copied"),
4044 "NoSuchKey",
4045 );
4046 }
4047
4048 #[test]
4049 fn copy_object_source_bucket_not_found() {
4050 let svc = make_service();
4051 seed_bucket(&svc, "dst-b2");
4052 let mut req = make_request(Method::PUT, "/dst-b2/copied", &[], b"");
4053 req.headers
4054 .insert("x-amz-copy-source", "nope-bucket/key".parse().unwrap());
4055 assert_aws_err(
4056 svc.copy_object("123456789012", &req, "dst-b2", "copied"),
4057 "NoSuchBucket",
4058 );
4059 }
4060
4061 #[test]
4062 fn list_objects_v2_nonexistent_bucket() {
4063 let svc = make_service();
4064 let req = make_request(Method::GET, "/nope", &[("list-type", "2")], b"");
4065 assert_aws_err(
4066 svc.list_objects_v2("123456789012", &req, "nope"),
4067 "NoSuchBucket",
4068 );
4069 }
4070
4071 #[test]
4072 fn list_objects_v2_empty_continuation_token() {
4073 let svc = make_service();
4074 seed_bucket(&svc, "b");
4075 let req = make_request(
4076 Method::GET,
4077 "/b",
4078 &[("list-type", "2"), ("continuation-token", "")],
4079 b"",
4080 );
4081 assert_aws_err(
4082 svc.list_objects_v2("123456789012", &req, "b"),
4083 "InvalidArgument",
4084 );
4085 }
4086
4087 #[test]
4088 fn list_objects_v1_nonexistent_bucket() {
4089 let svc = make_service();
4090 let req = make_request(Method::GET, "/nope", &[], b"");
4091 assert_aws_err(
4092 svc.list_objects_v1("123456789012", &req, "nope"),
4093 "NoSuchBucket",
4094 );
4095 }
4096
4097 #[test]
4098 fn list_object_versions_nonexistent_bucket() {
4099 let svc = make_service();
4100 let req = make_request(Method::GET, "/nope", &[("versions", "")], b"");
4101 assert_aws_err(
4102 svc.list_object_versions("123456789012", &req, "nope"),
4103 "NoSuchBucket",
4104 );
4105 }
4106
4107 #[test]
4110 fn create_multipart_nonexistent_bucket() {
4111 let svc = make_service();
4112 let req = make_request(Method::POST, "/nope/key", &[("uploads", "")], b"");
4113 assert_aws_err(
4114 svc.create_multipart_upload("123456789012", &req, "nope", "key"),
4115 "NoSuchBucket",
4116 );
4117 }
4118
4119 #[tokio::test]
4120 async fn upload_part_nonexistent_upload() {
4121 let svc = make_service();
4122 seed_bucket(&svc, "b");
4123 let req = make_request(
4124 Method::PUT,
4125 "/b/key",
4126 &[("uploadId", "bogus"), ("partNumber", "1")],
4127 b"data",
4128 );
4129 assert_aws_err(
4130 svc.upload_part("123456789012", &req, "b", "key", "bogus", 1)
4131 .await,
4132 "NoSuchUpload",
4133 );
4134 }
4135
4136 #[test]
4137 fn complete_multipart_nonexistent_upload() {
4138 let svc = make_service();
4139 seed_bucket(&svc, "b");
4140 let req = make_request(
4141 Method::POST,
4142 "/b/key",
4143 &[("uploadId", "bogus")],
4144 b"<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>\"abc\"</ETag></Part></CompleteMultipartUpload>",
4145 );
4146 assert_aws_err(
4147 svc.complete_multipart_upload("123456789012", &req, "b", "key", "bogus"),
4148 "NoSuchUpload",
4149 );
4150 }
4151
4152 #[test]
4153 fn abort_multipart_nonexistent_upload() {
4154 let svc = make_service();
4155 seed_bucket(&svc, "b");
4156 assert_aws_err(
4157 svc.abort_multipart_upload("123456789012", "b", "key", "bogus"),
4158 "NoSuchUpload",
4159 );
4160 }
4161
4162 #[test]
4163 fn list_parts_nonexistent_upload() {
4164 let svc = make_service();
4165 seed_bucket(&svc, "b");
4166 let req = make_request(Method::GET, "/b/key", &[("uploadId", "bogus")], b"");
4167 assert_aws_err(
4168 svc.list_parts("123456789012", &req, "b", "key", "bogus"),
4169 "NoSuchUpload",
4170 );
4171 }
4172
4173 #[test]
4176 fn get_bucket_acl_nonexistent() {
4177 let svc = make_service();
4178 let req = make_request(Method::GET, "/nope", &[("acl", "")], b"");
4179 assert_aws_err(
4180 svc.get_bucket_acl("123456789012", &req, "nope"),
4181 "NoSuchBucket",
4182 );
4183 }
4184
4185 #[test]
4186 fn get_bucket_versioning_nonexistent() {
4187 let svc = make_service();
4188 assert_aws_err(
4189 svc.get_bucket_versioning("123456789012", "nope"),
4190 "NoSuchBucket",
4191 );
4192 }
4193
4194 #[test]
4195 fn put_bucket_versioning_nonexistent() {
4196 let svc = make_service();
4197 let req = make_request(
4198 Method::PUT,
4199 "/nope",
4200 &[("versioning", "")],
4201 b"<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>",
4202 );
4203 assert_aws_err(
4204 svc.put_bucket_versioning("123456789012", &req, "nope"),
4205 "NoSuchBucket",
4206 );
4207 }
4208
4209 #[test]
4210 fn get_bucket_location_nonexistent() {
4211 let svc = make_service();
4212 assert_aws_err(
4213 svc.get_bucket_location("123456789012", "nope"),
4214 "NoSuchBucket",
4215 );
4216 }
4217
4218 #[test]
4219 fn get_bucket_lifecycle_nonexistent() {
4220 let svc = make_service();
4221 assert_aws_err(
4222 svc.get_bucket_lifecycle("123456789012", "nope"),
4223 "NoSuchBucket",
4224 );
4225 }
4226
4227 #[test]
4228 fn get_bucket_notification_nonexistent() {
4229 let svc = make_service();
4230 assert_aws_err(
4231 svc.get_bucket_notification("123456789012", "nope"),
4232 "NoSuchBucket",
4233 );
4234 }
4235
4236 #[test]
4237 fn get_bucket_encryption_nonexistent() {
4238 let svc = make_service();
4239 assert_aws_err(
4240 svc.get_bucket_encryption("123456789012", "nope"),
4241 "NoSuchBucket",
4242 );
4243 }
4244
4245 #[test]
4246 fn get_bucket_logging_nonexistent() {
4247 let svc = make_service();
4248 assert_aws_err(
4249 svc.get_bucket_logging("123456789012", "nope"),
4250 "NoSuchBucket",
4251 );
4252 }
4253
4254 #[test]
4255 fn get_object_lock_nonexistent() {
4256 let svc = make_service();
4257 assert_aws_err(
4258 svc.get_object_lock_configuration("123456789012", "nope"),
4259 "NoSuchBucket",
4260 );
4261 }
4262
4263 #[test]
4264 fn get_object_attributes_nonexistent_key() {
4265 let svc = make_service();
4266 seed_bucket(&svc, "b");
4267 let req = make_request(Method::GET, "/b/missing", &[("attributes", "")], b"");
4268 assert_aws_err(
4269 svc.get_object_attributes("123456789012", &req, "b", "missing"),
4270 "NoSuchKey",
4271 );
4272 }
4273
4274 #[test]
4275 fn get_object_attributes_nonexistent_bucket() {
4276 let svc = make_service();
4277 let req = make_request(Method::GET, "/nope/key", &[("attributes", "")], b"");
4278 assert_aws_err(
4279 svc.get_object_attributes("123456789012", &req, "nope", "key"),
4280 "NoSuchBucket",
4281 );
4282 }
4283
4284 #[test]
4285 fn restore_object_nonexistent_bucket() {
4286 let svc = make_service();
4287 let req = make_request(
4288 Method::POST,
4289 "/nope/key",
4290 &[("restore", "")],
4291 b"<RestoreRequest><Days>1</Days></RestoreRequest>",
4292 );
4293 assert_aws_err(
4294 svc.restore_object("123456789012", &req, "nope", "key"),
4295 "NoSuchBucket",
4296 );
4297 }
4298
4299 #[test]
4300 fn get_public_access_block_nonexistent() {
4301 let svc = make_service();
4302 assert_aws_err(
4303 svc.get_public_access_block("123456789012", "nope"),
4304 "NoSuchBucket",
4305 );
4306 }
4307
4308 #[test]
4309 fn get_bucket_policy_nonexistent() {
4310 let svc = make_service();
4311 assert_aws_err(
4312 svc.get_bucket_policy("123456789012", "nope"),
4313 "NoSuchBucket",
4314 );
4315 }
4316
4317 #[test]
4318 fn get_bucket_cors_nonexistent() {
4319 let svc = make_service();
4320 assert_aws_err(svc.get_bucket_cors("123456789012", "nope"), "NoSuchBucket");
4321 }
4322
4323 #[test]
4324 fn get_bucket_tagging_nonexistent() {
4325 let svc = make_service();
4326 let req = make_request(Method::GET, "/nope", &[("tagging", "")], b"");
4327 assert_aws_err(
4328 svc.get_bucket_tagging("123456789012", &req, "nope"),
4329 "NoSuchBucket",
4330 );
4331 }
4332
4333 #[test]
4334 fn get_bucket_website_nonexistent() {
4335 let svc = make_service();
4336 assert_aws_err(
4337 svc.get_bucket_website("123456789012", "nope"),
4338 "NoSuchBucket",
4339 );
4340 }
4341
4342 #[test]
4345 fn put_and_get_object_retention() {
4346 let svc = make_service();
4347 seed_bucket(&svc, "lock-b");
4348 seed_object(&svc, "lock-b", "retained.txt", b"data");
4349
4350 let body = b"<Retention><Mode>GOVERNANCE</Mode><RetainUntilDate>2030-01-01T00:00:00Z</RetainUntilDate></Retention>";
4351 let req = make_request(
4352 Method::PUT,
4353 "/lock-b/retained.txt",
4354 &[("retention", "")],
4355 body,
4356 );
4357 svc.put_object_retention("123456789012", &req, "lock-b", "retained.txt")
4358 .unwrap();
4359
4360 let req = make_request(
4361 Method::GET,
4362 "/lock-b/retained.txt",
4363 &[("retention", "")],
4364 b"",
4365 );
4366 let resp = svc
4367 .get_object_retention("123456789012", &req, "lock-b", "retained.txt")
4368 .unwrap();
4369 let body_str = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4370 assert!(body_str.contains("GOVERNANCE"));
4371 }
4372
4373 #[test]
4374 fn get_object_retention_nonexistent_bucket() {
4375 let svc = make_service();
4376 let req = make_request(Method::GET, "/nope/key", &[("retention", "")], b"");
4377 assert_aws_err(
4378 svc.get_object_retention("123456789012", &req, "nope", "key"),
4379 "NoSuchBucket",
4380 );
4381 }
4382
4383 #[test]
4384 fn get_object_retention_nonexistent_key() {
4385 let svc = make_service();
4386 seed_bucket(&svc, "lock-b2");
4387 let req = make_request(Method::GET, "/lock-b2/missing", &[("retention", "")], b"");
4388 assert_aws_err(
4389 svc.get_object_retention("123456789012", &req, "lock-b2", "missing"),
4390 "NoSuchKey",
4391 );
4392 }
4393
4394 #[test]
4395 fn put_and_get_object_legal_hold() {
4396 let svc = make_service();
4397 seed_bucket(&svc, "hold-b");
4398 seed_object(&svc, "hold-b", "held.txt", b"data");
4399
4400 let body = b"<LegalHold><Status>ON</Status></LegalHold>";
4401 let req = make_request(Method::PUT, "/hold-b/held.txt", &[("legal-hold", "")], body);
4402 svc.put_object_legal_hold("123456789012", &req, "hold-b", "held.txt")
4403 .unwrap();
4404
4405 let req = make_request(Method::GET, "/hold-b/held.txt", &[("legal-hold", "")], b"");
4406 let resp = svc
4407 .get_object_legal_hold("123456789012", &req, "hold-b", "held.txt")
4408 .unwrap();
4409 let body_str = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4410 assert!(body_str.contains("ON"));
4411 }
4412
4413 #[test]
4414 fn get_object_legal_hold_nonexistent() {
4415 let svc = make_service();
4416 let req = make_request(Method::GET, "/nope/key", &[("legal-hold", "")], b"");
4417 assert_aws_err(
4418 svc.get_object_legal_hold("123456789012", &req, "nope", "key"),
4419 "NoSuchBucket",
4420 );
4421 }
4422
4423 #[test]
4426 fn get_object_acl_default() {
4427 let svc = make_service();
4428 seed_bucket(&svc, "acl-b");
4429 seed_object(&svc, "acl-b", "file.txt", b"data");
4430
4431 let req = make_request(Method::GET, "/acl-b/file.txt", &[("acl", "")], b"");
4432 let resp = svc
4433 .get_object_acl("123456789012", &req, "acl-b", "file.txt")
4434 .unwrap();
4435 let body_str = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4436 assert!(body_str.contains("AccessControlPolicy"));
4437 }
4438
4439 #[test]
4440 fn get_object_acl_nonexistent_bucket() {
4441 let svc = make_service();
4442 let req = make_request(Method::GET, "/nope/key", &[("acl", "")], b"");
4443 assert_aws_err(
4444 svc.get_object_acl("123456789012", &req, "nope", "key"),
4445 "NoSuchBucket",
4446 );
4447 }
4448
4449 #[test]
4450 fn get_object_acl_nonexistent_key() {
4451 let svc = make_service();
4452 seed_bucket(&svc, "acl-b2");
4453 let req = make_request(Method::GET, "/acl-b2/missing", &[("acl", "")], b"");
4454 assert_aws_err(
4455 svc.get_object_acl("123456789012", &req, "acl-b2", "missing"),
4456 "NoSuchKey",
4457 );
4458 }
4459
4460 #[test]
4461 fn put_object_acl() {
4462 let svc = make_service();
4463 seed_bucket(&svc, "acl-put-b");
4464 seed_object(&svc, "acl-put-b", "file.txt", b"data");
4465
4466 let acl_xml = b"<AccessControlPolicy><Owner><ID>owner</ID></Owner><AccessControlList><Grant><Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"><ID>owner</ID></Grantee><Permission>FULL_CONTROL</Permission></Grant></AccessControlList></AccessControlPolicy>";
4467 let req = make_request(Method::PUT, "/acl-put-b/file.txt", &[("acl", "")], acl_xml);
4468 svc.put_object_acl("123456789012", &req, "acl-put-b", "file.txt")
4469 .unwrap();
4470 }
4471
4472 #[tokio::test]
4475 async fn put_object_via_handler_and_get_back() {
4476 let svc = make_service();
4477 seed_bucket(&svc, "hp");
4478
4479 let req = make_request(Method::PUT, "/hp/test.txt", &[], b"hello world");
4481 let resp = svc
4482 .put_object("123456789012", &req, "hp", "test.txt")
4483 .await
4484 .unwrap();
4485 assert_eq!(resp.status, StatusCode::OK);
4486
4487 let req = make_request(Method::GET, "/hp/test.txt", &[], b"");
4489 let resp = svc
4490 .get_object("123456789012", &req, "hp", "test.txt")
4491 .unwrap();
4492 assert_eq!(resp.body.expect_bytes(), b"hello world");
4493 }
4494
4495 #[tokio::test]
4496 async fn put_object_with_content_type() {
4497 let svc = make_service();
4498 seed_bucket(&svc, "ct");
4499
4500 let mut req = make_request(Method::PUT, "/ct/doc.json", &[], b"{\"key\":\"val\"}");
4501 req.headers
4502 .insert("content-type", "application/json".parse().unwrap());
4503 svc.put_object("123456789012", &req, "ct", "doc.json")
4504 .await
4505 .unwrap();
4506
4507 let req = make_request(Method::GET, "/ct/doc.json", &[], b"");
4508 let resp = svc
4509 .get_object("123456789012", &req, "ct", "doc.json")
4510 .unwrap();
4511 assert_eq!(resp.content_type, "application/json");
4512 }
4513
4514 #[tokio::test]
4515 async fn put_object_with_metadata() {
4516 let svc = make_service();
4517 seed_bucket(&svc, "meta");
4518
4519 let mut req = make_request(Method::PUT, "/meta/obj", &[], b"data");
4520 req.headers
4521 .insert("x-amz-meta-color", "blue".parse().unwrap());
4522 req.headers
4523 .insert("x-amz-meta-size", "large".parse().unwrap());
4524 svc.put_object("123456789012", &req, "meta", "obj")
4525 .await
4526 .unwrap();
4527
4528 let req = make_request(Method::HEAD, "/meta/obj", &[], b"");
4529 let resp = svc
4530 .head_object("123456789012", &req, "meta", "obj")
4531 .unwrap();
4532 assert!(resp
4533 .headers
4534 .get("x-amz-meta-color")
4535 .is_some_and(|v| v == "blue"));
4536 }
4537
4538 #[tokio::test]
4539 async fn put_object_returns_etag() {
4540 let svc = make_service();
4541 seed_bucket(&svc, "etag");
4542
4543 let req = make_request(Method::PUT, "/etag/f.txt", &[], b"content");
4544 let resp = svc
4545 .put_object("123456789012", &req, "etag", "f.txt")
4546 .await
4547 .unwrap();
4548 assert!(resp.headers.get("etag").is_some());
4549 }
4550
4551 #[tokio::test]
4552 async fn head_object_returns_headers() {
4553 let svc = make_service();
4554 seed_bucket(&svc, "head");
4555
4556 let req = make_request(Method::PUT, "/head/f.txt", &[], b"12345");
4557 svc.put_object("123456789012", &req, "head", "f.txt")
4558 .await
4559 .unwrap();
4560
4561 let req = make_request(Method::HEAD, "/head/f.txt", &[], b"");
4562 let resp = svc
4563 .head_object("123456789012", &req, "head", "f.txt")
4564 .unwrap();
4565 assert_eq!(
4566 resp.headers
4567 .get("content-length")
4568 .unwrap()
4569 .to_str()
4570 .unwrap(),
4571 "5"
4572 );
4573 }
4574
4575 #[tokio::test]
4576 async fn delete_object_via_handler() {
4577 let svc = make_service();
4578 seed_bucket(&svc, "del");
4579
4580 let req = make_request(Method::PUT, "/del/rm.txt", &[], b"bye");
4581 svc.put_object("123456789012", &req, "del", "rm.txt")
4582 .await
4583 .unwrap();
4584
4585 let req = make_request(Method::DELETE, "/del/rm.txt", &[], b"");
4586 svc.delete_object("123456789012", &req, "del", "rm.txt")
4587 .unwrap();
4588
4589 let req = make_request(Method::GET, "/del/rm.txt", &[], b"");
4590 assert_aws_err(
4591 svc.get_object("123456789012", &req, "del", "rm.txt"),
4592 "NoSuchKey",
4593 );
4594 }
4595
4596 #[tokio::test]
4597 async fn copy_object_via_handler() {
4598 let svc = make_service();
4599 seed_bucket(&svc, "cpsrc");
4600 seed_bucket(&svc, "cpdst");
4601
4602 let req = make_request(Method::PUT, "/cpsrc/orig.txt", &[], b"original");
4603 svc.put_object("123456789012", &req, "cpsrc", "orig.txt")
4604 .await
4605 .unwrap();
4606
4607 let mut req = make_request(Method::PUT, "/cpdst/copy.txt", &[], b"");
4608 req.headers
4609 .insert("x-amz-copy-source", "cpsrc/orig.txt".parse().unwrap());
4610 svc.copy_object("123456789012", &req, "cpdst", "copy.txt")
4611 .unwrap();
4612
4613 let req = make_request(Method::GET, "/cpdst/copy.txt", &[], b"");
4614 let resp = svc
4615 .get_object("123456789012", &req, "cpdst", "copy.txt")
4616 .unwrap();
4617 assert_eq!(resp.body.expect_bytes(), b"original");
4618 }
4619
4620 #[tokio::test]
4621 async fn copy_object_within_same_bucket() {
4622 let svc = make_service();
4623 seed_bucket(&svc, "same");
4624
4625 let req = make_request(Method::PUT, "/same/a.txt", &[], b"aaa");
4626 svc.put_object("123456789012", &req, "same", "a.txt")
4627 .await
4628 .unwrap();
4629
4630 let mut req = make_request(Method::PUT, "/same/b.txt", &[], b"");
4631 req.headers
4632 .insert("x-amz-copy-source", "same/a.txt".parse().unwrap());
4633 svc.copy_object("123456789012", &req, "same", "b.txt")
4634 .unwrap();
4635
4636 let req = make_request(Method::GET, "/same/b.txt", &[], b"");
4637 let resp = svc
4638 .get_object("123456789012", &req, "same", "b.txt")
4639 .unwrap();
4640 assert_eq!(resp.body.expect_bytes(), b"aaa");
4641 }
4642
4643 #[tokio::test]
4644 async fn list_objects_v2_via_handler() {
4645 let svc = make_service();
4646 seed_bucket(&svc, "lsv2");
4647
4648 for i in 0..3 {
4649 let key = format!("file{i}.txt");
4650 let req = make_request(Method::PUT, &format!("/lsv2/{key}"), &[], b"data");
4651 svc.put_object("123456789012", &req, "lsv2", &key)
4652 .await
4653 .unwrap();
4654 }
4655
4656 let req = make_request(Method::GET, "/lsv2", &[("list-type", "2")], b"");
4657 let resp = svc.list_objects_v2("123456789012", &req, "lsv2").unwrap();
4658 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4659 assert!(body.contains("<KeyCount>3</KeyCount>"));
4660 }
4661
4662 #[tokio::test]
4663 async fn list_objects_v2_with_prefix() {
4664 let svc = make_service();
4665 seed_bucket(&svc, "pfx");
4666
4667 for key in &["docs/a.txt", "docs/b.txt", "images/c.png"] {
4668 let req = make_request(Method::PUT, &format!("/pfx/{key}"), &[], b"x");
4669 svc.put_object("123456789012", &req, "pfx", key)
4670 .await
4671 .unwrap();
4672 }
4673
4674 let req = make_request(
4675 Method::GET,
4676 "/pfx",
4677 &[("list-type", "2"), ("prefix", "docs/")],
4678 b"",
4679 );
4680 let resp = svc.list_objects_v2("123456789012", &req, "pfx").unwrap();
4681 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4682 assert!(body.contains("<KeyCount>2</KeyCount>"));
4683 }
4684
4685 #[tokio::test]
4686 async fn list_objects_v2_with_delimiter() {
4687 let svc = make_service();
4688 seed_bucket(&svc, "dlm");
4689
4690 for key in &["a/1.txt", "a/2.txt", "b/3.txt", "root.txt"] {
4691 let req = make_request(Method::PUT, &format!("/dlm/{key}"), &[], b"x");
4692 svc.put_object("123456789012", &req, "dlm", key)
4693 .await
4694 .unwrap();
4695 }
4696
4697 let req = make_request(
4698 Method::GET,
4699 "/dlm",
4700 &[("list-type", "2"), ("delimiter", "/")],
4701 b"",
4702 );
4703 let resp = svc.list_objects_v2("123456789012", &req, "dlm").unwrap();
4704 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4705 assert!(body.contains("<CommonPrefixes>"));
4707 assert!(body.contains("root.txt"));
4709 }
4710
4711 #[tokio::test]
4712 async fn list_objects_v1_via_handler() {
4713 let svc = make_service();
4714 seed_bucket(&svc, "lsv1");
4715
4716 let req = make_request(Method::PUT, "/lsv1/test.txt", &[], b"data");
4717 svc.put_object("123456789012", &req, "lsv1", "test.txt")
4718 .await
4719 .unwrap();
4720
4721 let req = make_request(Method::GET, "/lsv1", &[], b"");
4722 let resp = svc.list_objects_v1("123456789012", &req, "lsv1").unwrap();
4723 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4724 assert!(body.contains("<Key>test.txt</Key>"));
4725 }
4726
4727 #[tokio::test]
4728 async fn delete_objects_batch() {
4729 let svc = make_service();
4730 seed_bucket(&svc, "bdel");
4731
4732 for i in 0..3 {
4733 let key = format!("d{i}.txt");
4734 let req = make_request(Method::PUT, &format!("/bdel/{key}"), &[], b"x");
4735 svc.put_object("123456789012", &req, "bdel", &key)
4736 .await
4737 .unwrap();
4738 }
4739
4740 let delete_xml = b"<Delete><Object><Key>d0.txt</Key></Object><Object><Key>d1.txt</Key></Object></Delete>";
4741 let req = make_request(Method::POST, "/bdel", &[("delete", "")], delete_xml);
4742 let resp = svc.delete_objects("123456789012", &req, "bdel").unwrap();
4743 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4744 assert!(body.contains("<Deleted>"));
4745
4746 let req = make_request(Method::GET, "/bdel/d2.txt", &[], b"");
4748 svc.get_object("123456789012", &req, "bdel", "d2.txt")
4749 .unwrap();
4750 }
4751
4752 #[tokio::test]
4753 async fn put_object_overwrites_existing() {
4754 let svc = make_service();
4755 seed_bucket(&svc, "ow");
4756
4757 let req = make_request(Method::PUT, "/ow/f.txt", &[], b"version1");
4758 svc.put_object("123456789012", &req, "ow", "f.txt")
4759 .await
4760 .unwrap();
4761
4762 let req = make_request(Method::PUT, "/ow/f.txt", &[], b"version2");
4763 svc.put_object("123456789012", &req, "ow", "f.txt")
4764 .await
4765 .unwrap();
4766
4767 let req = make_request(Method::GET, "/ow/f.txt", &[], b"");
4768 let resp = svc.get_object("123456789012", &req, "ow", "f.txt").unwrap();
4769 assert_eq!(resp.body.expect_bytes(), b"version2");
4770 }
4771
4772 #[tokio::test]
4773 async fn get_object_attributes_via_handler() {
4774 let svc = make_service();
4775 seed_bucket(&svc, "attr");
4776
4777 let req = make_request(Method::PUT, "/attr/f.txt", &[], b"content");
4778 svc.put_object("123456789012", &req, "attr", "f.txt")
4779 .await
4780 .unwrap();
4781
4782 let mut req = make_request(Method::GET, "/attr/f.txt", &[], b"");
4783 req.headers.insert(
4784 "x-amz-object-attributes",
4785 "ETag,ObjectSize".parse().unwrap(),
4786 );
4787 let resp = svc
4788 .get_object_attributes("123456789012", &req, "attr", "f.txt")
4789 .unwrap();
4790 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4791 assert!(body.contains("<ObjectSize>"));
4792 }
4793
4794 #[tokio::test]
4797 async fn multipart_upload_lifecycle() {
4798 let svc = make_service();
4799 seed_bucket(&svc, "mp");
4800
4801 let req = make_request(Method::POST, "/mp/big.bin", &[("uploads", "")], b"");
4803 let resp = svc
4804 .create_multipart_upload("123456789012", &req, "mp", "big.bin")
4805 .unwrap();
4806 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4807 let uid_start = body.find("<UploadId>").unwrap() + 10;
4808 let uid_end = body.find("</UploadId>").unwrap();
4809 let upload_id = &body[uid_start..uid_end];
4810
4811 let big_data = vec![b'A'; 5 * 1024 * 1024 + 1];
4813 let req = make_request(Method::PUT, "/mp/big.bin", &[], &big_data);
4814 let resp = svc
4815 .upload_part("123456789012", &req, "mp", "big.bin", upload_id, 1)
4816 .await
4817 .unwrap();
4818 let etag1 = resp
4819 .headers
4820 .get("etag")
4821 .unwrap()
4822 .to_str()
4823 .unwrap()
4824 .to_string();
4825
4826 let req = make_request(Method::PUT, "/mp/big.bin", &[], b"part2-data");
4828 let resp = svc
4829 .upload_part("123456789012", &req, "mp", "big.bin", upload_id, 2)
4830 .await
4831 .unwrap();
4832 let etag2 = resp
4833 .headers
4834 .get("etag")
4835 .unwrap()
4836 .to_str()
4837 .unwrap()
4838 .to_string();
4839
4840 let req = make_request(Method::GET, "/mp/big.bin", &[], b"");
4842 let resp = svc
4843 .list_parts("123456789012", &req, "mp", "big.bin", upload_id)
4844 .unwrap();
4845 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4846 assert!(body.contains("<Part>"));
4847
4848 let complete_xml = format!(
4850 "<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>{etag1}</ETag></Part><Part><PartNumber>2</PartNumber><ETag>{etag2}</ETag></Part></CompleteMultipartUpload>"
4851 );
4852 let req = make_request(Method::POST, "/mp/big.bin", &[], complete_xml.as_bytes());
4853 svc.complete_multipart_upload("123456789012", &req, "mp", "big.bin", upload_id)
4854 .unwrap();
4855
4856 let req = make_request(Method::GET, "/mp/big.bin", &[], b"");
4858 let resp = svc
4859 .get_object("123456789012", &req, "mp", "big.bin")
4860 .unwrap();
4861 let body = resp.body.expect_bytes();
4862 assert!(body.len() > 5 * 1024 * 1024);
4864 }
4865
4866 #[test]
4867 fn multipart_upload_abort() {
4868 let svc = make_service();
4869 seed_bucket(&svc, "mpa");
4870
4871 let req = make_request(Method::POST, "/mpa/abort.bin", &[("uploads", "")], b"");
4872 let resp = svc
4873 .create_multipart_upload("123456789012", &req, "mpa", "abort.bin")
4874 .unwrap();
4875 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4876 let uid_start = body.find("<UploadId>").unwrap() + 10;
4877 let uid_end = body.find("</UploadId>").unwrap();
4878 let upload_id = body[uid_start..uid_end].to_string();
4879
4880 svc.abort_multipart_upload("123456789012", "mpa", "abort.bin", &upload_id)
4881 .unwrap();
4882
4883 let req = make_request(Method::GET, "/mpa/abort.bin", &[], b"");
4885 assert_aws_err(
4886 svc.list_parts("123456789012", &req, "mpa", "abort.bin", &upload_id),
4887 "NoSuchUpload",
4888 );
4889 }
4890
4891 #[test]
4892 fn list_multipart_uploads() {
4893 let svc = make_service();
4894 seed_bucket(&svc, "mpl");
4895
4896 let req = make_request(Method::POST, "/mpl/f1.bin", &[("uploads", "")], b"");
4897 svc.create_multipart_upload("123456789012", &req, "mpl", "f1.bin")
4898 .unwrap();
4899
4900 let resp = svc.list_multipart_uploads("123456789012", "mpl").unwrap();
4901 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4902 assert!(body.contains("<Upload>"));
4903 assert!(body.contains("f1.bin"));
4904 }
4905
4906 #[test]
4909 fn put_and_get_bucket_versioning() {
4910 let svc = make_service();
4911 seed_bucket(&svc, "ver");
4912
4913 let req = make_request(
4914 Method::PUT,
4915 "/ver",
4916 &[("versioning", "")],
4917 b"<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>",
4918 );
4919 svc.put_bucket_versioning("123456789012", &req, "ver")
4920 .unwrap();
4921
4922 let resp = svc.get_bucket_versioning("123456789012", "ver").unwrap();
4923 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4924 assert!(body.contains("Enabled"));
4925 }
4926
4927 #[test]
4928 fn put_and_get_bucket_lifecycle() {
4929 let svc = make_service();
4930 seed_bucket(&svc, "lc");
4931
4932 let xml = b"<LifecycleConfiguration><Rule><ID>expire</ID><Filter><Prefix></Prefix></Filter><Status>Enabled</Status><Expiration><Days>30</Days></Expiration></Rule></LifecycleConfiguration>";
4933 let req = make_request(Method::PUT, "/lc", &[("lifecycle", "")], xml);
4934 svc.put_bucket_lifecycle("123456789012", &req, "lc")
4935 .unwrap();
4936
4937 let resp = svc.get_bucket_lifecycle("123456789012", "lc").unwrap();
4938 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4939 assert!(body.contains("<Rule>"));
4940 }
4941
4942 #[test]
4943 fn put_and_get_bucket_notification() {
4944 let svc = make_service();
4945 seed_bucket(&svc, "notif");
4946
4947 let xml = b"<NotificationConfiguration></NotificationConfiguration>";
4948 let req = make_request(Method::PUT, "/notif", &[("notification", "")], xml);
4949 svc.put_bucket_notification("123456789012", &req, "notif")
4950 .unwrap();
4951
4952 let resp = svc
4953 .get_bucket_notification("123456789012", "notif")
4954 .unwrap();
4955 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4956 assert!(body.contains("NotificationConfiguration"));
4957 }
4958
4959 #[test]
4960 fn put_and_get_and_delete_bucket_encryption() {
4961 let svc = make_service();
4962 seed_bucket(&svc, "enc");
4963
4964 let xml = b"<ServerSideEncryptionConfiguration><Rule><ApplyServerSideEncryptionByDefault><SSEAlgorithm>AES256</SSEAlgorithm></ApplyServerSideEncryptionByDefault></Rule></ServerSideEncryptionConfiguration>";
4965 let req = make_request(Method::PUT, "/enc", &[("encryption", "")], xml);
4966 svc.put_bucket_encryption("123456789012", &req, "enc")
4967 .unwrap();
4968
4969 let resp = svc.get_bucket_encryption("123456789012", "enc").unwrap();
4970 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4971 assert!(body.contains("AES256"));
4972
4973 svc.delete_bucket_encryption("123456789012", "enc").unwrap();
4974 }
4975
4976 #[test]
4977 fn bucket_logging_put_and_get() {
4978 let svc = make_service();
4979 seed_bucket(&svc, "logging-b");
4980
4981 let xml = b"<BucketLoggingStatus><LoggingEnabled><TargetBucket>logging-b</TargetBucket><TargetPrefix>logs/</TargetPrefix></LoggingEnabled></BucketLoggingStatus>";
4982 let req = make_request(Method::PUT, "/logging-b", &[("logging", "")], xml);
4983 svc.put_bucket_logging("123456789012", &req, "logging-b")
4984 .unwrap();
4985
4986 let resp = svc.get_bucket_logging("123456789012", "logging-b").unwrap();
4987 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
4988 assert!(body.contains("LoggingEnabled"));
4989 }
4990
4991 #[tokio::test]
4994 async fn versioned_put_and_get() {
4995 let svc = make_service();
4996 seed_bucket(&svc, "vb");
4997
4998 let req = make_request(
5000 Method::PUT,
5001 "/vb",
5002 &[("versioning", "")],
5003 b"<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>",
5004 );
5005 svc.put_bucket_versioning("123456789012", &req, "vb")
5006 .unwrap();
5007
5008 let req = make_request(Method::PUT, "/vb/key", &[], b"version1");
5010 let resp = svc
5011 .put_object("123456789012", &req, "vb", "key")
5012 .await
5013 .unwrap();
5014 let v1 = resp
5015 .headers
5016 .get("x-amz-version-id")
5017 .map(|h| h.to_str().unwrap().to_string());
5018 assert!(v1.is_some());
5019
5020 let req = make_request(Method::PUT, "/vb/key", &[], b"version2");
5022 let resp = svc
5023 .put_object("123456789012", &req, "vb", "key")
5024 .await
5025 .unwrap();
5026 let _v2 = resp
5027 .headers
5028 .get("x-amz-version-id")
5029 .map(|h| h.to_str().unwrap().to_string());
5030
5031 let req = make_request(Method::GET, "/vb/key", &[], b"");
5033 let resp = svc.get_object("123456789012", &req, "vb", "key").unwrap();
5034 assert_eq!(resp.body.expect_bytes(), b"version2");
5035
5036 let req = make_request(Method::GET, "/vb", &[("versions", "")], b"");
5038 let resp = svc
5039 .list_object_versions("123456789012", &req, "vb")
5040 .unwrap();
5041 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5042 assert!(body.contains("<Version>"));
5043 }
5044
5045 #[tokio::test]
5048 async fn get_object_if_match_succeeds() {
5049 let svc = make_service();
5050 seed_bucket(&svc, "cond");
5051
5052 let req = make_request(Method::PUT, "/cond/f.txt", &[], b"data");
5053 let resp = svc
5054 .put_object("123456789012", &req, "cond", "f.txt")
5055 .await
5056 .unwrap();
5057 let etag = resp
5058 .headers
5059 .get("etag")
5060 .unwrap()
5061 .to_str()
5062 .unwrap()
5063 .to_string();
5064
5065 let mut req = make_request(Method::GET, "/cond/f.txt", &[], b"");
5066 req.headers.insert("if-match", etag.parse().unwrap());
5067 let resp = svc
5068 .get_object("123456789012", &req, "cond", "f.txt")
5069 .unwrap();
5070 assert_eq!(resp.body.expect_bytes(), b"data");
5071 }
5072
5073 #[tokio::test]
5074 async fn get_object_if_match_fails() {
5075 let svc = make_service();
5076 seed_bucket(&svc, "cond2");
5077
5078 let req = make_request(Method::PUT, "/cond2/f.txt", &[], b"data");
5079 svc.put_object("123456789012", &req, "cond2", "f.txt")
5080 .await
5081 .unwrap();
5082
5083 let mut req = make_request(Method::GET, "/cond2/f.txt", &[], b"");
5084 req.headers
5085 .insert("if-match", "\"wrong-etag\"".parse().unwrap());
5086 assert_aws_err(
5087 svc.get_object("123456789012", &req, "cond2", "f.txt"),
5088 "PreconditionFailed",
5089 );
5090 }
5091
5092 #[tokio::test]
5093 async fn get_object_if_none_match_returns_304() {
5094 let svc = make_service();
5095 seed_bucket(&svc, "cond3");
5096
5097 let req = make_request(Method::PUT, "/cond3/f.txt", &[], b"data");
5098 let resp = svc
5099 .put_object("123456789012", &req, "cond3", "f.txt")
5100 .await
5101 .unwrap();
5102 let etag = resp
5103 .headers
5104 .get("etag")
5105 .unwrap()
5106 .to_str()
5107 .unwrap()
5108 .to_string();
5109
5110 let mut req = make_request(Method::GET, "/cond3/f.txt", &[], b"");
5111 req.headers.insert("if-none-match", etag.parse().unwrap());
5112 let err = svc.get_object("123456789012", &req, "cond3", "f.txt");
5113 assert!(err.is_err());
5115 }
5116
5117 #[tokio::test]
5120 async fn put_object_if_none_match_prevents_overwrite() {
5121 let svc = make_service();
5122 seed_bucket(&svc, "cnm");
5123
5124 let req = make_request(Method::PUT, "/cnm/f.txt", &[], b"first");
5125 svc.put_object("123456789012", &req, "cnm", "f.txt")
5126 .await
5127 .unwrap();
5128
5129 let mut req = make_request(Method::PUT, "/cnm/f.txt", &[], b"second");
5131 req.headers.insert("if-none-match", "*".parse().unwrap());
5132 assert_aws_err(
5133 svc.put_object("123456789012", &req, "cnm", "f.txt").await,
5134 "PreconditionFailed",
5135 );
5136 }
5137
5138 #[tokio::test]
5141 async fn put_object_with_storage_class() {
5142 let svc = make_service();
5143 seed_bucket(&svc, "sc");
5144
5145 let mut req = make_request(Method::PUT, "/sc/f.txt", &[], b"data");
5146 req.headers
5147 .insert("x-amz-storage-class", "GLACIER".parse().unwrap());
5148 svc.put_object("123456789012", &req, "sc", "f.txt")
5149 .await
5150 .unwrap();
5151
5152 let req = make_request(Method::HEAD, "/sc/f.txt", &[], b"");
5153 let resp = svc
5154 .head_object("123456789012", &req, "sc", "f.txt")
5155 .unwrap();
5156 assert_eq!(
5157 resp.headers
5158 .get("x-amz-storage-class")
5159 .unwrap()
5160 .to_str()
5161 .unwrap(),
5162 "GLACIER"
5163 );
5164 }
5165
5166 #[tokio::test]
5169 async fn delete_versioned_object_creates_marker() {
5170 let svc = make_service();
5171 seed_bucket(&svc, "dv");
5172
5173 let req = make_request(
5174 Method::PUT,
5175 "/dv",
5176 &[("versioning", "")],
5177 b"<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>",
5178 );
5179 svc.put_bucket_versioning("123456789012", &req, "dv")
5180 .unwrap();
5181
5182 let req = make_request(Method::PUT, "/dv/key", &[], b"data");
5183 svc.put_object("123456789012", &req, "dv", "key")
5184 .await
5185 .unwrap();
5186
5187 let req = make_request(Method::DELETE, "/dv/key", &[], b"");
5188 let resp = svc
5189 .delete_object("123456789012", &req, "dv", "key")
5190 .unwrap();
5191 assert!(resp.headers.get("x-amz-delete-marker").is_some());
5192
5193 let req = make_request(Method::GET, "/dv/key", &[], b"");
5195 assert_aws_err(
5196 svc.get_object("123456789012", &req, "dv", "key"),
5197 "NoSuchKey",
5198 );
5199 }
5200
5201 #[tokio::test]
5204 async fn copy_object_with_metadata_replace() {
5205 let svc = make_service();
5206 seed_bucket(&svc, "cpm");
5207
5208 let mut req = make_request(Method::PUT, "/cpm/src", &[], b"data");
5209 req.headers
5210 .insert("x-amz-meta-original", "yes".parse().unwrap());
5211 svc.put_object("123456789012", &req, "cpm", "src")
5212 .await
5213 .unwrap();
5214
5215 let mut req = make_request(Method::PUT, "/cpm/dst", &[], b"");
5216 req.headers
5217 .insert("x-amz-copy-source", "cpm/src".parse().unwrap());
5218 req.headers
5219 .insert("x-amz-metadata-directive", "REPLACE".parse().unwrap());
5220 req.headers
5221 .insert("x-amz-meta-new-key", "new-val".parse().unwrap());
5222 svc.copy_object("123456789012", &req, "cpm", "dst").unwrap();
5223
5224 let req = make_request(Method::HEAD, "/cpm/dst", &[], b"");
5225 let resp = svc.head_object("123456789012", &req, "cpm", "dst").unwrap();
5226 assert!(resp
5227 .headers
5228 .get("x-amz-meta-new-key")
5229 .is_some_and(|v| v == "new-val"));
5230 assert!(resp.headers.get("x-amz-meta-original").is_none());
5232 }
5233
5234 #[tokio::test]
5237 async fn list_objects_v2_with_max_keys() {
5238 let svc = make_service();
5239 seed_bucket(&svc, "pg");
5240
5241 for i in 0..5 {
5242 let key = format!("k{i}");
5243 let req = make_request(Method::PUT, &format!("/pg/{key}"), &[], b"x");
5244 svc.put_object("123456789012", &req, "pg", &key)
5245 .await
5246 .unwrap();
5247 }
5248
5249 let req = make_request(
5250 Method::GET,
5251 "/pg",
5252 &[("list-type", "2"), ("max-keys", "2")],
5253 b"",
5254 );
5255 let resp = svc.list_objects_v2("123456789012", &req, "pg").unwrap();
5256 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5257 assert!(body.contains("<IsTruncated>true</IsTruncated>"));
5258 assert!(body.contains("<MaxKeys>2</MaxKeys>"));
5259 }
5260
5261 #[test]
5264 fn list_buckets_empty_account() {
5265 let svc = make_service();
5266 let req = make_request(Method::GET, "/", &[], b"");
5267 let resp = svc.list_buckets("123456789012", &req).unwrap();
5268 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5269 assert!(body.contains("<ListAllMyBucketsResult"));
5270 assert!(body.contains("<Owner><ID>123456789012</ID>"));
5271 assert!(body.contains("<Buckets></Buckets>"));
5272 }
5273
5274 #[test]
5275 fn list_buckets_sorted_by_name() {
5276 let svc = make_service();
5277 seed_bucket(&svc, "zeta");
5278 seed_bucket(&svc, "alpha");
5279 seed_bucket(&svc, "middle");
5280
5281 let req = make_request(Method::GET, "/", &[], b"");
5282 let resp = svc.list_buckets("123456789012", &req).unwrap();
5283 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5284 let a = body.find("alpha").unwrap();
5285 let m = body.find("middle").unwrap();
5286 let z = body.find("zeta").unwrap();
5287 assert!(a < m && m < z, "buckets must be sorted");
5288 }
5289
5290 fn seed_bucket_in_region(svc: &S3Service, name: &str, region: &str) {
5291 let mut mas = svc.state.write();
5292 let state = mas.default_mut();
5293 state
5294 .buckets
5295 .insert(name.to_string(), S3Bucket::new(name, region, "owner"));
5296 }
5297
5298 #[test]
5299 fn list_buckets_includes_bucket_region() {
5300 let svc = make_service();
5301 seed_bucket(&svc, "alpha");
5302
5303 let req = make_request(Method::GET, "/", &[], b"");
5304 let resp = svc.list_buckets("123456789012", &req).unwrap();
5305 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5306 assert!(
5307 body.contains("<BucketRegion>us-east-1</BucketRegion>"),
5308 "response should include BucketRegion per-bucket: {body}"
5309 );
5310 }
5311
5312 #[test]
5313 fn list_buckets_filter_by_bucket_region() {
5314 let svc = make_service();
5315 seed_bucket_in_region(&svc, "east-bucket", "us-east-1");
5316 seed_bucket_in_region(&svc, "west-bucket", "us-west-2");
5317
5318 let req = make_request(Method::GET, "/", &[("bucket-region", "us-west-2")], b"");
5319 let resp = svc.list_buckets("123456789012", &req).unwrap();
5320 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5321 assert!(body.contains("west-bucket"));
5322 assert!(!body.contains("east-bucket"));
5323 assert!(body.contains("<BucketRegion>us-west-2</BucketRegion>"));
5324 }
5325
5326 #[test]
5327 fn list_buckets_filter_by_prefix() {
5328 let svc = make_service();
5329 seed_bucket(&svc, "foo-1");
5330 seed_bucket(&svc, "foo-2");
5331 seed_bucket(&svc, "bar");
5332
5333 let req = make_request(Method::GET, "/", &[("prefix", "foo-")], b"");
5334 let resp = svc.list_buckets("123456789012", &req).unwrap();
5335 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5336 assert!(body.contains("foo-1"));
5337 assert!(body.contains("foo-2"));
5338 assert!(!body.contains("<Name>bar</Name>"));
5339 assert!(body.contains("<Prefix>foo-</Prefix>"));
5340 }
5341
5342 #[test]
5343 fn list_buckets_max_buckets_paginates() {
5344 let svc = make_service();
5345 for n in &["a", "b", "c", "d", "e"] {
5346 seed_bucket(&svc, n);
5347 }
5348
5349 let req = make_request(Method::GET, "/", &[("max-buckets", "2")], b"");
5350 let resp = svc.list_buckets("123456789012", &req).unwrap();
5351 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5352 assert!(body.contains("<Name>a</Name>"));
5353 assert!(body.contains("<Name>b</Name>"));
5354 assert!(!body.contains("<Name>c</Name>"));
5355 assert!(body.contains("<ContinuationToken>"));
5356 }
5357
5358 #[test]
5359 fn list_buckets_continuation_token_resumes() {
5360 let svc = make_service();
5361 for n in &["a", "b", "c", "d", "e"] {
5362 seed_bucket(&svc, n);
5363 }
5364
5365 let req = make_request(Method::GET, "/", &[("max-buckets", "2")], b"");
5366 let resp = svc.list_buckets("123456789012", &req).unwrap();
5367 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5368 let start = body.find("<ContinuationToken>").unwrap() + "<ContinuationToken>".len();
5369 let end = body.find("</ContinuationToken>").unwrap();
5370 let token = body[start..end].to_string();
5371
5372 let req2 = make_request(
5373 Method::GET,
5374 "/",
5375 &[("max-buckets", "2"), ("continuation-token", &token)],
5376 b"",
5377 );
5378 let resp2 = svc.list_buckets("123456789012", &req2).unwrap();
5379 let body2 = std::str::from_utf8(resp2.body.expect_bytes()).unwrap();
5380 assert!(body2.contains("<Name>c</Name>"));
5381 assert!(body2.contains("<Name>d</Name>"));
5382 assert!(!body2.contains("<Name>a</Name>"));
5383 assert!(!body2.contains("<Name>b</Name>"));
5384 assert!(body2.contains("<ContinuationToken>"));
5386
5387 let start = body2.find("<ContinuationToken>").unwrap() + "<ContinuationToken>".len();
5389 let end = body2.find("</ContinuationToken>").unwrap();
5390 let token2 = body2[start..end].to_string();
5391 let req3 = make_request(
5392 Method::GET,
5393 "/",
5394 &[("max-buckets", "2"), ("continuation-token", &token2)],
5395 b"",
5396 );
5397 let resp3 = svc.list_buckets("123456789012", &req3).unwrap();
5398 let body3 = std::str::from_utf8(resp3.body.expect_bytes()).unwrap();
5399 assert!(body3.contains("<Name>e</Name>"));
5400 assert!(!body3.contains("<ContinuationToken>"));
5401 }
5402
5403 #[test]
5404 fn list_buckets_invalid_max_buckets_errors() {
5405 let svc = make_service();
5406 let req = make_request(Method::GET, "/", &[("max-buckets", "0")], b"");
5407 assert_aws_err(svc.list_buckets("123456789012", &req), "InvalidArgument");
5408
5409 let req2 = make_request(Method::GET, "/", &[("max-buckets", "20000")], b"");
5410 assert_aws_err(svc.list_buckets("123456789012", &req2), "InvalidArgument");
5411
5412 let req3 = make_request(Method::GET, "/", &[("max-buckets", "abc")], b"");
5413 assert_aws_err(svc.list_buckets("123456789012", &req3), "InvalidArgument");
5414 }
5415
5416 #[test]
5417 fn list_buckets_invalid_continuation_token_errors() {
5418 let svc = make_service();
5419 let req = make_request(
5420 Method::GET,
5421 "/",
5422 &[("continuation-token", "!!!notb64!!!")],
5423 b"",
5424 );
5425 assert_aws_err(svc.list_buckets("123456789012", &req), "InvalidArgument");
5426
5427 let req2 = make_request(Method::GET, "/", &[("continuation-token", "")], b"");
5428 assert_aws_err(svc.list_buckets("123456789012", &req2), "InvalidArgument");
5429 }
5430
5431 #[test]
5432 fn create_bucket_invalid_name_errors() {
5433 let svc = make_service();
5434 let req = make_request(Method::PUT, "/AB", &[], b"");
5435 assert_aws_err(
5436 svc.create_bucket("123456789012", &req, "AB"),
5437 "InvalidBucketName",
5438 );
5439 }
5440
5441 #[test]
5442 fn create_bucket_idempotent_same_region_us_east_1() {
5443 let svc = make_service();
5444 let req = make_request(Method::PUT, "/idem", &[], b"");
5445 svc.create_bucket("123456789012", &req, "idem").unwrap();
5446 let resp = svc.create_bucket("123456789012", &req, "idem").unwrap();
5447 assert_eq!(resp.status, StatusCode::OK);
5448 }
5449
5450 #[test]
5451 fn create_bucket_already_owned_other_region() {
5452 let svc = make_service();
5453 let mut req = make_request(
5454 Method::PUT,
5455 "/bk1",
5456 &[],
5457 b"<CreateBucketConfiguration><LocationConstraint>eu-west-1</LocationConstraint></CreateBucketConfiguration>",
5458 );
5459 req.region = "eu-west-1".to_string();
5460 svc.create_bucket("123456789012", &req, "bk1").unwrap();
5461 assert_aws_err(
5462 svc.create_bucket("123456789012", &req, "bk1"),
5463 "BucketAlreadyOwnedByYou",
5464 );
5465 }
5466
5467 #[test]
5468 fn create_bucket_us_east_1_with_explicit_constraint_invalid() {
5469 let svc = make_service();
5470 let req = make_request(
5471 Method::PUT,
5472 "/bk2",
5473 &[],
5474 b"<CreateBucketConfiguration><LocationConstraint>us-east-1</LocationConstraint></CreateBucketConfiguration>",
5475 );
5476 assert_aws_err(
5477 svc.create_bucket("123456789012", &req, "bk2"),
5478 "InvalidLocationConstraint",
5479 );
5480 }
5481
5482 #[test]
5483 fn create_bucket_constraint_mismatch_region_errors() {
5484 let svc = make_service();
5485 let mut req = make_request(
5486 Method::PUT,
5487 "/bk3",
5488 &[],
5489 b"<CreateBucketConfiguration><LocationConstraint>us-west-2</LocationConstraint></CreateBucketConfiguration>",
5490 );
5491 req.region = "eu-west-1".to_string();
5492 assert_aws_err(
5493 svc.create_bucket("123456789012", &req, "bk3"),
5494 "IllegalLocationConstraintException",
5495 );
5496 }
5497
5498 #[test]
5499 fn create_bucket_missing_constraint_in_non_default_region_errors() {
5500 let svc = make_service();
5501 let mut req = make_request(Method::PUT, "/bk4", &[], b"");
5502 req.region = "eu-west-1".to_string();
5503 assert_aws_err(
5504 svc.create_bucket("123456789012", &req, "bk4"),
5505 "IllegalLocationConstraintException",
5506 );
5507 }
5508
5509 #[test]
5510 fn create_bucket_invalid_region_constraint() {
5511 let svc = make_service();
5512 let req = make_request(
5513 Method::PUT,
5514 "/bk5",
5515 &[],
5516 b"<CreateBucketConfiguration><LocationConstraint>not-a-region</LocationConstraint></CreateBucketConfiguration>",
5517 );
5518 assert_aws_err(
5519 svc.create_bucket("123456789012", &req, "bk5"),
5520 "InvalidLocationConstraint",
5521 );
5522 }
5523
5524 #[test]
5525 fn create_bucket_with_us_east_1_constraint_when_region_not_matching() {
5526 let svc = make_service();
5527 let mut req = make_request(
5528 Method::PUT,
5529 "/bk6",
5530 &[],
5531 b"<CreateBucketConfiguration><LocationConstraint>us-east-1</LocationConstraint></CreateBucketConfiguration>",
5532 );
5533 req.region = "eu-west-1".to_string();
5534 assert_aws_err(
5535 svc.create_bucket("123456789012", &req, "bk6"),
5536 "IllegalLocationConstraintException",
5537 );
5538 }
5539
5540 #[test]
5541 fn create_bucket_with_object_lock_enables_versioning() {
5542 let svc = make_service();
5543 let mut req = make_request(Method::PUT, "/olb", &[], b"");
5544 req.headers
5545 .insert("x-amz-bucket-object-lock-enabled", "true".parse().unwrap());
5546 svc.create_bucket("123456789012", &req, "olb").unwrap();
5547 let accts = svc.state.read();
5548 let state = accts.get("123456789012").unwrap();
5549 let b = state.buckets.get("olb").unwrap();
5550 assert_eq!(b.versioning.as_deref(), Some("Enabled"));
5551 assert!(b
5552 .object_lock_config
5553 .as_deref()
5554 .unwrap_or("")
5555 .contains("ObjectLockEnabled"));
5556 }
5557
5558 #[test]
5559 fn create_bucket_with_object_ownership_header() {
5560 let svc = make_service();
5561 let mut req = make_request(Method::PUT, "/own", &[], b"");
5562 req.headers.insert(
5563 "x-amz-object-ownership",
5564 "BucketOwnerEnforced".parse().unwrap(),
5565 );
5566 svc.create_bucket("123456789012", &req, "own").unwrap();
5567 let accts = svc.state.read();
5568 let state = accts.get("123456789012").unwrap();
5569 let b = state.buckets.get("own").unwrap();
5570 assert!(b
5571 .ownership_controls
5572 .as_deref()
5573 .unwrap_or("")
5574 .contains("BucketOwnerEnforced"));
5575 }
5576
5577 #[test]
5578 fn create_bucket_with_canned_acl_public_read() {
5579 let svc = make_service();
5580 let mut req = make_request(Method::PUT, "/prb", &[], b"");
5581 req.headers
5582 .insert("x-amz-acl", "public-read".parse().unwrap());
5583 svc.create_bucket("123456789012", &req, "prb").unwrap();
5584 let accts = svc.state.read();
5585 let state = accts.get("123456789012").unwrap();
5586 let b = state.buckets.get("prb").unwrap();
5587 assert!(!b.acl_grants.is_empty());
5588 }
5589
5590 #[test]
5591 fn delete_bucket_nonexistent_errors() {
5592 let svc = make_service();
5593 let req = make_request(Method::DELETE, "/nope", &[], b"");
5594 assert_aws_err(
5595 svc.delete_bucket("123456789012", &req, "nope"),
5596 "NoSuchBucket",
5597 );
5598 }
5599
5600 #[test]
5601 fn delete_bucket_not_empty_errors() {
5602 let svc = make_service();
5603 seed_bucket(&svc, "full");
5604 seed_object(&svc, "full", "k", b"x");
5605 let req = make_request(Method::DELETE, "/full", &[], b"");
5606 assert_aws_err(
5607 svc.delete_bucket("123456789012", &req, "full"),
5608 "BucketNotEmpty",
5609 );
5610 }
5611
5612 #[test]
5613 fn delete_bucket_with_versions_not_empty_errors() {
5614 let svc = make_service();
5615 seed_bucket(&svc, "ver");
5616 {
5617 let mut mas = svc.state.write();
5618 let state = mas.default_mut();
5619 let b = state.buckets.get_mut("ver").unwrap();
5620 b.object_versions.insert(
5621 "k".to_string(),
5622 vec![S3Object {
5623 key: "k".to_string(),
5624 body: fakecloud_persistence::BodyRef::Memory(Bytes::from_static(b"v")),
5625 content_type: "text/plain".to_string(),
5626 etag: "\"abc\"".to_string(),
5627 size: 1,
5628 last_modified: chrono::Utc::now(),
5629 ..Default::default()
5630 }],
5631 );
5632 }
5633 let req = make_request(Method::DELETE, "/ver", &[], b"");
5634 assert_aws_err(
5635 svc.delete_bucket("123456789012", &req, "ver"),
5636 "BucketNotEmpty",
5637 );
5638 }
5639
5640 #[test]
5641 fn delete_bucket_empty_succeeds() {
5642 let svc = make_service();
5643 seed_bucket(&svc, "empty");
5644 let req = make_request(Method::DELETE, "/empty", &[], b"");
5645 let resp = svc.delete_bucket("123456789012", &req, "empty").unwrap();
5646 assert_eq!(resp.status, StatusCode::NO_CONTENT);
5647 }
5648
5649 #[test]
5650 fn head_bucket_missing_errors() {
5651 let svc = make_service();
5652 assert_aws_err(svc.head_bucket("123456789012", "nope"), "NoSuchBucket");
5653 }
5654
5655 #[test]
5656 fn head_bucket_exists_returns_ok() {
5657 let svc = make_service();
5658 seed_bucket(&svc, "hb");
5659 let resp = svc.head_bucket("123456789012", "hb").unwrap();
5660 assert_eq!(resp.status, StatusCode::OK);
5661 }
5662
5663 #[test]
5664 fn get_bucket_location_us_east_1_returns_empty() {
5665 let svc = make_service();
5666 seed_bucket(&svc, "loc");
5667 let resp = svc.get_bucket_location("123456789012", "loc").unwrap();
5668 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5669 assert!(body.contains("<LocationConstraint"));
5670 assert!(body.contains("></LocationConstraint>"));
5671 }
5672
5673 #[test]
5674 fn get_bucket_location_other_region_returns_region() {
5675 let svc = make_service();
5676 {
5677 let mut mas = svc.state.write();
5678 let state = mas.default_mut();
5679 state
5680 .buckets
5681 .insert("eu".to_string(), S3Bucket::new("eu", "eu-west-1", "owner"));
5682 }
5683 let resp = svc.get_bucket_location("123456789012", "eu").unwrap();
5684 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5685 assert!(body.contains(">eu-west-1<"));
5686 }
5687
5688 #[tokio::test]
5691 async fn get_object_range_request() {
5692 let svc = make_service();
5693 seed_bucket(&svc, "range");
5694 let req = make_request(Method::PUT, "/range/k", &[], b"0123456789ABCDEF");
5695 svc.put_object("123456789012", &req, "range", "k")
5696 .await
5697 .unwrap();
5698
5699 let mut req = make_request(Method::GET, "/range/k", &[], b"");
5700 req.headers.insert("range", "bytes=2-5".parse().unwrap());
5701 let resp = svc.get_object("123456789012", &req, "range", "k").unwrap();
5702 assert_eq!(resp.status, StatusCode::PARTIAL_CONTENT);
5703 assert_eq!(resp.body.expect_bytes(), b"2345");
5704 }
5705
5706 #[tokio::test]
5707 async fn get_object_range_suffix() {
5708 let svc = make_service();
5709 seed_bucket(&svc, "rsx");
5710 let req = make_request(Method::PUT, "/rsx/k", &[], b"0123456789");
5711 svc.put_object("123456789012", &req, "rsx", "k")
5712 .await
5713 .unwrap();
5714
5715 let mut req = make_request(Method::GET, "/rsx/k", &[], b"");
5716 req.headers.insert("range", "bytes=-3".parse().unwrap());
5717 let resp = svc.get_object("123456789012", &req, "rsx", "k").unwrap();
5718 assert_eq!(resp.status, StatusCode::PARTIAL_CONTENT);
5719 assert_eq!(resp.body.expect_bytes(), b"789");
5720 }
5721
5722 #[tokio::test]
5723 async fn get_object_range_open_ended() {
5724 let svc = make_service();
5725 seed_bucket(&svc, "roe");
5726 let req = make_request(Method::PUT, "/roe/k", &[], b"0123456789");
5727 svc.put_object("123456789012", &req, "roe", "k")
5728 .await
5729 .unwrap();
5730
5731 let mut req = make_request(Method::GET, "/roe/k", &[], b"");
5732 req.headers.insert("range", "bytes=7-".parse().unwrap());
5733 let resp = svc.get_object("123456789012", &req, "roe", "k").unwrap();
5734 assert_eq!(resp.status, StatusCode::PARTIAL_CONTENT);
5735 assert_eq!(resp.body.expect_bytes(), b"789");
5736 }
5737
5738 #[tokio::test]
5739 async fn get_object_range_invalid_format() {
5740 let svc = make_service();
5741 seed_bucket(&svc, "rinv");
5742 let req = make_request(Method::PUT, "/rinv/k", &[], b"hello");
5743 svc.put_object("123456789012", &req, "rinv", "k")
5744 .await
5745 .unwrap();
5746
5747 let mut req = make_request(Method::GET, "/rinv/k", &[], b"");
5748 req.headers.insert("range", "bogus=2-5".parse().unwrap());
5749 let resp = svc.get_object("123456789012", &req, "rinv", "k").unwrap();
5751 assert_eq!(resp.status, StatusCode::OK);
5752 assert_eq!(resp.body.expect_bytes(), b"hello");
5753 }
5754
5755 #[tokio::test]
5756 async fn get_object_if_match_mismatch_errors() {
5757 let svc = make_service();
5758 seed_bucket(&svc, "ifm");
5759 let req = make_request(Method::PUT, "/ifm/k", &[], b"abc");
5760 svc.put_object("123456789012", &req, "ifm", "k")
5761 .await
5762 .unwrap();
5763
5764 let mut req = make_request(Method::GET, "/ifm/k", &[], b"");
5765 req.headers
5766 .insert("if-match", "\"nomatch\"".parse().unwrap());
5767 let err = svc.get_object("123456789012", &req, "ifm", "k");
5768 assert_aws_err(err, "PreconditionFailed");
5769 }
5770
5771 #[tokio::test]
5772 async fn get_object_if_none_match_star_not_modified() {
5773 let svc = make_service();
5774 seed_bucket(&svc, "inm");
5775 let req = make_request(Method::PUT, "/inm/k", &[], b"abc");
5776 svc.put_object("123456789012", &req, "inm", "k")
5777 .await
5778 .unwrap();
5779
5780 let mut req = make_request(Method::GET, "/inm/k", &[], b"");
5781 req.headers.insert("if-none-match", "*".parse().unwrap());
5782 let err = svc.get_object("123456789012", &req, "inm", "k");
5783 assert!(err.is_err());
5784 }
5785
5786 #[tokio::test]
5787 async fn head_object_range_request() {
5788 let svc = make_service();
5789 seed_bucket(&svc, "hrng");
5790 let req = make_request(Method::PUT, "/hrng/k", &[], b"0123456789");
5791 svc.put_object("123456789012", &req, "hrng", "k")
5792 .await
5793 .unwrap();
5794
5795 let mut req = make_request(Method::HEAD, "/hrng/k", &[], b"");
5796 req.headers.insert("range", "bytes=2-5".parse().unwrap());
5797 let resp = svc.head_object("123456789012", &req, "hrng", "k").unwrap();
5798 assert_eq!(resp.status, StatusCode::PARTIAL_CONTENT);
5799 }
5800
5801 #[tokio::test]
5802 async fn put_object_with_metadata_headers() {
5803 let svc = make_service();
5804 seed_bucket(&svc, "meta");
5805 let mut req = make_request(Method::PUT, "/meta/k", &[], b"x");
5806 req.headers
5807 .insert("x-amz-meta-user", "alice".parse().unwrap());
5808 req.headers
5809 .insert("x-amz-meta-env", "prod".parse().unwrap());
5810 svc.put_object("123456789012", &req, "meta", "k")
5811 .await
5812 .unwrap();
5813
5814 let req = make_request(Method::HEAD, "/meta/k", &[], b"");
5815 let resp = svc.head_object("123456789012", &req, "meta", "k").unwrap();
5816 assert_eq!(resp.headers.get("x-amz-meta-user").unwrap(), "alice");
5817 assert_eq!(resp.headers.get("x-amz-meta-env").unwrap(), "prod");
5818 }
5819
5820 #[tokio::test]
5821 async fn put_object_with_storage_class_header() {
5822 let svc = make_service();
5823 seed_bucket(&svc, "stor");
5824 let mut req = make_request(Method::PUT, "/stor/k", &[], b"x");
5825 req.headers
5826 .insert("x-amz-storage-class", "STANDARD_IA".parse().unwrap());
5827 svc.put_object("123456789012", &req, "stor", "k")
5828 .await
5829 .unwrap();
5830
5831 let req = make_request(Method::HEAD, "/stor/k", &[], b"");
5832 let resp = svc.head_object("123456789012", &req, "stor", "k").unwrap();
5833 assert_eq!(
5834 resp.headers.get("x-amz-storage-class").unwrap(),
5835 "STANDARD_IA"
5836 );
5837 }
5838
5839 #[tokio::test]
5840 async fn put_object_with_website_redirect() {
5841 let svc = make_service();
5842 seed_bucket(&svc, "wr");
5843 let mut req = make_request(Method::PUT, "/wr/k", &[], b"x");
5844 req.headers.insert(
5845 "x-amz-website-redirect-location",
5846 "/elsewhere".parse().unwrap(),
5847 );
5848 svc.put_object("123456789012", &req, "wr", "k")
5849 .await
5850 .unwrap();
5851 let req = make_request(Method::GET, "/wr/k", &[], b"");
5852 let resp = svc.get_object("123456789012", &req, "wr", "k").unwrap();
5853 assert_eq!(
5854 resp.headers.get("x-amz-website-redirect-location").unwrap(),
5855 "/elsewhere"
5856 );
5857 }
5858
5859 #[test]
5860 fn delete_object_nonexistent_is_ok() {
5861 let svc = make_service();
5862 seed_bucket(&svc, "dne");
5863 let req = make_request(Method::DELETE, "/dne/missing", &[], b"");
5864 let resp = svc
5865 .delete_object("123456789012", &req, "dne", "missing")
5866 .unwrap();
5867 assert_eq!(resp.status, StatusCode::NO_CONTENT);
5868 }
5869
5870 #[test]
5871 fn delete_object_bucket_not_found() {
5872 let svc = make_service();
5873 let req = make_request(Method::DELETE, "/nope/k", &[], b"");
5874 assert_aws_err(
5875 svc.delete_object("123456789012", &req, "nope", "k"),
5876 "NoSuchBucket",
5877 );
5878 }
5879
5880 #[tokio::test]
5881 async fn list_objects_v2_with_prefix_and_delimiter() {
5882 let svc = make_service();
5883 seed_bucket(&svc, "pfxd");
5884 for k in &["a/1", "a/2", "b/1"] {
5885 let req = make_request(Method::PUT, &format!("/pfxd/{k}"), &[], b"x");
5886 svc.put_object("123456789012", &req, "pfxd", k)
5887 .await
5888 .unwrap();
5889 }
5890 let req = make_request(
5891 Method::GET,
5892 "/pfxd",
5893 &[("list-type", "2"), ("prefix", "a/"), ("delimiter", "/")],
5894 b"",
5895 );
5896 let resp = svc.list_objects_v2("123456789012", &req, "pfxd").unwrap();
5897 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5898 assert!(body.contains("<Contents>"));
5899 }
5900
5901 #[tokio::test]
5902 async fn list_objects_v1_basic() {
5903 let svc = make_service();
5904 seed_bucket(&svc, "v1");
5905 for k in &["a", "b"] {
5906 let req = make_request(Method::PUT, &format!("/v1/{k}"), &[], b"x");
5907 svc.put_object("123456789012", &req, "v1", k).await.unwrap();
5908 }
5909 let req = make_request(Method::GET, "/v1", &[], b"");
5910 let resp = svc.list_objects_v1("123456789012", &req, "v1").unwrap();
5911 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
5912 assert!(body.contains("<ListBucketResult"));
5913 assert!(body.contains("<Key>a</Key>"));
5914 assert!(body.contains("<Key>b</Key>"));
5915 }
5916
5917 #[test]
5918 fn get_object_key_not_found() {
5919 let svc = make_service();
5920 seed_bucket(&svc, "gkn");
5921 let req = make_request(Method::GET, "/gkn/missing", &[], b"");
5922 assert_aws_err(
5923 svc.get_object("123456789012", &req, "gkn", "missing"),
5924 "NoSuchKey",
5925 );
5926 }
5927
5928 #[test]
5929 fn get_object_bucket_not_found() {
5930 let svc = make_service();
5931 let req = make_request(Method::GET, "/ghost/k", &[], b"");
5932 assert_aws_err(
5933 svc.get_object("123456789012", &req, "ghost", "k"),
5934 "NoSuchBucket",
5935 );
5936 }
5937
5938 #[tokio::test]
5941 async fn restore_object_non_archival_errors() {
5942 let svc = make_service();
5943 seed_bucket(&svc, "roc");
5944 let req = make_request(Method::PUT, "/roc/k", &[], b"x");
5945 svc.put_object("123456789012", &req, "roc", "k")
5946 .await
5947 .unwrap();
5948
5949 let req = make_request(Method::POST, "/roc/k", &[("restore", "")], b"");
5950 assert_aws_err(
5951 svc.restore_object("123456789012", &req, "roc", "k"),
5952 "InvalidObjectState",
5953 );
5954 }
5955
5956 #[tokio::test]
5957 async fn restore_object_glacier_accepted() {
5958 let svc = make_service();
5959 seed_bucket(&svc, "rog");
5960 let mut req = make_request(Method::PUT, "/rog/k", &[], b"x");
5961 req.headers
5962 .insert("x-amz-storage-class", "GLACIER".parse().unwrap());
5963 svc.put_object("123456789012", &req, "rog", "k")
5964 .await
5965 .unwrap();
5966
5967 let req = make_request(Method::POST, "/rog/k", &[("restore", "")], b"");
5968 let resp = svc
5969 .restore_object("123456789012", &req, "rog", "k")
5970 .unwrap();
5971 assert_eq!(resp.status, StatusCode::ACCEPTED);
5972 }
5973
5974 #[test]
5975 fn restore_object_nonexistent_key() {
5976 let svc = make_service();
5977 seed_bucket(&svc, "rnk");
5978 let req = make_request(Method::POST, "/rnk/ghost", &[("restore", "")], b"");
5979 assert_aws_err(
5980 svc.restore_object("123456789012", &req, "rnk", "ghost"),
5981 "NoSuchKey",
5982 );
5983 }
5984
5985 #[tokio::test]
5988 async fn list_object_versions_basic() {
5989 let svc = make_service();
5990 seed_bucket(&svc, "lov");
5991 {
5992 let mut mas = svc.state.write();
5993 let state = mas.default_mut();
5994 let b = state.buckets.get_mut("lov").unwrap();
5995 b.versioning = Some("Enabled".to_string());
5996 }
5997 let req = make_request(Method::PUT, "/lov/k", &[], b"v1");
5998 svc.put_object("123456789012", &req, "lov", "k")
5999 .await
6000 .unwrap();
6001 let req = make_request(Method::PUT, "/lov/k", &[], b"v2");
6002 svc.put_object("123456789012", &req, "lov", "k")
6003 .await
6004 .unwrap();
6005
6006 let req = make_request(Method::GET, "/lov", &[("versions", "")], b"");
6007 let resp = svc
6008 .list_object_versions("123456789012", &req, "lov")
6009 .unwrap();
6010 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
6011 assert!(body.contains("<ListVersionsResult"));
6012 }
6013
6014 #[test]
6015 fn delete_objects_nonexistent_bucket() {
6016 let svc = make_service();
6017 let xml = b"<Delete><Object><Key>k</Key></Object></Delete>";
6018 let req = make_request(Method::POST, "/ghost", &[("delete", "")], xml);
6019 assert_aws_err(
6020 svc.delete_objects("123456789012", &req, "ghost"),
6021 "NoSuchBucket",
6022 );
6023 }
6024
6025 #[tokio::test]
6026 async fn put_object_custom_content_type() {
6027 let svc = make_service();
6028 seed_bucket(&svc, "ct");
6029 let mut req = make_request(Method::PUT, "/ct/k", &[], b"hi");
6030 req.headers
6031 .insert("content-type", "text/plain".parse().unwrap());
6032 svc.put_object("123456789012", &req, "ct", "k")
6033 .await
6034 .unwrap();
6035 let req = make_request(Method::GET, "/ct/k", &[], b"");
6036 let resp = svc.get_object("123456789012", &req, "ct", "k").unwrap();
6037 assert_eq!(resp.content_type, "text/plain");
6038 }
6039
6040 #[test]
6041 fn head_object_bucket_not_found() {
6042 let svc = make_service();
6043 let req = make_request(Method::HEAD, "/ghost/k", &[], b"");
6044 let result = svc.head_object("123456789012", &req, "ghost", "k");
6045 assert!(result.is_err());
6046 }
6047
6048 #[tokio::test]
6049 async fn get_object_attributes_basic() {
6050 let svc = make_service();
6051 seed_bucket(&svc, "goa");
6052 let req = make_request(Method::PUT, "/goa/k", &[], b"hi");
6053 svc.put_object("123456789012", &req, "goa", "k")
6054 .await
6055 .unwrap();
6056
6057 let mut req = make_request(Method::GET, "/goa/k", &[("attributes", "")], b"");
6058 req.headers.insert(
6059 "x-amz-object-attributes",
6060 "ETag,ObjectSize".parse().unwrap(),
6061 );
6062 let resp = svc
6063 .get_object_attributes("123456789012", &req, "goa", "k")
6064 .unwrap();
6065 assert_eq!(resp.status, StatusCode::OK);
6066 }
6067
6068 #[test]
6069 fn get_object_attributes_bucket_not_found() {
6070 let svc = make_service();
6071 let req = make_request(Method::GET, "/ghost/k", &[("attributes", "")], b"");
6072 let result = svc.get_object_attributes("123456789012", &req, "ghost", "k");
6073 assert!(result.is_err());
6074 }
6075
6076 #[test]
6077 fn get_object_attributes_key_not_found() {
6078 let svc = make_service();
6079 seed_bucket(&svc, "gak");
6080 let req = make_request(Method::GET, "/gak/ghost", &[("attributes", "")], b"");
6081 let result = svc.get_object_attributes("123456789012", &req, "gak", "ghost");
6082 assert!(result.is_err());
6083 }
6084
6085 #[test]
6088 fn get_object_acl_bucket_not_found() {
6089 let svc = make_service();
6090 let req = make_request(Method::GET, "/ghost/k", &[("acl", "")], b"");
6091 assert!(svc
6092 .get_object_acl("123456789012", &req, "ghost", "k")
6093 .is_err());
6094 }
6095
6096 #[test]
6097 fn put_object_acl_bucket_not_found() {
6098 let svc = make_service();
6099 let mut req = make_request(Method::PUT, "/ghost/k", &[("acl", "")], b"");
6100 req.headers.insert("x-amz-acl", "private".parse().unwrap());
6101 assert!(svc
6102 .put_object_acl("123456789012", &req, "ghost", "k")
6103 .is_err());
6104 }
6105
6106 #[tokio::test]
6107 async fn get_object_acl_returns_acl_xml() {
6108 let svc = make_service();
6109 seed_bucket(&svc, "acl");
6110 let req = make_request(Method::PUT, "/acl/k", &[], b"x");
6111 svc.put_object("123456789012", &req, "acl", "k")
6112 .await
6113 .unwrap();
6114 let req = make_request(Method::GET, "/acl/k", &[("acl", "")], b"");
6115 let resp = svc
6116 .get_object_acl("123456789012", &req, "acl", "k")
6117 .unwrap();
6118 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
6119 assert!(body.contains("AccessControlPolicy"));
6120 }
6121
6122 #[test]
6125 fn put_object_retention_bucket_not_found() {
6126 let svc = make_service();
6127 let xml = b"<Retention><Mode>GOVERNANCE</Mode><RetainUntilDate>2030-01-01T00:00:00Z</RetainUntilDate></Retention>";
6128 let req = make_request(Method::PUT, "/ghost/k", &[("retention", "")], xml);
6129 assert!(svc
6130 .put_object_retention("123456789012", &req, "ghost", "k")
6131 .is_err());
6132 }
6133
6134 #[test]
6135 fn get_object_legal_hold_bucket_not_found() {
6136 let svc = make_service();
6137 let req = make_request(Method::GET, "/ghost/k", &[("legal-hold", "")], b"");
6138 assert!(svc
6139 .get_object_legal_hold("123456789012", &req, "ghost", "k")
6140 .is_err());
6141 }
6142
6143 #[test]
6144 fn get_object_retention_bucket_not_found() {
6145 let svc = make_service();
6146 let req = make_request(Method::GET, "/ghost/k", &[("retention", "")], b"");
6147 assert!(svc
6148 .get_object_retention("123456789012", &req, "ghost", "k")
6149 .is_err());
6150 }
6151
6152 #[test]
6155 fn list_multipart_uploads_nonexistent_bucket() {
6156 let svc = make_service();
6157 assert!(svc.list_multipart_uploads("123456789012", "ghost").is_err());
6158 }
6159
6160 #[test]
6161 fn list_multipart_uploads_empty() {
6162 let svc = make_service();
6163 seed_bucket(&svc, "empmp");
6164 let resp = svc.list_multipart_uploads("123456789012", "empmp").unwrap();
6165 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
6166 assert!(body.contains("<ListMultipartUploadsResult"));
6167 }
6168
6169 #[test]
6170 fn put_public_access_block_bucket_not_found() {
6171 let svc = make_service();
6172 let xml = b"<PublicAccessBlockConfiguration><BlockPublicAcls>true</BlockPublicAcls></PublicAccessBlockConfiguration>";
6173 let req = make_request(Method::PUT, "/ghost", &[("publicAccessBlock", "")], xml);
6174 assert!(svc
6175 .put_public_access_block("123456789012", &req, "ghost")
6176 .is_err());
6177 }
6178
6179 #[test]
6180 fn public_access_block_lifecycle() {
6181 let svc = make_service();
6182 seed_bucket(&svc, "pab");
6183 let xml = b"<PublicAccessBlockConfiguration><BlockPublicAcls>true</BlockPublicAcls><IgnorePublicAcls>true</IgnorePublicAcls></PublicAccessBlockConfiguration>";
6184 let req = make_request(Method::PUT, "/pab", &[("publicAccessBlock", "")], xml);
6185 svc.put_public_access_block("123456789012", &req, "pab")
6186 .unwrap();
6187
6188 let resp = svc.get_public_access_block("123456789012", "pab").unwrap();
6189 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
6190 assert!(body.contains("BlockPublicAcls"));
6191
6192 svc.delete_public_access_block("123456789012", "pab")
6193 .unwrap();
6194 }
6195
6196 #[test]
6197 fn put_bucket_replication_bucket_not_found() {
6198 let svc = make_service();
6199 let xml = b"<ReplicationConfiguration><Role>arn</Role></ReplicationConfiguration>";
6200 let req = make_request(Method::PUT, "/ghost", &[("replication", "")], xml);
6201 assert!(svc
6202 .put_bucket_replication("123456789012", &req, "ghost")
6203 .is_err());
6204 }
6205
6206 #[test]
6207 fn put_ownership_controls_bucket_not_found() {
6208 let svc = make_service();
6209 let xml = b"<OwnershipControls><Rule><ObjectOwnership>BucketOwnerEnforced</ObjectOwnership></Rule></OwnershipControls>";
6210 let req = make_request(Method::PUT, "/ghost", &[("ownershipControls", "")], xml);
6211 assert!(svc
6212 .put_bucket_ownership_controls("123456789012", &req, "ghost")
6213 .is_err());
6214 }
6215
6216 #[test]
6217 fn put_bucket_accelerate_bucket_not_found() {
6218 let svc = make_service();
6219 let xml = b"<AccelerateConfiguration><Status>Enabled</Status></AccelerateConfiguration>";
6220 let req = make_request(Method::PUT, "/ghost", &[("accelerate", "")], xml);
6221 assert!(svc
6222 .put_bucket_accelerate("123456789012", &req, "ghost")
6223 .is_err());
6224 }
6225
6226 #[test]
6227 fn get_bucket_accelerate_lifecycle() {
6228 let svc = make_service();
6229 seed_bucket(&svc, "acc");
6230 let xml = b"<AccelerateConfiguration><Status>Enabled</Status></AccelerateConfiguration>";
6231 let req = make_request(Method::PUT, "/acc", &[("accelerate", "")], xml);
6232 svc.put_bucket_accelerate("123456789012", &req, "acc")
6233 .unwrap();
6234 let resp = svc.get_bucket_accelerate("123456789012", "acc").unwrap();
6235 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
6236 assert!(body.contains("AccelerateConfiguration"));
6237 }
6238
6239 #[test]
6240 fn put_bucket_website_bucket_not_found() {
6241 let svc = make_service();
6242 let xml = b"<WebsiteConfiguration><IndexDocument><Suffix>index.html</Suffix></IndexDocument></WebsiteConfiguration>";
6243 let req = make_request(Method::PUT, "/ghost", &[("website", "")], xml);
6244 assert!(svc
6245 .put_bucket_website("123456789012", &req, "ghost")
6246 .is_err());
6247 }
6248
6249 #[test]
6250 fn put_object_tagging_bucket_not_found() {
6251 let svc = make_service();
6252 let xml = b"<Tagging><TagSet></TagSet></Tagging>";
6253 let req = make_request(Method::PUT, "/ghost/k", &[("tagging", "")], xml);
6254 assert!(svc
6255 .put_object_tagging("123456789012", &req, "ghost", "k")
6256 .is_err());
6257 }
6258
6259 #[test]
6260 fn put_object_tagging_key_not_found() {
6261 let svc = make_service();
6262 seed_bucket(&svc, "pot");
6263 let xml = b"<Tagging><TagSet></TagSet></Tagging>";
6264 let req = make_request(Method::PUT, "/pot/ghost", &[("tagging", "")], xml);
6265 assert!(svc
6266 .put_object_tagging("123456789012", &req, "pot", "ghost")
6267 .is_err());
6268 }
6269
6270 #[tokio::test]
6271 async fn put_object_tagging_lifecycle() {
6272 let svc = make_service();
6273 seed_bucket(&svc, "pota");
6274 let req = make_request(Method::PUT, "/pota/k", &[], b"x");
6275 svc.put_object("123456789012", &req, "pota", "k")
6276 .await
6277 .unwrap();
6278
6279 let xml =
6280 b"<Tagging><TagSet><Tag><Key>env</Key><Value>prod</Value></Tag></TagSet></Tagging>";
6281 let req = make_request(Method::PUT, "/pota/k", &[("tagging", "")], xml);
6282 svc.put_object_tagging("123456789012", &req, "pota", "k")
6283 .unwrap();
6284
6285 let req = make_request(Method::GET, "/pota/k", &[("tagging", "")], b"");
6286 let resp = svc
6287 .get_object_tagging("123456789012", &req, "pota", "k")
6288 .unwrap();
6289 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
6290 assert!(body.contains("<Key>env</Key>"));
6291
6292 svc.delete_object_tagging("123456789012", "pota", "k")
6293 .unwrap();
6294 }
6295
6296 #[test]
6297 fn delete_object_tagging_bucket_not_found() {
6298 let svc = make_service();
6299 assert!(svc
6300 .delete_object_tagging("123456789012", "ghost", "k")
6301 .is_err());
6302 }
6303
6304 #[test]
6305 fn put_bucket_tagging_bucket_not_found() {
6306 let svc = make_service();
6307 let xml = b"<Tagging><TagSet></TagSet></Tagging>";
6308 let req = make_request(Method::PUT, "/ghost", &[("tagging", "")], xml);
6309 assert!(svc
6310 .put_bucket_tagging("123456789012", &req, "ghost")
6311 .is_err());
6312 }
6313
6314 #[test]
6315 fn bucket_tagging_lifecycle() {
6316 let svc = make_service();
6317 seed_bucket(&svc, "bt");
6318 let xml =
6319 b"<Tagging><TagSet><Tag><Key>env</Key><Value>prod</Value></Tag></TagSet></Tagging>";
6320 let req = make_request(Method::PUT, "/bt", &[("tagging", "")], xml);
6321 svc.put_bucket_tagging("123456789012", &req, "bt").unwrap();
6322
6323 let req = make_request(Method::GET, "/bt", &[("tagging", "")], b"");
6324 let resp = svc.get_bucket_tagging("123456789012", &req, "bt").unwrap();
6325 let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
6326 assert!(body.contains("<Key>env</Key>"));
6327
6328 let req = make_request(Method::DELETE, "/bt", &[("tagging", "")], b"");
6329 svc.delete_bucket_tagging("123456789012", &req, "bt")
6330 .unwrap();
6331 }
6332}