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
676pub(crate) fn truncate_to_seconds(dt: DateTime<Utc>) -> DateTime<Utc> {
680 dt.with_nanosecond(0).unwrap_or(dt)
681}
682
683pub(crate) fn check_get_conditionals(
684 req: &AwsRequest,
685 obj: &S3Object,
686) -> Result<(), AwsServiceError> {
687 let obj_etag = format!("\"{}\"", obj.etag);
688 let obj_time = truncate_to_seconds(obj.last_modified);
689
690 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
692 if !etag_matches(if_match, &obj_etag) {
693 return Err(precondition_failed("If-Match"));
694 }
695 }
696
697 if let Some(if_none_match) = req
699 .headers
700 .get("if-none-match")
701 .and_then(|v| v.to_str().ok())
702 {
703 if etag_matches(if_none_match, &obj_etag) {
704 return Err(not_modified_with_etag(&obj_etag));
705 }
706 }
707
708 if let Some(since) = req
710 .headers
711 .get("if-unmodified-since")
712 .and_then(|v| v.to_str().ok())
713 {
714 if let Some(dt) = parse_http_date(since) {
715 if obj_time > dt {
716 return Err(precondition_failed("If-Unmodified-Since"));
717 }
718 }
719 }
720
721 if let Some(since) = req
723 .headers
724 .get("if-modified-since")
725 .and_then(|v| v.to_str().ok())
726 {
727 if let Some(dt) = parse_http_date(since) {
728 if obj_time <= dt {
729 return Err(not_modified());
730 }
731 }
732 }
733
734 Ok(())
735}
736
737pub(crate) fn check_head_conditionals(
738 req: &AwsRequest,
739 obj: &S3Object,
740) -> Result<(), AwsServiceError> {
741 let obj_etag = format!("\"{}\"", obj.etag);
742 let obj_time = truncate_to_seconds(obj.last_modified);
743
744 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
746 if !etag_matches(if_match, &obj_etag) {
747 return Err(AwsServiceError::aws_error(
748 StatusCode::PRECONDITION_FAILED,
749 "412",
750 "Precondition Failed",
751 ));
752 }
753 }
754
755 if let Some(if_none_match) = req
757 .headers
758 .get("if-none-match")
759 .and_then(|v| v.to_str().ok())
760 {
761 if etag_matches(if_none_match, &obj_etag) {
762 return Err(not_modified_with_etag(&obj_etag));
763 }
764 }
765
766 if let Some(since) = req
768 .headers
769 .get("if-unmodified-since")
770 .and_then(|v| v.to_str().ok())
771 {
772 if let Some(dt) = parse_http_date(since) {
773 if obj_time > dt {
774 return Err(AwsServiceError::aws_error(
775 StatusCode::PRECONDITION_FAILED,
776 "412",
777 "Precondition Failed",
778 ));
779 }
780 }
781 }
782
783 if let Some(since) = req
785 .headers
786 .get("if-modified-since")
787 .and_then(|v| v.to_str().ok())
788 {
789 if let Some(dt) = parse_http_date(since) {
790 if obj_time <= dt {
791 return Err(not_modified());
792 }
793 }
794 }
795
796 Ok(())
797}
798
799pub(crate) fn etag_matches(condition: &str, obj_etag: &str) -> bool {
800 let condition = condition.trim();
801 if condition == "*" {
802 return true;
803 }
804 let clean_etag = obj_etag.replace('"', "");
805 for part in condition.split(',') {
807 let part = part.trim().replace('"', "");
808 if part == clean_etag {
809 return true;
810 }
811 }
812 false
813}
814
815pub(crate) fn parse_http_date(s: &str) -> Option<DateTime<Utc>> {
816 if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
818 return Some(dt.with_timezone(&Utc));
819 }
820 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
822 return Some(dt.with_timezone(&Utc));
823 }
824 if let Ok(dt) =
826 chrono::NaiveDateTime::parse_from_str(s.trim_end_matches(" GMT"), "%a, %d %b %Y %H:%M:%S")
827 {
828 return Some(dt.and_utc());
829 }
830 if let Ok(dt) = s.parse::<DateTime<Utc>>() {
832 return Some(dt);
833 }
834 None
835}
836
837pub(crate) fn not_modified() -> AwsServiceError {
838 AwsServiceError::aws_error(StatusCode::NOT_MODIFIED, "304", "Not Modified")
839}
840
841pub(crate) fn not_modified_with_etag(etag: &str) -> AwsServiceError {
842 AwsServiceError::aws_error_with_headers(
843 StatusCode::NOT_MODIFIED,
844 "304",
845 "Not Modified",
846 vec![("etag".to_string(), etag.to_string())],
847 )
848}
849
850pub(crate) fn precondition_failed(condition: &str) -> AwsServiceError {
851 AwsServiceError::aws_error_with_fields(
852 StatusCode::PRECONDITION_FAILED,
853 "PreconditionFailed",
854 "At least one of the pre-conditions you specified did not hold",
855 vec![("Condition".to_string(), condition.to_string())],
856 )
857}
858
859pub(crate) fn build_acl_xml(owner_id: &str, grants: &[AclGrant], _account_id: &str) -> String {
862 let mut grants_xml = String::new();
863 for g in grants {
864 let grantee_xml = if g.grantee_type == "Group" {
865 let uri = g.grantee_uri.as_deref().unwrap_or("");
866 format!(
867 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\">\
868 <URI>{}</URI></Grantee>",
869 xml_escape(uri),
870 )
871 } else {
872 let id = g.grantee_id.as_deref().unwrap_or("");
873 format!(
874 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">\
875 <ID>{}</ID></Grantee>",
876 xml_escape(id),
877 )
878 };
879 grants_xml.push_str(&format!(
880 "<Grant>{grantee_xml}<Permission>{}</Permission></Grant>",
881 xml_escape(&g.permission),
882 ));
883 }
884
885 format!(
886 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
887 <AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
888 <Owner><ID>{owner_id}</ID><DisplayName>{owner_id}</DisplayName></Owner>\
889 <AccessControlList>{grants_xml}</AccessControlList>\
890 </AccessControlPolicy>",
891 owner_id = xml_escape(owner_id),
892 )
893}
894
895pub(crate) fn canned_acl_grants(acl: &str, owner_id: &str) -> Vec<AclGrant> {
896 let owner_grant = AclGrant {
897 grantee_type: "CanonicalUser".to_string(),
898 grantee_id: Some(owner_id.to_string()),
899 grantee_display_name: Some(owner_id.to_string()),
900 grantee_uri: None,
901 permission: "FULL_CONTROL".to_string(),
902 };
903 match acl {
904 "private" => vec![owner_grant],
905 "public-read" => vec![
906 owner_grant,
907 AclGrant {
908 grantee_type: "Group".to_string(),
909 grantee_id: None,
910 grantee_display_name: None,
911 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
912 permission: "READ".to_string(),
913 },
914 ],
915 "public-read-write" => vec![
916 owner_grant,
917 AclGrant {
918 grantee_type: "Group".to_string(),
919 grantee_id: None,
920 grantee_display_name: None,
921 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
922 permission: "READ".to_string(),
923 },
924 AclGrant {
925 grantee_type: "Group".to_string(),
926 grantee_id: None,
927 grantee_display_name: None,
928 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
929 permission: "WRITE".to_string(),
930 },
931 ],
932 "authenticated-read" => vec![
933 owner_grant,
934 AclGrant {
935 grantee_type: "Group".to_string(),
936 grantee_id: None,
937 grantee_display_name: None,
938 grantee_uri: Some(
939 "http://acs.amazonaws.com/groups/global/AuthenticatedUsers".to_string(),
940 ),
941 permission: "READ".to_string(),
942 },
943 ],
944 "bucket-owner-full-control" => vec![owner_grant],
945 _ => vec![owner_grant],
946 }
947}
948
949pub(crate) fn canned_acl_grants_for_object(acl: &str, owner_id: &str) -> Vec<AclGrant> {
950 canned_acl_grants(acl, owner_id)
952}
953
954pub(crate) fn parse_grant_headers(headers: &HeaderMap) -> Vec<AclGrant> {
955 let mut grants = Vec::new();
956 let header_permission_map = [
957 ("x-amz-grant-read", "READ"),
958 ("x-amz-grant-write", "WRITE"),
959 ("x-amz-grant-read-acp", "READ_ACP"),
960 ("x-amz-grant-write-acp", "WRITE_ACP"),
961 ("x-amz-grant-full-control", "FULL_CONTROL"),
962 ];
963
964 for (header, permission) in &header_permission_map {
965 if let Some(value) = headers.get(*header).and_then(|v| v.to_str().ok()) {
966 for part in value.split(',') {
968 let part = part.trim();
969 if let Some((key, val)) = part.split_once('=') {
970 let val = val.trim().trim_matches('"');
971 let key = key.trim().to_lowercase();
972 match key.as_str() {
973 "id" => {
974 grants.push(AclGrant {
975 grantee_type: "CanonicalUser".to_string(),
976 grantee_id: Some(val.to_string()),
977 grantee_display_name: Some(val.to_string()),
978 grantee_uri: None,
979 permission: permission.to_string(),
980 });
981 }
982 "uri" | "url" => {
983 grants.push(AclGrant {
984 grantee_type: "Group".to_string(),
985 grantee_id: None,
986 grantee_display_name: None,
987 grantee_uri: Some(val.to_string()),
988 permission: permission.to_string(),
989 });
990 }
991 _ => {}
992 }
993 }
994 }
995 }
996 }
997 grants
998}
999
1000pub(crate) fn parse_acl_xml(xml: &str) -> Result<Vec<AclGrant>, AwsServiceError> {
1001 if xml.contains("<AccessControlPolicy") && !xml.contains("<Owner>") {
1003 return Err(AwsServiceError::aws_error(
1004 StatusCode::BAD_REQUEST,
1005 "MalformedACLError",
1006 "The XML you provided was not well-formed or did not validate against our published schema",
1007 ));
1008 }
1009
1010 let valid_permissions = ["READ", "WRITE", "READ_ACP", "WRITE_ACP", "FULL_CONTROL"];
1011
1012 let mut grants = Vec::new();
1013 let mut remaining = xml;
1014 while let Some(start) = remaining.find("<Grant>") {
1015 let after = &remaining[start + 7..];
1016 if let Some(end) = after.find("</Grant>") {
1017 let grant_body = &after[..end];
1018
1019 let permission = extract_xml_value(grant_body, "Permission").unwrap_or_default();
1021 if !valid_permissions.contains(&permission.as_str()) {
1022 return Err(AwsServiceError::aws_error(
1023 StatusCode::BAD_REQUEST,
1024 "MalformedACLError",
1025 "The XML you provided was not well-formed or did not validate against our published schema",
1026 ));
1027 }
1028
1029 if grant_body.contains("xsi:type=\"Group\"") || grant_body.contains("<URI>") {
1031 let uri = extract_xml_value(grant_body, "URI").unwrap_or_default();
1032 grants.push(AclGrant {
1033 grantee_type: "Group".to_string(),
1034 grantee_id: None,
1035 grantee_display_name: None,
1036 grantee_uri: Some(uri),
1037 permission,
1038 });
1039 } else {
1040 let id = extract_xml_value(grant_body, "ID").unwrap_or_default();
1041 let display =
1042 extract_xml_value(grant_body, "DisplayName").unwrap_or_else(|| id.clone());
1043 grants.push(AclGrant {
1044 grantee_type: "CanonicalUser".to_string(),
1045 grantee_id: Some(id),
1046 grantee_display_name: Some(display),
1047 grantee_uri: None,
1048 permission,
1049 });
1050 }
1051
1052 remaining = &after[end + 8..];
1053 } else {
1054 break;
1055 }
1056 }
1057 Ok(grants)
1058}
1059
1060pub(crate) enum RangeResult {
1063 Satisfiable { start: usize, end: usize },
1064 NotSatisfiable,
1065 Ignored,
1066}
1067
1068pub(crate) fn parse_range_header(range_str: &str, total_size: usize) -> Option<RangeResult> {
1069 let range_str = range_str.strip_prefix("bytes=")?;
1070 let (start_str, end_str) = range_str.split_once('-')?;
1071 if start_str.is_empty() {
1072 let suffix_len: usize = end_str.parse().ok()?;
1073 if suffix_len == 0 || total_size == 0 {
1074 return Some(RangeResult::NotSatisfiable);
1075 }
1076 let start = total_size.saturating_sub(suffix_len);
1077 Some(RangeResult::Satisfiable {
1078 start,
1079 end: total_size - 1,
1080 })
1081 } else {
1082 let start: usize = start_str.parse().ok()?;
1083 if start >= total_size {
1084 return Some(RangeResult::NotSatisfiable);
1085 }
1086 let end = if end_str.is_empty() {
1087 total_size - 1
1088 } else {
1089 let e: usize = end_str.parse().ok()?;
1090 if e < start {
1091 return Some(RangeResult::Ignored);
1092 }
1093 std::cmp::min(e, total_size - 1)
1094 };
1095 Some(RangeResult::Satisfiable { start, end })
1096 }
1097}
1098
1099pub(crate) fn s3_xml(status: StatusCode, body: impl Into<Bytes>) -> AwsResponse {
1103 AwsResponse {
1104 status,
1105 content_type: "application/xml".to_string(),
1106 body: body.into().into(),
1107 headers: HeaderMap::new(),
1108 }
1109}
1110
1111pub(crate) fn empty_response(status: StatusCode) -> AwsResponse {
1112 AwsResponse {
1113 status,
1114 content_type: "application/xml".to_string(),
1115 body: Bytes::new().into(),
1116 headers: HeaderMap::new(),
1117 }
1118}
1119
1120pub(crate) fn is_frozen(obj: &S3Object) -> bool {
1123 matches!(obj.storage_class.as_str(), "GLACIER" | "DEEP_ARCHIVE")
1124 && obj.restore_ongoing != Some(false)
1125}
1126
1127pub(crate) fn no_such_bucket(bucket: &str) -> AwsServiceError {
1128 AwsServiceError::aws_error_with_fields(
1129 StatusCode::NOT_FOUND,
1130 "NoSuchBucket",
1131 "The specified bucket does not exist",
1132 vec![("BucketName".to_string(), bucket.to_string())],
1133 )
1134}
1135
1136pub(crate) fn no_such_key(key: &str) -> AwsServiceError {
1137 AwsServiceError::aws_error_with_fields(
1138 StatusCode::NOT_FOUND,
1139 "NoSuchKey",
1140 "The specified key does not exist.",
1141 vec![("Key".to_string(), key.to_string())],
1142 )
1143}
1144
1145pub(crate) fn no_such_upload(upload_id: &str) -> AwsServiceError {
1146 AwsServiceError::aws_error_with_fields(
1147 StatusCode::NOT_FOUND,
1148 "NoSuchUpload",
1149 "The specified upload does not exist. The upload ID may be invalid, \
1150 or the upload may have been aborted or completed.",
1151 vec![("UploadId".to_string(), upload_id.to_string())],
1152 )
1153}
1154
1155pub(crate) fn no_such_key_with_detail(key: &str) -> AwsServiceError {
1156 AwsServiceError::aws_error_with_fields(
1157 StatusCode::NOT_FOUND,
1158 "NoSuchKey",
1159 "The specified key does not exist.",
1160 vec![("Key".to_string(), key.to_string())],
1161 )
1162}
1163
1164pub(crate) fn compute_md5(data: &[u8]) -> String {
1165 let digest = Md5::digest(data);
1166 format!("{:x}", digest)
1167}
1168
1169pub(crate) fn compute_checksum(algorithm: &str, data: &[u8]) -> String {
1170 match algorithm {
1171 "CRC32" => {
1172 let crc = crc32fast::hash(data);
1173 BASE64.encode(crc.to_be_bytes())
1174 }
1175 "SHA1" => {
1176 use sha1::Digest as _;
1177 let hash = sha1::Sha1::digest(data);
1178 BASE64.encode(hash)
1179 }
1180 "SHA256" => {
1181 use sha2::Digest as _;
1182 let hash = sha2::Sha256::digest(data);
1183 BASE64.encode(hash)
1184 }
1185 _ => String::new(),
1186 }
1187}
1188
1189pub(crate) fn url_encode_s3_key(s: &str) -> String {
1190 let mut out = String::with_capacity(s.len() * 2);
1191 for byte in s.bytes() {
1192 match byte {
1193 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
1194 out.push(byte as char);
1195 }
1196 _ => {
1197 out.push_str(&format!("%{:02X}", byte));
1198 }
1199 }
1200 }
1201 out
1202}
1203
1204pub(crate) use fakecloud_aws::xml::xml_escape;
1205
1206pub(crate) fn extract_user_metadata(
1207 headers: &HeaderMap,
1208) -> std::collections::HashMap<String, String> {
1209 let mut meta = std::collections::HashMap::new();
1210 for (name, value) in headers {
1211 if let Some(key) = name.as_str().strip_prefix("x-amz-meta-") {
1212 if let Ok(v) = value.to_str() {
1213 meta.insert(key.to_string(), v.to_string());
1214 }
1215 }
1216 }
1217 meta
1218}
1219
1220pub(crate) fn is_valid_storage_class(class: &str) -> bool {
1221 matches!(
1222 class,
1223 "STANDARD"
1224 | "REDUCED_REDUNDANCY"
1225 | "STANDARD_IA"
1226 | "ONEZONE_IA"
1227 | "INTELLIGENT_TIERING"
1228 | "GLACIER"
1229 | "DEEP_ARCHIVE"
1230 | "GLACIER_IR"
1231 | "OUTPOSTS"
1232 | "SNOW"
1233 | "EXPRESS_ONEZONE"
1234 )
1235}
1236
1237pub(crate) fn is_valid_bucket_name(name: &str) -> bool {
1238 if name.len() < 3 || name.len() > 63 {
1239 return false;
1240 }
1241 let bytes = name.as_bytes();
1243 if !bytes[0].is_ascii_alphanumeric() || !bytes[bytes.len() - 1].is_ascii_alphanumeric() {
1244 return false;
1245 }
1246 name.chars()
1248 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.' || c == '_')
1249}
1250
1251pub(crate) fn is_valid_region(region: &str) -> bool {
1252 let valid_regions = [
1254 "us-east-1",
1255 "us-east-2",
1256 "us-west-1",
1257 "us-west-2",
1258 "af-south-1",
1259 "ap-east-1",
1260 "ap-south-1",
1261 "ap-south-2",
1262 "ap-southeast-1",
1263 "ap-southeast-2",
1264 "ap-southeast-3",
1265 "ap-southeast-4",
1266 "ap-northeast-1",
1267 "ap-northeast-2",
1268 "ap-northeast-3",
1269 "ca-central-1",
1270 "ca-west-1",
1271 "eu-central-1",
1272 "eu-central-2",
1273 "eu-west-1",
1274 "eu-west-2",
1275 "eu-west-3",
1276 "eu-south-1",
1277 "eu-south-2",
1278 "eu-north-1",
1279 "il-central-1",
1280 "me-south-1",
1281 "me-central-1",
1282 "sa-east-1",
1283 "cn-north-1",
1284 "cn-northwest-1",
1285 "us-gov-east-1",
1286 "us-gov-east-2",
1287 "us-gov-west-1",
1288 "us-iso-east-1",
1289 "us-iso-west-1",
1290 "us-isob-east-1",
1291 "us-isof-south-1",
1292 ];
1293 valid_regions.contains(®ion)
1294}
1295
1296pub(crate) fn resolve_object<'a>(
1297 b: &'a S3Bucket,
1298 key: &str,
1299 version_id: Option<&String>,
1300) -> Result<&'a S3Object, AwsServiceError> {
1301 if let Some(vid) = version_id {
1302 if vid == "null" {
1304 if let Some(versions) = b.object_versions.get(key) {
1306 if let Some(obj) = versions
1307 .iter()
1308 .find(|o| o.version_id.is_none() || o.version_id.as_deref() == Some("null"))
1309 {
1310 return Ok(obj);
1311 }
1312 }
1313 if let Some(obj) = b.objects.get(key) {
1315 if obj.version_id.is_none() || obj.version_id.as_deref() == Some("null") {
1316 return Ok(obj);
1317 }
1318 }
1319 } else {
1320 if let Some(versions) = b.object_versions.get(key) {
1322 if let Some(obj) = versions
1323 .iter()
1324 .find(|o| o.version_id.as_deref() == Some(vid.as_str()))
1325 {
1326 return Ok(obj);
1327 }
1328 }
1329 if let Some(obj) = b.objects.get(key) {
1331 if obj.version_id.as_deref() == Some(vid.as_str()) {
1332 return Ok(obj);
1333 }
1334 }
1335 }
1336 if b.versioning.is_some() {
1338 Err(AwsServiceError::aws_error_with_fields(
1339 StatusCode::NOT_FOUND,
1340 "NoSuchVersion",
1341 "The specified version does not exist.",
1342 vec![
1343 ("Key".to_string(), key.to_string()),
1344 ("VersionId".to_string(), vid.to_string()),
1345 ],
1346 ))
1347 } else {
1348 Err(AwsServiceError::aws_error(
1349 StatusCode::BAD_REQUEST,
1350 "InvalidArgument",
1351 "Invalid version id specified",
1352 ))
1353 }
1354 } else {
1355 b.objects.get(key).ok_or_else(|| no_such_key(key))
1356 }
1357}
1358
1359pub(crate) fn make_delete_marker(key: &str, dm_id: &str) -> S3Object {
1360 S3Object {
1361 key: key.to_string(),
1362 last_modified: Utc::now(),
1363 storage_class: "STANDARD".to_string(),
1364 version_id: Some(dm_id.to_string()),
1365 is_delete_marker: true,
1366 ..Default::default()
1367 }
1368}
1369
1370pub(crate) struct DeleteObjectEntry {
1372 key: String,
1373 version_id: Option<String>,
1374}
1375
1376pub(crate) fn parse_delete_objects_xml(xml: &str) -> Vec<DeleteObjectEntry> {
1377 let mut entries = Vec::new();
1378 let mut remaining = xml;
1379 while let Some(obj_start) = remaining.find("<Object>") {
1380 let after = &remaining[obj_start + 8..];
1381 if let Some(obj_end) = after.find("</Object>") {
1382 let obj_body = &after[..obj_end];
1383 let key = extract_xml_value(obj_body, "Key");
1384 let version_id = extract_xml_value(obj_body, "VersionId");
1385 if let Some(k) = key {
1386 entries.push(DeleteObjectEntry { key: k, version_id });
1387 }
1388 remaining = &after[obj_end + 9..];
1389 } else {
1390 break;
1391 }
1392 }
1393 entries
1394}
1395
1396pub(crate) fn parse_tagging_xml(xml: &str) -> Vec<(String, String)> {
1399 let mut tags = Vec::new();
1400 let mut remaining = xml;
1401 while let Some(tag_start) = remaining.find("<Tag>") {
1402 let after = &remaining[tag_start + 5..];
1403 if let Some(tag_end) = after.find("</Tag>") {
1404 let tag_body = &after[..tag_end];
1405 let key = extract_xml_value(tag_body, "Key");
1406 let value = extract_xml_value(tag_body, "Value");
1407 if let (Some(k), Some(v)) = (key, value) {
1408 tags.push((k, v));
1409 }
1410 remaining = &after[tag_end + 6..];
1411 } else {
1412 break;
1413 }
1414 }
1415 tags
1416}
1417
1418pub(crate) fn validate_tags(tags: &[(String, String)]) -> Result<(), AwsServiceError> {
1419 let mut seen = std::collections::HashSet::new();
1421 for (k, _) in tags {
1422 if !seen.insert(k.as_str()) {
1423 return Err(AwsServiceError::aws_error(
1424 StatusCode::BAD_REQUEST,
1425 "InvalidTag",
1426 "Cannot provide multiple Tags with the same key",
1427 ));
1428 }
1429 if k.starts_with("aws:") {
1431 return Err(AwsServiceError::aws_error(
1432 StatusCode::BAD_REQUEST,
1433 "InvalidTag",
1434 "System tags cannot be added/updated by requester",
1435 ));
1436 }
1437 }
1438 Ok(())
1439}
1440
1441pub(crate) fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
1442 let self_closing1 = format!("<{tag} />");
1444 let self_closing2 = format!("<{tag}/>");
1445 if xml.contains(&self_closing1) || xml.contains(&self_closing2) {
1446 let self_pos = xml
1448 .find(&self_closing1)
1449 .or_else(|| xml.find(&self_closing2));
1450 let open = format!("<{tag}>");
1451 let open_pos = xml.find(&open);
1452 match (self_pos, open_pos) {
1453 (Some(sp), Some(op)) if sp < op => return Some(String::new()),
1454 (Some(_), None) => return Some(String::new()),
1455 _ => {}
1456 }
1457 }
1458
1459 let open = format!("<{tag}>");
1460 let close = format!("</{tag}>");
1461 let start = xml.find(&open)? + open.len();
1462 let end = xml.find(&close)?;
1463 Some(xml[start..end].to_string())
1464}
1465
1466pub(crate) fn parse_complete_multipart_xml(xml: &str) -> Vec<(u32, String)> {
1468 let mut parts = Vec::new();
1469 let mut remaining = xml;
1470 while let Some(part_start) = remaining.find("<Part>") {
1471 let after = &remaining[part_start + 6..];
1472 if let Some(part_end) = after.find("</Part>") {
1473 let part_body = &after[..part_end];
1474 let part_num =
1475 extract_xml_value(part_body, "PartNumber").and_then(|s| s.parse::<u32>().ok());
1476 let etag = extract_xml_value(part_body, "ETag")
1477 .map(|s| s.replace(""", "").replace('"', ""));
1478 if let (Some(num), Some(e)) = (part_num, etag) {
1479 parts.push((num, e));
1480 }
1481 remaining = &after[part_end + 7..];
1482 } else {
1483 break;
1484 }
1485 }
1486 parts
1487}
1488
1489pub(crate) fn parse_url_encoded_tags(s: &str) -> Vec<(String, String)> {
1490 let mut tags = Vec::new();
1491 for pair in s.split('&') {
1492 if pair.is_empty() {
1493 continue;
1494 }
1495 let (key, value) = match pair.find('=') {
1496 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
1497 None => (pair, ""),
1498 };
1499 tags.push((
1500 percent_encoding::percent_decode_str(key)
1501 .decode_utf8_lossy()
1502 .to_string(),
1503 percent_encoding::percent_decode_str(value)
1504 .decode_utf8_lossy()
1505 .to_string(),
1506 ));
1507 }
1508 tags
1509}
1510
1511pub(crate) fn validate_lifecycle_xml(xml: &str) -> Result<(), AwsServiceError> {
1513 let malformed = || {
1514 AwsServiceError::aws_error(
1515 StatusCode::BAD_REQUEST,
1516 "MalformedXML",
1517 "The XML you provided was not well-formed or did not validate against our published schema",
1518 )
1519 };
1520
1521 let mut remaining = xml;
1522 while let Some(rule_start) = remaining.find("<Rule>") {
1523 let after = &remaining[rule_start + 6..];
1524 if let Some(rule_end) = after.find("</Rule>") {
1525 let rule_body = &after[..rule_end];
1526
1527 let has_filter = rule_body.contains("<Filter>")
1529 || rule_body.contains("<Filter/>")
1530 || rule_body.contains("<Filter />");
1531
1532 let has_prefix_outside_filter = {
1534 if !rule_body.contains("<Prefix") {
1535 false
1536 } else if !has_filter {
1537 true } else {
1539 let mut stripped = rule_body.to_string();
1541 if let Some(fs) = stripped.find("<Filter") {
1543 if let Some(fe) = stripped.find("</Filter>") {
1544 stripped = format!("{}{}", &stripped[..fs], &stripped[fe + 9..]);
1545 }
1546 }
1547 stripped.contains("<Prefix")
1548 }
1549 };
1550
1551 if !has_filter && !has_prefix_outside_filter {
1552 return Err(malformed());
1553 }
1554 if has_filter && has_prefix_outside_filter {
1556 return Err(malformed());
1557 }
1558
1559 if let Some(exp_start) = rule_body.find("<Expiration>") {
1562 if let Some(exp_end) = rule_body[exp_start..].find("</Expiration>") {
1563 let exp_body = &rule_body[exp_start..exp_start + exp_end];
1564 if exp_body.contains("<ExpiredObjectDeleteMarker>")
1565 && (exp_body.contains("<Days>") || exp_body.contains("<Date>"))
1566 {
1567 return Err(malformed());
1568 }
1569 }
1570 }
1571
1572 if has_filter {
1574 if let Some(fs) = rule_body.find("<Filter>") {
1575 if let Some(fe) = rule_body.find("</Filter>") {
1576 let filter_body = &rule_body[fs + 8..fe];
1577 let has_prefix_in_filter = filter_body.contains("<Prefix");
1578 let has_tag_in_filter = filter_body.contains("<Tag>");
1579 let has_and_in_filter = filter_body.contains("<And>");
1580 if has_prefix_in_filter && has_tag_in_filter && !has_and_in_filter {
1582 return Err(malformed());
1583 }
1584 if has_tag_in_filter && has_and_in_filter {
1586 let and_start = filter_body.find("<And>").unwrap_or(0);
1588 let tag_pos = filter_body.find("<Tag>").unwrap_or(0);
1589 if tag_pos < and_start {
1590 return Err(malformed());
1591 }
1592 }
1593 }
1594 }
1595 }
1596
1597 if rule_body.contains("<NoncurrentVersionTransition>") {
1599 let mut nvt_remaining = rule_body;
1600 while let Some(nvt_start) = nvt_remaining.find("<NoncurrentVersionTransition>") {
1601 let nvt_after = &nvt_remaining[nvt_start + 29..];
1602 if let Some(nvt_end) = nvt_after.find("</NoncurrentVersionTransition>") {
1603 let nvt_body = &nvt_after[..nvt_end];
1604 if !nvt_body.contains("<NoncurrentDays>") {
1605 return Err(malformed());
1606 }
1607 if !nvt_body.contains("<StorageClass>") {
1608 return Err(malformed());
1609 }
1610 nvt_remaining = &nvt_after[nvt_end + 30..];
1611 } else {
1612 break;
1613 }
1614 }
1615 }
1616
1617 remaining = &after[rule_end + 7..];
1618 } else {
1619 break;
1620 }
1621 }
1622
1623 Ok(())
1624}
1625
1626pub(crate) struct CorsRule {
1628 allowed_origins: Vec<String>,
1629 allowed_methods: Vec<String>,
1630 allowed_headers: Vec<String>,
1631 expose_headers: Vec<String>,
1632 max_age_seconds: Option<u32>,
1633}
1634
1635pub(crate) fn parse_cors_config(xml: &str) -> Vec<CorsRule> {
1637 let mut rules = Vec::new();
1638 let mut remaining = xml;
1639 while let Some(start) = remaining.find("<CORSRule>") {
1640 let after = &remaining[start + 10..];
1641 if let Some(end) = after.find("</CORSRule>") {
1642 let block = &after[..end];
1643 let allowed_origins = extract_all_xml_values(block, "AllowedOrigin");
1644 let allowed_methods = extract_all_xml_values(block, "AllowedMethod");
1645 let allowed_headers = extract_all_xml_values(block, "AllowedHeader");
1646 let expose_headers = extract_all_xml_values(block, "ExposeHeader");
1647 let max_age_seconds =
1648 extract_xml_value(block, "MaxAgeSeconds").and_then(|s| s.parse().ok());
1649 rules.push(CorsRule {
1650 allowed_origins,
1651 allowed_methods,
1652 allowed_headers,
1653 expose_headers,
1654 max_age_seconds,
1655 });
1656 remaining = &after[end + 11..];
1657 } else {
1658 break;
1659 }
1660 }
1661 rules
1662}
1663
1664pub(crate) fn origin_matches(origin: &str, pattern: &str) -> bool {
1666 if pattern == "*" {
1667 return true;
1668 }
1669 if let Some(suffix) = pattern.strip_prefix('*') {
1671 return origin.ends_with(suffix);
1672 }
1673 origin == pattern
1674}
1675
1676pub(crate) fn find_cors_rule<'a>(
1678 rules: &'a [CorsRule],
1679 origin: &str,
1680 method: Option<&str>,
1681) -> Option<&'a CorsRule> {
1682 rules.iter().find(|rule| {
1683 let origin_ok = rule
1684 .allowed_origins
1685 .iter()
1686 .any(|o| origin_matches(origin, o));
1687 let method_ok = match method {
1688 Some(m) => rule.allowed_methods.iter().any(|am| am == m),
1689 None => true,
1690 };
1691 origin_ok && method_ok
1692 })
1693}
1694
1695pub(crate) fn check_object_lock_for_overwrite(
1698 obj: &S3Object,
1699 req: &AwsRequest,
1700) -> Option<&'static str> {
1701 if obj.lock_legal_hold.as_deref() == Some("ON") {
1703 return Some("AccessDenied");
1704 }
1705 if let (Some(mode), Some(until)) = (&obj.lock_mode, &obj.lock_retain_until) {
1707 if *until > Utc::now() {
1708 if mode == "COMPLIANCE" {
1709 return Some("AccessDenied");
1710 }
1711 if mode == "GOVERNANCE" {
1712 let bypass = req
1713 .headers
1714 .get("x-amz-bypass-governance-retention")
1715 .and_then(|v| v.to_str().ok())
1716 .map(|s| s.eq_ignore_ascii_case("true"))
1717 .unwrap_or(false);
1718 if !bypass {
1719 return Some("AccessDenied");
1720 }
1721 }
1722 }
1723 }
1724 None
1725}
1726
1727#[cfg(test)]
1728mod tests {
1729 use super::*;
1730
1731 #[test]
1732 fn valid_bucket_names() {
1733 assert!(is_valid_bucket_name("my-bucket"));
1734 assert!(is_valid_bucket_name("my.bucket.name"));
1735 assert!(is_valid_bucket_name("abc"));
1736 assert!(!is_valid_bucket_name("ab"));
1737 assert!(!is_valid_bucket_name("-bucket"));
1738 assert!(!is_valid_bucket_name("Bucket"));
1739 assert!(!is_valid_bucket_name("bucket-"));
1740 }
1741
1742 #[test]
1743 fn parse_delete_xml() {
1744 let xml = r#"<Delete><Object><Key>a.txt</Key></Object><Object><Key>b/c.txt</Key></Object></Delete>"#;
1745 let entries = parse_delete_objects_xml(xml);
1746 assert_eq!(entries.len(), 2);
1747 assert_eq!(entries[0].key, "a.txt");
1748 assert!(entries[0].version_id.is_none());
1749 assert_eq!(entries[1].key, "b/c.txt");
1750 }
1751
1752 #[test]
1753 fn parse_delete_xml_with_version() {
1754 let xml = r#"<Delete><Object><Key>a.txt</Key><VersionId>v1</VersionId></Object></Delete>"#;
1755 let entries = parse_delete_objects_xml(xml);
1756 assert_eq!(entries.len(), 1);
1757 assert_eq!(entries[0].key, "a.txt");
1758 assert_eq!(entries[0].version_id.as_deref(), Some("v1"));
1759 }
1760
1761 #[test]
1762 fn parse_tags_xml() {
1763 let xml =
1764 r#"<Tagging><TagSet><Tag><Key>env</Key><Value>prod</Value></Tag></TagSet></Tagging>"#;
1765 let tags = parse_tagging_xml(xml);
1766 assert_eq!(tags, vec![("env".to_string(), "prod".to_string())]);
1767 }
1768
1769 #[test]
1770 fn md5_hash() {
1771 let hash = compute_md5(b"hello");
1772 assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
1773 }
1774
1775 #[test]
1776 fn test_etag_matches() {
1777 assert!(etag_matches("\"abc\"", "\"abc\""));
1778 assert!(etag_matches("abc", "\"abc\""));
1779 assert!(etag_matches("*", "\"abc\""));
1780 assert!(!etag_matches("\"xyz\"", "\"abc\""));
1781 }
1782
1783 #[test]
1784 fn test_event_matches() {
1785 assert!(event_matches("s3:ObjectCreated:Put", "s3:ObjectCreated:*"));
1786 assert!(event_matches("s3:ObjectCreated:Copy", "s3:ObjectCreated:*"));
1787 assert!(event_matches(
1788 "s3:ObjectRemoved:Delete",
1789 "s3:ObjectRemoved:*"
1790 ));
1791 assert!(!event_matches(
1792 "s3:ObjectRemoved:Delete",
1793 "s3:ObjectCreated:*"
1794 ));
1795 assert!(event_matches(
1796 "s3:ObjectCreated:Put",
1797 "s3:ObjectCreated:Put"
1798 ));
1799 assert!(event_matches("s3:ObjectCreated:Put", "s3:*"));
1800 }
1801
1802 #[test]
1803 fn test_parse_notification_config() {
1804 let xml = r#"<NotificationConfiguration>
1805 <QueueConfiguration>
1806 <Queue>arn:aws:sqs:us-east-1:123456789012:my-queue</Queue>
1807 <Event>s3:ObjectCreated:*</Event>
1808 </QueueConfiguration>
1809 <TopicConfiguration>
1810 <Topic>arn:aws:sns:us-east-1:123456789012:my-topic</Topic>
1811 <Event>s3:ObjectRemoved:*</Event>
1812 </TopicConfiguration>
1813 </NotificationConfiguration>"#;
1814 let targets = parse_notification_config(xml);
1815 assert_eq!(targets.len(), 2);
1816 assert_eq!(
1817 targets[0].arn,
1818 "arn:aws:sqs:us-east-1:123456789012:my-queue"
1819 );
1820 assert_eq!(targets[0].events, vec!["s3:ObjectCreated:*"]);
1821 assert_eq!(
1822 targets[1].arn,
1823 "arn:aws:sns:us-east-1:123456789012:my-topic"
1824 );
1825 assert_eq!(targets[1].events, vec!["s3:ObjectRemoved:*"]);
1826 }
1827
1828 #[test]
1829 fn test_parse_notification_config_lambda() {
1830 let xml = r#"<NotificationConfiguration>
1832 <CloudFunctionConfiguration>
1833 <CloudFunction>arn:aws:lambda:us-east-1:123456789012:function:my-func</CloudFunction>
1834 <Event>s3:ObjectCreated:*</Event>
1835 </CloudFunctionConfiguration>
1836 </NotificationConfiguration>"#;
1837 let targets = parse_notification_config(xml);
1838 assert_eq!(targets.len(), 1);
1839 assert!(matches!(
1840 targets[0].target_type,
1841 NotificationTargetType::Lambda
1842 ));
1843 assert_eq!(
1844 targets[0].arn,
1845 "arn:aws:lambda:us-east-1:123456789012:function:my-func"
1846 );
1847 assert_eq!(targets[0].events, vec!["s3:ObjectCreated:*"]);
1848 }
1849
1850 #[test]
1851 fn test_parse_notification_config_lambda_new_format() {
1852 let xml = r#"<NotificationConfiguration>
1854 <LambdaFunctionConfiguration>
1855 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
1856 <Event>s3:ObjectCreated:Put</Event>
1857 <Event>s3:ObjectRemoved:*</Event>
1858 </LambdaFunctionConfiguration>
1859 </NotificationConfiguration>"#;
1860 let targets = parse_notification_config(xml);
1861 assert_eq!(targets.len(), 1);
1862 assert!(matches!(
1863 targets[0].target_type,
1864 NotificationTargetType::Lambda
1865 ));
1866 assert_eq!(
1867 targets[0].arn,
1868 "arn:aws:lambda:us-east-1:123456789012:function:my-func"
1869 );
1870 assert_eq!(
1871 targets[0].events,
1872 vec!["s3:ObjectCreated:Put", "s3:ObjectRemoved:*"]
1873 );
1874 }
1875
1876 #[test]
1877 fn test_parse_notification_config_all_types() {
1878 let xml = r#"<NotificationConfiguration>
1879 <QueueConfiguration>
1880 <Queue>arn:aws:sqs:us-east-1:123456789012:q</Queue>
1881 <Event>s3:ObjectCreated:*</Event>
1882 </QueueConfiguration>
1883 <TopicConfiguration>
1884 <Topic>arn:aws:sns:us-east-1:123456789012:t</Topic>
1885 <Event>s3:ObjectRemoved:*</Event>
1886 </TopicConfiguration>
1887 <LambdaFunctionConfiguration>
1888 <Function>arn:aws:lambda:us-east-1:123456789012:function:f</Function>
1889 <Event>s3:ObjectCreated:Put</Event>
1890 </LambdaFunctionConfiguration>
1891 </NotificationConfiguration>"#;
1892 let targets = parse_notification_config(xml);
1893 assert_eq!(targets.len(), 3);
1894 assert!(matches!(
1895 targets[0].target_type,
1896 NotificationTargetType::Sqs
1897 ));
1898 assert!(matches!(
1899 targets[1].target_type,
1900 NotificationTargetType::Sns
1901 ));
1902 assert!(matches!(
1903 targets[2].target_type,
1904 NotificationTargetType::Lambda
1905 ));
1906 }
1907
1908 #[test]
1909 fn test_parse_notification_config_with_filters() {
1910 let xml = r#"<NotificationConfiguration>
1911 <LambdaFunctionConfiguration>
1912 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
1913 <Event>s3:ObjectCreated:*</Event>
1914 <Filter>
1915 <S3Key>
1916 <FilterRule>
1917 <Name>prefix</Name>
1918 <Value>images/</Value>
1919 </FilterRule>
1920 <FilterRule>
1921 <Name>suffix</Name>
1922 <Value>.jpg</Value>
1923 </FilterRule>
1924 </S3Key>
1925 </Filter>
1926 </LambdaFunctionConfiguration>
1927 </NotificationConfiguration>"#;
1928 let targets = parse_notification_config(xml);
1929 assert_eq!(targets.len(), 1);
1930 assert_eq!(targets[0].prefix_filter, Some("images/".to_string()));
1931 assert_eq!(targets[0].suffix_filter, Some(".jpg".to_string()));
1932 }
1933
1934 #[test]
1935 fn test_parse_notification_config_no_filters() {
1936 let xml = r#"<NotificationConfiguration>
1937 <LambdaFunctionConfiguration>
1938 <Function>arn:aws:lambda:us-east-1:123456789012:function:my-func</Function>
1939 <Event>s3:ObjectCreated:*</Event>
1940 </LambdaFunctionConfiguration>
1941 </NotificationConfiguration>"#;
1942 let targets = parse_notification_config(xml);
1943 assert_eq!(targets.len(), 1);
1944 assert_eq!(targets[0].prefix_filter, None);
1945 assert_eq!(targets[0].suffix_filter, None);
1946 }
1947
1948 #[test]
1949 fn test_key_matches_filters() {
1950 assert!(key_matches_filters("anything", &None, &None));
1952
1953 assert!(key_matches_filters(
1955 "images/photo.jpg",
1956 &Some("images/".to_string()),
1957 &None
1958 ));
1959 assert!(!key_matches_filters(
1960 "docs/file.txt",
1961 &Some("images/".to_string()),
1962 &None
1963 ));
1964
1965 assert!(key_matches_filters(
1967 "images/photo.jpg",
1968 &None,
1969 &Some(".jpg".to_string())
1970 ));
1971 assert!(!key_matches_filters(
1972 "images/photo.png",
1973 &None,
1974 &Some(".jpg".to_string())
1975 ));
1976
1977 assert!(key_matches_filters(
1979 "images/photo.jpg",
1980 &Some("images/".to_string()),
1981 &Some(".jpg".to_string())
1982 ));
1983 assert!(!key_matches_filters(
1984 "images/photo.png",
1985 &Some("images/".to_string()),
1986 &Some(".jpg".to_string())
1987 ));
1988 assert!(!key_matches_filters(
1989 "docs/photo.jpg",
1990 &Some("images/".to_string()),
1991 &Some(".jpg".to_string())
1992 ));
1993 }
1994
1995 #[test]
1996 fn test_parse_cors_config() {
1997 let xml = r#"<CORSConfiguration>
1998 <CORSRule>
1999 <AllowedOrigin>https://example.com</AllowedOrigin>
2000 <AllowedMethod>GET</AllowedMethod>
2001 <AllowedMethod>PUT</AllowedMethod>
2002 <AllowedHeader>*</AllowedHeader>
2003 <ExposeHeader>x-amz-request-id</ExposeHeader>
2004 <MaxAgeSeconds>3600</MaxAgeSeconds>
2005 </CORSRule>
2006 </CORSConfiguration>"#;
2007 let rules = parse_cors_config(xml);
2008 assert_eq!(rules.len(), 1);
2009 assert_eq!(rules[0].allowed_origins, vec!["https://example.com"]);
2010 assert_eq!(rules[0].allowed_methods, vec!["GET", "PUT"]);
2011 assert_eq!(rules[0].allowed_headers, vec!["*"]);
2012 assert_eq!(rules[0].expose_headers, vec!["x-amz-request-id"]);
2013 assert_eq!(rules[0].max_age_seconds, Some(3600));
2014 }
2015
2016 #[test]
2017 fn test_origin_matches() {
2018 assert!(origin_matches("https://example.com", "https://example.com"));
2019 assert!(origin_matches("https://example.com", "*"));
2020 assert!(origin_matches("https://foo.example.com", "*.example.com"));
2021 assert!(!origin_matches("https://evil.com", "https://example.com"));
2022 }
2023
2024 #[test]
2027 fn resolve_null_version_matches_both_none_and_null_string() {
2028 use crate::state::S3Bucket;
2029 use bytes::Bytes;
2030 use chrono::Utc;
2031
2032 let mut b = S3Bucket::new("test", "us-east-1", "owner");
2033
2034 let make_obj = |key: &str, vid: Option<&str>| crate::state::S3Object {
2036 key: key.to_string(),
2037 body: crate::state::memory_body(Bytes::from_static(b"x")),
2038 content_type: "text/plain".to_string(),
2039 etag: "\"abc\"".to_string(),
2040 size: 1,
2041 last_modified: Utc::now(),
2042 storage_class: "STANDARD".to_string(),
2043 version_id: vid.map(|s| s.to_string()),
2044 ..Default::default()
2045 };
2046
2047 let obj = make_obj("file.txt", Some("null"));
2049 b.objects.insert("file.txt".to_string(), obj.clone());
2050 b.object_versions.insert("file.txt".to_string(), vec![obj]);
2051
2052 let null_str = "null".to_string();
2053 let result = resolve_object(&b, "file.txt", Some(&null_str));
2054 assert!(
2055 result.is_ok(),
2056 "versionId=null should match version_id=Some(\"null\")"
2057 );
2058
2059 let obj2 = make_obj("file2.txt", None);
2061 b.objects.insert("file2.txt".to_string(), obj2.clone());
2062 b.object_versions
2063 .insert("file2.txt".to_string(), vec![obj2]);
2064
2065 let result2 = resolve_object(&b, "file2.txt", Some(&null_str));
2066 assert!(
2067 result2.is_ok(),
2068 "versionId=null should match version_id=None"
2069 );
2070 }
2071
2072 #[test]
2073 fn test_parse_replication_rules() {
2074 let xml = r#"<ReplicationConfiguration>
2075 <Role>arn:aws:iam::role/replication</Role>
2076 <Rule>
2077 <Status>Enabled</Status>
2078 <Filter><Prefix>logs/</Prefix></Filter>
2079 <Destination><Bucket>arn:aws:s3:::dest-bucket</Bucket></Destination>
2080 </Rule>
2081 <Rule>
2082 <Status>Disabled</Status>
2083 <Filter><Prefix></Prefix></Filter>
2084 <Destination><Bucket>arn:aws:s3:::other-bucket</Bucket></Destination>
2085 </Rule>
2086 </ReplicationConfiguration>"#;
2087
2088 let rules = parse_replication_rules(xml);
2089 assert_eq!(rules.len(), 2);
2090 assert_eq!(rules[0].status, "Enabled");
2091 assert_eq!(rules[0].prefix, "logs/");
2092 assert_eq!(rules[0].dest_bucket, "dest-bucket");
2093 assert_eq!(rules[1].status, "Disabled");
2094 assert_eq!(rules[1].prefix, "");
2095 assert_eq!(rules[1].dest_bucket, "other-bucket");
2096 }
2097
2098 #[test]
2099 fn test_parse_normalized_replication_rules() {
2100 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>"#;
2102 let normalized = normalize_replication_xml(input_xml);
2103 eprintln!("Normalized XML: {normalized}");
2104 let rules = parse_replication_rules(&normalized);
2105 assert_eq!(rules.len(), 1, "Expected 1 rule, got {}", rules.len());
2106 assert_eq!(rules[0].status, "Enabled");
2107 assert_eq!(rules[0].dest_bucket, "repl-dest");
2108 }
2109
2110 #[test]
2111 fn test_replicate_object() {
2112 use crate::state::{S3Bucket, S3State};
2113
2114 let mut state = S3State::new("123456789012", "us-east-1");
2115
2116 let mut src = S3Bucket::new("source", "us-east-1", "owner");
2118 src.versioning = Some("Enabled".to_string());
2119 src.replication_config = Some(
2120 "<ReplicationConfiguration>\
2121 <Rule><Status>Enabled</Status>\
2122 <Filter><Prefix></Prefix></Filter>\
2123 <Destination><Bucket>arn:aws:s3:::destination</Bucket></Destination>\
2124 </Rule></ReplicationConfiguration>"
2125 .to_string(),
2126 );
2127 let obj = S3Object {
2128 key: "test-key".to_string(),
2129 body: crate::state::memory_body(Bytes::from_static(b"hello")),
2130 content_type: "text/plain".to_string(),
2131 etag: "abc".to_string(),
2132 size: 5,
2133 last_modified: Utc::now(),
2134 storage_class: "STANDARD".to_string(),
2135 version_id: Some("v1".to_string()),
2136 ..Default::default()
2137 };
2138 src.objects.insert("test-key".to_string(), obj);
2139 state.buckets.insert("source".to_string(), src);
2140
2141 let dest = S3Bucket::new("destination", "us-east-1", "owner");
2142 state.buckets.insert("destination".to_string(), dest);
2143
2144 replicate_object(&mut state, "source", "test-key");
2145
2146 let dest_obj = state
2148 .buckets
2149 .get("destination")
2150 .unwrap()
2151 .objects
2152 .get("test-key");
2153 assert!(dest_obj.is_some());
2154 assert_eq!(
2155 state.read_body(&dest_obj.unwrap().body).unwrap(),
2156 Bytes::from_static(b"hello")
2157 );
2158 }
2159
2160 #[test]
2161 fn cors_header_value_does_not_panic_on_unusual_input() {
2162 let valid_origin = "https://example.com";
2166 let result: Result<http::HeaderValue, _> = valid_origin.parse();
2167 assert!(result.is_ok());
2168
2169 let bad_origin = "https://ex\x01ample.com";
2171 let result: Result<http::HeaderValue, _> = bad_origin.parse();
2172 assert!(result.is_err());
2173 let fallback = bad_origin
2175 .parse()
2176 .unwrap_or_else(|_| http::HeaderValue::from_static(""));
2177 assert_eq!(fallback, "");
2178 }
2179}