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_aws::arn::Arn;
10use fakecloud_core::delivery::DeliveryBus;
11use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
12use fakecloud_kms::SharedKmsState;
13use fakecloud_persistence::{MemoryS3Store, S3Store, StoreError};
14
15use base64::engine::general_purpose::STANDARD as BASE64;
16use base64::Engine as _;
17
18use crate::logging;
19use crate::state::{AclGrant, S3Bucket, S3Object, SharedS3State};
20
21mod access_points;
22mod acl;
23mod buckets;
24pub(crate) mod config;
25mod lock;
26mod multipart;
27pub(crate) mod notifications;
28mod objects;
29mod tags;
30
31#[cfg(test)]
33use notifications::replicate_object;
34pub(super) use notifications::{
35 deliver_notifications, normalize_notification_ids, normalize_replication_xml,
36 replicate_through_store,
37};
38
39use notifications::extract_all_xml_values;
41
42#[cfg(test)]
44use notifications::{
45 event_matches, key_matches_filters, parse_notification_config, parse_replication_rules,
46 NotificationTargetType,
47};
48
49pub struct S3Service {
50 state: SharedS3State,
51 delivery: Arc<DeliveryBus>,
52 kms_state: Option<SharedKmsState>,
53 pub(crate) kms_hook: Option<Arc<dyn fakecloud_core::delivery::KmsHook>>,
54 store: Arc<dyn S3Store>,
55 pub(crate) credential_resolver: Option<Arc<dyn fakecloud_core::auth::CredentialResolver>>,
64}
65
66pub(crate) fn persistence_error(err: StoreError) -> AwsServiceError {
71 AwsServiceError::aws_error(
72 StatusCode::INTERNAL_SERVER_ERROR,
73 "InternalError",
74 format!("persistence store error: {err}"),
75 )
76}
77
78pub(crate) fn io_to_aws(err: std::io::Error) -> AwsServiceError {
81 AwsServiceError::aws_error(
82 StatusCode::INTERNAL_SERVER_ERROR,
83 "InternalError",
84 format!("failed to read object body from disk: {err}"),
85 )
86}
87
88impl S3Service {
89 pub fn new(state: SharedS3State, delivery: Arc<DeliveryBus>) -> Self {
90 Self::with_store(state, delivery, Arc::new(MemoryS3Store::new()))
91 }
92
93 pub fn with_store(
94 state: SharedS3State,
95 delivery: Arc<DeliveryBus>,
96 store: Arc<dyn S3Store>,
97 ) -> Self {
98 Self {
99 state,
100 delivery,
101 kms_state: None,
102 kms_hook: None,
103 store,
104 credential_resolver: None,
105 }
106 }
107
108 pub fn with_kms(mut self, kms_state: SharedKmsState) -> Self {
109 self.kms_state = Some(kms_state);
110 self
111 }
112
113 pub fn with_kms_hook(mut self, hook: Arc<dyn fakecloud_core::delivery::KmsHook>) -> Self {
114 self.kms_hook = Some(hook);
115 self
116 }
117
118 pub fn with_credential_resolver(
122 mut self,
123 resolver: Arc<dyn fakecloud_core::auth::CredentialResolver>,
124 ) -> Self {
125 self.credential_resolver = Some(resolver);
126 self
127 }
128
129 pub(crate) fn encrypt_object_body(
140 &self,
141 account_id: &str,
142 region: &str,
143 bucket: &str,
144 plaintext: &[u8],
145 kms_key_id: Option<&str>,
146 ) -> Result<bytes::Bytes, AwsServiceError> {
147 let Some(hook) = &self.kms_hook else {
148 return Ok(bytes::Bytes::copy_from_slice(plaintext));
149 };
150 let key = kms_key_id.filter(|k| !k.is_empty()).unwrap_or("aws/s3");
151 let bucket_arn = Arn::s3(bucket).to_string();
152 let mut ctx = std::collections::HashMap::new();
153 ctx.insert("aws:s3:arn".to_string(), bucket_arn);
154 match hook.encrypt(account_id, region, key, plaintext, "s3.amazonaws.com", ctx) {
155 Ok(envelope) => Ok(bytes::Bytes::from(envelope.into_bytes())),
156 Err(err) => {
157 tracing::warn!(bucket = %bucket, error = %err, "SSE-KMS encrypt failed");
158 Err(AwsServiceError::aws_error(
159 StatusCode::INTERNAL_SERVER_ERROR,
160 "KMS.InternalFailureException",
161 format!("Failed to encrypt object via KMS: {err}"),
162 ))
163 }
164 }
165 }
166
167 pub(crate) fn decrypt_object_body(
177 &self,
178 account_id: &str,
179 bucket: &str,
180 ciphertext: &[u8],
181 ) -> Result<bytes::Bytes, AwsServiceError> {
182 let Some(hook) = &self.kms_hook else {
183 return Ok(bytes::Bytes::copy_from_slice(ciphertext));
184 };
185 let envelope = match std::str::from_utf8(ciphertext) {
188 Ok(s) => s,
189 Err(_) => return Ok(bytes::Bytes::copy_from_slice(ciphertext)),
190 };
191 let bucket_arn = Arn::s3(bucket).to_string();
192 let mut ctx = std::collections::HashMap::new();
193 ctx.insert("aws:s3:arn".to_string(), bucket_arn);
194 match hook.decrypt(account_id, envelope, "s3.amazonaws.com", ctx) {
195 Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
196 Err(err) => {
197 tracing::warn!(bucket = %bucket, error = %err, "SSE-KMS decrypt failed");
198 Err(AwsServiceError::aws_error(
199 StatusCode::INTERNAL_SERVER_ERROR,
200 "KMS.InternalFailureException",
201 format!("Failed to decrypt object via KMS: {err}"),
202 ))
203 }
204 }
205 }
206}
207
208#[async_trait]
209impl AwsService for S3Service {
210 fn service_name(&self) -> &str {
211 "s3"
212 }
213
214 async fn handle(&self, mut req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
215 let is_put_to_key = req.method == Method::PUT
225 && req.path_segments.len() >= 2
226 && req
227 .path_segments
228 .first()
229 .map(|s| !s.is_empty())
230 .unwrap_or(false);
231 let q = &req.query_params;
232 let is_put_object = is_put_to_key
233 && !q.contains_key("tagging")
234 && !q.contains_key("acl")
235 && !q.contains_key("retention")
236 && !q.contains_key("legal-hold")
237 && !q.contains_key("renameObject")
238 && !q.contains_key("encryption")
239 && !req.headers.contains_key("x-amz-copy-source");
240 let is_upload_part =
245 is_put_to_key && q.contains_key("partNumber") && q.contains_key("uploadId");
246 if !is_put_object && !is_upload_part {
247 if let Some(stream) = req.take_body_stream() {
248 req.body = fakecloud_core::service::drain_request_stream(stream).await?;
249 }
250 }
251
252 access_points::resolve_access_point(self, &mut req)?;
254
255 let account_id = req.account_id.as_str();
256
257 let host = req
259 .headers
260 .get("host")
261 .and_then(|v| v.to_str().ok())
262 .unwrap_or("");
263 let is_control = host.to_ascii_lowercase().contains("s3-control");
264 if is_control {
265 let v1 = req.path_segments.first().map(|s| s.as_str());
266 let v2 = req.path_segments.get(1).map(|s| s.as_str());
267 let v3 = req.path_segments.get(2).map(|s| s.as_str());
268 if v1 == Some("v20180820") && v2 == Some("accesspoint") {
269 if let Some(name) = v3 {
270 match req.method {
271 Method::PUT => return self.create_access_point(account_id, &req, name),
272 Method::GET => return self.get_access_point(account_id, &req, name),
273 Method::DELETE => return self.delete_access_point(account_id, &req, name),
274 _ => {}
275 }
276 } else if req.method == Method::GET {
277 return self.list_access_points(account_id, &req);
278 }
279 }
280 }
281
282 if req.raw_path.starts_with("//") {
291 return Err(AwsServiceError::aws_error(
292 StatusCode::BAD_REQUEST,
293 "InvalidBucketName",
294 "The specified bucket is not valid: bucket name cannot be empty",
295 ));
296 }
297 let bucket = req.path_segments.first().map(|s| s.as_str());
298 let key = if let Some(b) = bucket {
301 let prefix = format!("/{b}/");
302 if req.raw_path.starts_with(&prefix) && req.raw_path.len() > prefix.len() {
303 let raw_key = &req.raw_path[prefix.len()..];
304 Some(
305 percent_encoding::percent_decode_str(raw_key)
306 .decode_utf8_lossy()
307 .into_owned(),
308 )
309 } else if req.path_segments.len() > 1 {
310 let raw = req.path_segments[1..].join("/");
311 Some(
312 percent_encoding::percent_decode_str(&raw)
313 .decode_utf8_lossy()
314 .into_owned(),
315 )
316 } else {
317 None
318 }
319 } else {
320 None
321 };
322
323 if let Some(b) = bucket {
325 if req.method == Method::POST
327 && key.is_some()
328 && req.query_params.contains_key("uploads")
329 {
330 return self.create_multipart_upload(account_id, &req, b, key.as_deref().unwrap());
331 }
332
333 if req.method == Method::POST
335 && key.is_some()
336 && req.query_params.contains_key("restore")
337 {
338 return self.restore_object(account_id, &req, b, key.as_deref().unwrap());
339 }
340
341 if req.method == Method::POST && key.is_some() {
343 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
344 return self.complete_multipart_upload(
345 account_id,
346 &req,
347 b,
348 key.as_deref().unwrap(),
349 &upload_id,
350 );
351 }
352 }
353
354 if req.method == Method::PUT && key.is_some() {
356 if let (Some(part_num_str), Some(upload_id)) = (
357 req.query_params.get("partNumber").cloned(),
358 req.query_params.get("uploadId").cloned(),
359 ) {
360 if let Ok(part_number) = part_num_str.parse::<i64>() {
361 if req.headers.contains_key("x-amz-copy-source") {
362 return self.upload_part_copy(
363 account_id,
364 &req,
365 b,
366 key.as_deref().unwrap(),
367 &upload_id,
368 part_number,
369 );
370 }
371 return self
372 .upload_part(
373 account_id,
374 &req,
375 b,
376 key.as_deref().unwrap(),
377 &upload_id,
378 part_number,
379 )
380 .await;
381 }
382 }
383 }
384
385 if req.method == Method::DELETE && key.is_some() {
387 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
388 return self.abort_multipart_upload(
389 account_id,
390 b,
391 key.as_deref().unwrap(),
392 &upload_id,
393 );
394 }
395 }
396
397 if req.method == Method::GET
399 && key.is_none()
400 && req.query_params.contains_key("uploads")
401 {
402 return self.list_multipart_uploads(account_id, b, &req.query_params);
403 }
404
405 if req.method == Method::GET && key.is_some() {
407 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
408 return self.list_parts(
409 account_id,
410 &req,
411 b,
412 key.as_deref().unwrap(),
413 &upload_id,
414 );
415 }
416 }
417 }
418
419 if req.method == Method::OPTIONS {
421 if let Some(b_name) = bucket {
422 let cors_config = {
423 let accounts = self.state.read();
424 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
425 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
426 state
427 .buckets
428 .get(b_name)
429 .and_then(|b| b.cors_config.clone())
430 };
431 if let Some(ref config) = cors_config {
432 let origin = req
433 .headers
434 .get("origin")
435 .and_then(|v| v.to_str().ok())
436 .unwrap_or("");
437 let request_method = req
438 .headers
439 .get("access-control-request-method")
440 .and_then(|v| v.to_str().ok())
441 .unwrap_or("");
442 let rules = parse_cors_config(config);
443 if let Some(rule) = find_cors_rule(&rules, origin, Some(request_method)) {
444 let mut headers = HeaderMap::new();
445 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
446 "*"
447 } else {
448 origin
449 };
450 headers.insert(
451 "access-control-allow-origin",
452 matched_origin
453 .parse()
454 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
455 );
456 headers.insert(
457 "access-control-allow-methods",
458 rule.allowed_methods
459 .join(", ")
460 .parse()
461 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
462 );
463 if !rule.allowed_headers.is_empty() {
464 let ah = if rule.allowed_headers.contains(&"*".to_string()) {
465 req.headers
466 .get("access-control-request-headers")
467 .and_then(|v| v.to_str().ok())
468 .unwrap_or("*")
469 .to_string()
470 } else {
471 rule.allowed_headers.join(", ")
472 };
473 headers.insert(
474 "access-control-allow-headers",
475 ah.parse()
476 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
477 );
478 }
479 if let Some(max_age) = rule.max_age_seconds {
480 headers.insert(
481 "access-control-max-age",
482 max_age
483 .to_string()
484 .parse()
485 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
486 );
487 }
488 return Ok(AwsResponse {
489 status: StatusCode::OK,
490 content_type: String::new(),
491 body: Bytes::new().into(),
492 headers,
493 });
494 }
495 }
496 return Err(AwsServiceError::aws_error(
497 StatusCode::FORBIDDEN,
498 "CORSResponse",
499 "CORS is not enabled for this bucket",
500 ));
501 }
502 }
503
504 let origin_header = req
506 .headers
507 .get("origin")
508 .and_then(|v| v.to_str().ok())
509 .map(|s| s.to_string());
510
511 let bucket_subresources: &[&str] = &[
518 "tagging",
519 "acl",
520 "versioning",
521 "cors",
522 "notification",
523 "website",
524 "accelerate",
525 "publicAccessBlock",
526 "encryption",
527 "lifecycle",
528 "logging",
529 "policy",
530 "policyStatus",
531 "replication",
532 "ownershipControls",
533 "inventory",
534 "analytics",
535 "intelligent-tiering",
536 "metrics",
537 "requestPayment",
538 "abac",
539 "metadataConfiguration",
540 "metadataTable",
541 "metadataInventoryTable",
542 "metadataJournalTable",
543 "object-lock",
544 "versions",
545 "session",
546 "retention",
547 "legal-hold",
548 "attributes",
549 "torrent",
550 "renameObject",
551 "uploads",
552 "restore",
553 "select",
554 "location",
561 "delete",
562 "list-type",
563 ];
564 if bucket.is_none()
565 && bucket_subresources
566 .iter()
567 .any(|q| req.query_params.contains_key(*q))
568 {
569 return Err(AwsServiceError::aws_error(
570 StatusCode::BAD_REQUEST,
571 "InvalidBucketName",
572 "The specified bucket is not valid: bucket name is required for this operation",
573 ));
574 }
575
576 let mut result = match (&req.method, bucket, key.as_deref()) {
577 (&Method::GET, None, None) => {
579 if req.query_params.get("x-id").map(|s| s.as_str()) == Some("ListDirectoryBuckets")
580 {
581 self.list_directory_buckets(account_id, &req)
582 } else {
583 self.list_buckets(account_id, &req)
584 }
585 }
586
587 (&Method::PUT, Some(b), None) => {
589 if req.query_params.contains_key("tagging") {
590 self.put_bucket_tagging(account_id, &req, b)
591 } else if req.query_params.contains_key("acl") {
592 self.put_bucket_acl(account_id, &req, b)
593 } else if req.query_params.contains_key("versioning") {
594 self.put_bucket_versioning(account_id, &req, b)
595 } else if req.query_params.contains_key("cors") {
596 self.put_bucket_cors(account_id, &req, b)
597 } else if req.query_params.contains_key("notification") {
598 self.put_bucket_notification(account_id, &req, b)
599 } else if req.query_params.contains_key("website") {
600 self.put_bucket_website(account_id, &req, b)
601 } else if req.query_params.contains_key("accelerate") {
602 self.put_bucket_accelerate(account_id, &req, b)
603 } else if req.query_params.contains_key("publicAccessBlock") {
604 self.put_public_access_block(account_id, &req, b)
605 } else if req.query_params.contains_key("encryption") {
606 self.put_bucket_encryption(account_id, &req, b)
607 } else if req.query_params.contains_key("lifecycle") {
608 self.put_bucket_lifecycle(account_id, &req, b)
609 } else if req.query_params.contains_key("logging") {
610 self.put_bucket_logging(account_id, &req, b)
611 } else if req.query_params.contains_key("policy") {
612 self.put_bucket_policy(account_id, &req, b)
613 } else if req.query_params.contains_key("object-lock") {
614 self.put_object_lock_config(account_id, &req, b)
615 } else if req.query_params.contains_key("replication") {
616 self.put_bucket_replication(account_id, &req, b)
617 } else if req.query_params.contains_key("ownershipControls") {
618 self.put_bucket_ownership_controls(account_id, &req, b)
619 } else if req.query_params.contains_key("inventory") {
620 self.put_bucket_inventory(account_id, &req, b)
621 } else if req.query_params.contains_key("analytics") {
622 self.put_bucket_analytics_config(account_id, &req, b)
623 } else if req.query_params.contains_key("intelligent-tiering") {
624 self.put_bucket_intelligent_tiering_config(account_id, &req, b)
625 } else if req.query_params.contains_key("metrics") {
626 self.put_bucket_metrics_config(account_id, &req, b)
627 } else if req.query_params.contains_key("requestPayment") {
628 self.put_bucket_request_payment(account_id, &req, b)
629 } else if req.query_params.contains_key("abac") {
630 self.put_bucket_abac(account_id, &req, b)
631 } else if req.query_params.contains_key("metadataInventoryTable") {
632 self.update_bucket_metadata_inventory_table(account_id, &req, b)
633 } else if req.query_params.contains_key("metadataJournalTable") {
634 self.update_bucket_metadata_journal_table(account_id, &req, b)
635 } else {
636 self.create_bucket(account_id, &req, b)
637 }
638 }
639 (&Method::DELETE, Some(b), None) => {
640 if req.query_params.contains_key("tagging") {
641 self.delete_bucket_tagging(account_id, &req, b)
642 } else if req.query_params.contains_key("cors") {
643 self.delete_bucket_cors(account_id, b)
644 } else if req.query_params.contains_key("website") {
645 self.delete_bucket_website(account_id, b)
646 } else if req.query_params.contains_key("publicAccessBlock") {
647 self.delete_public_access_block(account_id, b)
648 } else if req.query_params.contains_key("encryption") {
649 self.delete_bucket_encryption(account_id, b)
650 } else if req.query_params.contains_key("lifecycle") {
651 self.delete_bucket_lifecycle(account_id, b)
652 } else if req.query_params.contains_key("policy") {
653 self.delete_bucket_policy(account_id, b)
654 } else if req.query_params.contains_key("replication") {
655 self.delete_bucket_replication(account_id, b)
656 } else if req.query_params.contains_key("ownershipControls") {
657 self.delete_bucket_ownership_controls(account_id, b)
658 } else if req.query_params.contains_key("inventory") {
659 self.delete_bucket_inventory(account_id, &req, b)
660 } else if req.query_params.contains_key("analytics") {
661 self.delete_bucket_analytics_config(account_id, &req, b)
662 } else if req.query_params.contains_key("intelligent-tiering") {
663 self.delete_bucket_intelligent_tiering_config(account_id, &req, b)
664 } else if req.query_params.contains_key("metrics") {
665 self.delete_bucket_metrics_config(account_id, &req, b)
666 } else if req.query_params.contains_key("metadataConfiguration") {
667 self.delete_bucket_metadata_config(account_id, b)
668 } else if req.query_params.contains_key("metadataTable") {
669 self.delete_bucket_metadata_table_config(account_id, b)
670 } else {
671 self.delete_bucket(account_id, &req, b)
672 }
673 }
674 (&Method::HEAD, Some(b), None) => self.head_bucket(account_id, b),
675 (&Method::GET, Some(b), None) => {
676 if req.query_params.contains_key("tagging") {
677 self.get_bucket_tagging(account_id, &req, b)
678 } else if req.query_params.contains_key("location") {
679 self.get_bucket_location(account_id, b)
680 } else if req.query_params.contains_key("acl") {
681 self.get_bucket_acl(account_id, &req, b)
682 } else if req.query_params.contains_key("versioning") {
683 self.get_bucket_versioning(account_id, b)
684 } else if req.query_params.contains_key("versions") {
685 self.list_object_versions(account_id, &req, b)
686 } else if req.query_params.contains_key("object-lock") {
687 self.get_object_lock_configuration(account_id, b)
688 } else if req.query_params.contains_key("cors") {
689 self.get_bucket_cors(account_id, b)
690 } else if req.query_params.contains_key("notification") {
691 self.get_bucket_notification(account_id, b)
692 } else if req.query_params.contains_key("website") {
693 self.get_bucket_website(account_id, b)
694 } else if req.query_params.contains_key("accelerate") {
695 self.get_bucket_accelerate(account_id, b)
696 } else if req.query_params.contains_key("publicAccessBlock") {
697 self.get_public_access_block(account_id, b)
698 } else if req.query_params.contains_key("encryption") {
699 self.get_bucket_encryption(account_id, b)
700 } else if req.query_params.contains_key("lifecycle") {
701 self.get_bucket_lifecycle(account_id, b)
702 } else if req.query_params.contains_key("logging") {
703 self.get_bucket_logging(account_id, b)
704 } else if req.query_params.contains_key("policy") {
705 self.get_bucket_policy(account_id, b)
706 } else if req.query_params.contains_key("replication") {
707 self.get_bucket_replication(account_id, b)
708 } else if req.query_params.contains_key("ownershipControls") {
709 self.get_bucket_ownership_controls(account_id, b)
710 } else if req.query_params.contains_key("inventory") {
711 if req.query_params.contains_key("id") {
712 self.get_bucket_inventory(account_id, &req, b)
713 } else {
714 self.list_bucket_inventory_configurations(account_id, b)
715 }
716 } else if req.query_params.contains_key("analytics") {
717 if req.query_params.contains_key("id") {
718 self.get_bucket_analytics_config(account_id, &req, b)
719 } else {
720 self.list_bucket_analytics_configurations(account_id, b)
721 }
722 } else if req.query_params.contains_key("intelligent-tiering") {
723 if req.query_params.contains_key("id") {
724 self.get_bucket_intelligent_tiering_config(account_id, &req, b)
725 } else {
726 self.list_bucket_intelligent_tiering_configurations(account_id, b)
727 }
728 } else if req.query_params.contains_key("metrics") {
729 if req.query_params.contains_key("id") {
730 self.get_bucket_metrics_config(account_id, &req, b)
731 } else {
732 self.list_bucket_metrics_configurations(account_id, b)
733 }
734 } else if req.query_params.contains_key("requestPayment") {
735 self.get_bucket_request_payment(account_id, b)
736 } else if req.query_params.contains_key("abac") {
737 self.get_bucket_abac(account_id, b)
738 } else if req.query_params.contains_key("policyStatus") {
739 self.get_bucket_policy_status(account_id, b)
740 } else if req.query_params.contains_key("metadataConfiguration") {
741 self.get_bucket_metadata_config(account_id, b)
742 } else if req.query_params.contains_key("metadataTable") {
743 self.get_bucket_metadata_table_config(account_id, b)
744 } else if req.query_params.contains_key("session") {
745 self.create_session(account_id, &req, b)
746 } else if req.query_params.get("list-type").map(|s| s.as_str()) == Some("2") {
747 self.list_objects_v2(account_id, &req, b)
748 } else if req.query_params.is_empty() {
749 let website_config = {
751 let accounts = self.state.read();
752 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
753 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
754 state
755 .buckets
756 .get(b)
757 .and_then(|bkt| bkt.website_config.clone())
758 };
759 if let Some(ref config) = website_config {
760 if let Some(index_doc) = extract_xml_value(config, "Suffix").or_else(|| {
761 extract_xml_value(config, "IndexDocument").and_then(|inner| {
762 let open = "<Suffix>";
763 let close = "</Suffix>";
764 let s = inner.find(open)? + open.len();
765 let e = inner.find(close)?;
766 Some(inner[s..e].trim().to_string())
767 })
768 }) {
769 self.serve_website_object(account_id, &req, b, &index_doc, config)
770 } else {
771 self.list_objects_v1(account_id, &req, b)
772 }
773 } else {
774 self.list_objects_v1(account_id, &req, b)
775 }
776 } else {
777 self.list_objects_v1(account_id, &req, b)
778 }
779 }
780
781 (&Method::PUT, Some(b), Some(k)) => {
783 if req.query_params.contains_key("tagging") {
784 self.put_object_tagging(account_id, &req, b, k)
785 } else if req.query_params.contains_key("acl") {
786 self.put_object_acl(account_id, &req, b, k)
787 } else if req.query_params.contains_key("retention") {
788 self.put_object_retention(account_id, &req, b, k)
789 } else if req.query_params.contains_key("legal-hold") {
790 self.put_object_legal_hold(account_id, &req, b, k)
791 } else if req.query_params.contains_key("renameObject") {
792 self.rename_object(account_id, &req, b, k)
793 } else if req.query_params.contains_key("encryption") {
794 self.update_object_encryption(account_id, &req, b, k)
795 } else if req.headers.contains_key("x-amz-copy-source") {
796 self.copy_object(account_id, &req, b, k)
797 } else {
798 self.put_object(account_id, &req, b, k).await
799 }
800 }
801 (&Method::GET, Some(b), Some(k)) => {
802 if req.query_params.contains_key("tagging") {
803 self.get_object_tagging(account_id, &req, b, k)
804 } else if req.query_params.contains_key("acl") {
805 self.get_object_acl(account_id, &req, b, k)
806 } else if req.query_params.contains_key("retention") {
807 self.get_object_retention(account_id, &req, b, k)
808 } else if req.query_params.contains_key("legal-hold") {
809 self.get_object_legal_hold(account_id, &req, b, k)
810 } else if req.query_params.contains_key("attributes") {
811 self.get_object_attributes(account_id, &req, b, k)
812 } else if req.query_params.contains_key("torrent") {
813 self.get_object_torrent(account_id, &req, b, k)
814 } else {
815 let result = self.get_object(account_id, &req, b, k);
816 let is_not_found = matches!(
818 &result,
819 Err(e) if e.code() == "NoSuchKey"
820 );
821 if is_not_found {
822 let website_config = {
823 let accounts = self.state.read();
824 let _empty_s3 =
825 crate::state::S3State::new(&req.account_id, &req.region);
826 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
827 state
828 .buckets
829 .get(b)
830 .and_then(|bkt| bkt.website_config.clone())
831 };
832 if let Some(ref config) = website_config {
833 if let Some(error_key) = extract_xml_value(config, "ErrorDocument")
834 .and_then(|inner| {
835 let open = "<Key>";
836 let close = "</Key>";
837 let s = inner.find(open)? + open.len();
838 let e = inner.find(close)?;
839 Some(inner[s..e].trim().to_string())
840 })
841 .or_else(|| extract_xml_value(config, "Key"))
842 {
843 return self.serve_website_error(account_id, &req, b, &error_key);
844 }
845 }
846 }
847 result
848 }
849 }
850 (&Method::DELETE, Some(b), Some(k)) => {
851 if req.query_params.contains_key("tagging") {
852 self.delete_object_tagging(account_id, b, k)
853 } else {
854 self.delete_object(account_id, &req, b, k)
855 }
856 }
857 (&Method::HEAD, Some(b), Some(k)) => self.head_object(account_id, &req, b, k),
858
859 (&Method::POST, Some(b), None) if req.query_params.contains_key("delete") => {
861 self.delete_objects(account_id, &req, b)
862 }
863 (&Method::POST, Some(b), None)
864 if req.query_params.contains_key("metadataConfiguration") =>
865 {
866 self.create_bucket_metadata_config(account_id, &req, b)
867 }
868 (&Method::POST, Some(b), None) if req.query_params.contains_key("metadataTable") => {
869 self.create_bucket_metadata_table_config(account_id, &req, b)
870 }
871 (&Method::POST, Some(b), Some(k))
872 if req.query_params.get("select-type").map(|s| s.as_str()) == Some("2") =>
873 {
874 self.select_object_content(account_id, &req, b, k)
875 }
876 (&Method::POST, Some("WriteGetObjectResponse"), None) => {
877 self.write_get_object_response(account_id, &req)
878 }
879
880 (&Method::POST, Some(b), None)
887 if req
888 .headers
889 .get(http::header::CONTENT_TYPE)
890 .and_then(|v| v.to_str().ok())
891 .is_some_and(|ct| {
892 ct.to_ascii_lowercase().starts_with("multipart/form-data")
893 }) =>
894 {
895 self.post_object(account_id, &req, b).await
896 }
897
898 _ => Err(AwsServiceError::aws_error(
899 StatusCode::METHOD_NOT_ALLOWED,
900 "MethodNotAllowed",
901 "The specified method is not allowed against this resource",
902 )),
903 };
904
905 if let (Some(ref origin), Some(b_name)) = (&origin_header, bucket) {
907 let cors_config = {
908 let accounts = self.state.read();
909 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
910 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
911 state
912 .buckets
913 .get(b_name)
914 .and_then(|b| b.cors_config.clone())
915 };
916 if let Some(ref config) = cors_config {
917 let rules = parse_cors_config(config);
918 if let Some(rule) = find_cors_rule(&rules, origin, None) {
919 if let Ok(ref mut resp) = result {
920 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
921 "*"
922 } else {
923 origin
924 };
925 resp.headers.insert(
926 "access-control-allow-origin",
927 matched_origin
928 .parse()
929 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
930 );
931 if !rule.expose_headers.is_empty() {
932 resp.headers.insert(
933 "access-control-expose-headers",
934 rule.expose_headers
935 .join(", ")
936 .parse()
937 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
938 );
939 }
940 }
941 }
942 }
943 }
944
945 if let Some(b_name) = bucket {
947 let status_code = match &result {
948 Ok(resp) => resp.status.as_u16(),
949 Err(e) => e.status().as_u16(),
950 };
951 let op = logging::operation_name(&req.method, key.as_deref());
952 logging::maybe_write_access_log(
953 &self.state,
954 &self.store,
955 b_name,
956 &logging::AccessLogRequest {
957 operation: op,
958 key: key.as_deref(),
959 status: status_code,
960 request_id: &req.request_id,
961 method: req.method.as_str(),
962 path: &req.raw_path,
963 },
964 );
965 }
966
967 result
968 }
969
970 fn supported_actions(&self) -> &[&str] {
971 &[
972 "ListBuckets",
974 "CreateBucket",
975 "DeleteBucket",
976 "HeadBucket",
977 "GetBucketLocation",
978 "PutObject",
980 "GetObject",
981 "DeleteObject",
982 "HeadObject",
983 "CopyObject",
984 "DeleteObjects",
985 "ListObjectsV2",
986 "ListObjects",
987 "ListObjectVersions",
988 "GetObjectAttributes",
989 "RestoreObject",
990 "PutObjectTagging",
992 "GetObjectTagging",
993 "DeleteObjectTagging",
994 "PutObjectAcl",
995 "GetObjectAcl",
996 "PutObjectRetention",
997 "GetObjectRetention",
998 "PutObjectLegalHold",
999 "GetObjectLegalHold",
1000 "PutBucketTagging",
1002 "GetBucketTagging",
1003 "DeleteBucketTagging",
1004 "PutBucketAcl",
1005 "GetBucketAcl",
1006 "PutBucketVersioning",
1007 "GetBucketVersioning",
1008 "PutBucketCors",
1009 "GetBucketCors",
1010 "DeleteBucketCors",
1011 "PutBucketNotificationConfiguration",
1012 "GetBucketNotificationConfiguration",
1013 "PutBucketWebsite",
1014 "GetBucketWebsite",
1015 "DeleteBucketWebsite",
1016 "PutBucketAccelerateConfiguration",
1017 "GetBucketAccelerateConfiguration",
1018 "PutPublicAccessBlock",
1019 "GetPublicAccessBlock",
1020 "DeletePublicAccessBlock",
1021 "PutBucketEncryption",
1022 "GetBucketEncryption",
1023 "DeleteBucketEncryption",
1024 "PutBucketLifecycleConfiguration",
1025 "GetBucketLifecycleConfiguration",
1026 "DeleteBucketLifecycle",
1027 "PutBucketLogging",
1028 "GetBucketLogging",
1029 "PutBucketPolicy",
1030 "GetBucketPolicy",
1031 "DeleteBucketPolicy",
1032 "PutObjectLockConfiguration",
1033 "GetObjectLockConfiguration",
1034 "PutBucketReplication",
1035 "GetBucketReplication",
1036 "DeleteBucketReplication",
1037 "PutBucketOwnershipControls",
1038 "GetBucketOwnershipControls",
1039 "DeleteBucketOwnershipControls",
1040 "PutBucketInventoryConfiguration",
1041 "GetBucketInventoryConfiguration",
1042 "DeleteBucketInventoryConfiguration",
1043 "ListBucketInventoryConfigurations",
1044 "PutBucketAnalyticsConfiguration",
1045 "GetBucketAnalyticsConfiguration",
1046 "DeleteBucketAnalyticsConfiguration",
1047 "ListBucketAnalyticsConfigurations",
1048 "PutBucketIntelligentTieringConfiguration",
1049 "GetBucketIntelligentTieringConfiguration",
1050 "DeleteBucketIntelligentTieringConfiguration",
1051 "ListBucketIntelligentTieringConfigurations",
1052 "PutBucketMetricsConfiguration",
1053 "GetBucketMetricsConfiguration",
1054 "DeleteBucketMetricsConfiguration",
1055 "ListBucketMetricsConfigurations",
1056 "PutBucketRequestPayment",
1057 "GetBucketRequestPayment",
1058 "PutBucketAbac",
1059 "GetBucketAbac",
1060 "GetBucketPolicyStatus",
1061 "CreateBucketMetadataConfiguration",
1062 "GetBucketMetadataConfiguration",
1063 "DeleteBucketMetadataConfiguration",
1064 "CreateBucketMetadataTableConfiguration",
1065 "GetBucketMetadataTableConfiguration",
1066 "DeleteBucketMetadataTableConfiguration",
1067 "UpdateBucketMetadataInventoryTableConfiguration",
1068 "UpdateBucketMetadataJournalTableConfiguration",
1069 "GetObjectTorrent",
1070 "RenameObject",
1071 "SelectObjectContent",
1072 "UpdateObjectEncryption",
1073 "WriteGetObjectResponse",
1074 "ListDirectoryBuckets",
1075 "CreateSession",
1076 "CreateMultipartUpload",
1078 "UploadPart",
1079 "UploadPartCopy",
1080 "CompleteMultipartUpload",
1081 "AbortMultipartUpload",
1082 "ListParts",
1083 "ListMultipartUploads",
1084 ]
1085 }
1086
1087 fn iam_enforceable(&self) -> bool {
1088 true
1089 }
1090
1091 fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
1101 let bucket = request.path_segments.first().map(|s| s.as_str());
1106 let key = if request.path_segments.len() > 1 {
1107 Some(request.path_segments[1..].join("/"))
1108 } else {
1109 None
1110 };
1111 let host = request
1118 .headers
1119 .get("host")
1120 .and_then(|v| v.to_str().ok())
1121 .unwrap_or("");
1122 if host.to_ascii_lowercase().contains("s3-control")
1123 && request.path_segments.first().map(|s| s.as_str()) == Some("v20180820")
1124 && request.path_segments.get(1).map(|s| s.as_str()) == Some("accesspoint")
1125 {
1126 let has_name = request.path_segments.get(2).is_some();
1127 let ap_action = match (request.method.as_str(), has_name) {
1128 ("PUT", true) => Some("CreateAccessPoint"),
1129 ("GET", true) => Some("GetAccessPoint"),
1130 ("DELETE", true) => Some("DeleteAccessPoint"),
1131 ("GET", false) => Some("ListAccessPoints"),
1132 _ => None,
1133 };
1134 if let Some(action) = ap_action {
1135 let resource = request
1136 .path_segments
1137 .get(2)
1138 .map(|name| format!("arn:aws:s3:::accesspoint/{name}"))
1139 .unwrap_or_else(|| "*".to_string());
1140 return Some(fakecloud_core::auth::IamAction {
1141 service: "s3",
1142 action,
1143 resource,
1144 });
1145 }
1146 }
1147 let action = s3_detect_action(
1148 request.method.as_str(),
1149 bucket,
1150 key.as_deref(),
1151 &request.query_params,
1152 )?;
1153 let service = match action {
1157 "CreateSession" | "ListAllMyDirectoryBuckets" => "s3express",
1158 "WriteGetObjectResponse" => "s3-object-lambda",
1159 _ => "s3",
1160 };
1161 let resource = s3_resource_for(action, bucket, key.as_deref());
1162 Some(fakecloud_core::auth::IamAction {
1163 service,
1164 action,
1165 resource,
1166 })
1167 }
1168
1169 fn iam_condition_keys_for(
1170 &self,
1171 request: &AwsRequest,
1172 action: &fakecloud_core::auth::IamAction,
1173 ) -> std::collections::BTreeMap<String, Vec<String>> {
1174 s3_condition_keys(action.action, &request.query_params)
1175 }
1176
1177 fn resource_tags_for(
1178 &self,
1179 resource_arn: &str,
1180 ) -> Option<std::collections::HashMap<String, String>> {
1181 s3_resource_tags(&self.state, resource_arn)
1182 .map(|m| m.into_iter().collect::<std::collections::HashMap<_, _>>())
1183 }
1184
1185 fn request_tags_from(
1186 &self,
1187 request: &AwsRequest,
1188 action: &str,
1189 ) -> Option<std::collections::HashMap<String, String>> {
1190 s3_request_tags(request, action)
1191 .map(|m| m.into_iter().collect::<std::collections::HashMap<_, _>>())
1192 }
1193}
1194
1195fn s3_condition_keys(
1202 action: &str,
1203 query: &std::collections::HashMap<String, String>,
1204) -> std::collections::BTreeMap<String, Vec<String>> {
1205 let mut out = std::collections::BTreeMap::new();
1206 if matches!(action, "ListObjects" | "ListObjectsV2") {
1207 if let Some(prefix) = query.get("prefix") {
1209 out.insert("s3:prefix".to_string(), vec![prefix.clone()]);
1210 }
1211 if let Some(delimiter) = query.get("delimiter") {
1212 out.insert("s3:delimiter".to_string(), vec![delimiter.clone()]);
1213 }
1214 if let Some(max_keys) = query.get("max-keys") {
1215 out.insert("s3:max-keys".to_string(), vec![max_keys.clone()]);
1216 }
1217 }
1218 out
1219}
1220
1221fn s3_resource_tags(
1227 state: &SharedS3State,
1228 resource_arn: &str,
1229) -> Option<std::collections::BTreeMap<String, String>> {
1230 if resource_arn == "*" {
1231 return Some(std::collections::BTreeMap::new());
1232 }
1233 let after_prefix = resource_arn.strip_prefix("arn:aws:s3:::")?;
1235 let mas = state.read();
1236 let bucket_name = after_prefix.split('/').next().unwrap_or(after_prefix);
1238 let state = mas
1239 .find_account(|s| s.buckets.contains_key(bucket_name))
1240 .and_then(|id| mas.get(id))
1241 .or_else(|| Some(mas.default_ref()))?;
1242 if let Some(slash_pos) = after_prefix.find('/') {
1243 let bucket_name = &after_prefix[..slash_pos];
1245 let key = &after_prefix[slash_pos + 1..];
1246 let bucket = state.buckets.get(bucket_name)?;
1247 if let Some(obj) = bucket.objects.get(key) {
1249 Some(obj.tags.clone())
1250 } else if let Some(versions) = bucket.object_versions.get(key) {
1251 versions.last().map(|v| v.tags.clone())
1252 } else {
1253 Some(std::collections::BTreeMap::new())
1255 }
1256 } else {
1257 let bucket = state.buckets.get(after_prefix)?;
1259 Some(bucket.tags.clone())
1260 }
1261}
1262
1263fn s3_request_tags(
1269 request: &AwsRequest,
1270 action: &str,
1271) -> Option<std::collections::BTreeMap<String, String>> {
1272 match action {
1273 "PutObject" | "CopyObject" | "CreateMultipartUpload" => {
1274 if let Some(tagging) = request.headers.get("x-amz-tagging") {
1276 let tags = parse_url_encoded_tags(tagging.to_str().unwrap_or(""));
1277 Some(tags.into_iter().collect())
1278 } else {
1279 Some(std::collections::BTreeMap::new())
1280 }
1281 }
1282 "PutBucketTagging" | "PutObjectTagging" => {
1283 let body = std::str::from_utf8(&request.body).unwrap_or("");
1285 let tags = parse_tagging_xml(body);
1286 Some(tags.into_iter().collect())
1287 }
1288 _ => Some(std::collections::BTreeMap::new()),
1289 }
1290}
1291
1292fn s3_detect_action(
1305 method: &str,
1306 bucket: Option<&str>,
1307 key: Option<&str>,
1308 query: &std::collections::HashMap<String, String>,
1309) -> Option<&'static str> {
1310 let has = |q: &str| query.contains_key(q);
1311 let is_get = method == "GET";
1312 let is_put = method == "PUT";
1313 let is_post = method == "POST";
1314 let is_delete = method == "DELETE";
1315
1316 if bucket.is_none() {
1318 return match method {
1319 "GET" if query.get("x-id").map(|s| s.as_str()) == Some("ListDirectoryBuckets") => {
1326 Some("ListAllMyDirectoryBuckets")
1327 }
1328 "GET" => Some("ListBuckets"),
1329 _ => None,
1330 };
1331 }
1332
1333 if is_post && bucket == Some("WriteGetObjectResponse") && key.is_none() {
1340 return Some("WriteGetObjectResponse");
1341 }
1342
1343 let has_key = key.is_some();
1344
1345 if has_key && is_post && has("uploads") {
1347 return Some("CreateMultipartUpload");
1348 }
1349 if has_key && is_post && has("uploadId") {
1350 return Some("CompleteMultipartUpload");
1351 }
1352 if has_key && is_put && has("partNumber") && has("uploadId") {
1353 return Some("UploadPart");
1354 }
1355 if has_key && is_delete && has("uploadId") {
1356 return Some("AbortMultipartUpload");
1357 }
1358 if has_key && is_get && has("uploadId") {
1359 return Some("ListParts");
1360 }
1361 if !has_key && is_get && has("uploads") {
1362 return Some("ListMultipartUploads");
1363 }
1364
1365 if has_key {
1369 if has("tagging") {
1370 return Some(match method {
1371 "GET" => "GetObjectTagging",
1372 "PUT" => "PutObjectTagging",
1373 "DELETE" => "DeleteObjectTagging",
1374 _ => return None,
1375 });
1376 }
1377 if has("acl") {
1378 return Some(match method {
1379 "GET" => "GetObjectAcl",
1380 "PUT" => "PutObjectAcl",
1381 _ => return None,
1382 });
1383 }
1384 if has("retention") {
1385 return Some(match method {
1386 "GET" => "GetObjectRetention",
1387 "PUT" => "PutObjectRetention",
1388 _ => return None,
1389 });
1390 }
1391 if has("legal-hold") {
1392 return Some(match method {
1393 "GET" => "GetObjectLegalHold",
1394 "PUT" => "PutObjectLegalHold",
1395 _ => return None,
1396 });
1397 }
1398 if has("attributes") && is_get {
1403 return Some("GetObjectAttributes");
1404 }
1405 if has("restore") && is_post {
1406 return Some("RestoreObject");
1407 }
1408 if has("renameObject") && is_put {
1414 return Some("RenameObject");
1415 }
1416 if has("encryption") && is_put {
1422 return Some("PutObject");
1423 }
1424 if has("select-type") && is_post {
1428 return Some("GetObject");
1429 }
1430 }
1431
1432 if !has_key {
1434 if has("tagging") {
1435 return Some(match method {
1436 "GET" => "GetBucketTagging",
1437 "PUT" => "PutBucketTagging",
1438 "DELETE" => "DeleteBucketTagging",
1439 _ => return None,
1440 });
1441 }
1442 if has("acl") {
1443 return Some(match method {
1444 "GET" => "GetBucketAcl",
1445 "PUT" => "PutBucketAcl",
1446 _ => return None,
1447 });
1448 }
1449 if has("versioning") {
1450 return Some(match method {
1451 "GET" => "GetBucketVersioning",
1452 "PUT" => "PutBucketVersioning",
1453 _ => return None,
1454 });
1455 }
1456 if has("cors") {
1457 return Some(match method {
1458 "GET" => "GetBucketCors",
1459 "PUT" => "PutBucketCors",
1460 "DELETE" => "DeleteBucketCors",
1461 _ => return None,
1462 });
1463 }
1464 if has("policy") {
1465 return Some(match method {
1466 "GET" => "GetBucketPolicy",
1467 "PUT" => "PutBucketPolicy",
1468 "DELETE" => "DeleteBucketPolicy",
1469 _ => return None,
1470 });
1471 }
1472 if has("website") {
1473 return Some(match method {
1474 "GET" => "GetBucketWebsite",
1475 "PUT" => "PutBucketWebsite",
1476 "DELETE" => "DeleteBucketWebsite",
1477 _ => return None,
1478 });
1479 }
1480 if has("lifecycle") {
1481 return Some(match method {
1482 "GET" => "GetBucketLifecycleConfiguration",
1483 "PUT" => "PutBucketLifecycleConfiguration",
1484 "DELETE" => "DeleteBucketLifecycle",
1485 _ => return None,
1486 });
1487 }
1488 if has("encryption") {
1489 return Some(match method {
1490 "GET" => "GetBucketEncryption",
1491 "PUT" => "PutBucketEncryption",
1492 "DELETE" => "DeleteBucketEncryption",
1493 _ => return None,
1494 });
1495 }
1496 if has("logging") {
1497 return Some(match method {
1498 "GET" => "GetBucketLogging",
1499 "PUT" => "PutBucketLogging",
1500 _ => return None,
1501 });
1502 }
1503 if has("notification") {
1504 return Some(match method {
1505 "GET" => "GetBucketNotificationConfiguration",
1506 "PUT" => "PutBucketNotificationConfiguration",
1507 _ => return None,
1508 });
1509 }
1510 if has("replication") {
1511 return Some(match method {
1512 "GET" => "GetBucketReplication",
1513 "PUT" => "PutBucketReplication",
1514 "DELETE" => "DeleteBucketReplication",
1515 _ => return None,
1516 });
1517 }
1518 if has("ownershipControls") {
1519 return Some(match method {
1520 "GET" => "GetBucketOwnershipControls",
1521 "PUT" => "PutBucketOwnershipControls",
1522 "DELETE" => "DeleteBucketOwnershipControls",
1523 _ => return None,
1524 });
1525 }
1526 if has("publicAccessBlock") {
1527 return Some(match method {
1528 "GET" => "GetPublicAccessBlock",
1529 "PUT" => "PutPublicAccessBlock",
1530 "DELETE" => "DeletePublicAccessBlock",
1531 _ => return None,
1532 });
1533 }
1534 if has("accelerate") {
1535 return Some(match method {
1536 "GET" => "GetBucketAccelerateConfiguration",
1537 "PUT" => "PutBucketAccelerateConfiguration",
1538 _ => return None,
1539 });
1540 }
1541 if has("inventory") {
1542 return Some(match method {
1543 "GET" => "GetBucketInventoryConfiguration",
1544 "PUT" => "PutBucketInventoryConfiguration",
1545 "DELETE" => "DeleteBucketInventoryConfiguration",
1546 _ => return None,
1547 });
1548 }
1549 if has("analytics") {
1550 return Some(match method {
1551 "GET" if has("id") => "GetBucketAnalyticsConfiguration",
1552 "GET" => "ListBucketAnalyticsConfigurations",
1553 "PUT" => "PutBucketAnalyticsConfiguration",
1554 "DELETE" => "DeleteBucketAnalyticsConfiguration",
1555 _ => return None,
1556 });
1557 }
1558 if has("intelligent-tiering") {
1559 return Some(match method {
1560 "GET" if has("id") => "GetBucketIntelligentTieringConfiguration",
1561 "GET" => "ListBucketIntelligentTieringConfigurations",
1562 "PUT" => "PutBucketIntelligentTieringConfiguration",
1563 "DELETE" => "DeleteBucketIntelligentTieringConfiguration",
1564 _ => return None,
1565 });
1566 }
1567 if has("metrics") {
1568 return Some(match method {
1569 "GET" if has("id") => "GetBucketMetricsConfiguration",
1570 "GET" => "ListBucketMetricsConfigurations",
1571 "PUT" => "PutBucketMetricsConfiguration",
1572 "DELETE" => "DeleteBucketMetricsConfiguration",
1573 _ => return None,
1574 });
1575 }
1576 if has("requestPayment") {
1577 return Some(match method {
1578 "GET" => "GetBucketRequestPayment",
1579 "PUT" => "PutBucketRequestPayment",
1580 _ => return None,
1581 });
1582 }
1583 if has("policyStatus") && is_get {
1584 return Some("GetBucketPolicyStatus");
1585 }
1586 if has("metadataConfiguration") {
1587 return Some(match method {
1588 "GET" => "GetBucketMetadataConfiguration",
1589 "POST" => "CreateBucketMetadataConfiguration",
1590 "DELETE" => "DeleteBucketMetadataConfiguration",
1591 _ => return None,
1592 });
1593 }
1594 if has("metadataTable") {
1595 return Some(match method {
1596 "GET" => "GetBucketMetadataTableConfiguration",
1597 "POST" => "CreateBucketMetadataTableConfiguration",
1598 "DELETE" => "DeleteBucketMetadataTableConfiguration",
1599 _ => return None,
1600 });
1601 }
1602 if has("metadataInventoryTable") && is_put {
1603 return Some("UpdateBucketMetadataInventoryTableConfiguration");
1604 }
1605 if has("metadataJournalTable") && is_put {
1606 return Some("UpdateBucketMetadataJournalTableConfiguration");
1607 }
1608 if has("abac") {
1609 return Some(match method {
1615 "PUT" => "PutBucketAbacConfiguration",
1616 "GET" => "GetBucketAbacConfiguration",
1617 _ => return None,
1618 });
1619 }
1620 if has("session") && is_get {
1621 return Some("CreateSession");
1627 }
1628 if has("object-lock") {
1629 return Some(match method {
1630 "GET" => "GetObjectLockConfiguration",
1631 "PUT" => "PutObjectLockConfiguration",
1632 _ => return None,
1633 });
1634 }
1635 if has("location") {
1636 return Some("GetBucketLocation");
1637 }
1638 if is_post && has("delete") {
1639 return Some("DeleteObjects");
1640 }
1641 if is_get && has("versions") {
1642 return Some("ListObjectVersions");
1643 }
1644 }
1645
1646 if is_get && has_key && has("torrent") {
1651 return Some("GetObjectTorrent");
1652 }
1653
1654 match (method, has_key) {
1656 ("GET", true) => Some("GetObject"),
1657 ("PUT", true) => {
1658 Some("PutObject")
1664 }
1665 ("DELETE", true) => Some("DeleteObject"),
1666 ("HEAD", true) => Some("HeadObject"),
1667 ("GET", false) => {
1668 if query.contains_key("list-type") {
1669 Some("ListObjectsV2")
1670 } else {
1671 Some("ListObjects")
1672 }
1673 }
1674 ("PUT", false) => Some("CreateBucket"),
1675 ("DELETE", false) => Some("DeleteBucket"),
1676 ("HEAD", false) => Some("HeadBucket"),
1677 _ => None,
1678 }
1679}
1680
1681fn s3_resource_for(action: &'static str, bucket: Option<&str>, key: Option<&str>) -> String {
1685 const OBJECT_ACTIONS: &[&str] = &[
1687 "PutObject",
1688 "GetObject",
1689 "DeleteObject",
1690 "HeadObject",
1691 "CopyObject",
1692 "GetObjectAttributes",
1693 "RestoreObject",
1694 "PutObjectTagging",
1695 "GetObjectTagging",
1696 "DeleteObjectTagging",
1697 "PutObjectAcl",
1698 "GetObjectAcl",
1699 "PutObjectRetention",
1700 "GetObjectRetention",
1701 "PutObjectLegalHold",
1702 "GetObjectLegalHold",
1703 "CreateMultipartUpload",
1704 "UploadPart",
1705 "UploadPartCopy",
1706 "CompleteMultipartUpload",
1707 "AbortMultipartUpload",
1708 "ListParts",
1709 ];
1710 if action == "ListBuckets" {
1711 return "*".to_string();
1712 }
1713 let Some(bucket) = bucket else {
1714 return "*".to_string();
1715 };
1716 if OBJECT_ACTIONS.contains(&action) {
1717 match key {
1718 Some(k) if !k.is_empty() => Arn::s3(&format!("{bucket}/{k}")).to_string(),
1719 _ => Arn::s3(&format!("{bucket}/*")).to_string(),
1720 }
1721 } else {
1722 Arn::s3(bucket).to_string()
1724 }
1725}
1726
1727pub(crate) fn truncate_to_seconds(dt: DateTime<Utc>) -> DateTime<Utc> {
1731 dt.with_nanosecond(0).unwrap_or(dt)
1732}
1733
1734pub(crate) fn check_get_conditionals(
1735 req: &AwsRequest,
1736 obj: &S3Object,
1737) -> Result<(), AwsServiceError> {
1738 let obj_etag = format!("\"{}\"", obj.etag);
1739 let obj_time = truncate_to_seconds(obj.last_modified);
1740
1741 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
1743 if !etag_matches(if_match, &obj_etag) {
1744 return Err(precondition_failed("If-Match"));
1745 }
1746 }
1747
1748 if let Some(if_none_match) = req
1750 .headers
1751 .get("if-none-match")
1752 .and_then(|v| v.to_str().ok())
1753 {
1754 if etag_matches(if_none_match, &obj_etag) {
1755 return Err(not_modified_with_etag(&obj_etag));
1756 }
1757 }
1758
1759 if req.headers.get("if-match").is_none() {
1761 if let Some(since) = req
1762 .headers
1763 .get("if-unmodified-since")
1764 .and_then(|v| v.to_str().ok())
1765 {
1766 if let Some(dt) = parse_http_date(since) {
1767 if obj_time > dt {
1768 return Err(precondition_failed("If-Unmodified-Since"));
1769 }
1770 }
1771 }
1772 }
1773
1774 if req.headers.get("if-none-match").is_none() {
1776 if let Some(since) = req
1777 .headers
1778 .get("if-modified-since")
1779 .and_then(|v| v.to_str().ok())
1780 {
1781 if let Some(dt) = parse_http_date(since) {
1782 if obj_time <= dt {
1783 return Err(not_modified());
1784 }
1785 }
1786 }
1787 }
1788
1789 Ok(())
1790}
1791
1792pub(crate) fn check_head_conditionals(
1793 req: &AwsRequest,
1794 obj: &S3Object,
1795) -> Result<(), AwsServiceError> {
1796 let obj_etag = format!("\"{}\"", obj.etag);
1797 let obj_time = truncate_to_seconds(obj.last_modified);
1798
1799 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
1801 if !etag_matches(if_match, &obj_etag) {
1802 return Err(AwsServiceError::aws_error(
1803 StatusCode::PRECONDITION_FAILED,
1804 "412",
1805 "Precondition Failed",
1806 ));
1807 }
1808 }
1809
1810 if let Some(if_none_match) = req
1812 .headers
1813 .get("if-none-match")
1814 .and_then(|v| v.to_str().ok())
1815 {
1816 if etag_matches(if_none_match, &obj_etag) {
1817 return Err(not_modified_with_etag(&obj_etag));
1818 }
1819 }
1820
1821 if req.headers.get("if-match").is_none() {
1823 if let Some(since) = req
1824 .headers
1825 .get("if-unmodified-since")
1826 .and_then(|v| v.to_str().ok())
1827 {
1828 if let Some(dt) = parse_http_date(since) {
1829 if obj_time > dt {
1830 return Err(AwsServiceError::aws_error(
1831 StatusCode::PRECONDITION_FAILED,
1832 "412",
1833 "Precondition Failed",
1834 ));
1835 }
1836 }
1837 }
1838 }
1839
1840 if req.headers.get("if-none-match").is_none() {
1842 if let Some(since) = req
1843 .headers
1844 .get("if-modified-since")
1845 .and_then(|v| v.to_str().ok())
1846 {
1847 if let Some(dt) = parse_http_date(since) {
1848 if obj_time <= dt {
1849 return Err(not_modified());
1850 }
1851 }
1852 }
1853 }
1854
1855 Ok(())
1856}
1857
1858pub(crate) fn etag_matches(condition: &str, obj_etag: &str) -> bool {
1859 let condition = condition.trim();
1860 if condition == "*" {
1861 return true;
1862 }
1863 let clean_etag = obj_etag.replace('"', "");
1864 for part in condition.split(',') {
1866 let part = part.trim().replace('"', "");
1867 if part == clean_etag {
1868 return true;
1869 }
1870 }
1871 false
1872}
1873
1874pub(crate) fn parse_http_date(s: &str) -> Option<DateTime<Utc>> {
1875 if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
1877 return Some(dt.with_timezone(&Utc));
1878 }
1879 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
1881 return Some(dt.with_timezone(&Utc));
1882 }
1883 if let Ok(dt) =
1885 chrono::NaiveDateTime::parse_from_str(s.trim_end_matches(" GMT"), "%a, %d %b %Y %H:%M:%S")
1886 {
1887 return Some(dt.and_utc());
1888 }
1889 if let Ok(dt) = s.parse::<DateTime<Utc>>() {
1891 return Some(dt);
1892 }
1893 None
1894}
1895
1896pub(crate) fn not_modified() -> AwsServiceError {
1897 AwsServiceError::aws_error(StatusCode::NOT_MODIFIED, "304", "Not Modified")
1898}
1899
1900pub(crate) fn not_modified_with_etag(etag: &str) -> AwsServiceError {
1901 AwsServiceError::aws_error_with_headers(
1902 StatusCode::NOT_MODIFIED,
1903 "304",
1904 "Not Modified",
1905 vec![("etag".to_string(), etag.to_string())],
1906 )
1907}
1908
1909pub(crate) fn precondition_failed(condition: &str) -> AwsServiceError {
1910 AwsServiceError::aws_error_with_fields(
1911 StatusCode::PRECONDITION_FAILED,
1912 "PreconditionFailed",
1913 "At least one of the pre-conditions you specified did not hold",
1914 vec![("Condition".to_string(), condition.to_string())],
1915 )
1916}
1917
1918pub(crate) fn build_acl_xml(owner_id: &str, grants: &[AclGrant], _account_id: &str) -> String {
1921 let mut grants_xml = String::new();
1922 for g in grants {
1923 let grantee_xml = if g.grantee_type == "Group" {
1924 let uri = g.grantee_uri.as_deref().unwrap_or("");
1925 format!(
1926 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\">\
1927 <URI>{}</URI></Grantee>",
1928 xml_escape(uri),
1929 )
1930 } else {
1931 let id = g.grantee_id.as_deref().unwrap_or("");
1932 format!(
1933 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">\
1934 <ID>{}</ID></Grantee>",
1935 xml_escape(id),
1936 )
1937 };
1938 grants_xml.push_str(&format!(
1939 "<Grant>{grantee_xml}<Permission>{}</Permission></Grant>",
1940 xml_escape(&g.permission),
1941 ));
1942 }
1943
1944 format!(
1945 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
1946 <AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
1947 <Owner><ID>{owner_id}</ID><DisplayName>{owner_id}</DisplayName></Owner>\
1948 <AccessControlList>{grants_xml}</AccessControlList>\
1949 </AccessControlPolicy>",
1950 owner_id = xml_escape(owner_id),
1951 )
1952}
1953
1954pub(crate) fn canned_acl_grants(acl: &str, owner_id: &str) -> Vec<AclGrant> {
1955 let owner_grant = AclGrant {
1956 grantee_type: "CanonicalUser".to_string(),
1957 grantee_id: Some(owner_id.to_string()),
1958 grantee_display_name: Some(owner_id.to_string()),
1959 grantee_uri: None,
1960 permission: "FULL_CONTROL".to_string(),
1961 };
1962 match acl {
1963 "private" => vec![owner_grant],
1964 "public-read" => vec![
1965 owner_grant,
1966 AclGrant {
1967 grantee_type: "Group".to_string(),
1968 grantee_id: None,
1969 grantee_display_name: None,
1970 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1971 permission: "READ".to_string(),
1972 },
1973 ],
1974 "public-read-write" => vec![
1975 owner_grant,
1976 AclGrant {
1977 grantee_type: "Group".to_string(),
1978 grantee_id: None,
1979 grantee_display_name: None,
1980 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1981 permission: "READ".to_string(),
1982 },
1983 AclGrant {
1984 grantee_type: "Group".to_string(),
1985 grantee_id: None,
1986 grantee_display_name: None,
1987 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1988 permission: "WRITE".to_string(),
1989 },
1990 ],
1991 "authenticated-read" => vec![
1992 owner_grant,
1993 AclGrant {
1994 grantee_type: "Group".to_string(),
1995 grantee_id: None,
1996 grantee_display_name: None,
1997 grantee_uri: Some(
1998 "http://acs.amazonaws.com/groups/global/AuthenticatedUsers".to_string(),
1999 ),
2000 permission: "READ".to_string(),
2001 },
2002 ],
2003 "bucket-owner-full-control" => vec![owner_grant],
2004 _ => vec![owner_grant],
2005 }
2006}
2007
2008pub(crate) fn canned_acl_grants_for_object(acl: &str, owner_id: &str) -> Vec<AclGrant> {
2009 canned_acl_grants(acl, owner_id)
2011}
2012
2013pub(crate) fn parse_grant_headers(headers: &HeaderMap) -> Vec<AclGrant> {
2014 let mut grants = Vec::new();
2015 let header_permission_map = [
2016 ("x-amz-grant-read", "READ"),
2017 ("x-amz-grant-write", "WRITE"),
2018 ("x-amz-grant-read-acp", "READ_ACP"),
2019 ("x-amz-grant-write-acp", "WRITE_ACP"),
2020 ("x-amz-grant-full-control", "FULL_CONTROL"),
2021 ];
2022
2023 for (header, permission) in &header_permission_map {
2024 if let Some(value) = headers.get(*header).and_then(|v| v.to_str().ok()) {
2025 for part in value.split(',') {
2027 let part = part.trim();
2028 if let Some((key, val)) = part.split_once('=') {
2029 let val = val.trim().trim_matches('"');
2030 let key = key.trim().to_lowercase();
2031 match key.as_str() {
2032 "id" => {
2033 grants.push(AclGrant {
2034 grantee_type: "CanonicalUser".to_string(),
2035 grantee_id: Some(val.to_string()),
2036 grantee_display_name: Some(val.to_string()),
2037 grantee_uri: None,
2038 permission: permission.to_string(),
2039 });
2040 }
2041 "uri" | "url" => {
2042 grants.push(AclGrant {
2043 grantee_type: "Group".to_string(),
2044 grantee_id: None,
2045 grantee_display_name: None,
2046 grantee_uri: Some(val.to_string()),
2047 permission: permission.to_string(),
2048 });
2049 }
2050 _ => {}
2051 }
2052 }
2053 }
2054 }
2055 }
2056 grants
2057}
2058
2059pub(crate) fn parse_acl_xml(xml: &str) -> Result<Vec<AclGrant>, AwsServiceError> {
2060 if xml.contains("<AccessControlPolicy") && !xml.contains("<Owner>") {
2062 return Err(AwsServiceError::aws_error(
2063 StatusCode::BAD_REQUEST,
2064 "MalformedACLError",
2065 "The XML you provided was not well-formed or did not validate against our published schema",
2066 ));
2067 }
2068
2069 let valid_permissions = ["READ", "WRITE", "READ_ACP", "WRITE_ACP", "FULL_CONTROL"];
2070
2071 let mut grants = Vec::new();
2072 let mut remaining = xml;
2073 while let Some(start) = remaining.find("<Grant>") {
2074 let after = &remaining[start + 7..];
2075 if let Some(end) = after.find("</Grant>") {
2076 let grant_body = &after[..end];
2077
2078 let permission = extract_xml_value(grant_body, "Permission").unwrap_or_default();
2080 if !valid_permissions.contains(&permission.as_str()) {
2081 return Err(AwsServiceError::aws_error(
2082 StatusCode::BAD_REQUEST,
2083 "MalformedACLError",
2084 "The XML you provided was not well-formed or did not validate against our published schema",
2085 ));
2086 }
2087
2088 if grant_body.contains("xsi:type=\"Group\"") || grant_body.contains("<URI>") {
2090 let uri = extract_xml_value(grant_body, "URI").unwrap_or_default();
2091 grants.push(AclGrant {
2092 grantee_type: "Group".to_string(),
2093 grantee_id: None,
2094 grantee_display_name: None,
2095 grantee_uri: Some(uri),
2096 permission,
2097 });
2098 } else {
2099 let id = extract_xml_value(grant_body, "ID").unwrap_or_default();
2100 let display =
2101 extract_xml_value(grant_body, "DisplayName").unwrap_or_else(|| id.clone());
2102 grants.push(AclGrant {
2103 grantee_type: "CanonicalUser".to_string(),
2104 grantee_id: Some(id),
2105 grantee_display_name: Some(display),
2106 grantee_uri: None,
2107 permission,
2108 });
2109 }
2110
2111 remaining = &after[end + 8..];
2112 } else {
2113 break;
2114 }
2115 }
2116 Ok(grants)
2117}
2118
2119pub(crate) enum RangeResult {
2122 Satisfiable { start: usize, end: usize },
2123 NotSatisfiable,
2124 Ignored,
2125}
2126
2127pub(crate) fn parse_range_header(range_str: &str, total_size: usize) -> Option<RangeResult> {
2128 let range_str = range_str.strip_prefix("bytes=")?;
2129 let (start_str, end_str) = range_str.split_once('-')?;
2130 if start_str.is_empty() {
2131 let suffix_len: usize = end_str.parse().ok()?;
2132 if suffix_len == 0 || total_size == 0 {
2133 return Some(RangeResult::NotSatisfiable);
2134 }
2135 let start = total_size.saturating_sub(suffix_len);
2136 Some(RangeResult::Satisfiable {
2137 start,
2138 end: total_size - 1,
2139 })
2140 } else {
2141 let start: usize = start_str.parse().ok()?;
2142 if start >= total_size {
2143 return Some(RangeResult::NotSatisfiable);
2144 }
2145 let end = if end_str.is_empty() {
2146 total_size - 1
2147 } else {
2148 let e: usize = end_str.parse().ok()?;
2149 if e < start {
2150 return Some(RangeResult::Ignored);
2151 }
2152 std::cmp::min(e, total_size - 1)
2153 };
2154 Some(RangeResult::Satisfiable { start, end })
2155 }
2156}
2157
2158pub(crate) fn s3_xml(status: StatusCode, body: impl Into<Bytes>) -> AwsResponse {
2162 AwsResponse {
2163 status,
2164 content_type: "application/xml".to_string(),
2165 body: body.into().into(),
2166 headers: HeaderMap::new(),
2167 }
2168}
2169
2170pub(crate) fn empty_response(status: StatusCode) -> AwsResponse {
2171 AwsResponse {
2172 status,
2173 content_type: "application/xml".to_string(),
2174 body: Bytes::new().into(),
2175 headers: HeaderMap::new(),
2176 }
2177}
2178
2179pub(crate) fn is_frozen(obj: &S3Object) -> bool {
2182 matches!(obj.storage_class.as_str(), "GLACIER" | "DEEP_ARCHIVE")
2183 && obj.restore_ongoing != Some(false)
2184}
2185
2186pub(crate) fn no_such_bucket(bucket: &str) -> AwsServiceError {
2187 AwsServiceError::aws_error_with_fields(
2188 StatusCode::NOT_FOUND,
2189 "NoSuchBucket",
2190 "The specified bucket does not exist",
2191 vec![("BucketName".to_string(), bucket.to_string())],
2192 )
2193}
2194
2195pub(crate) fn no_such_key(key: &str) -> AwsServiceError {
2196 AwsServiceError::aws_error_with_fields(
2197 StatusCode::NOT_FOUND,
2198 "NoSuchKey",
2199 "The specified key does not exist.",
2200 vec![("Key".to_string(), key.to_string())],
2201 )
2202}
2203
2204pub(crate) fn no_such_upload(upload_id: &str) -> AwsServiceError {
2205 AwsServiceError::aws_error_with_fields(
2206 StatusCode::NOT_FOUND,
2207 "NoSuchUpload",
2208 "The specified upload does not exist. The upload ID may be invalid, \
2209 or the upload may have been aborted or completed.",
2210 vec![("UploadId".to_string(), upload_id.to_string())],
2211 )
2212}
2213
2214pub(crate) fn no_such_key_with_detail(key: &str) -> AwsServiceError {
2215 AwsServiceError::aws_error_with_fields(
2216 StatusCode::NOT_FOUND,
2217 "NoSuchKey",
2218 "The specified key does not exist.",
2219 vec![("Key".to_string(), key.to_string())],
2220 )
2221}
2222
2223pub(crate) fn compute_md5(data: &[u8]) -> String {
2224 let digest = Md5::digest(data);
2225 format!("{:x}", digest)
2226}
2227
2228pub(crate) fn compute_checksum(algorithm: &str, data: &[u8]) -> String {
2229 let raw = compute_checksum_raw(algorithm, data);
2230 if raw.is_empty() {
2231 String::new()
2232 } else {
2233 BASE64.encode(raw)
2234 }
2235}
2236
2237pub(crate) fn compute_checksum_raw(algorithm: &str, data: &[u8]) -> Vec<u8> {
2244 match algorithm {
2245 "CRC32" => crc32fast::hash(data).to_be_bytes().to_vec(),
2246 "CRC32C" => crc32c::crc32c(data).to_be_bytes().to_vec(),
2251 "CRC64NVME" => {
2252 let mut hasher = crc64fast_nvme::Digest::new();
2253 hasher.write(data);
2254 hasher.sum64().to_be_bytes().to_vec()
2255 }
2256 "SHA1" => {
2257 use sha1::Digest as _;
2258 sha1::Sha1::digest(data).to_vec()
2259 }
2260 "SHA256" => {
2261 use sha2::Digest as _;
2262 sha2::Sha256::digest(data).to_vec()
2263 }
2264 _ => Vec::new(),
2265 }
2266}
2267
2268pub(crate) fn compute_composite_checksum(
2274 algorithm: &str,
2275 part_raw_digests: &[Vec<u8>],
2276) -> Option<String> {
2277 if part_raw_digests.is_empty() {
2278 return None;
2279 }
2280 let mut concat = Vec::new();
2281 for d in part_raw_digests {
2282 concat.extend_from_slice(d);
2283 }
2284 let digest = compute_checksum_raw(algorithm, &concat);
2285 if digest.is_empty() {
2286 return None;
2287 }
2288 Some(format!(
2289 "{}-{}",
2290 BASE64.encode(digest),
2291 part_raw_digests.len()
2292 ))
2293}
2294
2295pub(crate) async fn compute_checksum_streaming(
2301 algorithm: &str,
2302 path: &std::path::Path,
2303) -> Result<String, std::io::Error> {
2304 use tokio::io::AsyncReadExt;
2305 let mut file = tokio::fs::File::open(path).await?;
2306 let mut buf = vec![0u8; 1024 * 1024];
2307 match algorithm {
2308 "CRC32" => {
2309 let mut hasher = crc32fast::Hasher::new();
2310 loop {
2311 let n = file.read(&mut buf).await?;
2312 if n == 0 {
2313 break;
2314 }
2315 hasher.update(&buf[..n]);
2316 }
2317 Ok(BASE64.encode(hasher.finalize().to_be_bytes()))
2318 }
2319 "CRC32C" => {
2320 let mut crc: u32 = 0;
2321 loop {
2322 let n = file.read(&mut buf).await?;
2323 if n == 0 {
2324 break;
2325 }
2326 crc = crc32c::crc32c_append(crc, &buf[..n]);
2327 }
2328 Ok(BASE64.encode(crc.to_be_bytes()))
2329 }
2330 "CRC64NVME" => {
2331 let mut hasher = crc64fast_nvme::Digest::new();
2332 loop {
2333 let n = file.read(&mut buf).await?;
2334 if n == 0 {
2335 break;
2336 }
2337 hasher.write(&buf[..n]);
2338 }
2339 Ok(BASE64.encode(hasher.sum64().to_be_bytes()))
2340 }
2341 "SHA1" => {
2342 use sha1::Digest as _;
2343 let mut hasher = sha1::Sha1::new();
2344 loop {
2345 let n = file.read(&mut buf).await?;
2346 if n == 0 {
2347 break;
2348 }
2349 hasher.update(&buf[..n]);
2350 }
2351 Ok(BASE64.encode(hasher.finalize()))
2352 }
2353 "SHA256" => {
2354 use sha2::Digest as _;
2355 let mut hasher = sha2::Sha256::new();
2356 loop {
2357 let n = file.read(&mut buf).await?;
2358 if n == 0 {
2359 break;
2360 }
2361 hasher.update(&buf[..n]);
2362 }
2363 Ok(BASE64.encode(hasher.finalize()))
2364 }
2365 _ => Ok(String::new()),
2366 }
2367}
2368
2369pub(crate) fn url_encode_s3_key(s: &str) -> String {
2370 let mut out = String::with_capacity(s.len() * 2);
2371 for byte in s.bytes() {
2372 match byte {
2373 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
2374 out.push(byte as char);
2375 }
2376 _ => {
2377 out.push_str(&format!("%{:02X}", byte));
2378 }
2379 }
2380 }
2381 out
2382}
2383
2384pub(crate) use fakecloud_aws::xml::xml_escape;
2385
2386pub(crate) fn extract_user_metadata(
2387 headers: &HeaderMap,
2388) -> std::collections::BTreeMap<String, String> {
2389 let mut meta = std::collections::BTreeMap::new();
2390 for (name, value) in headers {
2391 if let Some(key) = name.as_str().strip_prefix("x-amz-meta-") {
2392 if let Ok(v) = value.to_str() {
2393 meta.insert(key.to_string(), v.to_string());
2394 }
2395 }
2396 }
2397 meta
2398}
2399
2400pub(crate) fn is_valid_storage_class(class: &str) -> bool {
2401 matches!(
2402 class,
2403 "STANDARD"
2404 | "REDUCED_REDUNDANCY"
2405 | "STANDARD_IA"
2406 | "ONEZONE_IA"
2407 | "INTELLIGENT_TIERING"
2408 | "GLACIER"
2409 | "DEEP_ARCHIVE"
2410 | "GLACIER_IR"
2411 | "OUTPOSTS"
2412 | "SNOW"
2413 | "EXPRESS_ONEZONE"
2414 )
2415}
2416
2417pub(crate) fn is_valid_bucket_name(name: &str) -> bool {
2418 if name.len() < 3 || name.len() > 63 {
2421 return false;
2422 }
2423 if !name
2426 .bytes()
2427 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'.')
2428 {
2429 return false;
2430 }
2431 let bytes = name.as_bytes();
2433 if !bytes[0].is_ascii_alphanumeric() || !bytes[bytes.len() - 1].is_ascii_alphanumeric() {
2434 return false;
2435 }
2436 if name.contains("..") {
2438 return false;
2439 }
2440 if is_ipv4_format(name) {
2442 return false;
2443 }
2444 if name.starts_with("xn--")
2446 || name.starts_with("sthree-")
2447 || name.starts_with("amzn-s3-demo-")
2448 || name.ends_with("-s3alias")
2449 || name.ends_with("--ol-s3")
2450 || name.ends_with(".mrap")
2451 {
2452 return false;
2453 }
2454 true
2455}
2456
2457fn is_ipv4_format(name: &str) -> bool {
2460 let parts: Vec<&str> = name.split('.').collect();
2461 parts.len() == 4
2462 && parts.iter().all(|p| {
2463 !p.is_empty()
2464 && p.len() <= 3
2465 && p.bytes().all(|b| b.is_ascii_digit())
2466 && p.parse::<u8>().is_ok()
2467 })
2468}
2469
2470pub(crate) fn is_valid_region(region: &str) -> bool {
2471 let valid_regions = [
2473 "us-east-1",
2474 "us-east-2",
2475 "us-west-1",
2476 "us-west-2",
2477 "af-south-1",
2478 "ap-east-1",
2479 "ap-south-1",
2480 "ap-south-2",
2481 "ap-southeast-1",
2482 "ap-southeast-2",
2483 "ap-southeast-3",
2484 "ap-southeast-4",
2485 "ap-northeast-1",
2486 "ap-northeast-2",
2487 "ap-northeast-3",
2488 "ca-central-1",
2489 "ca-west-1",
2490 "eu-central-1",
2491 "eu-central-2",
2492 "eu-west-1",
2493 "eu-west-2",
2494 "eu-west-3",
2495 "eu-south-1",
2496 "eu-south-2",
2497 "eu-north-1",
2498 "il-central-1",
2499 "me-south-1",
2500 "me-central-1",
2501 "sa-east-1",
2502 "cn-north-1",
2503 "cn-northwest-1",
2504 "us-gov-east-1",
2505 "us-gov-east-2",
2506 "us-gov-west-1",
2507 "us-iso-east-1",
2508 "us-iso-west-1",
2509 "us-isob-east-1",
2510 "us-isof-south-1",
2511 ];
2512 valid_regions.contains(®ion)
2513}
2514
2515pub(crate) fn resolve_object<'a>(
2516 b: &'a S3Bucket,
2517 key: &str,
2518 version_id: Option<&String>,
2519) -> Result<&'a S3Object, AwsServiceError> {
2520 if let Some(vid) = version_id {
2521 if vid == "null" {
2523 if let Some(versions) = b.object_versions.get(key) {
2525 if let Some(obj) = versions
2526 .iter()
2527 .find(|o| o.version_id.is_none() || o.version_id.as_deref() == Some("null"))
2528 {
2529 return Ok(obj);
2530 }
2531 }
2532 if let Some(obj) = b.objects.get(key) {
2534 if obj.version_id.is_none() || obj.version_id.as_deref() == Some("null") {
2535 return Ok(obj);
2536 }
2537 }
2538 } else {
2539 if let Some(versions) = b.object_versions.get(key) {
2541 if let Some(obj) = versions
2542 .iter()
2543 .find(|o| o.version_id.as_deref() == Some(vid.as_str()))
2544 {
2545 return Ok(obj);
2546 }
2547 }
2548 if let Some(obj) = b.objects.get(key) {
2550 if obj.version_id.as_deref() == Some(vid.as_str()) {
2551 return Ok(obj);
2552 }
2553 }
2554 }
2555 if b.versioning.is_some() {
2557 Err(AwsServiceError::aws_error_with_fields(
2558 StatusCode::NOT_FOUND,
2559 "NoSuchVersion",
2560 "The specified version does not exist.",
2561 vec![
2562 ("Key".to_string(), key.to_string()),
2563 ("VersionId".to_string(), vid.to_string()),
2564 ],
2565 ))
2566 } else {
2567 Err(AwsServiceError::aws_error(
2568 StatusCode::BAD_REQUEST,
2569 "InvalidArgument",
2570 "Invalid version id specified",
2571 ))
2572 }
2573 } else {
2574 b.objects.get(key).ok_or_else(|| no_such_key(key))
2575 }
2576}
2577
2578pub(crate) fn make_delete_marker(key: &str, dm_id: &str) -> S3Object {
2579 S3Object {
2580 key: key.to_string(),
2581 last_modified: Utc::now(),
2582 storage_class: "STANDARD".to_string(),
2583 version_id: Some(dm_id.to_string()),
2584 is_delete_marker: true,
2585 ..Default::default()
2586 }
2587}
2588
2589pub(crate) struct DeleteObjectEntry {
2591 key: String,
2592 version_id: Option<String>,
2593}
2594
2595pub(crate) fn parse_delete_objects_xml(xml: &str) -> Vec<DeleteObjectEntry> {
2596 let mut entries = Vec::new();
2597 let mut remaining = xml;
2598 while let Some(obj_start) = remaining.find("<Object>") {
2599 let after = &remaining[obj_start + 8..];
2600 if let Some(obj_end) = after.find("</Object>") {
2601 let obj_body = &after[..obj_end];
2602 let key = extract_xml_value(obj_body, "Key");
2603 let version_id = extract_xml_value(obj_body, "VersionId");
2604 if let Some(k) = key {
2605 entries.push(DeleteObjectEntry { key: k, version_id });
2606 }
2607 remaining = &after[obj_end + 9..];
2608 } else {
2609 break;
2610 }
2611 }
2612 entries
2613}
2614
2615pub(crate) fn parse_delete_objects_quiet(xml: &str) -> bool {
2618 extract_xml_value(xml, "Quiet")
2619 .map(|v| v.eq_ignore_ascii_case("true"))
2620 .unwrap_or(false)
2621}
2622
2623pub(crate) fn parse_tagging_xml(xml: &str) -> Vec<(String, String)> {
2626 let mut tags = Vec::new();
2627 let mut remaining = xml;
2628 while let Some(tag_start) = remaining.find("<Tag>") {
2629 let after = &remaining[tag_start + 5..];
2630 if let Some(tag_end) = after.find("</Tag>") {
2631 let tag_body = &after[..tag_end];
2632 let key = extract_xml_value(tag_body, "Key");
2633 let value = extract_xml_value(tag_body, "Value");
2634 if let (Some(k), Some(v)) = (key, value) {
2635 tags.push((k, v));
2636 }
2637 remaining = &after[tag_end + 6..];
2638 } else {
2639 break;
2640 }
2641 }
2642 tags
2643}
2644
2645pub(crate) fn validate_tags(tags: &[(String, String)]) -> Result<(), AwsServiceError> {
2646 let mut seen = std::collections::HashSet::new();
2648 for (k, _) in tags {
2649 if !seen.insert(k.as_str()) {
2650 return Err(AwsServiceError::aws_error(
2651 StatusCode::BAD_REQUEST,
2652 "InvalidTag",
2653 "Cannot provide multiple Tags with the same key",
2654 ));
2655 }
2656 if k.starts_with("aws:") {
2658 return Err(AwsServiceError::aws_error(
2659 StatusCode::BAD_REQUEST,
2660 "InvalidTag",
2661 "System tags cannot be added/updated by requester",
2662 ));
2663 }
2664 }
2665 Ok(())
2666}
2667
2668pub(crate) fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
2669 let self_closing1 = format!("<{tag} />");
2671 let self_closing2 = format!("<{tag}/>");
2672 if xml.contains(&self_closing1) || xml.contains(&self_closing2) {
2673 let self_pos = xml
2675 .find(&self_closing1)
2676 .or_else(|| xml.find(&self_closing2));
2677 let open = format!("<{tag}>");
2678 let open_pos = xml.find(&open);
2679 match (self_pos, open_pos) {
2680 (Some(sp), Some(op)) if sp < op => return Some(String::new()),
2681 (Some(_), None) => return Some(String::new()),
2682 _ => {}
2683 }
2684 }
2685
2686 let open = format!("<{tag}>");
2687 let close = format!("</{tag}>");
2688 let start = xml.find(&open)? + open.len();
2689 let end = xml[start..].find(&close)? + start;
2693 Some(decode_xml_entities(&xml[start..end]))
2701}
2702
2703pub(crate) fn decode_xml_entities(s: &str) -> String {
2712 if !s.contains('&') {
2713 return s.to_string();
2714 }
2715 let mut out = String::with_capacity(s.len());
2716 let bytes = s.as_bytes();
2717 let mut i = 0;
2718 while i < s.len() {
2719 if bytes[i] == b'&' {
2720 if let Some(rel) = s[i + 1..].find(';') {
2723 if rel <= 10 {
2724 let entity = &s[i + 1..i + 1 + rel];
2725 let decoded = match entity {
2726 "amp" => Some('&'),
2727 "lt" => Some('<'),
2728 "gt" => Some('>'),
2729 "quot" => Some('"'),
2730 "apos" => Some('\''),
2731 _ => entity
2732 .strip_prefix("#x")
2733 .or_else(|| entity.strip_prefix("#X"))
2734 .and_then(|hex| u32::from_str_radix(hex, 16).ok())
2735 .or_else(|| {
2736 entity.strip_prefix('#').and_then(|d| d.parse::<u32>().ok())
2737 })
2738 .and_then(char::from_u32),
2739 };
2740 if let Some(c) = decoded {
2741 out.push(c);
2742 i += 1 + rel + 1;
2743 continue;
2744 }
2745 }
2746 }
2747 out.push('&');
2748 i += 1;
2749 } else {
2750 let ch = s[i..].chars().next().unwrap();
2751 out.push(ch);
2752 i += ch.len_utf8();
2753 }
2754 }
2755 out
2756}
2757
2758pub(crate) fn strip_etag_quotes(s: &str) -> String {
2768 s.replace(""", "")
2769 .replace(""", "")
2770 .replace(""", "")
2771 .replace(""", "")
2772 .replace('"', "")
2773 .trim()
2774 .to_string()
2775}
2776
2777pub(crate) fn parse_complete_multipart_xml(xml: &str) -> Vec<(u32, String)> {
2778 let mut parts = Vec::new();
2779 let mut remaining = xml;
2780 while let Some(part_start) = remaining.find("<Part>") {
2781 let after = &remaining[part_start + 6..];
2782 if let Some(part_end) = after.find("</Part>") {
2783 let part_body = &after[..part_end];
2784 let part_num =
2785 extract_xml_value(part_body, "PartNumber").and_then(|s| s.parse::<u32>().ok());
2786 let etag = extract_xml_value(part_body, "ETag").map(|s| strip_etag_quotes(&s));
2787 if let (Some(num), Some(e)) = (part_num, etag) {
2788 parts.push((num, e));
2789 }
2790 remaining = &after[part_end + 7..];
2791 } else {
2792 break;
2793 }
2794 }
2795 parts
2796}
2797
2798pub(crate) fn parse_url_encoded_tags(s: &str) -> Vec<(String, String)> {
2799 let mut tags = Vec::new();
2800 for pair in s.split('&') {
2801 if pair.is_empty() {
2802 continue;
2803 }
2804 let (key, value) = match pair.find('=') {
2805 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
2806 None => (pair, ""),
2807 };
2808 tags.push((
2809 percent_encoding::percent_decode_str(key)
2810 .decode_utf8_lossy()
2811 .to_string(),
2812 percent_encoding::percent_decode_str(value)
2813 .decode_utf8_lossy()
2814 .to_string(),
2815 ));
2816 }
2817 tags
2818}
2819
2820pub(crate) fn validate_lifecycle_xml(xml: &str) -> Result<(), AwsServiceError> {
2822 let malformed = || {
2823 AwsServiceError::aws_error(
2824 StatusCode::BAD_REQUEST,
2825 "MalformedXML",
2826 "The XML you provided was not well-formed or did not validate against our published schema",
2827 )
2828 };
2829
2830 let mut remaining = xml;
2831 while let Some(rule_start) = remaining.find("<Rule>") {
2832 let after = &remaining[rule_start + 6..];
2833 if let Some(rule_end) = after.find("</Rule>") {
2834 let rule_body = &after[..rule_end];
2835
2836 let has_filter = rule_body.contains("<Filter>")
2838 || rule_body.contains("<Filter/>")
2839 || rule_body.contains("<Filter />");
2840
2841 let has_prefix_outside_filter = {
2843 if !rule_body.contains("<Prefix") {
2844 false
2845 } else if !has_filter {
2846 true } else {
2848 let mut stripped = rule_body.to_string();
2850 if let Some(fs) = stripped.find("<Filter") {
2852 if let Some(fe) = stripped.find("</Filter>") {
2853 stripped = format!("{}{}", &stripped[..fs], &stripped[fe + 9..]);
2854 }
2855 }
2856 stripped.contains("<Prefix")
2857 }
2858 };
2859
2860 if !has_filter && !has_prefix_outside_filter {
2861 return Err(malformed());
2862 }
2863 if has_filter && has_prefix_outside_filter {
2865 return Err(malformed());
2866 }
2867
2868 if let Some(exp_start) = rule_body.find("<Expiration>") {
2871 if let Some(exp_end) = rule_body[exp_start..].find("</Expiration>") {
2872 let exp_body = &rule_body[exp_start..exp_start + exp_end];
2873 if exp_body.contains("<ExpiredObjectDeleteMarker>")
2874 && (exp_body.contains("<Days>") || exp_body.contains("<Date>"))
2875 {
2876 return Err(malformed());
2877 }
2878 }
2879 }
2880
2881 if has_filter {
2883 if let Some(fs) = rule_body.find("<Filter>") {
2884 if let Some(fe) = rule_body.find("</Filter>") {
2885 if fe < fs + 8 {
2890 return Err(malformed());
2891 }
2892 let filter_body = &rule_body[fs + 8..fe];
2893 let has_prefix_in_filter = filter_body.contains("<Prefix");
2894 let has_tag_in_filter = filter_body.contains("<Tag>");
2895 let has_and_in_filter = filter_body.contains("<And>");
2896 if has_prefix_in_filter && has_tag_in_filter && !has_and_in_filter {
2898 return Err(malformed());
2899 }
2900 if has_tag_in_filter && has_and_in_filter {
2902 let and_start = filter_body.find("<And>").unwrap_or(0);
2904 let tag_pos = filter_body.find("<Tag>").unwrap_or(0);
2905 if tag_pos < and_start {
2906 return Err(malformed());
2907 }
2908 }
2909 }
2910 }
2911 }
2912
2913 if rule_body.contains("<NoncurrentVersionTransition>") {
2915 let mut nvt_remaining = rule_body;
2916 while let Some(nvt_start) = nvt_remaining.find("<NoncurrentVersionTransition>") {
2917 let nvt_after = &nvt_remaining[nvt_start + 29..];
2918 if let Some(nvt_end) = nvt_after.find("</NoncurrentVersionTransition>") {
2919 let nvt_body = &nvt_after[..nvt_end];
2920 if !nvt_body.contains("<NoncurrentDays>") {
2921 return Err(malformed());
2922 }
2923 if !nvt_body.contains("<StorageClass>") {
2924 return Err(malformed());
2925 }
2926 nvt_remaining = &nvt_after[nvt_end + 30..];
2927 } else {
2928 break;
2929 }
2930 }
2931 }
2932
2933 remaining = &after[rule_end + 7..];
2934 } else {
2935 break;
2936 }
2937 }
2938
2939 Ok(())
2940}
2941
2942pub(crate) struct CorsRule {
2944 allowed_origins: Vec<String>,
2945 allowed_methods: Vec<String>,
2946 allowed_headers: Vec<String>,
2947 expose_headers: Vec<String>,
2948 max_age_seconds: Option<u32>,
2949}
2950
2951pub(crate) fn parse_cors_config(xml: &str) -> Vec<CorsRule> {
2953 let mut rules = Vec::new();
2954 let mut remaining = xml;
2955 while let Some(start) = remaining.find("<CORSRule>") {
2956 let after = &remaining[start + 10..];
2957 if let Some(end) = after.find("</CORSRule>") {
2958 let block = &after[..end];
2959 let allowed_origins = extract_all_xml_values(block, "AllowedOrigin");
2960 let allowed_methods = extract_all_xml_values(block, "AllowedMethod");
2961 let allowed_headers = extract_all_xml_values(block, "AllowedHeader");
2962 let expose_headers = extract_all_xml_values(block, "ExposeHeader");
2963 let max_age_seconds =
2964 extract_xml_value(block, "MaxAgeSeconds").and_then(|s| s.parse().ok());
2965 rules.push(CorsRule {
2966 allowed_origins,
2967 allowed_methods,
2968 allowed_headers,
2969 expose_headers,
2970 max_age_seconds,
2971 });
2972 remaining = &after[end + 11..];
2973 } else {
2974 break;
2975 }
2976 }
2977 rules
2978}
2979
2980pub(crate) fn origin_matches(origin: &str, pattern: &str) -> bool {
2982 if pattern == "*" {
2983 return true;
2984 }
2985 if let Some(suffix) = pattern.strip_prefix('*') {
2987 return origin.ends_with(suffix);
2988 }
2989 origin == pattern
2990}
2991
2992pub(crate) fn find_cors_rule<'a>(
2994 rules: &'a [CorsRule],
2995 origin: &str,
2996 method: Option<&str>,
2997) -> Option<&'a CorsRule> {
2998 rules.iter().find(|rule| {
2999 let origin_ok = rule
3000 .allowed_origins
3001 .iter()
3002 .any(|o| origin_matches(origin, o));
3003 let method_ok = match method {
3004 Some(m) => rule.allowed_methods.iter().any(|am| am == m),
3005 None => true,
3006 };
3007 origin_ok && method_ok
3008 })
3009}
3010
3011pub(crate) fn check_object_lock_for_overwrite(
3014 obj: &S3Object,
3015 req: &AwsRequest,
3016) -> Option<&'static str> {
3017 if obj.lock_legal_hold.as_deref() == Some("ON") {
3019 return Some("AccessDenied");
3020 }
3021 if let (Some(mode), Some(until)) = (&obj.lock_mode, &obj.lock_retain_until) {
3023 if *until > Utc::now() {
3024 if mode == "COMPLIANCE" {
3025 return Some("AccessDenied");
3026 }
3027 if mode == "GOVERNANCE" {
3028 let bypass = req
3029 .headers
3030 .get("x-amz-bypass-governance-retention")
3031 .and_then(|v| v.to_str().ok())
3032 .map(|s| s.eq_ignore_ascii_case("true"))
3033 .unwrap_or(false);
3034 if !bypass {
3035 return Some("AccessDenied");
3036 }
3037 }
3038 }
3039 }
3040 None
3041}
3042
3043#[cfg(test)]
3044mod tests;
3045
3046#[cfg(test)]
3047mod s3_detect_action_tests {
3048 use super::s3_detect_action;
3052 use std::collections::HashMap;
3053
3054 fn q(pairs: &[(&str, &str)]) -> HashMap<String, String> {
3055 pairs
3056 .iter()
3057 .map(|(k, v)| (k.to_string(), v.to_string()))
3058 .collect()
3059 }
3060
3061 #[test]
3062 fn select_object_content_maps_to_get_object() {
3063 let action = s3_detect_action(
3065 "POST",
3066 Some("bucket"),
3067 Some("key"),
3068 &q(&[("select", ""), ("select-type", "2")]),
3069 );
3070 assert_eq!(action, Some("GetObject"));
3071 }
3072
3073 #[test]
3074 fn write_get_object_response_is_detected() {
3075 let action = s3_detect_action("POST", Some("WriteGetObjectResponse"), None, &q(&[]));
3077 assert_eq!(action, Some("WriteGetObjectResponse"));
3078 }
3079
3080 #[test]
3081 fn list_directory_buckets_is_distinct_from_list_buckets() {
3082 let dir = s3_detect_action("GET", None, None, &q(&[("x-id", "ListDirectoryBuckets")]));
3084 assert_eq!(dir, Some("ListAllMyDirectoryBuckets"));
3085 let plain = s3_detect_action("GET", None, None, &q(&[]));
3087 assert_eq!(plain, Some("ListBuckets"));
3088 }
3089
3090 #[test]
3091 fn rename_object_is_detected_with_key() {
3092 let action = s3_detect_action(
3095 "PUT",
3096 Some("bucket"),
3097 Some("newkey"),
3098 &q(&[("renameObject", "")]),
3099 );
3100 assert_eq!(action, Some("RenameObject"));
3101 }
3102
3103 #[test]
3104 fn update_object_encryption_maps_to_put_object() {
3105 let action = s3_detect_action(
3109 "PUT",
3110 Some("bucket"),
3111 Some("key"),
3112 &q(&[("encryption", "")]),
3113 );
3114 assert_eq!(action, Some("PutObject"));
3115 }
3116
3117 #[test]
3118 fn create_session_not_misdetected_as_list_objects() {
3119 let action = s3_detect_action("GET", Some("bucket"), None, &q(&[("session", "")]));
3121 assert_eq!(action, Some("CreateSession"));
3122 }
3123
3124 #[test]
3125 fn get_bucket_abac_not_misdetected_as_list_objects() {
3126 let action = s3_detect_action("GET", Some("bucket"), None, &q(&[("abac", "")]));
3128 assert_eq!(action, Some("GetBucketAbacConfiguration"));
3129 }
3130
3131 #[test]
3132 fn put_bucket_abac_still_detected() {
3133 let action = s3_detect_action("PUT", Some("bucket"), None, &q(&[("abac", "")]));
3134 assert_eq!(action, Some("PutBucketAbacConfiguration"));
3135 }
3136}
3137
3138#[cfg(test)]
3139mod s3_iam_service_prefix_tests {
3140 use super::*;
3143 use parking_lot::RwLock;
3144 use std::sync::Arc;
3145
3146 fn make_service() -> S3Service {
3147 let state: SharedS3State = Arc::new(RwLock::new(
3148 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
3149 ));
3150 S3Service::new(state, Arc::new(DeliveryBus::new()))
3151 }
3152
3153 fn req(method: Method, path: &str, query: &[(&str, &str)]) -> AwsRequest {
3154 let path_segments: Vec<String> = path
3155 .split('/')
3156 .filter(|s| !s.is_empty())
3157 .map(|s| s.to_string())
3158 .collect();
3159 AwsRequest {
3160 service: "s3".to_string(),
3161 action: String::new(),
3162 region: "us-east-1".to_string(),
3163 account_id: "123456789012".to_string(),
3164 request_id: "rid".to_string(),
3165 headers: http::HeaderMap::new(),
3166 query_params: query
3167 .iter()
3168 .map(|(k, v)| (k.to_string(), v.to_string()))
3169 .collect(),
3170 body: bytes::Bytes::new(),
3171 body_stream: parking_lot::Mutex::new(None),
3172 path_segments,
3173 raw_path: path.to_string(),
3174 raw_query: String::new(),
3175 method,
3176 is_query_protocol: false,
3177 access_key_id: None,
3178 principal: None,
3179 }
3180 }
3181
3182 #[test]
3183 fn create_session_uses_s3express_prefix() {
3184 let svc = make_service();
3185 let action = svc
3186 .iam_action_for(&req(Method::GET, "/mybucket", &[("session", "")]))
3187 .expect("must be mapped");
3188 assert_eq!(action.service, "s3express");
3189 assert_eq!(action.action, "CreateSession");
3190 assert_eq!(action.action_string(), "s3express:CreateSession");
3191 }
3192
3193 #[test]
3194 fn write_get_object_response_uses_object_lambda_prefix() {
3195 let svc = make_service();
3196 let action = svc
3197 .iam_action_for(&req(Method::POST, "/WriteGetObjectResponse", &[]))
3198 .expect("must be mapped");
3199 assert_eq!(action.service, "s3-object-lambda");
3200 assert_eq!(action.action, "WriteGetObjectResponse");
3201 assert_eq!(
3202 action.action_string(),
3203 "s3-object-lambda:WriteGetObjectResponse"
3204 );
3205 }
3206
3207 #[test]
3208 fn get_object_torrent_maps_to_its_own_action() {
3209 let svc = make_service();
3212 let action = svc
3213 .iam_action_for(&req(Method::GET, "/mybucket/key.txt", &[("torrent", "")]))
3214 .expect("must be mapped");
3215 assert_eq!(action.action, "GetObjectTorrent");
3216 let plain = svc
3218 .iam_action_for(&req(Method::GET, "/mybucket/key.txt", &[]))
3219 .expect("must be mapped");
3220 assert_eq!(plain.action, "GetObject");
3221 }
3222}
3223
3224#[cfg(test)]
3225mod extract_xml_value_tests {
3226 use super::{decode_xml_entities, extract_xml_value};
3227
3228 #[test]
3229 fn returns_inner_value() {
3230 assert_eq!(
3231 extract_xml_value("<Root><Key>value</Key></Root>", "Key"),
3232 Some("value".to_string())
3233 );
3234 }
3235
3236 #[test]
3241 fn decodes_named_and_numeric_entities() {
3242 assert_eq!(
3243 extract_xml_value("<Key>a&b</Key>", "Key"),
3244 Some("a&b".to_string())
3245 );
3246 assert_eq!(
3247 extract_xml_value("<Key><x> "y" 'z'</Key>", "Key"),
3248 Some("<x> \"y\" 'z'".to_string())
3249 );
3250 assert_eq!(
3251 extract_xml_value("<Key>a&b&c</Key>", "Key"),
3252 Some("a&b&c".to_string())
3253 );
3254 }
3255
3256 #[test]
3260 fn does_not_double_decode() {
3261 assert_eq!(decode_xml_entities("&lt;"), "<");
3262 assert_eq!(decode_xml_entities("&amp;"), "&");
3263 }
3264
3265 #[test]
3266 fn leaves_bare_ampersand_and_unknown_entities_verbatim() {
3267 assert_eq!(decode_xml_entities("a & b"), "a & b");
3268 assert_eq!(decode_xml_entities("50% &bogus; done"), "50% &bogus; done");
3269 assert_eq!(decode_xml_entities("no entities here"), "no entities here");
3270 }
3271
3272 #[test]
3273 fn missing_tag_is_none() {
3274 assert_eq!(extract_xml_value("<Root></Root>", "Key"), None);
3275 }
3276
3277 #[test]
3280 fn close_before_open_does_not_panic() {
3281 assert_eq!(extract_xml_value("</Key>oops<Key>value", "Key"), None);
3282 }
3283
3284 #[test]
3285 fn open_without_close_is_none() {
3286 assert_eq!(extract_xml_value("<Key>value", "Key"), None);
3287 }
3288}
3289
3290#[cfg(test)]
3291mod compute_checksum_tests {
3292 use super::{compute_checksum, compute_checksum_streaming};
3293
3294 #[test]
3299 fn crc32c_is_non_empty_and_matches_known_vector() {
3300 let out = compute_checksum("CRC32C", b"123456789");
3302 assert!(!out.is_empty());
3303 let bytes = base64::engine::general_purpose::STANDARD
3304 .decode(&out)
3305 .unwrap();
3306 assert_eq!(bytes, 0xE306_9283u32.to_be_bytes());
3307 }
3308
3309 #[test]
3310 fn crc64nvme_is_non_empty() {
3311 let out = compute_checksum("CRC64NVME", b"hello world");
3312 assert!(!out.is_empty());
3313 let bytes = base64::engine::general_purpose::STANDARD
3315 .decode(&out)
3316 .unwrap();
3317 assert_eq!(bytes.len(), 8);
3318 }
3319
3320 #[tokio::test]
3321 async fn non_streaming_matches_streaming_for_all_algorithms() {
3322 let data = b"the quick brown fox jumps over the lazy dog".repeat(100);
3323 let tmp = tempfile::NamedTempFile::new().unwrap();
3324 tokio::fs::write(tmp.path(), &data).await.unwrap();
3325
3326 for algo in ["CRC32", "CRC32C", "CRC64NVME", "SHA1", "SHA256"] {
3327 let direct = compute_checksum(algo, &data);
3328 let streamed = compute_checksum_streaming(algo, tmp.path()).await.unwrap();
3329 assert!(!direct.is_empty(), "{algo} direct empty");
3330 assert_eq!(direct, streamed, "{algo} direct != streaming");
3331 }
3332 }
3333
3334 use base64::Engine as _;
3335}
3336
3337#[cfg(test)]
3338mod complete_multipart_parse_tests {
3339 use super::parse_complete_multipart_xml;
3340
3341 #[test]
3347 fn parses_go_sdk_numeric_quote_entity_etag() {
3348 let xml = "<CompleteMultipartUpload>\
3349 <Part><PartNumber>1</PartNumber><ETag>"100bffa249c1cbde1a66242835cdab03"</ETag></Part>\
3350 <Part><PartNumber>2</PartNumber><ETag>"5df1aff8ceab081b20d677b2d4f7eab1"</ETag></Part>\
3351 </CompleteMultipartUpload>";
3352 let parts = parse_complete_multipart_xml(xml);
3353 assert_eq!(
3354 parts,
3355 vec![
3356 (1, "100bffa249c1cbde1a66242835cdab03".to_string()),
3357 (2, "5df1aff8ceab081b20d677b2d4f7eab1".to_string()),
3358 ]
3359 );
3360 }
3361
3362 #[test]
3365 fn parses_cli_literal_and_named_quote_etag() {
3366 let xml = "<CompleteMultipartUpload>\
3367 <Part><PartNumber>1</PartNumber><ETag>\"abc123\"</ETag></Part>\
3368 <Part><PartNumber>2</PartNumber><ETag>"def456"</ETag></Part>\
3369 </CompleteMultipartUpload>";
3370 let parts = parse_complete_multipart_xml(xml);
3371 assert_eq!(
3372 parts,
3373 vec![(1, "abc123".to_string()), (2, "def456".to_string())]
3374 );
3375 }
3376}