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;
12
13use base64::engine::general_purpose::STANDARD as BASE64;
14use base64::Engine as _;
15
16use crate::logging;
17use crate::state::{AclGrant, S3Bucket, S3Object, SharedS3State};
18
19mod acl;
20mod buckets;
21mod config;
22mod lock;
23mod multipart;
24mod notifications;
25mod objects;
26mod tags;
27
28pub(super) use notifications::{
30 deliver_notifications, normalize_notification_ids, normalize_replication_xml, replicate_object,
31};
32
33use notifications::extract_all_xml_values;
35
36#[cfg(test)]
38use notifications::{
39 event_matches, key_matches_filters, parse_notification_config, parse_replication_rules,
40 NotificationTargetType,
41};
42
43pub struct S3Service {
44 state: SharedS3State,
45 delivery: Arc<DeliveryBus>,
46 kms_state: Option<SharedKmsState>,
47}
48
49impl S3Service {
50 pub fn new(state: SharedS3State, delivery: Arc<DeliveryBus>) -> Self {
51 Self {
52 state,
53 delivery,
54 kms_state: None,
55 }
56 }
57
58 pub fn with_kms(mut self, kms_state: SharedKmsState) -> Self {
59 self.kms_state = Some(kms_state);
60 self
61 }
62}
63
64#[async_trait]
65impl AwsService for S3Service {
66 fn service_name(&self) -> &str {
67 "s3"
68 }
69
70 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
71 let bucket = req.path_segments.first().map(|s| s.as_str());
73 let key = if let Some(b) = bucket {
76 let prefix = format!("/{b}/");
77 if req.raw_path.starts_with(&prefix) && req.raw_path.len() > prefix.len() {
78 let raw_key = &req.raw_path[prefix.len()..];
79 Some(
80 percent_encoding::percent_decode_str(raw_key)
81 .decode_utf8_lossy()
82 .into_owned(),
83 )
84 } else if req.path_segments.len() > 1 {
85 let raw = req.path_segments[1..].join("/");
86 Some(
87 percent_encoding::percent_decode_str(&raw)
88 .decode_utf8_lossy()
89 .into_owned(),
90 )
91 } else {
92 None
93 }
94 } else {
95 None
96 };
97
98 if let Some(b) = bucket {
100 if req.method == Method::POST
102 && key.is_some()
103 && req.query_params.contains_key("uploads")
104 {
105 return self.create_multipart_upload(&req, b, key.as_deref().unwrap());
106 }
107
108 if req.method == Method::POST
110 && key.is_some()
111 && req.query_params.contains_key("restore")
112 {
113 return self.restore_object(&req, b, key.as_deref().unwrap());
114 }
115
116 if req.method == Method::POST && key.is_some() {
118 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
119 return self.complete_multipart_upload(
120 &req,
121 b,
122 key.as_deref().unwrap(),
123 &upload_id,
124 );
125 }
126 }
127
128 if req.method == Method::PUT && key.is_some() {
130 if let (Some(part_num_str), Some(upload_id)) = (
131 req.query_params.get("partNumber").cloned(),
132 req.query_params.get("uploadId").cloned(),
133 ) {
134 if let Ok(part_number) = part_num_str.parse::<i64>() {
135 if req.headers.contains_key("x-amz-copy-source") {
136 return self.upload_part_copy(
137 &req,
138 b,
139 key.as_deref().unwrap(),
140 &upload_id,
141 part_number,
142 );
143 }
144 return self.upload_part(
145 &req,
146 b,
147 key.as_deref().unwrap(),
148 &upload_id,
149 part_number,
150 );
151 }
152 }
153 }
154
155 if req.method == Method::DELETE && key.is_some() {
157 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
158 return self.abort_multipart_upload(b, key.as_deref().unwrap(), &upload_id);
159 }
160 }
161
162 if req.method == Method::GET
164 && key.is_none()
165 && req.query_params.contains_key("uploads")
166 {
167 return self.list_multipart_uploads(b);
168 }
169
170 if req.method == Method::GET && key.is_some() {
172 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
173 return self.list_parts(&req, b, key.as_deref().unwrap(), &upload_id);
174 }
175 }
176 }
177
178 if req.method == Method::OPTIONS {
180 if let Some(b_name) = bucket {
181 let cors_config = {
182 let state = self.state.read();
183 state
184 .buckets
185 .get(b_name)
186 .and_then(|b| b.cors_config.clone())
187 };
188 if let Some(ref config) = cors_config {
189 let origin = req
190 .headers
191 .get("origin")
192 .and_then(|v| v.to_str().ok())
193 .unwrap_or("");
194 let request_method = req
195 .headers
196 .get("access-control-request-method")
197 .and_then(|v| v.to_str().ok())
198 .unwrap_or("");
199 let rules = parse_cors_config(config);
200 if let Some(rule) = find_cors_rule(&rules, origin, Some(request_method)) {
201 let mut headers = HeaderMap::new();
202 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
203 "*"
204 } else {
205 origin
206 };
207 headers.insert(
208 "access-control-allow-origin",
209 matched_origin
210 .parse()
211 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
212 );
213 headers.insert(
214 "access-control-allow-methods",
215 rule.allowed_methods
216 .join(", ")
217 .parse()
218 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
219 );
220 if !rule.allowed_headers.is_empty() {
221 let ah = if rule.allowed_headers.contains(&"*".to_string()) {
222 req.headers
223 .get("access-control-request-headers")
224 .and_then(|v| v.to_str().ok())
225 .unwrap_or("*")
226 .to_string()
227 } else {
228 rule.allowed_headers.join(", ")
229 };
230 headers.insert(
231 "access-control-allow-headers",
232 ah.parse()
233 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
234 );
235 }
236 if let Some(max_age) = rule.max_age_seconds {
237 headers.insert(
238 "access-control-max-age",
239 max_age
240 .to_string()
241 .parse()
242 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
243 );
244 }
245 return Ok(AwsResponse {
246 status: StatusCode::OK,
247 content_type: String::new(),
248 body: Bytes::new(),
249 headers,
250 });
251 }
252 }
253 return Err(AwsServiceError::aws_error(
254 StatusCode::FORBIDDEN,
255 "CORSResponse",
256 "CORS is not enabled for this bucket",
257 ));
258 }
259 }
260
261 let origin_header = req
263 .headers
264 .get("origin")
265 .and_then(|v| v.to_str().ok())
266 .map(|s| s.to_string());
267
268 let mut result = match (&req.method, bucket, key.as_deref()) {
269 (&Method::GET, None, None) => self.list_buckets(&req),
271
272 (&Method::PUT, Some(b), None) => {
274 if req.query_params.contains_key("tagging") {
275 self.put_bucket_tagging(&req, b)
276 } else if req.query_params.contains_key("acl") {
277 self.put_bucket_acl(&req, b)
278 } else if req.query_params.contains_key("versioning") {
279 self.put_bucket_versioning(&req, b)
280 } else if req.query_params.contains_key("cors") {
281 self.put_bucket_cors(&req, b)
282 } else if req.query_params.contains_key("notification") {
283 self.put_bucket_notification(&req, b)
284 } else if req.query_params.contains_key("website") {
285 self.put_bucket_website(&req, b)
286 } else if req.query_params.contains_key("accelerate") {
287 self.put_bucket_accelerate(&req, b)
288 } else if req.query_params.contains_key("publicAccessBlock") {
289 self.put_public_access_block(&req, b)
290 } else if req.query_params.contains_key("encryption") {
291 self.put_bucket_encryption(&req, b)
292 } else if req.query_params.contains_key("lifecycle") {
293 self.put_bucket_lifecycle(&req, b)
294 } else if req.query_params.contains_key("logging") {
295 self.put_bucket_logging(&req, b)
296 } else if req.query_params.contains_key("policy") {
297 self.put_bucket_policy(&req, b)
298 } else if req.query_params.contains_key("object-lock") {
299 self.put_object_lock_config(&req, b)
300 } else if req.query_params.contains_key("replication") {
301 self.put_bucket_replication(&req, b)
302 } else if req.query_params.contains_key("ownershipControls") {
303 self.put_bucket_ownership_controls(&req, b)
304 } else if req.query_params.contains_key("inventory") {
305 self.put_bucket_inventory(&req, b)
306 } else {
307 self.create_bucket(&req, b)
308 }
309 }
310 (&Method::DELETE, Some(b), None) => {
311 if req.query_params.contains_key("tagging") {
312 self.delete_bucket_tagging(&req, b)
313 } else if req.query_params.contains_key("cors") {
314 self.delete_bucket_cors(b)
315 } else if req.query_params.contains_key("website") {
316 self.delete_bucket_website(b)
317 } else if req.query_params.contains_key("publicAccessBlock") {
318 self.delete_public_access_block(b)
319 } else if req.query_params.contains_key("encryption") {
320 self.delete_bucket_encryption(b)
321 } else if req.query_params.contains_key("lifecycle") {
322 self.delete_bucket_lifecycle(b)
323 } else if req.query_params.contains_key("policy") {
324 self.delete_bucket_policy(b)
325 } else if req.query_params.contains_key("replication") {
326 self.delete_bucket_replication(b)
327 } else if req.query_params.contains_key("ownershipControls") {
328 self.delete_bucket_ownership_controls(b)
329 } else if req.query_params.contains_key("inventory") {
330 self.delete_bucket_inventory(&req, b)
331 } else {
332 self.delete_bucket(&req, b)
333 }
334 }
335 (&Method::HEAD, Some(b), None) => self.head_bucket(b),
336 (&Method::GET, Some(b), None) => {
337 if req.query_params.contains_key("tagging") {
338 self.get_bucket_tagging(&req, b)
339 } else if req.query_params.contains_key("location") {
340 self.get_bucket_location(b)
341 } else if req.query_params.contains_key("acl") {
342 self.get_bucket_acl(&req, b)
343 } else if req.query_params.contains_key("versioning") {
344 self.get_bucket_versioning(b)
345 } else if req.query_params.contains_key("versions") {
346 self.list_object_versions(&req, b)
347 } else if req.query_params.contains_key("object-lock") {
348 self.get_object_lock_configuration(b)
349 } else if req.query_params.contains_key("cors") {
350 self.get_bucket_cors(b)
351 } else if req.query_params.contains_key("notification") {
352 self.get_bucket_notification(b)
353 } else if req.query_params.contains_key("website") {
354 self.get_bucket_website(b)
355 } else if req.query_params.contains_key("accelerate") {
356 self.get_bucket_accelerate(b)
357 } else if req.query_params.contains_key("publicAccessBlock") {
358 self.get_public_access_block(b)
359 } else if req.query_params.contains_key("encryption") {
360 self.get_bucket_encryption(b)
361 } else if req.query_params.contains_key("lifecycle") {
362 self.get_bucket_lifecycle(b)
363 } else if req.query_params.contains_key("logging") {
364 self.get_bucket_logging(b)
365 } else if req.query_params.contains_key("policy") {
366 self.get_bucket_policy(b)
367 } else if req.query_params.contains_key("replication") {
368 self.get_bucket_replication(b)
369 } else if req.query_params.contains_key("ownershipControls") {
370 self.get_bucket_ownership_controls(b)
371 } else if req.query_params.contains_key("inventory") {
372 if req.query_params.contains_key("id") {
373 self.get_bucket_inventory(&req, b)
374 } else {
375 self.list_bucket_inventory_configurations(b)
376 }
377 } else if req.query_params.get("list-type").map(|s| s.as_str()) == Some("2") {
378 self.list_objects_v2(&req, b)
379 } else if req.query_params.is_empty() {
380 let website_config = {
382 let state = self.state.read();
383 state
384 .buckets
385 .get(b)
386 .and_then(|bkt| bkt.website_config.clone())
387 };
388 if let Some(ref config) = website_config {
389 if let Some(index_doc) = extract_xml_value(config, "Suffix").or_else(|| {
390 extract_xml_value(config, "IndexDocument").and_then(|inner| {
391 let open = "<Suffix>";
392 let close = "</Suffix>";
393 let s = inner.find(open)? + open.len();
394 let e = inner.find(close)?;
395 Some(inner[s..e].trim().to_string())
396 })
397 }) {
398 self.serve_website_object(&req, b, &index_doc, config)
399 } else {
400 self.list_objects_v1(&req, b)
401 }
402 } else {
403 self.list_objects_v1(&req, b)
404 }
405 } else {
406 self.list_objects_v1(&req, b)
407 }
408 }
409
410 (&Method::PUT, Some(b), Some(k)) => {
412 if req.query_params.contains_key("tagging") {
413 self.put_object_tagging(&req, b, k)
414 } else if req.query_params.contains_key("acl") {
415 self.put_object_acl(&req, b, k)
416 } else if req.query_params.contains_key("retention") {
417 self.put_object_retention(&req, b, k)
418 } else if req.query_params.contains_key("legal-hold") {
419 self.put_object_legal_hold(&req, b, k)
420 } else if req.headers.contains_key("x-amz-copy-source") {
421 self.copy_object(&req, b, k)
422 } else {
423 self.put_object(&req, b, k)
424 }
425 }
426 (&Method::GET, Some(b), Some(k)) => {
427 if req.query_params.contains_key("tagging") {
428 self.get_object_tagging(&req, b, k)
429 } else if req.query_params.contains_key("acl") {
430 self.get_object_acl(&req, b, k)
431 } else if req.query_params.contains_key("retention") {
432 self.get_object_retention(&req, b, k)
433 } else if req.query_params.contains_key("legal-hold") {
434 self.get_object_legal_hold(&req, b, k)
435 } else if req.query_params.contains_key("attributes") {
436 self.get_object_attributes(&req, b, k)
437 } else {
438 let result = self.get_object(&req, b, k);
439 let is_not_found = matches!(
441 &result,
442 Err(e) if e.code() == "NoSuchKey"
443 );
444 if is_not_found {
445 let website_config = {
446 let state = self.state.read();
447 state
448 .buckets
449 .get(b)
450 .and_then(|bkt| bkt.website_config.clone())
451 };
452 if let Some(ref config) = website_config {
453 if let Some(error_key) = extract_xml_value(config, "ErrorDocument")
454 .and_then(|inner| {
455 let open = "<Key>";
456 let close = "</Key>";
457 let s = inner.find(open)? + open.len();
458 let e = inner.find(close)?;
459 Some(inner[s..e].trim().to_string())
460 })
461 .or_else(|| extract_xml_value(config, "Key"))
462 {
463 return self.serve_website_error(&req, b, &error_key);
464 }
465 }
466 }
467 result
468 }
469 }
470 (&Method::DELETE, Some(b), Some(k)) => {
471 if req.query_params.contains_key("tagging") {
472 self.delete_object_tagging(b, k)
473 } else {
474 self.delete_object(&req, b, k)
475 }
476 }
477 (&Method::HEAD, Some(b), Some(k)) => self.head_object(&req, b, k),
478
479 (&Method::POST, Some(b), None) if req.query_params.contains_key("delete") => {
481 self.delete_objects(&req, b)
482 }
483
484 _ => Err(AwsServiceError::aws_error(
485 StatusCode::METHOD_NOT_ALLOWED,
486 "MethodNotAllowed",
487 "The specified method is not allowed against this resource",
488 )),
489 };
490
491 if let (Some(ref origin), Some(b_name)) = (&origin_header, bucket) {
493 let cors_config = {
494 let state = self.state.read();
495 state
496 .buckets
497 .get(b_name)
498 .and_then(|b| b.cors_config.clone())
499 };
500 if let Some(ref config) = cors_config {
501 let rules = parse_cors_config(config);
502 if let Some(rule) = find_cors_rule(&rules, origin, None) {
503 if let Ok(ref mut resp) = result {
504 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
505 "*"
506 } else {
507 origin
508 };
509 resp.headers.insert(
510 "access-control-allow-origin",
511 matched_origin
512 .parse()
513 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
514 );
515 if !rule.expose_headers.is_empty() {
516 resp.headers.insert(
517 "access-control-expose-headers",
518 rule.expose_headers
519 .join(", ")
520 .parse()
521 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
522 );
523 }
524 }
525 }
526 }
527 }
528
529 if let Some(b_name) = bucket {
531 let status_code = match &result {
532 Ok(resp) => resp.status.as_u16(),
533 Err(e) => e.status().as_u16(),
534 };
535 let op = logging::operation_name(&req.method, key.as_deref());
536 logging::maybe_write_access_log(
537 &self.state,
538 b_name,
539 op,
540 key.as_deref(),
541 status_code,
542 &req.request_id,
543 req.method.as_str(),
544 &req.raw_path,
545 );
546 }
547
548 result
549 }
550
551 fn supported_actions(&self) -> &[&str] {
552 &[
553 "ListBuckets",
555 "CreateBucket",
556 "DeleteBucket",
557 "HeadBucket",
558 "GetBucketLocation",
559 "PutObject",
561 "GetObject",
562 "DeleteObject",
563 "HeadObject",
564 "CopyObject",
565 "DeleteObjects",
566 "ListObjectsV2",
567 "ListObjects",
568 "ListObjectVersions",
569 "GetObjectAttributes",
570 "RestoreObject",
571 "PutObjectTagging",
573 "GetObjectTagging",
574 "DeleteObjectTagging",
575 "PutObjectAcl",
576 "GetObjectAcl",
577 "PutObjectRetention",
578 "GetObjectRetention",
579 "PutObjectLegalHold",
580 "GetObjectLegalHold",
581 "PutBucketTagging",
583 "GetBucketTagging",
584 "DeleteBucketTagging",
585 "PutBucketAcl",
586 "GetBucketAcl",
587 "PutBucketVersioning",
588 "GetBucketVersioning",
589 "PutBucketCors",
590 "GetBucketCors",
591 "DeleteBucketCors",
592 "PutBucketNotificationConfiguration",
593 "GetBucketNotificationConfiguration",
594 "PutBucketWebsite",
595 "GetBucketWebsite",
596 "DeleteBucketWebsite",
597 "PutBucketAccelerateConfiguration",
598 "GetBucketAccelerateConfiguration",
599 "PutPublicAccessBlock",
600 "GetPublicAccessBlock",
601 "DeletePublicAccessBlock",
602 "PutBucketEncryption",
603 "GetBucketEncryption",
604 "DeleteBucketEncryption",
605 "PutBucketLifecycleConfiguration",
606 "GetBucketLifecycleConfiguration",
607 "DeleteBucketLifecycle",
608 "PutBucketLogging",
609 "GetBucketLogging",
610 "PutBucketPolicy",
611 "GetBucketPolicy",
612 "DeleteBucketPolicy",
613 "PutObjectLockConfiguration",
614 "GetObjectLockConfiguration",
615 "PutBucketReplication",
616 "GetBucketReplication",
617 "DeleteBucketReplication",
618 "PutBucketOwnershipControls",
619 "GetBucketOwnershipControls",
620 "DeleteBucketOwnershipControls",
621 "PutBucketInventoryConfiguration",
622 "GetBucketInventoryConfiguration",
623 "DeleteBucketInventoryConfiguration",
624 "CreateMultipartUpload",
626 "UploadPart",
627 "UploadPartCopy",
628 "CompleteMultipartUpload",
629 "AbortMultipartUpload",
630 "ListParts",
631 "ListMultipartUploads",
632 ]
633 }
634}
635
636pub(crate) fn truncate_to_seconds(dt: DateTime<Utc>) -> DateTime<Utc> {
642 dt.with_nanosecond(0).unwrap_or(dt)
643}
644
645pub(crate) fn check_get_conditionals(
646 req: &AwsRequest,
647 obj: &S3Object,
648) -> Result<(), AwsServiceError> {
649 let obj_etag = format!("\"{}\"", obj.etag);
650 let obj_time = truncate_to_seconds(obj.last_modified);
651
652 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
654 if !etag_matches(if_match, &obj_etag) {
655 return Err(precondition_failed("If-Match"));
656 }
657 }
658
659 if let Some(if_none_match) = req
661 .headers
662 .get("if-none-match")
663 .and_then(|v| v.to_str().ok())
664 {
665 if etag_matches(if_none_match, &obj_etag) {
666 return Err(not_modified_with_etag(&obj_etag));
667 }
668 }
669
670 if let Some(since) = req
672 .headers
673 .get("if-unmodified-since")
674 .and_then(|v| v.to_str().ok())
675 {
676 if let Some(dt) = parse_http_date(since) {
677 if obj_time > dt {
678 return Err(precondition_failed("If-Unmodified-Since"));
679 }
680 }
681 }
682
683 if let Some(since) = req
685 .headers
686 .get("if-modified-since")
687 .and_then(|v| v.to_str().ok())
688 {
689 if let Some(dt) = parse_http_date(since) {
690 if obj_time <= dt {
691 return Err(not_modified());
692 }
693 }
694 }
695
696 Ok(())
697}
698
699pub(crate) fn check_head_conditionals(
700 req: &AwsRequest,
701 obj: &S3Object,
702) -> Result<(), AwsServiceError> {
703 let obj_etag = format!("\"{}\"", obj.etag);
704 let obj_time = truncate_to_seconds(obj.last_modified);
705
706 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
708 if !etag_matches(if_match, &obj_etag) {
709 return Err(AwsServiceError::aws_error(
710 StatusCode::PRECONDITION_FAILED,
711 "412",
712 "Precondition Failed",
713 ));
714 }
715 }
716
717 if let Some(if_none_match) = req
719 .headers
720 .get("if-none-match")
721 .and_then(|v| v.to_str().ok())
722 {
723 if etag_matches(if_none_match, &obj_etag) {
724 return Err(not_modified_with_etag(&obj_etag));
725 }
726 }
727
728 if let Some(since) = req
730 .headers
731 .get("if-unmodified-since")
732 .and_then(|v| v.to_str().ok())
733 {
734 if let Some(dt) = parse_http_date(since) {
735 if obj_time > dt {
736 return Err(AwsServiceError::aws_error(
737 StatusCode::PRECONDITION_FAILED,
738 "412",
739 "Precondition Failed",
740 ));
741 }
742 }
743 }
744
745 if let Some(since) = req
747 .headers
748 .get("if-modified-since")
749 .and_then(|v| v.to_str().ok())
750 {
751 if let Some(dt) = parse_http_date(since) {
752 if obj_time <= dt {
753 return Err(not_modified());
754 }
755 }
756 }
757
758 Ok(())
759}
760
761pub(crate) fn etag_matches(condition: &str, obj_etag: &str) -> bool {
762 let condition = condition.trim();
763 if condition == "*" {
764 return true;
765 }
766 let clean_etag = obj_etag.replace('"', "");
767 for part in condition.split(',') {
769 let part = part.trim().replace('"', "");
770 if part == clean_etag {
771 return true;
772 }
773 }
774 false
775}
776
777pub(crate) fn parse_http_date(s: &str) -> Option<DateTime<Utc>> {
778 if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
780 return Some(dt.with_timezone(&Utc));
781 }
782 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
784 return Some(dt.with_timezone(&Utc));
785 }
786 if let Ok(dt) =
788 chrono::NaiveDateTime::parse_from_str(s.trim_end_matches(" GMT"), "%a, %d %b %Y %H:%M:%S")
789 {
790 return Some(dt.and_utc());
791 }
792 if let Ok(dt) = s.parse::<DateTime<Utc>>() {
794 return Some(dt);
795 }
796 None
797}
798
799pub(crate) fn not_modified() -> AwsServiceError {
800 AwsServiceError::aws_error(StatusCode::NOT_MODIFIED, "304", "Not Modified")
801}
802
803pub(crate) fn not_modified_with_etag(etag: &str) -> AwsServiceError {
804 AwsServiceError::aws_error_with_headers(
805 StatusCode::NOT_MODIFIED,
806 "304",
807 "Not Modified",
808 vec![("etag".to_string(), etag.to_string())],
809 )
810}
811
812pub(crate) fn precondition_failed(condition: &str) -> AwsServiceError {
813 AwsServiceError::aws_error_with_fields(
814 StatusCode::PRECONDITION_FAILED,
815 "PreconditionFailed",
816 "At least one of the pre-conditions you specified did not hold",
817 vec![("Condition".to_string(), condition.to_string())],
818 )
819}
820
821pub(crate) fn build_acl_xml(owner_id: &str, grants: &[AclGrant], _account_id: &str) -> String {
826 let mut grants_xml = String::new();
827 for g in grants {
828 let grantee_xml = if g.grantee_type == "Group" {
829 let uri = g.grantee_uri.as_deref().unwrap_or("");
830 format!(
831 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\">\
832 <URI>{}</URI></Grantee>",
833 xml_escape(uri),
834 )
835 } else {
836 let id = g.grantee_id.as_deref().unwrap_or("");
837 format!(
838 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">\
839 <ID>{}</ID></Grantee>",
840 xml_escape(id),
841 )
842 };
843 grants_xml.push_str(&format!(
844 "<Grant>{grantee_xml}<Permission>{}</Permission></Grant>",
845 xml_escape(&g.permission),
846 ));
847 }
848
849 format!(
850 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
851 <AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
852 <Owner><ID>{owner_id}</ID><DisplayName>{owner_id}</DisplayName></Owner>\
853 <AccessControlList>{grants_xml}</AccessControlList>\
854 </AccessControlPolicy>",
855 owner_id = xml_escape(owner_id),
856 )
857}
858
859pub(crate) fn canned_acl_grants(acl: &str, owner_id: &str) -> Vec<AclGrant> {
860 let owner_grant = AclGrant {
861 grantee_type: "CanonicalUser".to_string(),
862 grantee_id: Some(owner_id.to_string()),
863 grantee_display_name: Some(owner_id.to_string()),
864 grantee_uri: None,
865 permission: "FULL_CONTROL".to_string(),
866 };
867 match acl {
868 "private" => vec![owner_grant],
869 "public-read" => vec![
870 owner_grant,
871 AclGrant {
872 grantee_type: "Group".to_string(),
873 grantee_id: None,
874 grantee_display_name: None,
875 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
876 permission: "READ".to_string(),
877 },
878 ],
879 "public-read-write" => vec![
880 owner_grant,
881 AclGrant {
882 grantee_type: "Group".to_string(),
883 grantee_id: None,
884 grantee_display_name: None,
885 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
886 permission: "READ".to_string(),
887 },
888 AclGrant {
889 grantee_type: "Group".to_string(),
890 grantee_id: None,
891 grantee_display_name: None,
892 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
893 permission: "WRITE".to_string(),
894 },
895 ],
896 "authenticated-read" => vec![
897 owner_grant,
898 AclGrant {
899 grantee_type: "Group".to_string(),
900 grantee_id: None,
901 grantee_display_name: None,
902 grantee_uri: Some(
903 "http://acs.amazonaws.com/groups/global/AuthenticatedUsers".to_string(),
904 ),
905 permission: "READ".to_string(),
906 },
907 ],
908 "bucket-owner-full-control" => vec![owner_grant],
909 _ => vec![owner_grant],
910 }
911}
912
913pub(crate) fn canned_acl_grants_for_object(acl: &str, owner_id: &str) -> Vec<AclGrant> {
914 canned_acl_grants(acl, owner_id)
916}
917
918pub(crate) fn parse_grant_headers(headers: &HeaderMap) -> Vec<AclGrant> {
919 let mut grants = Vec::new();
920 let header_permission_map = [
921 ("x-amz-grant-read", "READ"),
922 ("x-amz-grant-write", "WRITE"),
923 ("x-amz-grant-read-acp", "READ_ACP"),
924 ("x-amz-grant-write-acp", "WRITE_ACP"),
925 ("x-amz-grant-full-control", "FULL_CONTROL"),
926 ];
927
928 for (header, permission) in &header_permission_map {
929 if let Some(value) = headers.get(*header).and_then(|v| v.to_str().ok()) {
930 for part in value.split(',') {
932 let part = part.trim();
933 if let Some((key, val)) = part.split_once('=') {
934 let val = val.trim().trim_matches('"');
935 let key = key.trim().to_lowercase();
936 match key.as_str() {
937 "id" => {
938 grants.push(AclGrant {
939 grantee_type: "CanonicalUser".to_string(),
940 grantee_id: Some(val.to_string()),
941 grantee_display_name: Some(val.to_string()),
942 grantee_uri: None,
943 permission: permission.to_string(),
944 });
945 }
946 "uri" | "url" => {
947 grants.push(AclGrant {
948 grantee_type: "Group".to_string(),
949 grantee_id: None,
950 grantee_display_name: None,
951 grantee_uri: Some(val.to_string()),
952 permission: permission.to_string(),
953 });
954 }
955 _ => {}
956 }
957 }
958 }
959 }
960 }
961 grants
962}
963
964pub(crate) fn parse_acl_xml(xml: &str) -> Result<Vec<AclGrant>, AwsServiceError> {
965 if xml.contains("<AccessControlPolicy") && !xml.contains("<Owner>") {
967 return Err(AwsServiceError::aws_error(
968 StatusCode::BAD_REQUEST,
969 "MalformedACLError",
970 "The XML you provided was not well-formed or did not validate against our published schema",
971 ));
972 }
973
974 let valid_permissions = ["READ", "WRITE", "READ_ACP", "WRITE_ACP", "FULL_CONTROL"];
975
976 let mut grants = Vec::new();
977 let mut remaining = xml;
978 while let Some(start) = remaining.find("<Grant>") {
979 let after = &remaining[start + 7..];
980 if let Some(end) = after.find("</Grant>") {
981 let grant_body = &after[..end];
982
983 let permission = extract_xml_value(grant_body, "Permission").unwrap_or_default();
985 if !valid_permissions.contains(&permission.as_str()) {
986 return Err(AwsServiceError::aws_error(
987 StatusCode::BAD_REQUEST,
988 "MalformedACLError",
989 "The XML you provided was not well-formed or did not validate against our published schema",
990 ));
991 }
992
993 if grant_body.contains("xsi:type=\"Group\"") || grant_body.contains("<URI>") {
995 let uri = extract_xml_value(grant_body, "URI").unwrap_or_default();
996 grants.push(AclGrant {
997 grantee_type: "Group".to_string(),
998 grantee_id: None,
999 grantee_display_name: None,
1000 grantee_uri: Some(uri),
1001 permission,
1002 });
1003 } else {
1004 let id = extract_xml_value(grant_body, "ID").unwrap_or_default();
1005 let display =
1006 extract_xml_value(grant_body, "DisplayName").unwrap_or_else(|| id.clone());
1007 grants.push(AclGrant {
1008 grantee_type: "CanonicalUser".to_string(),
1009 grantee_id: Some(id),
1010 grantee_display_name: Some(display),
1011 grantee_uri: None,
1012 permission,
1013 });
1014 }
1015
1016 remaining = &after[end + 8..];
1017 } else {
1018 break;
1019 }
1020 }
1021 Ok(grants)
1022}
1023
1024pub(crate) enum RangeResult {
1029 Satisfiable { start: usize, end: usize },
1030 NotSatisfiable,
1031 Ignored,
1032}
1033
1034pub(crate) fn parse_range_header(range_str: &str, total_size: usize) -> Option<RangeResult> {
1035 let range_str = range_str.strip_prefix("bytes=")?;
1036 let (start_str, end_str) = range_str.split_once('-')?;
1037 if start_str.is_empty() {
1038 let suffix_len: usize = end_str.parse().ok()?;
1039 if suffix_len == 0 || total_size == 0 {
1040 return Some(RangeResult::NotSatisfiable);
1041 }
1042 let start = total_size.saturating_sub(suffix_len);
1043 Some(RangeResult::Satisfiable {
1044 start,
1045 end: total_size - 1,
1046 })
1047 } else {
1048 let start: usize = start_str.parse().ok()?;
1049 if start >= total_size {
1050 return Some(RangeResult::NotSatisfiable);
1051 }
1052 let end = if end_str.is_empty() {
1053 total_size - 1
1054 } else {
1055 let e: usize = end_str.parse().ok()?;
1056 if e < start {
1057 return Some(RangeResult::Ignored);
1058 }
1059 std::cmp::min(e, total_size - 1)
1060 };
1061 Some(RangeResult::Satisfiable { start, end })
1062 }
1063}
1064
1065pub(crate) fn s3_xml(status: StatusCode, body: impl Into<Bytes>) -> AwsResponse {
1071 AwsResponse {
1072 status,
1073 content_type: "application/xml".to_string(),
1074 body: body.into(),
1075 headers: HeaderMap::new(),
1076 }
1077}
1078
1079pub(crate) fn empty_response(status: StatusCode) -> AwsResponse {
1080 AwsResponse {
1081 status,
1082 content_type: "application/xml".to_string(),
1083 body: Bytes::new(),
1084 headers: HeaderMap::new(),
1085 }
1086}
1087
1088pub(crate) fn is_frozen(obj: &S3Object) -> bool {
1091 matches!(obj.storage_class.as_str(), "GLACIER" | "DEEP_ARCHIVE")
1092 && obj.restore_ongoing != Some(false)
1093}
1094
1095pub(crate) fn no_such_bucket(bucket: &str) -> AwsServiceError {
1096 AwsServiceError::aws_error_with_fields(
1097 StatusCode::NOT_FOUND,
1098 "NoSuchBucket",
1099 "The specified bucket does not exist",
1100 vec![("BucketName".to_string(), bucket.to_string())],
1101 )
1102}
1103
1104pub(crate) fn no_such_key(key: &str) -> AwsServiceError {
1105 AwsServiceError::aws_error_with_fields(
1106 StatusCode::NOT_FOUND,
1107 "NoSuchKey",
1108 "The specified key does not exist.",
1109 vec![("Key".to_string(), key.to_string())],
1110 )
1111}
1112
1113pub(crate) fn no_such_upload(upload_id: &str) -> AwsServiceError {
1114 AwsServiceError::aws_error_with_fields(
1115 StatusCode::NOT_FOUND,
1116 "NoSuchUpload",
1117 "The specified upload does not exist. The upload ID may be invalid, \
1118 or the upload may have been aborted or completed.",
1119 vec![("UploadId".to_string(), upload_id.to_string())],
1120 )
1121}
1122
1123pub(crate) fn no_such_key_with_detail(key: &str) -> AwsServiceError {
1124 AwsServiceError::aws_error_with_fields(
1125 StatusCode::NOT_FOUND,
1126 "NoSuchKey",
1127 "The specified key does not exist.",
1128 vec![("Key".to_string(), key.to_string())],
1129 )
1130}
1131
1132pub(crate) fn compute_md5(data: &[u8]) -> String {
1133 let digest = Md5::digest(data);
1134 format!("{:x}", digest)
1135}
1136
1137pub(crate) fn compute_checksum(algorithm: &str, data: &[u8]) -> String {
1138 match algorithm {
1139 "CRC32" => {
1140 let crc = crc32fast::hash(data);
1141 BASE64.encode(crc.to_be_bytes())
1142 }
1143 "SHA1" => {
1144 use sha1::Digest as _;
1145 let hash = sha1::Sha1::digest(data);
1146 BASE64.encode(hash)
1147 }
1148 "SHA256" => {
1149 use sha2::Digest as _;
1150 let hash = sha2::Sha256::digest(data);
1151 BASE64.encode(hash)
1152 }
1153 _ => String::new(),
1154 }
1155}
1156
1157pub(crate) fn url_encode_s3_key(s: &str) -> String {
1158 let mut out = String::with_capacity(s.len() * 2);
1159 for byte in s.bytes() {
1160 match byte {
1161 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
1162 out.push(byte as char);
1163 }
1164 _ => {
1165 out.push_str(&format!("%{:02X}", byte));
1166 }
1167 }
1168 }
1169 out
1170}
1171
1172pub(crate) use fakecloud_aws::xml::xml_escape;
1173
1174pub(crate) fn extract_user_metadata(
1175 headers: &HeaderMap,
1176) -> std::collections::HashMap<String, String> {
1177 let mut meta = std::collections::HashMap::new();
1178 for (name, value) in headers {
1179 if let Some(key) = name.as_str().strip_prefix("x-amz-meta-") {
1180 if let Ok(v) = value.to_str() {
1181 meta.insert(key.to_string(), v.to_string());
1182 }
1183 }
1184 }
1185 meta
1186}
1187
1188pub(crate) fn is_valid_storage_class(class: &str) -> bool {
1189 matches!(
1190 class,
1191 "STANDARD"
1192 | "REDUCED_REDUNDANCY"
1193 | "STANDARD_IA"
1194 | "ONEZONE_IA"
1195 | "INTELLIGENT_TIERING"
1196 | "GLACIER"
1197 | "DEEP_ARCHIVE"
1198 | "GLACIER_IR"
1199 | "OUTPOSTS"
1200 | "SNOW"
1201 | "EXPRESS_ONEZONE"
1202 )
1203}
1204
1205pub(crate) fn is_valid_bucket_name(name: &str) -> bool {
1206 if name.len() < 3 || name.len() > 63 {
1207 return false;
1208 }
1209 let bytes = name.as_bytes();
1211 if !bytes[0].is_ascii_alphanumeric() || !bytes[bytes.len() - 1].is_ascii_alphanumeric() {
1212 return false;
1213 }
1214 name.chars()
1216 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.' || c == '_')
1217}
1218
1219pub(crate) fn is_valid_region(region: &str) -> bool {
1220 let valid_regions = [
1222 "us-east-1",
1223 "us-east-2",
1224 "us-west-1",
1225 "us-west-2",
1226 "af-south-1",
1227 "ap-east-1",
1228 "ap-south-1",
1229 "ap-south-2",
1230 "ap-southeast-1",
1231 "ap-southeast-2",
1232 "ap-southeast-3",
1233 "ap-southeast-4",
1234 "ap-northeast-1",
1235 "ap-northeast-2",
1236 "ap-northeast-3",
1237 "ca-central-1",
1238 "ca-west-1",
1239 "eu-central-1",
1240 "eu-central-2",
1241 "eu-west-1",
1242 "eu-west-2",
1243 "eu-west-3",
1244 "eu-south-1",
1245 "eu-south-2",
1246 "eu-north-1",
1247 "il-central-1",
1248 "me-south-1",
1249 "me-central-1",
1250 "sa-east-1",
1251 "cn-north-1",
1252 "cn-northwest-1",
1253 "us-gov-east-1",
1254 "us-gov-east-2",
1255 "us-gov-west-1",
1256 "us-iso-east-1",
1257 "us-iso-west-1",
1258 "us-isob-east-1",
1259 "us-isof-south-1",
1260 ];
1261 valid_regions.contains(®ion)
1262}
1263
1264pub(crate) fn resolve_object<'a>(
1265 b: &'a S3Bucket,
1266 key: &str,
1267 version_id: Option<&String>,
1268) -> Result<&'a S3Object, AwsServiceError> {
1269 if let Some(vid) = version_id {
1270 if vid == "null" {
1272 if let Some(versions) = b.object_versions.get(key) {
1274 if let Some(obj) = versions
1275 .iter()
1276 .find(|o| o.version_id.is_none() || o.version_id.as_deref() == Some("null"))
1277 {
1278 return Ok(obj);
1279 }
1280 }
1281 if let Some(obj) = b.objects.get(key) {
1283 if obj.version_id.is_none() || obj.version_id.as_deref() == Some("null") {
1284 return Ok(obj);
1285 }
1286 }
1287 } else {
1288 if let Some(versions) = b.object_versions.get(key) {
1290 if let Some(obj) = versions
1291 .iter()
1292 .find(|o| o.version_id.as_deref() == Some(vid.as_str()))
1293 {
1294 return Ok(obj);
1295 }
1296 }
1297 if let Some(obj) = b.objects.get(key) {
1299 if obj.version_id.as_deref() == Some(vid.as_str()) {
1300 return Ok(obj);
1301 }
1302 }
1303 }
1304 if b.versioning.is_some() {
1306 Err(AwsServiceError::aws_error_with_fields(
1307 StatusCode::NOT_FOUND,
1308 "NoSuchVersion",
1309 "The specified version does not exist.",
1310 vec![
1311 ("Key".to_string(), key.to_string()),
1312 ("VersionId".to_string(), vid.to_string()),
1313 ],
1314 ))
1315 } else {
1316 Err(AwsServiceError::aws_error(
1317 StatusCode::BAD_REQUEST,
1318 "InvalidArgument",
1319 "Invalid version id specified",
1320 ))
1321 }
1322 } else {
1323 b.objects.get(key).ok_or_else(|| no_such_key(key))
1324 }
1325}
1326
1327pub(crate) fn make_delete_marker(key: &str, dm_id: &str) -> S3Object {
1328 S3Object {
1329 key: key.to_string(),
1330 data: Bytes::new(),
1331 content_type: String::new(),
1332 etag: String::new(),
1333 size: 0,
1334 last_modified: Utc::now(),
1335 metadata: std::collections::HashMap::new(),
1336 storage_class: "STANDARD".to_string(),
1337 tags: std::collections::HashMap::new(),
1338 acl_grants: vec![],
1339 acl_owner_id: None,
1340 parts_count: None,
1341 part_sizes: None,
1342 sse_algorithm: None,
1343 sse_kms_key_id: None,
1344 bucket_key_enabled: None,
1345 version_id: Some(dm_id.to_string()),
1346 is_delete_marker: true,
1347 content_encoding: None,
1348 website_redirect_location: None,
1349 restore_ongoing: None,
1350 restore_expiry: None,
1351 checksum_algorithm: None,
1352 checksum_value: None,
1353 lock_mode: None,
1354 lock_retain_until: None,
1355 lock_legal_hold: None,
1356 }
1357}
1358
1359pub(crate) struct DeleteObjectEntry {
1361 key: String,
1362 version_id: Option<String>,
1363}
1364
1365pub(crate) fn parse_delete_objects_xml(xml: &str) -> Vec<DeleteObjectEntry> {
1366 let mut entries = Vec::new();
1367 let mut remaining = xml;
1368 while let Some(obj_start) = remaining.find("<Object>") {
1369 let after = &remaining[obj_start + 8..];
1370 if let Some(obj_end) = after.find("</Object>") {
1371 let obj_body = &after[..obj_end];
1372 let key = extract_xml_value(obj_body, "Key");
1373 let version_id = extract_xml_value(obj_body, "VersionId");
1374 if let Some(k) = key {
1375 entries.push(DeleteObjectEntry { key: k, version_id });
1376 }
1377 remaining = &after[obj_end + 9..];
1378 } else {
1379 break;
1380 }
1381 }
1382 entries
1383}
1384
1385pub(crate) fn parse_tagging_xml(xml: &str) -> Vec<(String, String)> {
1388 let mut tags = Vec::new();
1389 let mut remaining = xml;
1390 while let Some(tag_start) = remaining.find("<Tag>") {
1391 let after = &remaining[tag_start + 5..];
1392 if let Some(tag_end) = after.find("</Tag>") {
1393 let tag_body = &after[..tag_end];
1394 let key = extract_xml_value(tag_body, "Key");
1395 let value = extract_xml_value(tag_body, "Value");
1396 if let (Some(k), Some(v)) = (key, value) {
1397 tags.push((k, v));
1398 }
1399 remaining = &after[tag_end + 6..];
1400 } else {
1401 break;
1402 }
1403 }
1404 tags
1405}
1406
1407pub(crate) fn validate_tags(tags: &[(String, String)]) -> Result<(), AwsServiceError> {
1408 let mut seen = std::collections::HashSet::new();
1410 for (k, _) in tags {
1411 if !seen.insert(k.as_str()) {
1412 return Err(AwsServiceError::aws_error(
1413 StatusCode::BAD_REQUEST,
1414 "InvalidTag",
1415 "Cannot provide multiple Tags with the same key",
1416 ));
1417 }
1418 if k.starts_with("aws:") {
1420 return Err(AwsServiceError::aws_error(
1421 StatusCode::BAD_REQUEST,
1422 "InvalidTag",
1423 "System tags cannot be added/updated by requester",
1424 ));
1425 }
1426 }
1427 Ok(())
1428}
1429
1430pub(crate) fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
1431 let self_closing1 = format!("<{tag} />");
1433 let self_closing2 = format!("<{tag}/>");
1434 if xml.contains(&self_closing1) || xml.contains(&self_closing2) {
1435 let self_pos = xml
1437 .find(&self_closing1)
1438 .or_else(|| xml.find(&self_closing2));
1439 let open = format!("<{tag}>");
1440 let open_pos = xml.find(&open);
1441 match (self_pos, open_pos) {
1442 (Some(sp), Some(op)) if sp < op => return Some(String::new()),
1443 (Some(_), None) => return Some(String::new()),
1444 _ => {}
1445 }
1446 }
1447
1448 let open = format!("<{tag}>");
1449 let close = format!("</{tag}>");
1450 let start = xml.find(&open)? + open.len();
1451 let end = xml.find(&close)?;
1452 Some(xml[start..end].to_string())
1453}
1454
1455pub(crate) fn parse_complete_multipart_xml(xml: &str) -> Vec<(u32, String)> {
1457 let mut parts = Vec::new();
1458 let mut remaining = xml;
1459 while let Some(part_start) = remaining.find("<Part>") {
1460 let after = &remaining[part_start + 6..];
1461 if let Some(part_end) = after.find("</Part>") {
1462 let part_body = &after[..part_end];
1463 let part_num =
1464 extract_xml_value(part_body, "PartNumber").and_then(|s| s.parse::<u32>().ok());
1465 let etag = extract_xml_value(part_body, "ETag")
1466 .map(|s| s.replace(""", "").replace('"', ""));
1467 if let (Some(num), Some(e)) = (part_num, etag) {
1468 parts.push((num, e));
1469 }
1470 remaining = &after[part_end + 7..];
1471 } else {
1472 break;
1473 }
1474 }
1475 parts
1476}
1477
1478pub(crate) fn parse_url_encoded_tags(s: &str) -> Vec<(String, String)> {
1479 let mut tags = Vec::new();
1480 for pair in s.split('&') {
1481 if pair.is_empty() {
1482 continue;
1483 }
1484 let (key, value) = match pair.find('=') {
1485 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
1486 None => (pair, ""),
1487 };
1488 tags.push((
1489 percent_encoding::percent_decode_str(key)
1490 .decode_utf8_lossy()
1491 .to_string(),
1492 percent_encoding::percent_decode_str(value)
1493 .decode_utf8_lossy()
1494 .to_string(),
1495 ));
1496 }
1497 tags
1498}
1499
1500pub(crate) fn validate_lifecycle_xml(xml: &str) -> Result<(), AwsServiceError> {
1502 let malformed = || {
1503 AwsServiceError::aws_error(
1504 StatusCode::BAD_REQUEST,
1505 "MalformedXML",
1506 "The XML you provided was not well-formed or did not validate against our published schema",
1507 )
1508 };
1509
1510 let mut remaining = xml;
1511 while let Some(rule_start) = remaining.find("<Rule>") {
1512 let after = &remaining[rule_start + 6..];
1513 if let Some(rule_end) = after.find("</Rule>") {
1514 let rule_body = &after[..rule_end];
1515
1516 let has_filter = rule_body.contains("<Filter>")
1518 || rule_body.contains("<Filter/>")
1519 || rule_body.contains("<Filter />");
1520
1521 let has_prefix_outside_filter = {
1523 if !rule_body.contains("<Prefix") {
1524 false
1525 } else if !has_filter {
1526 true } else {
1528 let mut stripped = rule_body.to_string();
1530 if let Some(fs) = stripped.find("<Filter") {
1532 if let Some(fe) = stripped.find("</Filter>") {
1533 stripped = format!("{}{}", &stripped[..fs], &stripped[fe + 9..]);
1534 }
1535 }
1536 stripped.contains("<Prefix")
1537 }
1538 };
1539
1540 if !has_filter && !has_prefix_outside_filter {
1541 return Err(malformed());
1542 }
1543 if has_filter && has_prefix_outside_filter {
1545 return Err(malformed());
1546 }
1547
1548 if let Some(exp_start) = rule_body.find("<Expiration>") {
1551 if let Some(exp_end) = rule_body[exp_start..].find("</Expiration>") {
1552 let exp_body = &rule_body[exp_start..exp_start + exp_end];
1553 if exp_body.contains("<ExpiredObjectDeleteMarker>")
1554 && (exp_body.contains("<Days>") || exp_body.contains("<Date>"))
1555 {
1556 return Err(malformed());
1557 }
1558 }
1559 }
1560
1561 if has_filter {
1563 if let Some(fs) = rule_body.find("<Filter>") {
1564 if let Some(fe) = rule_body.find("</Filter>") {
1565 let filter_body = &rule_body[fs + 8..fe];
1566 let has_prefix_in_filter = filter_body.contains("<Prefix");
1567 let has_tag_in_filter = filter_body.contains("<Tag>");
1568 let has_and_in_filter = filter_body.contains("<And>");
1569 if has_prefix_in_filter && has_tag_in_filter && !has_and_in_filter {
1571 return Err(malformed());
1572 }
1573 if has_tag_in_filter && has_and_in_filter {
1575 let and_start = filter_body.find("<And>").unwrap_or(0);
1577 let tag_pos = filter_body.find("<Tag>").unwrap_or(0);
1578 if tag_pos < and_start {
1579 return Err(malformed());
1580 }
1581 }
1582 }
1583 }
1584 }
1585
1586 if rule_body.contains("<NoncurrentVersionTransition>") {
1588 let mut nvt_remaining = rule_body;
1589 while let Some(nvt_start) = nvt_remaining.find("<NoncurrentVersionTransition>") {
1590 let nvt_after = &nvt_remaining[nvt_start + 29..];
1591 if let Some(nvt_end) = nvt_after.find("</NoncurrentVersionTransition>") {
1592 let nvt_body = &nvt_after[..nvt_end];
1593 if !nvt_body.contains("<NoncurrentDays>") {
1594 return Err(malformed());
1595 }
1596 if !nvt_body.contains("<StorageClass>") {
1597 return Err(malformed());
1598 }
1599 nvt_remaining = &nvt_after[nvt_end + 30..];
1600 } else {
1601 break;
1602 }
1603 }
1604 }
1605
1606 remaining = &after[rule_end + 7..];
1607 } else {
1608 break;
1609 }
1610 }
1611
1612 Ok(())
1613}
1614
1615pub(crate) struct CorsRule {
1617 allowed_origins: Vec<String>,
1618 allowed_methods: Vec<String>,
1619 allowed_headers: Vec<String>,
1620 expose_headers: Vec<String>,
1621 max_age_seconds: Option<u32>,
1622}
1623
1624pub(crate) fn parse_cors_config(xml: &str) -> Vec<CorsRule> {
1626 let mut rules = Vec::new();
1627 let mut remaining = xml;
1628 while let Some(start) = remaining.find("<CORSRule>") {
1629 let after = &remaining[start + 10..];
1630 if let Some(end) = after.find("</CORSRule>") {
1631 let block = &after[..end];
1632 let allowed_origins = extract_all_xml_values(block, "AllowedOrigin");
1633 let allowed_methods = extract_all_xml_values(block, "AllowedMethod");
1634 let allowed_headers = extract_all_xml_values(block, "AllowedHeader");
1635 let expose_headers = extract_all_xml_values(block, "ExposeHeader");
1636 let max_age_seconds =
1637 extract_xml_value(block, "MaxAgeSeconds").and_then(|s| s.parse().ok());
1638 rules.push(CorsRule {
1639 allowed_origins,
1640 allowed_methods,
1641 allowed_headers,
1642 expose_headers,
1643 max_age_seconds,
1644 });
1645 remaining = &after[end + 11..];
1646 } else {
1647 break;
1648 }
1649 }
1650 rules
1651}
1652
1653pub(crate) fn origin_matches(origin: &str, pattern: &str) -> bool {
1655 if pattern == "*" {
1656 return true;
1657 }
1658 if let Some(suffix) = pattern.strip_prefix('*') {
1660 return origin.ends_with(suffix);
1661 }
1662 origin == pattern
1663}
1664
1665pub(crate) fn find_cors_rule<'a>(
1667 rules: &'a [CorsRule],
1668 origin: &str,
1669 method: Option<&str>,
1670) -> Option<&'a CorsRule> {
1671 rules.iter().find(|rule| {
1672 let origin_ok = rule
1673 .allowed_origins
1674 .iter()
1675 .any(|o| origin_matches(origin, o));
1676 let method_ok = match method {
1677 Some(m) => rule.allowed_methods.iter().any(|am| am == m),
1678 None => true,
1679 };
1680 origin_ok && method_ok
1681 })
1682}
1683
1684pub(crate) fn check_object_lock_for_overwrite(
1687 obj: &S3Object,
1688 req: &AwsRequest,
1689) -> Option<&'static str> {
1690 if obj.lock_legal_hold.as_deref() == Some("ON") {
1692 return Some("AccessDenied");
1693 }
1694 if let (Some(mode), Some(until)) = (&obj.lock_mode, &obj.lock_retain_until) {
1696 if *until > Utc::now() {
1697 if mode == "COMPLIANCE" {
1698 return Some("AccessDenied");
1699 }
1700 if mode == "GOVERNANCE" {
1701 let bypass = req
1702 .headers
1703 .get("x-amz-bypass-governance-retention")
1704 .and_then(|v| v.to_str().ok())
1705 .map(|s| s.eq_ignore_ascii_case("true"))
1706 .unwrap_or(false);
1707 if !bypass {
1708 return Some("AccessDenied");
1709 }
1710 }
1711 }
1712 }
1713 None
1714}
1715
1716#[cfg(test)]
1717mod tests {
1718 use super::*;
1719
1720 #[test]
1721 fn valid_bucket_names() {
1722 assert!(is_valid_bucket_name("my-bucket"));
1723 assert!(is_valid_bucket_name("my.bucket.name"));
1724 assert!(is_valid_bucket_name("abc"));
1725 assert!(!is_valid_bucket_name("ab"));
1726 assert!(!is_valid_bucket_name("-bucket"));
1727 assert!(!is_valid_bucket_name("Bucket"));
1728 assert!(!is_valid_bucket_name("bucket-"));
1729 }
1730
1731 #[test]
1732 fn parse_delete_xml() {
1733 let xml = r#"<Delete><Object><Key>a.txt</Key></Object><Object><Key>b/c.txt</Key></Object></Delete>"#;
1734 let entries = parse_delete_objects_xml(xml);
1735 assert_eq!(entries.len(), 2);
1736 assert_eq!(entries[0].key, "a.txt");
1737 assert!(entries[0].version_id.is_none());
1738 assert_eq!(entries[1].key, "b/c.txt");
1739 }
1740
1741 #[test]
1742 fn parse_delete_xml_with_version() {
1743 let xml = r#"<Delete><Object><Key>a.txt</Key><VersionId>v1</VersionId></Object></Delete>"#;
1744 let entries = parse_delete_objects_xml(xml);
1745 assert_eq!(entries.len(), 1);
1746 assert_eq!(entries[0].key, "a.txt");
1747 assert_eq!(entries[0].version_id.as_deref(), Some("v1"));
1748 }
1749
1750 #[test]
1751 fn parse_tags_xml() {
1752 let xml =
1753 r#"<Tagging><TagSet><Tag><Key>env</Key><Value>prod</Value></Tag></TagSet></Tagging>"#;
1754 let tags = parse_tagging_xml(xml);
1755 assert_eq!(tags, vec![("env".to_string(), "prod".to_string())]);
1756 }
1757
1758 #[test]
1759 fn md5_hash() {
1760 let hash = compute_md5(b"hello");
1761 assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
1762 }
1763
1764 #[test]
1765 fn test_etag_matches() {
1766 assert!(etag_matches("\"abc\"", "\"abc\""));
1767 assert!(etag_matches("abc", "\"abc\""));
1768 assert!(etag_matches("*", "\"abc\""));
1769 assert!(!etag_matches("\"xyz\"", "\"abc\""));
1770 }
1771
1772 #[test]
1773 fn test_event_matches() {
1774 assert!(event_matches("s3:ObjectCreated:Put", "s3:ObjectCreated:*"));
1775 assert!(event_matches("s3:ObjectCreated:Copy", "s3:ObjectCreated:*"));
1776 assert!(event_matches(
1777 "s3:ObjectRemoved:Delete",
1778 "s3:ObjectRemoved:*"
1779 ));
1780 assert!(!event_matches(
1781 "s3:ObjectRemoved:Delete",
1782 "s3:ObjectCreated:*"
1783 ));
1784 assert!(event_matches(
1785 "s3:ObjectCreated:Put",
1786 "s3:ObjectCreated:Put"
1787 ));
1788 assert!(event_matches("s3:ObjectCreated:Put", "s3:*"));
1789 }
1790
1791 #[test]
1792 fn test_parse_notification_config() {
1793 let xml = r#"<NotificationConfiguration>
1794 <QueueConfiguration>
1795 <Queue>arn:aws:sqs:us-east-1:123456789012:my-queue</Queue>
1796 <Event>s3:ObjectCreated:*</Event>
1797 </QueueConfiguration>
1798 <TopicConfiguration>
1799 <Topic>arn:aws:sns:us-east-1:123456789012:my-topic</Topic>
1800 <Event>s3:ObjectRemoved:*</Event>
1801 </TopicConfiguration>
1802 </NotificationConfiguration>"#;
1803 let targets = parse_notification_config(xml);
1804 assert_eq!(targets.len(), 2);
1805 assert_eq!(
1806 targets[0].arn,
1807 "arn:aws:sqs:us-east-1:123456789012:my-queue"
1808 );
1809 assert_eq!(targets[0].events, vec!["s3:ObjectCreated:*"]);
1810 assert_eq!(
1811 targets[1].arn,
1812 "arn:aws:sns:us-east-1:123456789012:my-topic"
1813 );
1814 assert_eq!(targets[1].events, vec!["s3:ObjectRemoved:*"]);
1815 }
1816
1817 #[test]
1818 fn test_parse_notification_config_lambda() {
1819 let xml = r#"<NotificationConfiguration>
1821 <CloudFunctionConfiguration>
1822 <CloudFunction>arn:aws:lambda:us-east-1:123456789012:function:my-func</CloudFunction>
1823 <Event>s3:ObjectCreated:*</Event>
1824 </CloudFunctionConfiguration>
1825 </NotificationConfiguration>"#;
1826 let targets = parse_notification_config(xml);
1827 assert_eq!(targets.len(), 1);
1828 assert!(matches!(
1829 targets[0].target_type,
1830 NotificationTargetType::Lambda
1831 ));
1832 assert_eq!(
1833 targets[0].arn,
1834 "arn:aws:lambda:us-east-1:123456789012:function:my-func"
1835 );
1836 assert_eq!(targets[0].events, vec!["s3:ObjectCreated:*"]);
1837 }
1838
1839 #[test]
1840 fn test_parse_notification_config_lambda_new_format() {
1841 let xml = r#"<NotificationConfiguration>
1843 <LambdaFunctionConfiguration>
1844 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
1845 <Event>s3:ObjectCreated:Put</Event>
1846 <Event>s3:ObjectRemoved:*</Event>
1847 </LambdaFunctionConfiguration>
1848 </NotificationConfiguration>"#;
1849 let targets = parse_notification_config(xml);
1850 assert_eq!(targets.len(), 1);
1851 assert!(matches!(
1852 targets[0].target_type,
1853 NotificationTargetType::Lambda
1854 ));
1855 assert_eq!(
1856 targets[0].arn,
1857 "arn:aws:lambda:us-east-1:123456789012:function:my-func"
1858 );
1859 assert_eq!(
1860 targets[0].events,
1861 vec!["s3:ObjectCreated:Put", "s3:ObjectRemoved:*"]
1862 );
1863 }
1864
1865 #[test]
1866 fn test_parse_notification_config_all_types() {
1867 let xml = r#"<NotificationConfiguration>
1868 <QueueConfiguration>
1869 <Queue>arn:aws:sqs:us-east-1:123456789012:q</Queue>
1870 <Event>s3:ObjectCreated:*</Event>
1871 </QueueConfiguration>
1872 <TopicConfiguration>
1873 <Topic>arn:aws:sns:us-east-1:123456789012:t</Topic>
1874 <Event>s3:ObjectRemoved:*</Event>
1875 </TopicConfiguration>
1876 <LambdaFunctionConfiguration>
1877 <Function>arn:aws:lambda:us-east-1:123456789012:function:f</Function>
1878 <Event>s3:ObjectCreated:Put</Event>
1879 </LambdaFunctionConfiguration>
1880 </NotificationConfiguration>"#;
1881 let targets = parse_notification_config(xml);
1882 assert_eq!(targets.len(), 3);
1883 assert!(matches!(
1884 targets[0].target_type,
1885 NotificationTargetType::Sqs
1886 ));
1887 assert!(matches!(
1888 targets[1].target_type,
1889 NotificationTargetType::Sns
1890 ));
1891 assert!(matches!(
1892 targets[2].target_type,
1893 NotificationTargetType::Lambda
1894 ));
1895 }
1896
1897 #[test]
1898 fn test_parse_notification_config_with_filters() {
1899 let xml = r#"<NotificationConfiguration>
1900 <LambdaFunctionConfiguration>
1901 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
1902 <Event>s3:ObjectCreated:*</Event>
1903 <Filter>
1904 <S3Key>
1905 <FilterRule>
1906 <Name>prefix</Name>
1907 <Value>images/</Value>
1908 </FilterRule>
1909 <FilterRule>
1910 <Name>suffix</Name>
1911 <Value>.jpg</Value>
1912 </FilterRule>
1913 </S3Key>
1914 </Filter>
1915 </LambdaFunctionConfiguration>
1916 </NotificationConfiguration>"#;
1917 let targets = parse_notification_config(xml);
1918 assert_eq!(targets.len(), 1);
1919 assert_eq!(targets[0].prefix_filter, Some("images/".to_string()));
1920 assert_eq!(targets[0].suffix_filter, Some(".jpg".to_string()));
1921 }
1922
1923 #[test]
1924 fn test_parse_notification_config_no_filters() {
1925 let xml = r#"<NotificationConfiguration>
1926 <LambdaFunctionConfiguration>
1927 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
1928 <Event>s3:ObjectCreated:*</Event>
1929 </LambdaFunctionConfiguration>
1930 </NotificationConfiguration>"#;
1931 let targets = parse_notification_config(xml);
1932 assert_eq!(targets.len(), 1);
1933 assert_eq!(targets[0].prefix_filter, None);
1934 assert_eq!(targets[0].suffix_filter, None);
1935 }
1936
1937 #[test]
1938 fn test_key_matches_filters() {
1939 assert!(key_matches_filters("anything", &None, &None));
1941
1942 assert!(key_matches_filters(
1944 "images/photo.jpg",
1945 &Some("images/".to_string()),
1946 &None
1947 ));
1948 assert!(!key_matches_filters(
1949 "docs/file.txt",
1950 &Some("images/".to_string()),
1951 &None
1952 ));
1953
1954 assert!(key_matches_filters(
1956 "images/photo.jpg",
1957 &None,
1958 &Some(".jpg".to_string())
1959 ));
1960 assert!(!key_matches_filters(
1961 "images/photo.png",
1962 &None,
1963 &Some(".jpg".to_string())
1964 ));
1965
1966 assert!(key_matches_filters(
1968 "images/photo.jpg",
1969 &Some("images/".to_string()),
1970 &Some(".jpg".to_string())
1971 ));
1972 assert!(!key_matches_filters(
1973 "images/photo.png",
1974 &Some("images/".to_string()),
1975 &Some(".jpg".to_string())
1976 ));
1977 assert!(!key_matches_filters(
1978 "docs/photo.jpg",
1979 &Some("images/".to_string()),
1980 &Some(".jpg".to_string())
1981 ));
1982 }
1983
1984 #[test]
1985 fn test_parse_cors_config() {
1986 let xml = r#"<CORSConfiguration>
1987 <CORSRule>
1988 <AllowedOrigin>https://example.com</AllowedOrigin>
1989 <AllowedMethod>GET</AllowedMethod>
1990 <AllowedMethod>PUT</AllowedMethod>
1991 <AllowedHeader>*</AllowedHeader>
1992 <ExposeHeader>x-amz-request-id</ExposeHeader>
1993 <MaxAgeSeconds>3600</MaxAgeSeconds>
1994 </CORSRule>
1995 </CORSConfiguration>"#;
1996 let rules = parse_cors_config(xml);
1997 assert_eq!(rules.len(), 1);
1998 assert_eq!(rules[0].allowed_origins, vec!["https://example.com"]);
1999 assert_eq!(rules[0].allowed_methods, vec!["GET", "PUT"]);
2000 assert_eq!(rules[0].allowed_headers, vec!["*"]);
2001 assert_eq!(rules[0].expose_headers, vec!["x-amz-request-id"]);
2002 assert_eq!(rules[0].max_age_seconds, Some(3600));
2003 }
2004
2005 #[test]
2006 fn test_origin_matches() {
2007 assert!(origin_matches("https://example.com", "https://example.com"));
2008 assert!(origin_matches("https://example.com", "*"));
2009 assert!(origin_matches("https://foo.example.com", "*.example.com"));
2010 assert!(!origin_matches("https://evil.com", "https://example.com"));
2011 }
2012
2013 #[test]
2016 fn resolve_null_version_matches_both_none_and_null_string() {
2017 use crate::state::S3Bucket;
2018 use bytes::Bytes;
2019 use chrono::Utc;
2020
2021 let mut b = S3Bucket::new("test", "us-east-1", "owner");
2022
2023 let make_obj = |key: &str, vid: Option<&str>| crate::state::S3Object {
2025 key: key.to_string(),
2026 data: Bytes::from_static(b"x"),
2027 content_type: "text/plain".to_string(),
2028 etag: "\"abc\"".to_string(),
2029 size: 1,
2030 last_modified: Utc::now(),
2031 metadata: Default::default(),
2032 storage_class: "STANDARD".to_string(),
2033 tags: Default::default(),
2034 acl_grants: vec![],
2035 acl_owner_id: None,
2036 parts_count: None,
2037 part_sizes: None,
2038 sse_algorithm: None,
2039 sse_kms_key_id: None,
2040 bucket_key_enabled: None,
2041 version_id: vid.map(|s| s.to_string()),
2042 is_delete_marker: false,
2043 content_encoding: None,
2044 website_redirect_location: None,
2045 restore_ongoing: None,
2046 restore_expiry: None,
2047 checksum_algorithm: None,
2048 checksum_value: None,
2049 lock_mode: None,
2050 lock_retain_until: None,
2051 lock_legal_hold: None,
2052 };
2053
2054 let obj = make_obj("file.txt", Some("null"));
2056 b.objects.insert("file.txt".to_string(), obj.clone());
2057 b.object_versions.insert("file.txt".to_string(), vec![obj]);
2058
2059 let null_str = "null".to_string();
2060 let result = resolve_object(&b, "file.txt", Some(&null_str));
2061 assert!(
2062 result.is_ok(),
2063 "versionId=null should match version_id=Some(\"null\")"
2064 );
2065
2066 let obj2 = make_obj("file2.txt", None);
2068 b.objects.insert("file2.txt".to_string(), obj2.clone());
2069 b.object_versions
2070 .insert("file2.txt".to_string(), vec![obj2]);
2071
2072 let result2 = resolve_object(&b, "file2.txt", Some(&null_str));
2073 assert!(
2074 result2.is_ok(),
2075 "versionId=null should match version_id=None"
2076 );
2077 }
2078
2079 #[test]
2080 fn test_parse_replication_rules() {
2081 let xml = r#"<ReplicationConfiguration>
2082 <Role>arn:aws:iam::role/replication</Role>
2083 <Rule>
2084 <Status>Enabled</Status>
2085 <Filter><Prefix>logs/</Prefix></Filter>
2086 <Destination><Bucket>arn:aws:s3:::dest-bucket</Bucket></Destination>
2087 </Rule>
2088 <Rule>
2089 <Status>Disabled</Status>
2090 <Filter><Prefix></Prefix></Filter>
2091 <Destination><Bucket>arn:aws:s3:::other-bucket</Bucket></Destination>
2092 </Rule>
2093 </ReplicationConfiguration>"#;
2094
2095 let rules = parse_replication_rules(xml);
2096 assert_eq!(rules.len(), 2);
2097 assert_eq!(rules[0].status, "Enabled");
2098 assert_eq!(rules[0].prefix, "logs/");
2099 assert_eq!(rules[0].dest_bucket, "dest-bucket");
2100 assert_eq!(rules[1].status, "Disabled");
2101 assert_eq!(rules[1].prefix, "");
2102 assert_eq!(rules[1].dest_bucket, "other-bucket");
2103 }
2104
2105 #[test]
2106 fn test_parse_normalized_replication_rules() {
2107 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>"#;
2109 let normalized = normalize_replication_xml(input_xml);
2110 eprintln!("Normalized XML: {normalized}");
2111 let rules = parse_replication_rules(&normalized);
2112 assert_eq!(rules.len(), 1, "Expected 1 rule, got {}", rules.len());
2113 assert_eq!(rules[0].status, "Enabled");
2114 assert_eq!(rules[0].dest_bucket, "repl-dest");
2115 }
2116
2117 #[test]
2118 fn test_replicate_object() {
2119 use crate::state::{S3Bucket, S3State};
2120
2121 let mut state = S3State::new("123456789012", "us-east-1");
2122
2123 let mut src = S3Bucket::new("source", "us-east-1", "owner");
2125 src.versioning = Some("Enabled".to_string());
2126 src.replication_config = Some(
2127 "<ReplicationConfiguration>\
2128 <Rule><Status>Enabled</Status>\
2129 <Filter><Prefix></Prefix></Filter>\
2130 <Destination><Bucket>arn:aws:s3:::destination</Bucket></Destination>\
2131 </Rule></ReplicationConfiguration>"
2132 .to_string(),
2133 );
2134 let obj = S3Object {
2135 key: "test-key".to_string(),
2136 data: Bytes::from_static(b"hello"),
2137 content_type: "text/plain".to_string(),
2138 etag: "abc".to_string(),
2139 size: 5,
2140 last_modified: Utc::now(),
2141 metadata: Default::default(),
2142 storage_class: "STANDARD".to_string(),
2143 tags: Default::default(),
2144 acl_grants: Vec::new(),
2145 acl_owner_id: None,
2146 parts_count: None,
2147 part_sizes: None,
2148 sse_algorithm: None,
2149 sse_kms_key_id: None,
2150 bucket_key_enabled: None,
2151 version_id: Some("v1".to_string()),
2152 is_delete_marker: false,
2153 content_encoding: None,
2154 website_redirect_location: None,
2155 restore_ongoing: None,
2156 restore_expiry: None,
2157 checksum_algorithm: None,
2158 checksum_value: None,
2159 lock_mode: None,
2160 lock_retain_until: None,
2161 lock_legal_hold: None,
2162 };
2163 src.objects.insert("test-key".to_string(), obj);
2164 state.buckets.insert("source".to_string(), src);
2165
2166 let dest = S3Bucket::new("destination", "us-east-1", "owner");
2167 state.buckets.insert("destination".to_string(), dest);
2168
2169 replicate_object(&mut state, "source", "test-key");
2170
2171 let dest_obj = state
2173 .buckets
2174 .get("destination")
2175 .unwrap()
2176 .objects
2177 .get("test-key");
2178 assert!(dest_obj.is_some());
2179 assert_eq!(dest_obj.unwrap().data, Bytes::from_static(b"hello"));
2180 }
2181
2182 #[test]
2183 fn cors_header_value_does_not_panic_on_unusual_input() {
2184 let valid_origin = "https://example.com";
2188 let result: Result<http::HeaderValue, _> = valid_origin.parse();
2189 assert!(result.is_ok());
2190
2191 let bad_origin = "https://ex\x01ample.com";
2193 let result: Result<http::HeaderValue, _> = bad_origin.parse();
2194 assert!(result.is_err());
2195 let fallback = bad_origin
2197 .parse()
2198 .unwrap_or_else(|_| http::HeaderValue::from_static(""));
2199 assert_eq!(fallback, "");
2200 }
2201}