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 #[allow(dead_code)]
52 store: Arc<dyn S3Store>,
53}
54
55pub(crate) fn persistence_error(err: StoreError) -> AwsServiceError {
60 AwsServiceError::aws_error(
61 StatusCode::INTERNAL_SERVER_ERROR,
62 "InternalError",
63 format!("persistence store error: {err}"),
64 )
65}
66
67pub(crate) fn io_to_aws(err: std::io::Error) -> AwsServiceError {
70 AwsServiceError::aws_error(
71 StatusCode::INTERNAL_SERVER_ERROR,
72 "InternalError",
73 format!("failed to read object body from disk: {err}"),
74 )
75}
76
77impl S3Service {
78 pub fn new(state: SharedS3State, delivery: Arc<DeliveryBus>) -> Self {
79 Self::with_store(state, delivery, Arc::new(MemoryS3Store::new()))
80 }
81
82 pub fn with_store(
83 state: SharedS3State,
84 delivery: Arc<DeliveryBus>,
85 store: Arc<dyn S3Store>,
86 ) -> Self {
87 Self {
88 state,
89 delivery,
90 kms_state: None,
91 store,
92 }
93 }
94
95 pub fn with_kms(mut self, kms_state: SharedKmsState) -> Self {
96 self.kms_state = Some(kms_state);
97 self
98 }
99}
100
101#[async_trait]
102impl AwsService for S3Service {
103 fn service_name(&self) -> &str {
104 "s3"
105 }
106
107 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
108 let bucket = req.path_segments.first().map(|s| s.as_str());
110 let key = if let Some(b) = bucket {
113 let prefix = format!("/{b}/");
114 if req.raw_path.starts_with(&prefix) && req.raw_path.len() > prefix.len() {
115 let raw_key = &req.raw_path[prefix.len()..];
116 Some(
117 percent_encoding::percent_decode_str(raw_key)
118 .decode_utf8_lossy()
119 .into_owned(),
120 )
121 } else if req.path_segments.len() > 1 {
122 let raw = req.path_segments[1..].join("/");
123 Some(
124 percent_encoding::percent_decode_str(&raw)
125 .decode_utf8_lossy()
126 .into_owned(),
127 )
128 } else {
129 None
130 }
131 } else {
132 None
133 };
134
135 if let Some(b) = bucket {
137 if req.method == Method::POST
139 && key.is_some()
140 && req.query_params.contains_key("uploads")
141 {
142 return self.create_multipart_upload(&req, b, key.as_deref().unwrap());
143 }
144
145 if req.method == Method::POST
147 && key.is_some()
148 && req.query_params.contains_key("restore")
149 {
150 return self.restore_object(&req, b, key.as_deref().unwrap());
151 }
152
153 if req.method == Method::POST && key.is_some() {
155 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
156 return self.complete_multipart_upload(
157 &req,
158 b,
159 key.as_deref().unwrap(),
160 &upload_id,
161 );
162 }
163 }
164
165 if req.method == Method::PUT && key.is_some() {
167 if let (Some(part_num_str), Some(upload_id)) = (
168 req.query_params.get("partNumber").cloned(),
169 req.query_params.get("uploadId").cloned(),
170 ) {
171 if let Ok(part_number) = part_num_str.parse::<i64>() {
172 if req.headers.contains_key("x-amz-copy-source") {
173 return self.upload_part_copy(
174 &req,
175 b,
176 key.as_deref().unwrap(),
177 &upload_id,
178 part_number,
179 );
180 }
181 return self.upload_part(
182 &req,
183 b,
184 key.as_deref().unwrap(),
185 &upload_id,
186 part_number,
187 );
188 }
189 }
190 }
191
192 if req.method == Method::DELETE && key.is_some() {
194 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
195 return self.abort_multipart_upload(b, key.as_deref().unwrap(), &upload_id);
196 }
197 }
198
199 if req.method == Method::GET
201 && key.is_none()
202 && req.query_params.contains_key("uploads")
203 {
204 return self.list_multipart_uploads(b);
205 }
206
207 if req.method == Method::GET && key.is_some() {
209 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
210 return self.list_parts(&req, b, key.as_deref().unwrap(), &upload_id);
211 }
212 }
213 }
214
215 if req.method == Method::OPTIONS {
217 if let Some(b_name) = bucket {
218 let cors_config = {
219 let state = self.state.read();
220 state
221 .buckets
222 .get(b_name)
223 .and_then(|b| b.cors_config.clone())
224 };
225 if let Some(ref config) = cors_config {
226 let origin = req
227 .headers
228 .get("origin")
229 .and_then(|v| v.to_str().ok())
230 .unwrap_or("");
231 let request_method = req
232 .headers
233 .get("access-control-request-method")
234 .and_then(|v| v.to_str().ok())
235 .unwrap_or("");
236 let rules = parse_cors_config(config);
237 if let Some(rule) = find_cors_rule(&rules, origin, Some(request_method)) {
238 let mut headers = HeaderMap::new();
239 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
240 "*"
241 } else {
242 origin
243 };
244 headers.insert(
245 "access-control-allow-origin",
246 matched_origin
247 .parse()
248 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
249 );
250 headers.insert(
251 "access-control-allow-methods",
252 rule.allowed_methods
253 .join(", ")
254 .parse()
255 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
256 );
257 if !rule.allowed_headers.is_empty() {
258 let ah = if rule.allowed_headers.contains(&"*".to_string()) {
259 req.headers
260 .get("access-control-request-headers")
261 .and_then(|v| v.to_str().ok())
262 .unwrap_or("*")
263 .to_string()
264 } else {
265 rule.allowed_headers.join(", ")
266 };
267 headers.insert(
268 "access-control-allow-headers",
269 ah.parse()
270 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
271 );
272 }
273 if let Some(max_age) = rule.max_age_seconds {
274 headers.insert(
275 "access-control-max-age",
276 max_age
277 .to_string()
278 .parse()
279 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
280 );
281 }
282 return Ok(AwsResponse {
283 status: StatusCode::OK,
284 content_type: String::new(),
285 body: Bytes::new().into(),
286 headers,
287 });
288 }
289 }
290 return Err(AwsServiceError::aws_error(
291 StatusCode::FORBIDDEN,
292 "CORSResponse",
293 "CORS is not enabled for this bucket",
294 ));
295 }
296 }
297
298 let origin_header = req
300 .headers
301 .get("origin")
302 .and_then(|v| v.to_str().ok())
303 .map(|s| s.to_string());
304
305 let mut result = match (&req.method, bucket, key.as_deref()) {
306 (&Method::GET, None, None) => self.list_buckets(&req),
308
309 (&Method::PUT, Some(b), None) => {
311 if req.query_params.contains_key("tagging") {
312 self.put_bucket_tagging(&req, b)
313 } else if req.query_params.contains_key("acl") {
314 self.put_bucket_acl(&req, b)
315 } else if req.query_params.contains_key("versioning") {
316 self.put_bucket_versioning(&req, b)
317 } else if req.query_params.contains_key("cors") {
318 self.put_bucket_cors(&req, b)
319 } else if req.query_params.contains_key("notification") {
320 self.put_bucket_notification(&req, b)
321 } else if req.query_params.contains_key("website") {
322 self.put_bucket_website(&req, b)
323 } else if req.query_params.contains_key("accelerate") {
324 self.put_bucket_accelerate(&req, b)
325 } else if req.query_params.contains_key("publicAccessBlock") {
326 self.put_public_access_block(&req, b)
327 } else if req.query_params.contains_key("encryption") {
328 self.put_bucket_encryption(&req, b)
329 } else if req.query_params.contains_key("lifecycle") {
330 self.put_bucket_lifecycle(&req, b)
331 } else if req.query_params.contains_key("logging") {
332 self.put_bucket_logging(&req, b)
333 } else if req.query_params.contains_key("policy") {
334 self.put_bucket_policy(&req, b)
335 } else if req.query_params.contains_key("object-lock") {
336 self.put_object_lock_config(&req, b)
337 } else if req.query_params.contains_key("replication") {
338 self.put_bucket_replication(&req, b)
339 } else if req.query_params.contains_key("ownershipControls") {
340 self.put_bucket_ownership_controls(&req, b)
341 } else if req.query_params.contains_key("inventory") {
342 self.put_bucket_inventory(&req, b)
343 } else {
344 self.create_bucket(&req, b)
345 }
346 }
347 (&Method::DELETE, Some(b), None) => {
348 if req.query_params.contains_key("tagging") {
349 self.delete_bucket_tagging(&req, b)
350 } else if req.query_params.contains_key("cors") {
351 self.delete_bucket_cors(b)
352 } else if req.query_params.contains_key("website") {
353 self.delete_bucket_website(b)
354 } else if req.query_params.contains_key("publicAccessBlock") {
355 self.delete_public_access_block(b)
356 } else if req.query_params.contains_key("encryption") {
357 self.delete_bucket_encryption(b)
358 } else if req.query_params.contains_key("lifecycle") {
359 self.delete_bucket_lifecycle(b)
360 } else if req.query_params.contains_key("policy") {
361 self.delete_bucket_policy(b)
362 } else if req.query_params.contains_key("replication") {
363 self.delete_bucket_replication(b)
364 } else if req.query_params.contains_key("ownershipControls") {
365 self.delete_bucket_ownership_controls(b)
366 } else if req.query_params.contains_key("inventory") {
367 self.delete_bucket_inventory(&req, b)
368 } else {
369 self.delete_bucket(&req, b)
370 }
371 }
372 (&Method::HEAD, Some(b), None) => self.head_bucket(b),
373 (&Method::GET, Some(b), None) => {
374 if req.query_params.contains_key("tagging") {
375 self.get_bucket_tagging(&req, b)
376 } else if req.query_params.contains_key("location") {
377 self.get_bucket_location(b)
378 } else if req.query_params.contains_key("acl") {
379 self.get_bucket_acl(&req, b)
380 } else if req.query_params.contains_key("versioning") {
381 self.get_bucket_versioning(b)
382 } else if req.query_params.contains_key("versions") {
383 self.list_object_versions(&req, b)
384 } else if req.query_params.contains_key("object-lock") {
385 self.get_object_lock_configuration(b)
386 } else if req.query_params.contains_key("cors") {
387 self.get_bucket_cors(b)
388 } else if req.query_params.contains_key("notification") {
389 self.get_bucket_notification(b)
390 } else if req.query_params.contains_key("website") {
391 self.get_bucket_website(b)
392 } else if req.query_params.contains_key("accelerate") {
393 self.get_bucket_accelerate(b)
394 } else if req.query_params.contains_key("publicAccessBlock") {
395 self.get_public_access_block(b)
396 } else if req.query_params.contains_key("encryption") {
397 self.get_bucket_encryption(b)
398 } else if req.query_params.contains_key("lifecycle") {
399 self.get_bucket_lifecycle(b)
400 } else if req.query_params.contains_key("logging") {
401 self.get_bucket_logging(b)
402 } else if req.query_params.contains_key("policy") {
403 self.get_bucket_policy(b)
404 } else if req.query_params.contains_key("replication") {
405 self.get_bucket_replication(b)
406 } else if req.query_params.contains_key("ownershipControls") {
407 self.get_bucket_ownership_controls(b)
408 } else if req.query_params.contains_key("inventory") {
409 if req.query_params.contains_key("id") {
410 self.get_bucket_inventory(&req, b)
411 } else {
412 self.list_bucket_inventory_configurations(b)
413 }
414 } else if req.query_params.get("list-type").map(|s| s.as_str()) == Some("2") {
415 self.list_objects_v2(&req, b)
416 } else if req.query_params.is_empty() {
417 let website_config = {
419 let state = self.state.read();
420 state
421 .buckets
422 .get(b)
423 .and_then(|bkt| bkt.website_config.clone())
424 };
425 if let Some(ref config) = website_config {
426 if let Some(index_doc) = extract_xml_value(config, "Suffix").or_else(|| {
427 extract_xml_value(config, "IndexDocument").and_then(|inner| {
428 let open = "<Suffix>";
429 let close = "</Suffix>";
430 let s = inner.find(open)? + open.len();
431 let e = inner.find(close)?;
432 Some(inner[s..e].trim().to_string())
433 })
434 }) {
435 self.serve_website_object(&req, b, &index_doc, config)
436 } else {
437 self.list_objects_v1(&req, b)
438 }
439 } else {
440 self.list_objects_v1(&req, b)
441 }
442 } else {
443 self.list_objects_v1(&req, b)
444 }
445 }
446
447 (&Method::PUT, Some(b), Some(k)) => {
449 if req.query_params.contains_key("tagging") {
450 self.put_object_tagging(&req, b, k)
451 } else if req.query_params.contains_key("acl") {
452 self.put_object_acl(&req, b, k)
453 } else if req.query_params.contains_key("retention") {
454 self.put_object_retention(&req, b, k)
455 } else if req.query_params.contains_key("legal-hold") {
456 self.put_object_legal_hold(&req, b, k)
457 } else if req.headers.contains_key("x-amz-copy-source") {
458 self.copy_object(&req, b, k)
459 } else {
460 self.put_object(&req, b, k)
461 }
462 }
463 (&Method::GET, Some(b), Some(k)) => {
464 if req.query_params.contains_key("tagging") {
465 self.get_object_tagging(&req, b, k)
466 } else if req.query_params.contains_key("acl") {
467 self.get_object_acl(&req, b, k)
468 } else if req.query_params.contains_key("retention") {
469 self.get_object_retention(&req, b, k)
470 } else if req.query_params.contains_key("legal-hold") {
471 self.get_object_legal_hold(&req, b, k)
472 } else if req.query_params.contains_key("attributes") {
473 self.get_object_attributes(&req, b, k)
474 } else {
475 let result = self.get_object(&req, b, k);
476 let is_not_found = matches!(
478 &result,
479 Err(e) if e.code() == "NoSuchKey"
480 );
481 if is_not_found {
482 let website_config = {
483 let state = self.state.read();
484 state
485 .buckets
486 .get(b)
487 .and_then(|bkt| bkt.website_config.clone())
488 };
489 if let Some(ref config) = website_config {
490 if let Some(error_key) = extract_xml_value(config, "ErrorDocument")
491 .and_then(|inner| {
492 let open = "<Key>";
493 let close = "</Key>";
494 let s = inner.find(open)? + open.len();
495 let e = inner.find(close)?;
496 Some(inner[s..e].trim().to_string())
497 })
498 .or_else(|| extract_xml_value(config, "Key"))
499 {
500 return self.serve_website_error(&req, b, &error_key);
501 }
502 }
503 }
504 result
505 }
506 }
507 (&Method::DELETE, Some(b), Some(k)) => {
508 if req.query_params.contains_key("tagging") {
509 self.delete_object_tagging(b, k)
510 } else {
511 self.delete_object(&req, b, k)
512 }
513 }
514 (&Method::HEAD, Some(b), Some(k)) => self.head_object(&req, b, k),
515
516 (&Method::POST, Some(b), None) if req.query_params.contains_key("delete") => {
518 self.delete_objects(&req, b)
519 }
520
521 _ => Err(AwsServiceError::aws_error(
522 StatusCode::METHOD_NOT_ALLOWED,
523 "MethodNotAllowed",
524 "The specified method is not allowed against this resource",
525 )),
526 };
527
528 if let (Some(ref origin), Some(b_name)) = (&origin_header, bucket) {
530 let cors_config = {
531 let state = self.state.read();
532 state
533 .buckets
534 .get(b_name)
535 .and_then(|b| b.cors_config.clone())
536 };
537 if let Some(ref config) = cors_config {
538 let rules = parse_cors_config(config);
539 if let Some(rule) = find_cors_rule(&rules, origin, None) {
540 if let Ok(ref mut resp) = result {
541 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
542 "*"
543 } else {
544 origin
545 };
546 resp.headers.insert(
547 "access-control-allow-origin",
548 matched_origin
549 .parse()
550 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
551 );
552 if !rule.expose_headers.is_empty() {
553 resp.headers.insert(
554 "access-control-expose-headers",
555 rule.expose_headers
556 .join(", ")
557 .parse()
558 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
559 );
560 }
561 }
562 }
563 }
564 }
565
566 if let Some(b_name) = bucket {
568 let status_code = match &result {
569 Ok(resp) => resp.status.as_u16(),
570 Err(e) => e.status().as_u16(),
571 };
572 let op = logging::operation_name(&req.method, key.as_deref());
573 logging::maybe_write_access_log(
574 &self.state,
575 &self.store,
576 b_name,
577 &logging::AccessLogRequest {
578 operation: op,
579 key: key.as_deref(),
580 status: status_code,
581 request_id: &req.request_id,
582 method: req.method.as_str(),
583 path: &req.raw_path,
584 },
585 );
586 }
587
588 result
589 }
590
591 fn supported_actions(&self) -> &[&str] {
592 &[
593 "ListBuckets",
595 "CreateBucket",
596 "DeleteBucket",
597 "HeadBucket",
598 "GetBucketLocation",
599 "PutObject",
601 "GetObject",
602 "DeleteObject",
603 "HeadObject",
604 "CopyObject",
605 "DeleteObjects",
606 "ListObjectsV2",
607 "ListObjects",
608 "ListObjectVersions",
609 "GetObjectAttributes",
610 "RestoreObject",
611 "PutObjectTagging",
613 "GetObjectTagging",
614 "DeleteObjectTagging",
615 "PutObjectAcl",
616 "GetObjectAcl",
617 "PutObjectRetention",
618 "GetObjectRetention",
619 "PutObjectLegalHold",
620 "GetObjectLegalHold",
621 "PutBucketTagging",
623 "GetBucketTagging",
624 "DeleteBucketTagging",
625 "PutBucketAcl",
626 "GetBucketAcl",
627 "PutBucketVersioning",
628 "GetBucketVersioning",
629 "PutBucketCors",
630 "GetBucketCors",
631 "DeleteBucketCors",
632 "PutBucketNotificationConfiguration",
633 "GetBucketNotificationConfiguration",
634 "PutBucketWebsite",
635 "GetBucketWebsite",
636 "DeleteBucketWebsite",
637 "PutBucketAccelerateConfiguration",
638 "GetBucketAccelerateConfiguration",
639 "PutPublicAccessBlock",
640 "GetPublicAccessBlock",
641 "DeletePublicAccessBlock",
642 "PutBucketEncryption",
643 "GetBucketEncryption",
644 "DeleteBucketEncryption",
645 "PutBucketLifecycleConfiguration",
646 "GetBucketLifecycleConfiguration",
647 "DeleteBucketLifecycle",
648 "PutBucketLogging",
649 "GetBucketLogging",
650 "PutBucketPolicy",
651 "GetBucketPolicy",
652 "DeleteBucketPolicy",
653 "PutObjectLockConfiguration",
654 "GetObjectLockConfiguration",
655 "PutBucketReplication",
656 "GetBucketReplication",
657 "DeleteBucketReplication",
658 "PutBucketOwnershipControls",
659 "GetBucketOwnershipControls",
660 "DeleteBucketOwnershipControls",
661 "PutBucketInventoryConfiguration",
662 "GetBucketInventoryConfiguration",
663 "DeleteBucketInventoryConfiguration",
664 "CreateMultipartUpload",
666 "UploadPart",
667 "UploadPartCopy",
668 "CompleteMultipartUpload",
669 "AbortMultipartUpload",
670 "ListParts",
671 "ListMultipartUploads",
672 ]
673 }
674
675 fn iam_enforceable(&self) -> bool {
676 true
677 }
678
679 fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
689 let bucket = request.path_segments.first().map(|s| s.as_str());
694 let key = if request.path_segments.len() > 1 {
695 Some(request.path_segments[1..].join("/"))
696 } else {
697 None
698 };
699 let action = s3_detect_action(
700 request.method.as_str(),
701 bucket,
702 key.as_deref(),
703 &request.query_params,
704 )?;
705 let resource = s3_resource_for(action, bucket, key.as_deref());
706 Some(fakecloud_core::auth::IamAction {
707 service: "s3",
708 action,
709 resource,
710 })
711 }
712}
713
714fn s3_detect_action(
727 method: &str,
728 bucket: Option<&str>,
729 key: Option<&str>,
730 query: &std::collections::HashMap<String, String>,
731) -> Option<&'static str> {
732 let has = |q: &str| query.contains_key(q);
733 let is_get = method == "GET";
734 let is_put = method == "PUT";
735 let is_post = method == "POST";
736 let is_delete = method == "DELETE";
737
738 if bucket.is_none() {
740 return match method {
741 "GET" => Some("ListBuckets"),
742 _ => None,
743 };
744 }
745 let has_key = key.is_some();
746
747 if has_key && is_post && has("uploads") {
749 return Some("CreateMultipartUpload");
750 }
751 if has_key && is_post && has("uploadId") {
752 return Some("CompleteMultipartUpload");
753 }
754 if has_key && is_put && has("partNumber") && has("uploadId") {
755 return Some("UploadPart");
756 }
757 if has_key && is_delete && has("uploadId") {
758 return Some("AbortMultipartUpload");
759 }
760 if has_key && is_get && has("uploadId") {
761 return Some("ListParts");
762 }
763 if !has_key && is_get && has("uploads") {
764 return Some("ListMultipartUploads");
765 }
766
767 if has_key {
771 if has("tagging") {
772 return Some(match method {
773 "GET" => "GetObjectTagging",
774 "PUT" => "PutObjectTagging",
775 "DELETE" => "DeleteObjectTagging",
776 _ => return None,
777 });
778 }
779 if has("acl") {
780 return Some(match method {
781 "GET" => "GetObjectAcl",
782 "PUT" => "PutObjectAcl",
783 _ => return None,
784 });
785 }
786 if has("retention") {
787 return Some(match method {
788 "GET" => "GetObjectRetention",
789 "PUT" => "PutObjectRetention",
790 _ => return None,
791 });
792 }
793 if has("legal-hold") {
794 return Some(match method {
795 "GET" => "GetObjectLegalHold",
796 "PUT" => "PutObjectLegalHold",
797 _ => return None,
798 });
799 }
800 if has("attributes") && is_get {
805 return Some("GetObjectAttributes");
806 }
807 if has("restore") && is_post {
808 return Some("RestoreObject");
809 }
810 }
811
812 if !has_key {
814 if has("tagging") {
815 return Some(match method {
816 "GET" => "GetBucketTagging",
817 "PUT" => "PutBucketTagging",
818 "DELETE" => "DeleteBucketTagging",
819 _ => return None,
820 });
821 }
822 if has("acl") {
823 return Some(match method {
824 "GET" => "GetBucketAcl",
825 "PUT" => "PutBucketAcl",
826 _ => return None,
827 });
828 }
829 if has("versioning") {
830 return Some(match method {
831 "GET" => "GetBucketVersioning",
832 "PUT" => "PutBucketVersioning",
833 _ => return None,
834 });
835 }
836 if has("cors") {
837 return Some(match method {
838 "GET" => "GetBucketCors",
839 "PUT" => "PutBucketCors",
840 "DELETE" => "DeleteBucketCors",
841 _ => return None,
842 });
843 }
844 if has("policy") {
845 return Some(match method {
846 "GET" => "GetBucketPolicy",
847 "PUT" => "PutBucketPolicy",
848 "DELETE" => "DeleteBucketPolicy",
849 _ => return None,
850 });
851 }
852 if has("website") {
853 return Some(match method {
854 "GET" => "GetBucketWebsite",
855 "PUT" => "PutBucketWebsite",
856 "DELETE" => "DeleteBucketWebsite",
857 _ => return None,
858 });
859 }
860 if has("lifecycle") {
861 return Some(match method {
862 "GET" => "GetBucketLifecycleConfiguration",
863 "PUT" => "PutBucketLifecycleConfiguration",
864 "DELETE" => "DeleteBucketLifecycle",
865 _ => return None,
866 });
867 }
868 if has("encryption") {
869 return Some(match method {
870 "GET" => "GetBucketEncryption",
871 "PUT" => "PutBucketEncryption",
872 "DELETE" => "DeleteBucketEncryption",
873 _ => return None,
874 });
875 }
876 if has("logging") {
877 return Some(match method {
878 "GET" => "GetBucketLogging",
879 "PUT" => "PutBucketLogging",
880 _ => return None,
881 });
882 }
883 if has("notification") {
884 return Some(match method {
885 "GET" => "GetBucketNotificationConfiguration",
886 "PUT" => "PutBucketNotificationConfiguration",
887 _ => return None,
888 });
889 }
890 if has("replication") {
891 return Some(match method {
892 "GET" => "GetBucketReplication",
893 "PUT" => "PutBucketReplication",
894 "DELETE" => "DeleteBucketReplication",
895 _ => return None,
896 });
897 }
898 if has("ownershipControls") {
899 return Some(match method {
900 "GET" => "GetBucketOwnershipControls",
901 "PUT" => "PutBucketOwnershipControls",
902 "DELETE" => "DeleteBucketOwnershipControls",
903 _ => return None,
904 });
905 }
906 if has("publicAccessBlock") {
907 return Some(match method {
908 "GET" => "GetPublicAccessBlock",
909 "PUT" => "PutPublicAccessBlock",
910 "DELETE" => "DeletePublicAccessBlock",
911 _ => return None,
912 });
913 }
914 if has("accelerate") {
915 return Some(match method {
916 "GET" => "GetBucketAccelerateConfiguration",
917 "PUT" => "PutBucketAccelerateConfiguration",
918 _ => return None,
919 });
920 }
921 if has("inventory") {
922 return Some(match method {
923 "GET" => "GetBucketInventoryConfiguration",
924 "PUT" => "PutBucketInventoryConfiguration",
925 "DELETE" => "DeleteBucketInventoryConfiguration",
926 _ => return None,
927 });
928 }
929 if has("object-lock") {
930 return Some(match method {
931 "GET" => "GetObjectLockConfiguration",
932 "PUT" => "PutObjectLockConfiguration",
933 _ => return None,
934 });
935 }
936 if has("location") {
937 return Some("GetBucketLocation");
938 }
939 if is_post && has("delete") {
940 return Some("DeleteObjects");
941 }
942 if is_get && has("versions") {
943 return Some("ListObjectVersions");
944 }
945 }
946
947 match (method, has_key) {
949 ("GET", true) => Some("GetObject"),
950 ("PUT", true) => {
951 Some("PutObject")
957 }
958 ("DELETE", true) => Some("DeleteObject"),
959 ("HEAD", true) => Some("HeadObject"),
960 ("GET", false) => {
961 if query.contains_key("list-type") {
962 Some("ListObjectsV2")
963 } else {
964 Some("ListObjects")
965 }
966 }
967 ("PUT", false) => Some("CreateBucket"),
968 ("DELETE", false) => Some("DeleteBucket"),
969 ("HEAD", false) => Some("HeadBucket"),
970 _ => None,
971 }
972}
973
974#[allow(dead_code)]
980const S3_SUPPORTED_ACTIONS: &[&str] = &[
981 "ListBuckets",
982 "CreateBucket",
983 "DeleteBucket",
984 "HeadBucket",
985 "GetBucketLocation",
986 "PutObject",
987 "GetObject",
988 "DeleteObject",
989 "HeadObject",
990 "CopyObject",
991 "DeleteObjects",
992 "ListObjectsV2",
993 "ListObjects",
994 "ListObjectVersions",
995 "GetObjectAttributes",
996 "RestoreObject",
997 "PutObjectTagging",
998 "GetObjectTagging",
999 "DeleteObjectTagging",
1000 "PutObjectAcl",
1001 "GetObjectAcl",
1002 "PutObjectRetention",
1003 "GetObjectRetention",
1004 "PutObjectLegalHold",
1005 "GetObjectLegalHold",
1006 "PutBucketTagging",
1007 "GetBucketTagging",
1008 "DeleteBucketTagging",
1009 "PutBucketAcl",
1010 "GetBucketAcl",
1011 "PutBucketVersioning",
1012 "GetBucketVersioning",
1013 "PutBucketCors",
1014 "GetBucketCors",
1015 "DeleteBucketCors",
1016 "PutBucketNotificationConfiguration",
1017 "GetBucketNotificationConfiguration",
1018 "PutBucketWebsite",
1019 "GetBucketWebsite",
1020 "DeleteBucketWebsite",
1021 "PutBucketAccelerateConfiguration",
1022 "GetBucketAccelerateConfiguration",
1023 "PutPublicAccessBlock",
1024 "GetPublicAccessBlock",
1025 "DeletePublicAccessBlock",
1026 "PutBucketEncryption",
1027 "GetBucketEncryption",
1028 "DeleteBucketEncryption",
1029 "PutBucketLifecycleConfiguration",
1030 "GetBucketLifecycleConfiguration",
1031 "DeleteBucketLifecycle",
1032 "PutBucketLogging",
1033 "GetBucketLogging",
1034 "PutBucketPolicy",
1035 "GetBucketPolicy",
1036 "DeleteBucketPolicy",
1037 "PutObjectLockConfiguration",
1038 "GetObjectLockConfiguration",
1039 "PutBucketReplication",
1040 "GetBucketReplication",
1041 "DeleteBucketReplication",
1042 "PutBucketOwnershipControls",
1043 "GetBucketOwnershipControls",
1044 "DeleteBucketOwnershipControls",
1045 "PutBucketInventoryConfiguration",
1046 "GetBucketInventoryConfiguration",
1047 "DeleteBucketInventoryConfiguration",
1048 "CreateMultipartUpload",
1049 "UploadPart",
1050 "UploadPartCopy",
1051 "CompleteMultipartUpload",
1052 "AbortMultipartUpload",
1053 "ListParts",
1054 "ListMultipartUploads",
1055];
1056
1057fn s3_resource_for(action: &'static str, bucket: Option<&str>, key: Option<&str>) -> String {
1061 const OBJECT_ACTIONS: &[&str] = &[
1063 "PutObject",
1064 "GetObject",
1065 "DeleteObject",
1066 "HeadObject",
1067 "CopyObject",
1068 "GetObjectAttributes",
1069 "RestoreObject",
1070 "PutObjectTagging",
1071 "GetObjectTagging",
1072 "DeleteObjectTagging",
1073 "PutObjectAcl",
1074 "GetObjectAcl",
1075 "PutObjectRetention",
1076 "GetObjectRetention",
1077 "PutObjectLegalHold",
1078 "GetObjectLegalHold",
1079 "CreateMultipartUpload",
1080 "UploadPart",
1081 "UploadPartCopy",
1082 "CompleteMultipartUpload",
1083 "AbortMultipartUpload",
1084 "ListParts",
1085 ];
1086 if action == "ListBuckets" {
1087 return "*".to_string();
1088 }
1089 let Some(bucket) = bucket else {
1090 return "*".to_string();
1091 };
1092 if OBJECT_ACTIONS.contains(&action) {
1093 match key {
1094 Some(k) if !k.is_empty() => format!("arn:aws:s3:::{}/{}", bucket, k),
1095 _ => format!("arn:aws:s3:::{}/*", bucket),
1096 }
1097 } else {
1098 format!("arn:aws:s3:::{}", bucket)
1100 }
1101}
1102
1103pub(crate) fn truncate_to_seconds(dt: DateTime<Utc>) -> DateTime<Utc> {
1107 dt.with_nanosecond(0).unwrap_or(dt)
1108}
1109
1110pub(crate) fn check_get_conditionals(
1111 req: &AwsRequest,
1112 obj: &S3Object,
1113) -> Result<(), AwsServiceError> {
1114 let obj_etag = format!("\"{}\"", obj.etag);
1115 let obj_time = truncate_to_seconds(obj.last_modified);
1116
1117 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
1119 if !etag_matches(if_match, &obj_etag) {
1120 return Err(precondition_failed("If-Match"));
1121 }
1122 }
1123
1124 if let Some(if_none_match) = req
1126 .headers
1127 .get("if-none-match")
1128 .and_then(|v| v.to_str().ok())
1129 {
1130 if etag_matches(if_none_match, &obj_etag) {
1131 return Err(not_modified_with_etag(&obj_etag));
1132 }
1133 }
1134
1135 if let Some(since) = req
1137 .headers
1138 .get("if-unmodified-since")
1139 .and_then(|v| v.to_str().ok())
1140 {
1141 if let Some(dt) = parse_http_date(since) {
1142 if obj_time > dt {
1143 return Err(precondition_failed("If-Unmodified-Since"));
1144 }
1145 }
1146 }
1147
1148 if let Some(since) = req
1150 .headers
1151 .get("if-modified-since")
1152 .and_then(|v| v.to_str().ok())
1153 {
1154 if let Some(dt) = parse_http_date(since) {
1155 if obj_time <= dt {
1156 return Err(not_modified());
1157 }
1158 }
1159 }
1160
1161 Ok(())
1162}
1163
1164pub(crate) fn check_head_conditionals(
1165 req: &AwsRequest,
1166 obj: &S3Object,
1167) -> Result<(), AwsServiceError> {
1168 let obj_etag = format!("\"{}\"", obj.etag);
1169 let obj_time = truncate_to_seconds(obj.last_modified);
1170
1171 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
1173 if !etag_matches(if_match, &obj_etag) {
1174 return Err(AwsServiceError::aws_error(
1175 StatusCode::PRECONDITION_FAILED,
1176 "412",
1177 "Precondition Failed",
1178 ));
1179 }
1180 }
1181
1182 if let Some(if_none_match) = req
1184 .headers
1185 .get("if-none-match")
1186 .and_then(|v| v.to_str().ok())
1187 {
1188 if etag_matches(if_none_match, &obj_etag) {
1189 return Err(not_modified_with_etag(&obj_etag));
1190 }
1191 }
1192
1193 if let Some(since) = req
1195 .headers
1196 .get("if-unmodified-since")
1197 .and_then(|v| v.to_str().ok())
1198 {
1199 if let Some(dt) = parse_http_date(since) {
1200 if obj_time > dt {
1201 return Err(AwsServiceError::aws_error(
1202 StatusCode::PRECONDITION_FAILED,
1203 "412",
1204 "Precondition Failed",
1205 ));
1206 }
1207 }
1208 }
1209
1210 if let Some(since) = req
1212 .headers
1213 .get("if-modified-since")
1214 .and_then(|v| v.to_str().ok())
1215 {
1216 if let Some(dt) = parse_http_date(since) {
1217 if obj_time <= dt {
1218 return Err(not_modified());
1219 }
1220 }
1221 }
1222
1223 Ok(())
1224}
1225
1226pub(crate) fn etag_matches(condition: &str, obj_etag: &str) -> bool {
1227 let condition = condition.trim();
1228 if condition == "*" {
1229 return true;
1230 }
1231 let clean_etag = obj_etag.replace('"', "");
1232 for part in condition.split(',') {
1234 let part = part.trim().replace('"', "");
1235 if part == clean_etag {
1236 return true;
1237 }
1238 }
1239 false
1240}
1241
1242pub(crate) fn parse_http_date(s: &str) -> Option<DateTime<Utc>> {
1243 if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
1245 return Some(dt.with_timezone(&Utc));
1246 }
1247 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
1249 return Some(dt.with_timezone(&Utc));
1250 }
1251 if let Ok(dt) =
1253 chrono::NaiveDateTime::parse_from_str(s.trim_end_matches(" GMT"), "%a, %d %b %Y %H:%M:%S")
1254 {
1255 return Some(dt.and_utc());
1256 }
1257 if let Ok(dt) = s.parse::<DateTime<Utc>>() {
1259 return Some(dt);
1260 }
1261 None
1262}
1263
1264pub(crate) fn not_modified() -> AwsServiceError {
1265 AwsServiceError::aws_error(StatusCode::NOT_MODIFIED, "304", "Not Modified")
1266}
1267
1268pub(crate) fn not_modified_with_etag(etag: &str) -> AwsServiceError {
1269 AwsServiceError::aws_error_with_headers(
1270 StatusCode::NOT_MODIFIED,
1271 "304",
1272 "Not Modified",
1273 vec![("etag".to_string(), etag.to_string())],
1274 )
1275}
1276
1277pub(crate) fn precondition_failed(condition: &str) -> AwsServiceError {
1278 AwsServiceError::aws_error_with_fields(
1279 StatusCode::PRECONDITION_FAILED,
1280 "PreconditionFailed",
1281 "At least one of the pre-conditions you specified did not hold",
1282 vec![("Condition".to_string(), condition.to_string())],
1283 )
1284}
1285
1286pub(crate) fn build_acl_xml(owner_id: &str, grants: &[AclGrant], _account_id: &str) -> String {
1289 let mut grants_xml = String::new();
1290 for g in grants {
1291 let grantee_xml = if g.grantee_type == "Group" {
1292 let uri = g.grantee_uri.as_deref().unwrap_or("");
1293 format!(
1294 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\">\
1295 <URI>{}</URI></Grantee>",
1296 xml_escape(uri),
1297 )
1298 } else {
1299 let id = g.grantee_id.as_deref().unwrap_or("");
1300 format!(
1301 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">\
1302 <ID>{}</ID></Grantee>",
1303 xml_escape(id),
1304 )
1305 };
1306 grants_xml.push_str(&format!(
1307 "<Grant>{grantee_xml}<Permission>{}</Permission></Grant>",
1308 xml_escape(&g.permission),
1309 ));
1310 }
1311
1312 format!(
1313 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
1314 <AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
1315 <Owner><ID>{owner_id}</ID><DisplayName>{owner_id}</DisplayName></Owner>\
1316 <AccessControlList>{grants_xml}</AccessControlList>\
1317 </AccessControlPolicy>",
1318 owner_id = xml_escape(owner_id),
1319 )
1320}
1321
1322pub(crate) fn canned_acl_grants(acl: &str, owner_id: &str) -> Vec<AclGrant> {
1323 let owner_grant = AclGrant {
1324 grantee_type: "CanonicalUser".to_string(),
1325 grantee_id: Some(owner_id.to_string()),
1326 grantee_display_name: Some(owner_id.to_string()),
1327 grantee_uri: None,
1328 permission: "FULL_CONTROL".to_string(),
1329 };
1330 match acl {
1331 "private" => vec![owner_grant],
1332 "public-read" => vec![
1333 owner_grant,
1334 AclGrant {
1335 grantee_type: "Group".to_string(),
1336 grantee_id: None,
1337 grantee_display_name: None,
1338 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1339 permission: "READ".to_string(),
1340 },
1341 ],
1342 "public-read-write" => vec![
1343 owner_grant,
1344 AclGrant {
1345 grantee_type: "Group".to_string(),
1346 grantee_id: None,
1347 grantee_display_name: None,
1348 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1349 permission: "READ".to_string(),
1350 },
1351 AclGrant {
1352 grantee_type: "Group".to_string(),
1353 grantee_id: None,
1354 grantee_display_name: None,
1355 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1356 permission: "WRITE".to_string(),
1357 },
1358 ],
1359 "authenticated-read" => vec![
1360 owner_grant,
1361 AclGrant {
1362 grantee_type: "Group".to_string(),
1363 grantee_id: None,
1364 grantee_display_name: None,
1365 grantee_uri: Some(
1366 "http://acs.amazonaws.com/groups/global/AuthenticatedUsers".to_string(),
1367 ),
1368 permission: "READ".to_string(),
1369 },
1370 ],
1371 "bucket-owner-full-control" => vec![owner_grant],
1372 _ => vec![owner_grant],
1373 }
1374}
1375
1376pub(crate) fn canned_acl_grants_for_object(acl: &str, owner_id: &str) -> Vec<AclGrant> {
1377 canned_acl_grants(acl, owner_id)
1379}
1380
1381pub(crate) fn parse_grant_headers(headers: &HeaderMap) -> Vec<AclGrant> {
1382 let mut grants = Vec::new();
1383 let header_permission_map = [
1384 ("x-amz-grant-read", "READ"),
1385 ("x-amz-grant-write", "WRITE"),
1386 ("x-amz-grant-read-acp", "READ_ACP"),
1387 ("x-amz-grant-write-acp", "WRITE_ACP"),
1388 ("x-amz-grant-full-control", "FULL_CONTROL"),
1389 ];
1390
1391 for (header, permission) in &header_permission_map {
1392 if let Some(value) = headers.get(*header).and_then(|v| v.to_str().ok()) {
1393 for part in value.split(',') {
1395 let part = part.trim();
1396 if let Some((key, val)) = part.split_once('=') {
1397 let val = val.trim().trim_matches('"');
1398 let key = key.trim().to_lowercase();
1399 match key.as_str() {
1400 "id" => {
1401 grants.push(AclGrant {
1402 grantee_type: "CanonicalUser".to_string(),
1403 grantee_id: Some(val.to_string()),
1404 grantee_display_name: Some(val.to_string()),
1405 grantee_uri: None,
1406 permission: permission.to_string(),
1407 });
1408 }
1409 "uri" | "url" => {
1410 grants.push(AclGrant {
1411 grantee_type: "Group".to_string(),
1412 grantee_id: None,
1413 grantee_display_name: None,
1414 grantee_uri: Some(val.to_string()),
1415 permission: permission.to_string(),
1416 });
1417 }
1418 _ => {}
1419 }
1420 }
1421 }
1422 }
1423 }
1424 grants
1425}
1426
1427pub(crate) fn parse_acl_xml(xml: &str) -> Result<Vec<AclGrant>, AwsServiceError> {
1428 if xml.contains("<AccessControlPolicy") && !xml.contains("<Owner>") {
1430 return Err(AwsServiceError::aws_error(
1431 StatusCode::BAD_REQUEST,
1432 "MalformedACLError",
1433 "The XML you provided was not well-formed or did not validate against our published schema",
1434 ));
1435 }
1436
1437 let valid_permissions = ["READ", "WRITE", "READ_ACP", "WRITE_ACP", "FULL_CONTROL"];
1438
1439 let mut grants = Vec::new();
1440 let mut remaining = xml;
1441 while let Some(start) = remaining.find("<Grant>") {
1442 let after = &remaining[start + 7..];
1443 if let Some(end) = after.find("</Grant>") {
1444 let grant_body = &after[..end];
1445
1446 let permission = extract_xml_value(grant_body, "Permission").unwrap_or_default();
1448 if !valid_permissions.contains(&permission.as_str()) {
1449 return Err(AwsServiceError::aws_error(
1450 StatusCode::BAD_REQUEST,
1451 "MalformedACLError",
1452 "The XML you provided was not well-formed or did not validate against our published schema",
1453 ));
1454 }
1455
1456 if grant_body.contains("xsi:type=\"Group\"") || grant_body.contains("<URI>") {
1458 let uri = extract_xml_value(grant_body, "URI").unwrap_or_default();
1459 grants.push(AclGrant {
1460 grantee_type: "Group".to_string(),
1461 grantee_id: None,
1462 grantee_display_name: None,
1463 grantee_uri: Some(uri),
1464 permission,
1465 });
1466 } else {
1467 let id = extract_xml_value(grant_body, "ID").unwrap_or_default();
1468 let display =
1469 extract_xml_value(grant_body, "DisplayName").unwrap_or_else(|| id.clone());
1470 grants.push(AclGrant {
1471 grantee_type: "CanonicalUser".to_string(),
1472 grantee_id: Some(id),
1473 grantee_display_name: Some(display),
1474 grantee_uri: None,
1475 permission,
1476 });
1477 }
1478
1479 remaining = &after[end + 8..];
1480 } else {
1481 break;
1482 }
1483 }
1484 Ok(grants)
1485}
1486
1487pub(crate) enum RangeResult {
1490 Satisfiable { start: usize, end: usize },
1491 NotSatisfiable,
1492 Ignored,
1493}
1494
1495pub(crate) fn parse_range_header(range_str: &str, total_size: usize) -> Option<RangeResult> {
1496 let range_str = range_str.strip_prefix("bytes=")?;
1497 let (start_str, end_str) = range_str.split_once('-')?;
1498 if start_str.is_empty() {
1499 let suffix_len: usize = end_str.parse().ok()?;
1500 if suffix_len == 0 || total_size == 0 {
1501 return Some(RangeResult::NotSatisfiable);
1502 }
1503 let start = total_size.saturating_sub(suffix_len);
1504 Some(RangeResult::Satisfiable {
1505 start,
1506 end: total_size - 1,
1507 })
1508 } else {
1509 let start: usize = start_str.parse().ok()?;
1510 if start >= total_size {
1511 return Some(RangeResult::NotSatisfiable);
1512 }
1513 let end = if end_str.is_empty() {
1514 total_size - 1
1515 } else {
1516 let e: usize = end_str.parse().ok()?;
1517 if e < start {
1518 return Some(RangeResult::Ignored);
1519 }
1520 std::cmp::min(e, total_size - 1)
1521 };
1522 Some(RangeResult::Satisfiable { start, end })
1523 }
1524}
1525
1526pub(crate) fn s3_xml(status: StatusCode, body: impl Into<Bytes>) -> AwsResponse {
1530 AwsResponse {
1531 status,
1532 content_type: "application/xml".to_string(),
1533 body: body.into().into(),
1534 headers: HeaderMap::new(),
1535 }
1536}
1537
1538pub(crate) fn empty_response(status: StatusCode) -> AwsResponse {
1539 AwsResponse {
1540 status,
1541 content_type: "application/xml".to_string(),
1542 body: Bytes::new().into(),
1543 headers: HeaderMap::new(),
1544 }
1545}
1546
1547pub(crate) fn is_frozen(obj: &S3Object) -> bool {
1550 matches!(obj.storage_class.as_str(), "GLACIER" | "DEEP_ARCHIVE")
1551 && obj.restore_ongoing != Some(false)
1552}
1553
1554pub(crate) fn no_such_bucket(bucket: &str) -> AwsServiceError {
1555 AwsServiceError::aws_error_with_fields(
1556 StatusCode::NOT_FOUND,
1557 "NoSuchBucket",
1558 "The specified bucket does not exist",
1559 vec![("BucketName".to_string(), bucket.to_string())],
1560 )
1561}
1562
1563pub(crate) fn no_such_key(key: &str) -> AwsServiceError {
1564 AwsServiceError::aws_error_with_fields(
1565 StatusCode::NOT_FOUND,
1566 "NoSuchKey",
1567 "The specified key does not exist.",
1568 vec![("Key".to_string(), key.to_string())],
1569 )
1570}
1571
1572pub(crate) fn no_such_upload(upload_id: &str) -> AwsServiceError {
1573 AwsServiceError::aws_error_with_fields(
1574 StatusCode::NOT_FOUND,
1575 "NoSuchUpload",
1576 "The specified upload does not exist. The upload ID may be invalid, \
1577 or the upload may have been aborted or completed.",
1578 vec![("UploadId".to_string(), upload_id.to_string())],
1579 )
1580}
1581
1582pub(crate) fn no_such_key_with_detail(key: &str) -> AwsServiceError {
1583 AwsServiceError::aws_error_with_fields(
1584 StatusCode::NOT_FOUND,
1585 "NoSuchKey",
1586 "The specified key does not exist.",
1587 vec![("Key".to_string(), key.to_string())],
1588 )
1589}
1590
1591pub(crate) fn compute_md5(data: &[u8]) -> String {
1592 let digest = Md5::digest(data);
1593 format!("{:x}", digest)
1594}
1595
1596pub(crate) fn compute_checksum(algorithm: &str, data: &[u8]) -> String {
1597 match algorithm {
1598 "CRC32" => {
1599 let crc = crc32fast::hash(data);
1600 BASE64.encode(crc.to_be_bytes())
1601 }
1602 "SHA1" => {
1603 use sha1::Digest as _;
1604 let hash = sha1::Sha1::digest(data);
1605 BASE64.encode(hash)
1606 }
1607 "SHA256" => {
1608 use sha2::Digest as _;
1609 let hash = sha2::Sha256::digest(data);
1610 BASE64.encode(hash)
1611 }
1612 _ => String::new(),
1613 }
1614}
1615
1616pub(crate) fn url_encode_s3_key(s: &str) -> String {
1617 let mut out = String::with_capacity(s.len() * 2);
1618 for byte in s.bytes() {
1619 match byte {
1620 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
1621 out.push(byte as char);
1622 }
1623 _ => {
1624 out.push_str(&format!("%{:02X}", byte));
1625 }
1626 }
1627 }
1628 out
1629}
1630
1631pub(crate) use fakecloud_aws::xml::xml_escape;
1632
1633pub(crate) fn extract_user_metadata(
1634 headers: &HeaderMap,
1635) -> std::collections::HashMap<String, String> {
1636 let mut meta = std::collections::HashMap::new();
1637 for (name, value) in headers {
1638 if let Some(key) = name.as_str().strip_prefix("x-amz-meta-") {
1639 if let Ok(v) = value.to_str() {
1640 meta.insert(key.to_string(), v.to_string());
1641 }
1642 }
1643 }
1644 meta
1645}
1646
1647pub(crate) fn is_valid_storage_class(class: &str) -> bool {
1648 matches!(
1649 class,
1650 "STANDARD"
1651 | "REDUCED_REDUNDANCY"
1652 | "STANDARD_IA"
1653 | "ONEZONE_IA"
1654 | "INTELLIGENT_TIERING"
1655 | "GLACIER"
1656 | "DEEP_ARCHIVE"
1657 | "GLACIER_IR"
1658 | "OUTPOSTS"
1659 | "SNOW"
1660 | "EXPRESS_ONEZONE"
1661 )
1662}
1663
1664pub(crate) fn is_valid_bucket_name(name: &str) -> bool {
1665 if name.len() < 3 || name.len() > 63 {
1666 return false;
1667 }
1668 let bytes = name.as_bytes();
1670 if !bytes[0].is_ascii_alphanumeric() || !bytes[bytes.len() - 1].is_ascii_alphanumeric() {
1671 return false;
1672 }
1673 name.chars()
1675 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.' || c == '_')
1676}
1677
1678pub(crate) fn is_valid_region(region: &str) -> bool {
1679 let valid_regions = [
1681 "us-east-1",
1682 "us-east-2",
1683 "us-west-1",
1684 "us-west-2",
1685 "af-south-1",
1686 "ap-east-1",
1687 "ap-south-1",
1688 "ap-south-2",
1689 "ap-southeast-1",
1690 "ap-southeast-2",
1691 "ap-southeast-3",
1692 "ap-southeast-4",
1693 "ap-northeast-1",
1694 "ap-northeast-2",
1695 "ap-northeast-3",
1696 "ca-central-1",
1697 "ca-west-1",
1698 "eu-central-1",
1699 "eu-central-2",
1700 "eu-west-1",
1701 "eu-west-2",
1702 "eu-west-3",
1703 "eu-south-1",
1704 "eu-south-2",
1705 "eu-north-1",
1706 "il-central-1",
1707 "me-south-1",
1708 "me-central-1",
1709 "sa-east-1",
1710 "cn-north-1",
1711 "cn-northwest-1",
1712 "us-gov-east-1",
1713 "us-gov-east-2",
1714 "us-gov-west-1",
1715 "us-iso-east-1",
1716 "us-iso-west-1",
1717 "us-isob-east-1",
1718 "us-isof-south-1",
1719 ];
1720 valid_regions.contains(®ion)
1721}
1722
1723pub(crate) fn resolve_object<'a>(
1724 b: &'a S3Bucket,
1725 key: &str,
1726 version_id: Option<&String>,
1727) -> Result<&'a S3Object, AwsServiceError> {
1728 if let Some(vid) = version_id {
1729 if vid == "null" {
1731 if let Some(versions) = b.object_versions.get(key) {
1733 if let Some(obj) = versions
1734 .iter()
1735 .find(|o| o.version_id.is_none() || o.version_id.as_deref() == Some("null"))
1736 {
1737 return Ok(obj);
1738 }
1739 }
1740 if let Some(obj) = b.objects.get(key) {
1742 if obj.version_id.is_none() || obj.version_id.as_deref() == Some("null") {
1743 return Ok(obj);
1744 }
1745 }
1746 } else {
1747 if let Some(versions) = b.object_versions.get(key) {
1749 if let Some(obj) = versions
1750 .iter()
1751 .find(|o| o.version_id.as_deref() == Some(vid.as_str()))
1752 {
1753 return Ok(obj);
1754 }
1755 }
1756 if let Some(obj) = b.objects.get(key) {
1758 if obj.version_id.as_deref() == Some(vid.as_str()) {
1759 return Ok(obj);
1760 }
1761 }
1762 }
1763 if b.versioning.is_some() {
1765 Err(AwsServiceError::aws_error_with_fields(
1766 StatusCode::NOT_FOUND,
1767 "NoSuchVersion",
1768 "The specified version does not exist.",
1769 vec![
1770 ("Key".to_string(), key.to_string()),
1771 ("VersionId".to_string(), vid.to_string()),
1772 ],
1773 ))
1774 } else {
1775 Err(AwsServiceError::aws_error(
1776 StatusCode::BAD_REQUEST,
1777 "InvalidArgument",
1778 "Invalid version id specified",
1779 ))
1780 }
1781 } else {
1782 b.objects.get(key).ok_or_else(|| no_such_key(key))
1783 }
1784}
1785
1786pub(crate) fn make_delete_marker(key: &str, dm_id: &str) -> S3Object {
1787 S3Object {
1788 key: key.to_string(),
1789 last_modified: Utc::now(),
1790 storage_class: "STANDARD".to_string(),
1791 version_id: Some(dm_id.to_string()),
1792 is_delete_marker: true,
1793 ..Default::default()
1794 }
1795}
1796
1797pub(crate) struct DeleteObjectEntry {
1799 key: String,
1800 version_id: Option<String>,
1801}
1802
1803pub(crate) fn parse_delete_objects_xml(xml: &str) -> Vec<DeleteObjectEntry> {
1804 let mut entries = Vec::new();
1805 let mut remaining = xml;
1806 while let Some(obj_start) = remaining.find("<Object>") {
1807 let after = &remaining[obj_start + 8..];
1808 if let Some(obj_end) = after.find("</Object>") {
1809 let obj_body = &after[..obj_end];
1810 let key = extract_xml_value(obj_body, "Key");
1811 let version_id = extract_xml_value(obj_body, "VersionId");
1812 if let Some(k) = key {
1813 entries.push(DeleteObjectEntry { key: k, version_id });
1814 }
1815 remaining = &after[obj_end + 9..];
1816 } else {
1817 break;
1818 }
1819 }
1820 entries
1821}
1822
1823pub(crate) fn parse_tagging_xml(xml: &str) -> Vec<(String, String)> {
1826 let mut tags = Vec::new();
1827 let mut remaining = xml;
1828 while let Some(tag_start) = remaining.find("<Tag>") {
1829 let after = &remaining[tag_start + 5..];
1830 if let Some(tag_end) = after.find("</Tag>") {
1831 let tag_body = &after[..tag_end];
1832 let key = extract_xml_value(tag_body, "Key");
1833 let value = extract_xml_value(tag_body, "Value");
1834 if let (Some(k), Some(v)) = (key, value) {
1835 tags.push((k, v));
1836 }
1837 remaining = &after[tag_end + 6..];
1838 } else {
1839 break;
1840 }
1841 }
1842 tags
1843}
1844
1845pub(crate) fn validate_tags(tags: &[(String, String)]) -> Result<(), AwsServiceError> {
1846 let mut seen = std::collections::HashSet::new();
1848 for (k, _) in tags {
1849 if !seen.insert(k.as_str()) {
1850 return Err(AwsServiceError::aws_error(
1851 StatusCode::BAD_REQUEST,
1852 "InvalidTag",
1853 "Cannot provide multiple Tags with the same key",
1854 ));
1855 }
1856 if k.starts_with("aws:") {
1858 return Err(AwsServiceError::aws_error(
1859 StatusCode::BAD_REQUEST,
1860 "InvalidTag",
1861 "System tags cannot be added/updated by requester",
1862 ));
1863 }
1864 }
1865 Ok(())
1866}
1867
1868pub(crate) fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
1869 let self_closing1 = format!("<{tag} />");
1871 let self_closing2 = format!("<{tag}/>");
1872 if xml.contains(&self_closing1) || xml.contains(&self_closing2) {
1873 let self_pos = xml
1875 .find(&self_closing1)
1876 .or_else(|| xml.find(&self_closing2));
1877 let open = format!("<{tag}>");
1878 let open_pos = xml.find(&open);
1879 match (self_pos, open_pos) {
1880 (Some(sp), Some(op)) if sp < op => return Some(String::new()),
1881 (Some(_), None) => return Some(String::new()),
1882 _ => {}
1883 }
1884 }
1885
1886 let open = format!("<{tag}>");
1887 let close = format!("</{tag}>");
1888 let start = xml.find(&open)? + open.len();
1889 let end = xml.find(&close)?;
1890 Some(xml[start..end].to_string())
1891}
1892
1893pub(crate) fn parse_complete_multipart_xml(xml: &str) -> Vec<(u32, String)> {
1895 let mut parts = Vec::new();
1896 let mut remaining = xml;
1897 while let Some(part_start) = remaining.find("<Part>") {
1898 let after = &remaining[part_start + 6..];
1899 if let Some(part_end) = after.find("</Part>") {
1900 let part_body = &after[..part_end];
1901 let part_num =
1902 extract_xml_value(part_body, "PartNumber").and_then(|s| s.parse::<u32>().ok());
1903 let etag = extract_xml_value(part_body, "ETag")
1904 .map(|s| s.replace(""", "").replace('"', ""));
1905 if let (Some(num), Some(e)) = (part_num, etag) {
1906 parts.push((num, e));
1907 }
1908 remaining = &after[part_end + 7..];
1909 } else {
1910 break;
1911 }
1912 }
1913 parts
1914}
1915
1916pub(crate) fn parse_url_encoded_tags(s: &str) -> Vec<(String, String)> {
1917 let mut tags = Vec::new();
1918 for pair in s.split('&') {
1919 if pair.is_empty() {
1920 continue;
1921 }
1922 let (key, value) = match pair.find('=') {
1923 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
1924 None => (pair, ""),
1925 };
1926 tags.push((
1927 percent_encoding::percent_decode_str(key)
1928 .decode_utf8_lossy()
1929 .to_string(),
1930 percent_encoding::percent_decode_str(value)
1931 .decode_utf8_lossy()
1932 .to_string(),
1933 ));
1934 }
1935 tags
1936}
1937
1938pub(crate) fn validate_lifecycle_xml(xml: &str) -> Result<(), AwsServiceError> {
1940 let malformed = || {
1941 AwsServiceError::aws_error(
1942 StatusCode::BAD_REQUEST,
1943 "MalformedXML",
1944 "The XML you provided was not well-formed or did not validate against our published schema",
1945 )
1946 };
1947
1948 let mut remaining = xml;
1949 while let Some(rule_start) = remaining.find("<Rule>") {
1950 let after = &remaining[rule_start + 6..];
1951 if let Some(rule_end) = after.find("</Rule>") {
1952 let rule_body = &after[..rule_end];
1953
1954 let has_filter = rule_body.contains("<Filter>")
1956 || rule_body.contains("<Filter/>")
1957 || rule_body.contains("<Filter />");
1958
1959 let has_prefix_outside_filter = {
1961 if !rule_body.contains("<Prefix") {
1962 false
1963 } else if !has_filter {
1964 true } else {
1966 let mut stripped = rule_body.to_string();
1968 if let Some(fs) = stripped.find("<Filter") {
1970 if let Some(fe) = stripped.find("</Filter>") {
1971 stripped = format!("{}{}", &stripped[..fs], &stripped[fe + 9..]);
1972 }
1973 }
1974 stripped.contains("<Prefix")
1975 }
1976 };
1977
1978 if !has_filter && !has_prefix_outside_filter {
1979 return Err(malformed());
1980 }
1981 if has_filter && has_prefix_outside_filter {
1983 return Err(malformed());
1984 }
1985
1986 if let Some(exp_start) = rule_body.find("<Expiration>") {
1989 if let Some(exp_end) = rule_body[exp_start..].find("</Expiration>") {
1990 let exp_body = &rule_body[exp_start..exp_start + exp_end];
1991 if exp_body.contains("<ExpiredObjectDeleteMarker>")
1992 && (exp_body.contains("<Days>") || exp_body.contains("<Date>"))
1993 {
1994 return Err(malformed());
1995 }
1996 }
1997 }
1998
1999 if has_filter {
2001 if let Some(fs) = rule_body.find("<Filter>") {
2002 if let Some(fe) = rule_body.find("</Filter>") {
2003 let filter_body = &rule_body[fs + 8..fe];
2004 let has_prefix_in_filter = filter_body.contains("<Prefix");
2005 let has_tag_in_filter = filter_body.contains("<Tag>");
2006 let has_and_in_filter = filter_body.contains("<And>");
2007 if has_prefix_in_filter && has_tag_in_filter && !has_and_in_filter {
2009 return Err(malformed());
2010 }
2011 if has_tag_in_filter && has_and_in_filter {
2013 let and_start = filter_body.find("<And>").unwrap_or(0);
2015 let tag_pos = filter_body.find("<Tag>").unwrap_or(0);
2016 if tag_pos < and_start {
2017 return Err(malformed());
2018 }
2019 }
2020 }
2021 }
2022 }
2023
2024 if rule_body.contains("<NoncurrentVersionTransition>") {
2026 let mut nvt_remaining = rule_body;
2027 while let Some(nvt_start) = nvt_remaining.find("<NoncurrentVersionTransition>") {
2028 let nvt_after = &nvt_remaining[nvt_start + 29..];
2029 if let Some(nvt_end) = nvt_after.find("</NoncurrentVersionTransition>") {
2030 let nvt_body = &nvt_after[..nvt_end];
2031 if !nvt_body.contains("<NoncurrentDays>") {
2032 return Err(malformed());
2033 }
2034 if !nvt_body.contains("<StorageClass>") {
2035 return Err(malformed());
2036 }
2037 nvt_remaining = &nvt_after[nvt_end + 30..];
2038 } else {
2039 break;
2040 }
2041 }
2042 }
2043
2044 remaining = &after[rule_end + 7..];
2045 } else {
2046 break;
2047 }
2048 }
2049
2050 Ok(())
2051}
2052
2053pub(crate) struct CorsRule {
2055 allowed_origins: Vec<String>,
2056 allowed_methods: Vec<String>,
2057 allowed_headers: Vec<String>,
2058 expose_headers: Vec<String>,
2059 max_age_seconds: Option<u32>,
2060}
2061
2062pub(crate) fn parse_cors_config(xml: &str) -> Vec<CorsRule> {
2064 let mut rules = Vec::new();
2065 let mut remaining = xml;
2066 while let Some(start) = remaining.find("<CORSRule>") {
2067 let after = &remaining[start + 10..];
2068 if let Some(end) = after.find("</CORSRule>") {
2069 let block = &after[..end];
2070 let allowed_origins = extract_all_xml_values(block, "AllowedOrigin");
2071 let allowed_methods = extract_all_xml_values(block, "AllowedMethod");
2072 let allowed_headers = extract_all_xml_values(block, "AllowedHeader");
2073 let expose_headers = extract_all_xml_values(block, "ExposeHeader");
2074 let max_age_seconds =
2075 extract_xml_value(block, "MaxAgeSeconds").and_then(|s| s.parse().ok());
2076 rules.push(CorsRule {
2077 allowed_origins,
2078 allowed_methods,
2079 allowed_headers,
2080 expose_headers,
2081 max_age_seconds,
2082 });
2083 remaining = &after[end + 11..];
2084 } else {
2085 break;
2086 }
2087 }
2088 rules
2089}
2090
2091pub(crate) fn origin_matches(origin: &str, pattern: &str) -> bool {
2093 if pattern == "*" {
2094 return true;
2095 }
2096 if let Some(suffix) = pattern.strip_prefix('*') {
2098 return origin.ends_with(suffix);
2099 }
2100 origin == pattern
2101}
2102
2103pub(crate) fn find_cors_rule<'a>(
2105 rules: &'a [CorsRule],
2106 origin: &str,
2107 method: Option<&str>,
2108) -> Option<&'a CorsRule> {
2109 rules.iter().find(|rule| {
2110 let origin_ok = rule
2111 .allowed_origins
2112 .iter()
2113 .any(|o| origin_matches(origin, o));
2114 let method_ok = match method {
2115 Some(m) => rule.allowed_methods.iter().any(|am| am == m),
2116 None => true,
2117 };
2118 origin_ok && method_ok
2119 })
2120}
2121
2122pub(crate) fn check_object_lock_for_overwrite(
2125 obj: &S3Object,
2126 req: &AwsRequest,
2127) -> Option<&'static str> {
2128 if obj.lock_legal_hold.as_deref() == Some("ON") {
2130 return Some("AccessDenied");
2131 }
2132 if let (Some(mode), Some(until)) = (&obj.lock_mode, &obj.lock_retain_until) {
2134 if *until > Utc::now() {
2135 if mode == "COMPLIANCE" {
2136 return Some("AccessDenied");
2137 }
2138 if mode == "GOVERNANCE" {
2139 let bypass = req
2140 .headers
2141 .get("x-amz-bypass-governance-retention")
2142 .and_then(|v| v.to_str().ok())
2143 .map(|s| s.eq_ignore_ascii_case("true"))
2144 .unwrap_or(false);
2145 if !bypass {
2146 return Some("AccessDenied");
2147 }
2148 }
2149 }
2150 }
2151 None
2152}
2153
2154#[cfg(test)]
2155mod tests {
2156 use super::*;
2157
2158 #[test]
2159 fn valid_bucket_names() {
2160 assert!(is_valid_bucket_name("my-bucket"));
2161 assert!(is_valid_bucket_name("my.bucket.name"));
2162 assert!(is_valid_bucket_name("abc"));
2163 assert!(!is_valid_bucket_name("ab"));
2164 assert!(!is_valid_bucket_name("-bucket"));
2165 assert!(!is_valid_bucket_name("Bucket"));
2166 assert!(!is_valid_bucket_name("bucket-"));
2167 }
2168
2169 #[test]
2170 fn parse_delete_xml() {
2171 let xml = r#"<Delete><Object><Key>a.txt</Key></Object><Object><Key>b/c.txt</Key></Object></Delete>"#;
2172 let entries = parse_delete_objects_xml(xml);
2173 assert_eq!(entries.len(), 2);
2174 assert_eq!(entries[0].key, "a.txt");
2175 assert!(entries[0].version_id.is_none());
2176 assert_eq!(entries[1].key, "b/c.txt");
2177 }
2178
2179 #[test]
2180 fn parse_delete_xml_with_version() {
2181 let xml = r#"<Delete><Object><Key>a.txt</Key><VersionId>v1</VersionId></Object></Delete>"#;
2182 let entries = parse_delete_objects_xml(xml);
2183 assert_eq!(entries.len(), 1);
2184 assert_eq!(entries[0].key, "a.txt");
2185 assert_eq!(entries[0].version_id.as_deref(), Some("v1"));
2186 }
2187
2188 #[test]
2189 fn parse_tags_xml() {
2190 let xml =
2191 r#"<Tagging><TagSet><Tag><Key>env</Key><Value>prod</Value></Tag></TagSet></Tagging>"#;
2192 let tags = parse_tagging_xml(xml);
2193 assert_eq!(tags, vec![("env".to_string(), "prod".to_string())]);
2194 }
2195
2196 #[test]
2197 fn md5_hash() {
2198 let hash = compute_md5(b"hello");
2199 assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
2200 }
2201
2202 #[test]
2203 fn test_etag_matches() {
2204 assert!(etag_matches("\"abc\"", "\"abc\""));
2205 assert!(etag_matches("abc", "\"abc\""));
2206 assert!(etag_matches("*", "\"abc\""));
2207 assert!(!etag_matches("\"xyz\"", "\"abc\""));
2208 }
2209
2210 #[test]
2211 fn test_event_matches() {
2212 assert!(event_matches("s3:ObjectCreated:Put", "s3:ObjectCreated:*"));
2213 assert!(event_matches("s3:ObjectCreated:Copy", "s3:ObjectCreated:*"));
2214 assert!(event_matches(
2215 "s3:ObjectRemoved:Delete",
2216 "s3:ObjectRemoved:*"
2217 ));
2218 assert!(!event_matches(
2219 "s3:ObjectRemoved:Delete",
2220 "s3:ObjectCreated:*"
2221 ));
2222 assert!(event_matches(
2223 "s3:ObjectCreated:Put",
2224 "s3:ObjectCreated:Put"
2225 ));
2226 assert!(event_matches("s3:ObjectCreated:Put", "s3:*"));
2227 }
2228
2229 #[test]
2230 fn test_parse_notification_config() {
2231 let xml = r#"<NotificationConfiguration>
2232 <QueueConfiguration>
2233 <Queue>arn:aws:sqs:us-east-1:123456789012:my-queue</Queue>
2234 <Event>s3:ObjectCreated:*</Event>
2235 </QueueConfiguration>
2236 <TopicConfiguration>
2237 <Topic>arn:aws:sns:us-east-1:123456789012:my-topic</Topic>
2238 <Event>s3:ObjectRemoved:*</Event>
2239 </TopicConfiguration>
2240 </NotificationConfiguration>"#;
2241 let targets = parse_notification_config(xml);
2242 assert_eq!(targets.len(), 2);
2243 assert_eq!(
2244 targets[0].arn,
2245 "arn:aws:sqs:us-east-1:123456789012:my-queue"
2246 );
2247 assert_eq!(targets[0].events, vec!["s3:ObjectCreated:*"]);
2248 assert_eq!(
2249 targets[1].arn,
2250 "arn:aws:sns:us-east-1:123456789012:my-topic"
2251 );
2252 assert_eq!(targets[1].events, vec!["s3:ObjectRemoved:*"]);
2253 }
2254
2255 #[test]
2256 fn test_parse_notification_config_lambda() {
2257 let xml = r#"<NotificationConfiguration>
2259 <CloudFunctionConfiguration>
2260 <CloudFunction>arn:aws:lambda:us-east-1:123456789012:function:my-func</CloudFunction>
2261 <Event>s3:ObjectCreated:*</Event>
2262 </CloudFunctionConfiguration>
2263 </NotificationConfiguration>"#;
2264 let targets = parse_notification_config(xml);
2265 assert_eq!(targets.len(), 1);
2266 assert!(matches!(
2267 targets[0].target_type,
2268 NotificationTargetType::Lambda
2269 ));
2270 assert_eq!(
2271 targets[0].arn,
2272 "arn:aws:lambda:us-east-1:123456789012:function:my-func"
2273 );
2274 assert_eq!(targets[0].events, vec!["s3:ObjectCreated:*"]);
2275 }
2276
2277 #[test]
2278 fn test_parse_notification_config_lambda_new_format() {
2279 let xml = r#"<NotificationConfiguration>
2281 <LambdaFunctionConfiguration>
2282 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
2283 <Event>s3:ObjectCreated:Put</Event>
2284 <Event>s3:ObjectRemoved:*</Event>
2285 </LambdaFunctionConfiguration>
2286 </NotificationConfiguration>"#;
2287 let targets = parse_notification_config(xml);
2288 assert_eq!(targets.len(), 1);
2289 assert!(matches!(
2290 targets[0].target_type,
2291 NotificationTargetType::Lambda
2292 ));
2293 assert_eq!(
2294 targets[0].arn,
2295 "arn:aws:lambda:us-east-1:123456789012:function:my-func"
2296 );
2297 assert_eq!(
2298 targets[0].events,
2299 vec!["s3:ObjectCreated:Put", "s3:ObjectRemoved:*"]
2300 );
2301 }
2302
2303 #[test]
2304 fn test_parse_notification_config_all_types() {
2305 let xml = r#"<NotificationConfiguration>
2306 <QueueConfiguration>
2307 <Queue>arn:aws:sqs:us-east-1:123456789012:q</Queue>
2308 <Event>s3:ObjectCreated:*</Event>
2309 </QueueConfiguration>
2310 <TopicConfiguration>
2311 <Topic>arn:aws:sns:us-east-1:123456789012:t</Topic>
2312 <Event>s3:ObjectRemoved:*</Event>
2313 </TopicConfiguration>
2314 <LambdaFunctionConfiguration>
2315 <Function>arn:aws:lambda:us-east-1:123456789012:function:f</Function>
2316 <Event>s3:ObjectCreated:Put</Event>
2317 </LambdaFunctionConfiguration>
2318 </NotificationConfiguration>"#;
2319 let targets = parse_notification_config(xml);
2320 assert_eq!(targets.len(), 3);
2321 assert!(matches!(
2322 targets[0].target_type,
2323 NotificationTargetType::Sqs
2324 ));
2325 assert!(matches!(
2326 targets[1].target_type,
2327 NotificationTargetType::Sns
2328 ));
2329 assert!(matches!(
2330 targets[2].target_type,
2331 NotificationTargetType::Lambda
2332 ));
2333 }
2334
2335 #[test]
2336 fn test_parse_notification_config_with_filters() {
2337 let xml = r#"<NotificationConfiguration>
2338 <LambdaFunctionConfiguration>
2339 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
2340 <Event>s3:ObjectCreated:*</Event>
2341 <Filter>
2342 <S3Key>
2343 <FilterRule>
2344 <Name>prefix</Name>
2345 <Value>images/</Value>
2346 </FilterRule>
2347 <FilterRule>
2348 <Name>suffix</Name>
2349 <Value>.jpg</Value>
2350 </FilterRule>
2351 </S3Key>
2352 </Filter>
2353 </LambdaFunctionConfiguration>
2354 </NotificationConfiguration>"#;
2355 let targets = parse_notification_config(xml);
2356 assert_eq!(targets.len(), 1);
2357 assert_eq!(targets[0].prefix_filter, Some("images/".to_string()));
2358 assert_eq!(targets[0].suffix_filter, Some(".jpg".to_string()));
2359 }
2360
2361 #[test]
2362 fn test_parse_notification_config_no_filters() {
2363 let xml = r#"<NotificationConfiguration>
2364 <LambdaFunctionConfiguration>
2365 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
2366 <Event>s3:ObjectCreated:*</Event>
2367 </LambdaFunctionConfiguration>
2368 </NotificationConfiguration>"#;
2369 let targets = parse_notification_config(xml);
2370 assert_eq!(targets.len(), 1);
2371 assert_eq!(targets[0].prefix_filter, None);
2372 assert_eq!(targets[0].suffix_filter, None);
2373 }
2374
2375 #[test]
2376 fn test_key_matches_filters() {
2377 assert!(key_matches_filters("anything", &None, &None));
2379
2380 assert!(key_matches_filters(
2382 "images/photo.jpg",
2383 &Some("images/".to_string()),
2384 &None
2385 ));
2386 assert!(!key_matches_filters(
2387 "docs/file.txt",
2388 &Some("images/".to_string()),
2389 &None
2390 ));
2391
2392 assert!(key_matches_filters(
2394 "images/photo.jpg",
2395 &None,
2396 &Some(".jpg".to_string())
2397 ));
2398 assert!(!key_matches_filters(
2399 "images/photo.png",
2400 &None,
2401 &Some(".jpg".to_string())
2402 ));
2403
2404 assert!(key_matches_filters(
2406 "images/photo.jpg",
2407 &Some("images/".to_string()),
2408 &Some(".jpg".to_string())
2409 ));
2410 assert!(!key_matches_filters(
2411 "images/photo.png",
2412 &Some("images/".to_string()),
2413 &Some(".jpg".to_string())
2414 ));
2415 assert!(!key_matches_filters(
2416 "docs/photo.jpg",
2417 &Some("images/".to_string()),
2418 &Some(".jpg".to_string())
2419 ));
2420 }
2421
2422 #[test]
2423 fn test_parse_cors_config() {
2424 let xml = r#"<CORSConfiguration>
2425 <CORSRule>
2426 <AllowedOrigin>https://example.com</AllowedOrigin>
2427 <AllowedMethod>GET</AllowedMethod>
2428 <AllowedMethod>PUT</AllowedMethod>
2429 <AllowedHeader>*</AllowedHeader>
2430 <ExposeHeader>x-amz-request-id</ExposeHeader>
2431 <MaxAgeSeconds>3600</MaxAgeSeconds>
2432 </CORSRule>
2433 </CORSConfiguration>"#;
2434 let rules = parse_cors_config(xml);
2435 assert_eq!(rules.len(), 1);
2436 assert_eq!(rules[0].allowed_origins, vec!["https://example.com"]);
2437 assert_eq!(rules[0].allowed_methods, vec!["GET", "PUT"]);
2438 assert_eq!(rules[0].allowed_headers, vec!["*"]);
2439 assert_eq!(rules[0].expose_headers, vec!["x-amz-request-id"]);
2440 assert_eq!(rules[0].max_age_seconds, Some(3600));
2441 }
2442
2443 #[test]
2444 fn test_origin_matches() {
2445 assert!(origin_matches("https://example.com", "https://example.com"));
2446 assert!(origin_matches("https://example.com", "*"));
2447 assert!(origin_matches("https://foo.example.com", "*.example.com"));
2448 assert!(!origin_matches("https://evil.com", "https://example.com"));
2449 }
2450
2451 #[test]
2454 fn resolve_null_version_matches_both_none_and_null_string() {
2455 use crate::state::S3Bucket;
2456 use bytes::Bytes;
2457 use chrono::Utc;
2458
2459 let mut b = S3Bucket::new("test", "us-east-1", "owner");
2460
2461 let make_obj = |key: &str, vid: Option<&str>| crate::state::S3Object {
2463 key: key.to_string(),
2464 body: crate::state::memory_body(Bytes::from_static(b"x")),
2465 content_type: "text/plain".to_string(),
2466 etag: "\"abc\"".to_string(),
2467 size: 1,
2468 last_modified: Utc::now(),
2469 storage_class: "STANDARD".to_string(),
2470 version_id: vid.map(|s| s.to_string()),
2471 ..Default::default()
2472 };
2473
2474 let obj = make_obj("file.txt", Some("null"));
2476 b.objects.insert("file.txt".to_string(), obj.clone());
2477 b.object_versions.insert("file.txt".to_string(), vec![obj]);
2478
2479 let null_str = "null".to_string();
2480 let result = resolve_object(&b, "file.txt", Some(&null_str));
2481 assert!(
2482 result.is_ok(),
2483 "versionId=null should match version_id=Some(\"null\")"
2484 );
2485
2486 let obj2 = make_obj("file2.txt", None);
2488 b.objects.insert("file2.txt".to_string(), obj2.clone());
2489 b.object_versions
2490 .insert("file2.txt".to_string(), vec![obj2]);
2491
2492 let result2 = resolve_object(&b, "file2.txt", Some(&null_str));
2493 assert!(
2494 result2.is_ok(),
2495 "versionId=null should match version_id=None"
2496 );
2497 }
2498
2499 #[test]
2500 fn test_parse_replication_rules() {
2501 let xml = r#"<ReplicationConfiguration>
2502 <Role>arn:aws:iam::role/replication</Role>
2503 <Rule>
2504 <Status>Enabled</Status>
2505 <Filter><Prefix>logs/</Prefix></Filter>
2506 <Destination><Bucket>arn:aws:s3:::dest-bucket</Bucket></Destination>
2507 </Rule>
2508 <Rule>
2509 <Status>Disabled</Status>
2510 <Filter><Prefix></Prefix></Filter>
2511 <Destination><Bucket>arn:aws:s3:::other-bucket</Bucket></Destination>
2512 </Rule>
2513 </ReplicationConfiguration>"#;
2514
2515 let rules = parse_replication_rules(xml);
2516 assert_eq!(rules.len(), 2);
2517 assert_eq!(rules[0].status, "Enabled");
2518 assert_eq!(rules[0].prefix, "logs/");
2519 assert_eq!(rules[0].dest_bucket, "dest-bucket");
2520 assert_eq!(rules[1].status, "Disabled");
2521 assert_eq!(rules[1].prefix, "");
2522 assert_eq!(rules[1].dest_bucket, "other-bucket");
2523 }
2524
2525 #[test]
2526 fn test_parse_normalized_replication_rules() {
2527 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>"#;
2529 let normalized = normalize_replication_xml(input_xml);
2530 eprintln!("Normalized XML: {normalized}");
2531 let rules = parse_replication_rules(&normalized);
2532 assert_eq!(rules.len(), 1, "Expected 1 rule, got {}", rules.len());
2533 assert_eq!(rules[0].status, "Enabled");
2534 assert_eq!(rules[0].dest_bucket, "repl-dest");
2535 }
2536
2537 #[test]
2538 fn test_replicate_object() {
2539 use crate::state::{S3Bucket, S3State};
2540
2541 let mut state = S3State::new("123456789012", "us-east-1");
2542
2543 let mut src = S3Bucket::new("source", "us-east-1", "owner");
2545 src.versioning = Some("Enabled".to_string());
2546 src.replication_config = Some(
2547 "<ReplicationConfiguration>\
2548 <Rule><Status>Enabled</Status>\
2549 <Filter><Prefix></Prefix></Filter>\
2550 <Destination><Bucket>arn:aws:s3:::destination</Bucket></Destination>\
2551 </Rule></ReplicationConfiguration>"
2552 .to_string(),
2553 );
2554 let obj = S3Object {
2555 key: "test-key".to_string(),
2556 body: crate::state::memory_body(Bytes::from_static(b"hello")),
2557 content_type: "text/plain".to_string(),
2558 etag: "abc".to_string(),
2559 size: 5,
2560 last_modified: Utc::now(),
2561 storage_class: "STANDARD".to_string(),
2562 version_id: Some("v1".to_string()),
2563 ..Default::default()
2564 };
2565 src.objects.insert("test-key".to_string(), obj);
2566 state.buckets.insert("source".to_string(), src);
2567
2568 let dest = S3Bucket::new("destination", "us-east-1", "owner");
2569 state.buckets.insert("destination".to_string(), dest);
2570
2571 replicate_object(&mut state, "source", "test-key");
2572
2573 let dest_obj = state
2575 .buckets
2576 .get("destination")
2577 .unwrap()
2578 .objects
2579 .get("test-key");
2580 assert!(dest_obj.is_some());
2581 assert_eq!(
2582 state.read_body(&dest_obj.unwrap().body).unwrap(),
2583 Bytes::from_static(b"hello")
2584 );
2585 }
2586
2587 #[test]
2588 fn cors_header_value_does_not_panic_on_unusual_input() {
2589 let valid_origin = "https://example.com";
2593 let result: Result<http::HeaderValue, _> = valid_origin.parse();
2594 assert!(result.is_ok());
2595
2596 let bad_origin = "https://ex\x01ample.com";
2598 let result: Result<http::HeaderValue, _> = bad_origin.parse();
2599 assert!(result.is_err());
2600 let fallback = bad_origin
2602 .parse()
2603 .unwrap_or_else(|_| http::HeaderValue::from_static(""));
2604 assert_eq!(fallback, "");
2605 }
2606}