1use std::{
20 fmt::Display,
21 ops::{Range, RangeBounds},
22};
23
24use super::Result;
25use bytes::Bytes;
26use futures_util::{Stream, TryStreamExt, stream::StreamExt};
27
28#[cfg(any(feature = "azure-base", feature = "http-base"))]
29pub(crate) static RFC1123_FMT: &str = "%a, %d %h %Y %T GMT";
30
31#[cfg(any(feature = "azure-base", feature = "http-base"))]
33pub(crate) fn deserialize_rfc1123<'de, D>(
34 deserializer: D,
35) -> Result<chrono::DateTime<chrono::Utc>, D::Error>
36where
37 D: serde::Deserializer<'de>,
38{
39 let s: String = serde::Deserialize::deserialize(deserializer)?;
40 let naive =
41 chrono::NaiveDateTime::parse_from_str(&s, RFC1123_FMT).map_err(serde::de::Error::custom)?;
42 Ok(chrono::TimeZone::from_utc_datetime(&chrono::Utc, &naive))
43}
44
45pub async fn collect_bytes<S, E>(mut stream: S, size_hint: Option<u64>) -> Result<Bytes, E>
47where
48 E: Send,
49 S: Stream<Item = Result<Bytes, E>> + Send + Unpin,
50{
51 let first = stream.next().await.transpose()?.unwrap_or_default();
52
53 match stream.next().await.transpose()? {
55 None => Ok(first),
56 Some(second) => {
57 let size_hint = size_hint.unwrap_or_else(|| first.len() as u64 + second.len() as u64);
58
59 let mut buf = Vec::with_capacity(size_hint as usize);
60 buf.extend_from_slice(&first);
61 buf.extend_from_slice(&second);
62 while let Some(maybe_bytes) = stream.next().await {
63 buf.extend_from_slice(&maybe_bytes?);
64 }
65
66 Ok(buf.into())
67 }
68 }
69}
70
71#[cfg(all(feature = "fs", not(target_arch = "wasm32")))]
72pub(crate) async fn maybe_spawn_blocking<F, T>(f: F) -> Result<T>
74where
75 F: FnOnce() -> Result<T> + Send + 'static,
76 T: Send + 'static,
77{
78 match tokio::runtime::Handle::try_current() {
79 Ok(runtime) => runtime.spawn_blocking(f).await?,
80 Err(_) => f(),
81 }
82}
83
84pub const OBJECT_STORE_COALESCE_DEFAULT: u64 = 1024 * 1024;
87
88pub(crate) const OBJECT_STORE_COALESCE_PARALLEL: usize = 10;
90
91pub async fn coalesce_ranges<F, E, Fut>(
100 ranges: &[Range<u64>],
101 fetch: F,
102 coalesce: u64,
103) -> Result<Vec<Bytes>, E>
104where
105 F: Send + FnMut(Range<u64>) -> Fut,
106 E: Send,
107 Fut: std::future::Future<Output = Result<Bytes, E>> + Send,
108{
109 let fetch_ranges = merge_ranges(ranges, coalesce);
110
111 let fetched: Vec<_> = futures_util::stream::iter(fetch_ranges.iter().cloned())
112 .map(fetch)
113 .buffered(OBJECT_STORE_COALESCE_PARALLEL)
114 .try_collect()
115 .await?;
116
117 Ok(ranges
118 .iter()
119 .map(|range| {
120 let idx = fetch_ranges.partition_point(|v| v.start <= range.start) - 1;
121 let fetch_range = &fetch_ranges[idx];
122 let fetch_bytes = &fetched[idx];
123
124 let start = range.start - fetch_range.start;
125 let end = range.end - fetch_range.start;
126 let range = (start as usize)..(end as usize).min(fetch_bytes.len());
127 fetch_bytes.slice(range)
128 })
129 .collect())
130}
131
132pub(crate) fn merge_ranges(ranges: &[Range<u64>], coalesce: u64) -> Vec<Range<u64>> {
134 if ranges.is_empty() {
135 return vec![];
136 }
137
138 let mut ranges = ranges.to_vec();
139 ranges.sort_unstable_by_key(|range| range.start);
140
141 let mut ret = Vec::with_capacity(ranges.len());
142 let mut start_idx = 0;
143 let mut end_idx = 1;
144
145 while start_idx != ranges.len() {
146 let mut range_end = ranges[start_idx].end;
147
148 while end_idx != ranges.len()
149 && ranges[end_idx]
150 .start
151 .checked_sub(range_end)
152 .map(|delta| delta <= coalesce)
153 .unwrap_or(true)
154 {
155 range_end = range_end.max(ranges[end_idx].end);
156 end_idx += 1;
157 }
158
159 let start = ranges[start_idx].start;
160 let end = range_end;
161 ret.push(start..end);
162
163 start_idx = end_idx;
164 end_idx += 1;
165 }
166
167 ret
168}
169
170#[derive(Debug, PartialEq, Eq, Clone)]
187pub enum GetRange {
188 Bounded(Range<u64>),
198 Offset(u64),
200 Suffix(u64),
202}
203
204#[derive(Debug, thiserror::Error)]
205#[non_exhaustive]
206pub enum InvalidGetRange {
207 #[error("Wanted range starting at {requested}, but object was only {length} bytes long")]
208 StartTooLarge { requested: u64, length: u64 },
209
210 #[error("Range started at {start} and ended at {end}")]
211 Inconsistent { start: u64, end: u64 },
212
213 #[error("Range {requested} is larger than system memory limit {max}")]
214 TooLarge { requested: u64, max: u64 },
215}
216
217impl GetRange {
218 pub fn is_valid(&self) -> Result<(), InvalidGetRange> {
220 if let Self::Bounded(r) = self {
221 if r.end <= r.start {
222 return Err(InvalidGetRange::Inconsistent {
223 start: r.start,
224 end: r.end,
225 });
226 }
227 if (r.end - r.start) > usize::MAX as u64 {
228 return Err(InvalidGetRange::TooLarge {
229 requested: r.start,
230 max: usize::MAX as u64,
231 });
232 }
233 }
234 Ok(())
235 }
236
237 pub fn as_range(&self, len: u64) -> Result<Range<u64>, InvalidGetRange> {
239 self.is_valid()?;
240 match self {
241 Self::Bounded(r) => {
242 if r.start >= len {
243 Err(InvalidGetRange::StartTooLarge {
244 requested: r.start,
245 length: len,
246 })
247 } else if r.end > len {
248 Ok(r.start..len)
249 } else {
250 Ok(r.clone())
251 }
252 }
253 Self::Offset(o) => {
254 if *o >= len {
255 Err(InvalidGetRange::StartTooLarge {
256 requested: *o,
257 length: len,
258 })
259 } else {
260 Ok(*o..len)
261 }
262 }
263 Self::Suffix(n) => Ok(len.saturating_sub(*n)..len),
264 }
265 }
266}
267
268impl Display for GetRange {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 match self {
271 Self::Bounded(r) => write!(f, "bytes={}-{}", r.start, r.end - 1),
272 Self::Offset(o) => write!(f, "bytes={o}-"),
273 Self::Suffix(n) => write!(f, "bytes=-{n}"),
274 }
275 }
276}
277
278impl<T: RangeBounds<u64>> From<T> for GetRange {
279 fn from(value: T) -> Self {
280 use std::ops::Bound::*;
281 let first = match value.start_bound() {
282 Included(i) => *i,
283 Excluded(i) => i + 1,
284 Unbounded => 0,
285 };
286 match value.end_bound() {
287 Included(i) => Self::Bounded(first..(i + 1)),
288 Excluded(i) => Self::Bounded(first..*i),
289 Unbounded => Self::Offset(first),
290 }
291 }
292}
293#[cfg(any(feature = "aws-base", feature = "gcp-base"))]
298pub(crate) const STRICT_ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
299 .remove(b'-')
300 .remove(b'.')
301 .remove(b'_')
302 .remove(b'~');
303
304#[cfg(any(feature = "aws-base", feature = "gcp-base"))]
313pub(crate) fn append_strict_query_pairs(url: &mut url::Url, pairs: &[(String, String)]) {
314 use percent_encoding::utf8_percent_encode;
315 use std::fmt::Write;
316
317 if pairs.is_empty() {
318 return;
319 }
320
321 let mut query = url.query().unwrap_or_default().to_owned();
322 for (key, value) in pairs {
323 if !query.is_empty() {
324 query.push('&');
325 }
326 let _ = write!(
327 query,
328 "{}={}",
329 utf8_percent_encode(key, &STRICT_ENCODE_SET),
330 utf8_percent_encode(value, &STRICT_ENCODE_SET),
331 );
332 }
333 url.set_query(Some(&query));
334}
335
336#[cfg(any(feature = "aws-base", feature = "gcp-base"))]
340const RESERVED_SIGNED_HEADERS: [&str; 4] =
341 ["host", "authorization", "content-length", "user-agent"];
342
343#[cfg(any(feature = "aws-base", feature = "gcp-base"))]
356pub(crate) fn validate_signed_url_extras(
357 store: &'static str,
358 extra_query: &[(String, String)],
359 signed_headers: &http::HeaderMap,
360 reserved_query_prefix: &str,
361) -> Result<()> {
362 let err = |source: String| crate::Error::Generic {
363 store,
364 source: source.into(),
365 };
366
367 for (name, _) in extra_query {
368 if name.is_empty() {
369 return Err(err("query parameter name must not be empty".to_string()));
370 }
371 if name.to_ascii_lowercase().starts_with(reserved_query_prefix) {
372 return Err(err(format!(
373 "query parameter {name:?} is reserved for request signing and cannot be set via SignedUrlOptions"
374 )));
375 }
376 }
377
378 for (name, value) in signed_headers {
379 if RESERVED_SIGNED_HEADERS.contains(&name.as_str()) {
380 return Err(err(format!(
381 "header {name:?} is controlled by the signer and cannot be signed via SignedUrlOptions"
382 )));
383 }
384 if std::str::from_utf8(value.as_bytes()).is_err() {
385 return Err(err(format!(
386 "value of header {name:?} is not valid UTF-8 and cannot be signed via SignedUrlOptions"
387 )));
388 }
389 }
390
391 Ok(())
392}
393
394#[cfg(any(feature = "aws-base", feature = "gcp-base"))]
396pub(crate) fn hex_digest(
397 crypto: &dyn crate::client::CryptoProvider,
398 bytes: &[u8],
399) -> Result<String> {
400 let mut ctx = crypto.digest(crate::client::DigestAlgorithm::Sha256)?;
401 ctx.update(bytes);
402 Ok(hex_encode(ctx.finish()?))
403}
404
405#[cfg(any(feature = "aws-base", feature = "gcp-base"))]
407pub(crate) fn hex_encode(bytes: &[u8]) -> String {
408 use std::fmt::Write;
409 let mut out = String::with_capacity(bytes.len() * 2);
410 for byte in bytes {
411 let _ = write!(out, "{byte:02x}");
413 }
414 out
415}
416
417#[cfg(test)]
418mod tests {
419 use crate::Error;
420
421 use super::*;
422 use rand::{RngExt, rng};
423 use std::ops::Range;
424
425 async fn do_fetch(ranges: Vec<Range<u64>>, coalesce: u64) -> Vec<Range<u64>> {
429 let max = ranges.iter().map(|x| x.end).max().unwrap_or(0);
430 let src: Vec<_> = (0..max).map(|x| x as u8).collect();
431
432 let mut fetches = vec![];
433 let coalesced = coalesce_ranges::<_, Error, _>(
434 &ranges,
435 |range| {
436 fetches.push(range.clone());
437 let start = usize::try_from(range.start).unwrap();
438 let end = usize::try_from(range.end).unwrap();
439 futures_util::future::ready(Ok(Bytes::from(src[start..end].to_vec())))
440 },
441 coalesce,
442 )
443 .await
444 .unwrap();
445
446 assert_eq!(ranges.len(), coalesced.len());
447 for (range, bytes) in ranges.iter().zip(coalesced) {
448 assert_eq!(
449 bytes.as_ref(),
450 &src[usize::try_from(range.start).unwrap()..usize::try_from(range.end).unwrap()]
451 );
452 }
453 fetches
454 }
455
456 #[tokio::test]
457 async fn test_coalesce_ranges() {
458 let fetches = do_fetch(vec![], 0).await;
459 assert!(fetches.is_empty());
460
461 let fetches = do_fetch(vec![0..3; 1], 0).await;
462 assert_eq!(fetches, vec![0..3]);
463
464 let fetches = do_fetch(vec![0..2, 3..5], 0).await;
465 assert_eq!(fetches, vec![0..2, 3..5]);
466
467 let fetches = do_fetch(vec![0..1, 1..2], 0).await;
468 assert_eq!(fetches, vec![0..2]);
469
470 let fetches = do_fetch(vec![0..1, 2..72], 1).await;
471 assert_eq!(fetches, vec![0..72]);
472
473 let fetches = do_fetch(vec![0..1, 56..72, 73..75], 1).await;
474 assert_eq!(fetches, vec![0..1, 56..75]);
475
476 let fetches = do_fetch(vec![0..1, 5..6, 7..9, 2..3, 4..6], 1).await;
477 assert_eq!(fetches, vec![0..9]);
478
479 let fetches = do_fetch(vec![0..1, 5..6, 7..9, 2..3, 4..6], 1).await;
480 assert_eq!(fetches, vec![0..9]);
481
482 let fetches = do_fetch(vec![0..1, 6..7, 8..9, 10..14, 9..10], 4).await;
483 assert_eq!(fetches, vec![0..1, 6..14]);
484 }
485
486 #[tokio::test]
487 async fn test_coalesce_fuzz() {
488 let mut rand = rng();
489 for _ in 0..100 {
490 let object_len = rand.random_range(10..250);
491 let range_count = rand.random_range(0..10);
492 let ranges: Vec<_> = (0..range_count)
493 .map(|_| {
494 let start = rand.random_range(0..object_len);
495 let max_len = 20.min(object_len - start);
496 let len = rand.random_range(0..max_len);
497 start..start + len
498 })
499 .collect();
500
501 let coalesce = rand.random_range(1..5);
502 let fetches = do_fetch(ranges.clone(), coalesce).await;
503
504 for fetch in fetches.windows(2) {
505 assert!(
506 fetch[0].start <= fetch[1].start,
507 "fetches should be sorted, {:?} vs {:?}",
508 fetch[0],
509 fetch[1]
510 );
511
512 let delta = fetch[1].end - fetch[0].end;
513 assert!(
514 delta > coalesce,
515 "fetches should not overlap by {}, {:?} vs {:?} for {:?}",
516 coalesce,
517 fetch[0],
518 fetch[1],
519 ranges
520 );
521 }
522 }
523 }
524
525 #[test]
526 fn getrange_str() {
527 assert_eq!(GetRange::Offset(0).to_string(), "bytes=0-");
528 assert_eq!(GetRange::Bounded(10..19).to_string(), "bytes=10-18");
529 assert_eq!(GetRange::Suffix(10).to_string(), "bytes=-10");
530 }
531
532 #[test]
533 fn getrange_from() {
534 assert_eq!(Into::<GetRange>::into(10..15), GetRange::Bounded(10..15),);
535 assert_eq!(Into::<GetRange>::into(10..=15), GetRange::Bounded(10..16),);
536 assert_eq!(Into::<GetRange>::into(10..), GetRange::Offset(10),);
537 assert_eq!(Into::<GetRange>::into(..=15), GetRange::Bounded(0..16));
538 }
539
540 #[test]
541 fn test_as_range() {
542 let range = GetRange::Bounded(2..5);
543 assert_eq!(range.as_range(5).unwrap(), 2..5);
544
545 let range = range.as_range(4).unwrap();
546 assert_eq!(range, 2..4);
547
548 let range = GetRange::Bounded(3..3);
549 let err = range.as_range(2).unwrap_err().to_string();
550 assert_eq!(err, "Range started at 3 and ended at 3");
551
552 let range = GetRange::Bounded(2..2);
553 let err = range.as_range(3).unwrap_err().to_string();
554 assert_eq!(err, "Range started at 2 and ended at 2");
555
556 let range = GetRange::Suffix(3);
557 assert_eq!(range.as_range(3).unwrap(), 0..3);
558 assert_eq!(range.as_range(2).unwrap(), 0..2);
559
560 let range = GetRange::Suffix(0);
561 assert_eq!(range.as_range(0).unwrap(), 0..0);
562
563 let range = GetRange::Offset(2);
564 let err = range.as_range(2).unwrap_err().to_string();
565 assert_eq!(
566 err,
567 "Wanted range starting at 2, but object was only 2 bytes long"
568 );
569
570 let err = range.as_range(1).unwrap_err().to_string();
571 assert_eq!(
572 err,
573 "Wanted range starting at 2, but object was only 1 bytes long"
574 );
575
576 let range = GetRange::Offset(1);
577 assert_eq!(range.as_range(2).unwrap(), 1..2);
578 }
579
580 #[cfg(any(feature = "aws-base", feature = "gcp-base"))]
581 fn owned_pairs(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
582 pairs
583 .iter()
584 .map(|(k, v)| (k.to_string(), v.to_string()))
585 .collect()
586 }
587
588 #[cfg(any(feature = "aws-base", feature = "gcp-base"))]
589 #[test]
590 fn append_strict_query_pairs_uses_percent_encoding() {
591 let mut url = url::Url::parse("https://example.com/object").unwrap();
592 append_strict_query_pairs(
593 &mut url,
594 &owned_pairs(&[("a key", "a value"), ("plus", "a+b/c=d")]),
595 );
596 assert_eq!(url.query().unwrap(), "a%20key=a%20value&plus=a%2Bb%2Fc%3Dd");
598 append_strict_query_pairs(&mut url, &owned_pairs(&[("x", "y")]));
600 assert!(url.query().unwrap().ends_with("&x=y"));
601 let before = url.query().unwrap().to_owned();
603 append_strict_query_pairs(&mut url, &[]);
604 assert_eq!(url.query().unwrap(), before);
605 }
606
607 #[cfg(any(feature = "aws-base", feature = "gcp-base"))]
608 #[test]
609 fn validate_signed_url_extras_accepts_and_rejects() {
610 use http::{HeaderMap, HeaderName, HeaderValue};
611
612 let header = |name: &'static str| {
613 let mut h = HeaderMap::new();
614 h.insert(HeaderName::from_static(name), HeaderValue::from_static("v"));
615 h
616 };
617
618 validate_signed_url_extras(
620 "S3",
621 &owned_pairs(&[("partNumber", "1"), ("uploadId", "abc")]),
622 &header("content-type"),
623 "x-amz-",
624 )
625 .unwrap();
626
627 for key in ["X-Amz-Signature", "x-amz-expires", "X-Amz-Security-Token"] {
629 let err = validate_signed_url_extras(
630 "S3",
631 &owned_pairs(&[(key, "x")]),
632 &HeaderMap::new(),
633 "x-amz-",
634 )
635 .unwrap_err();
636 assert!(
637 matches!(err, Error::Generic { .. }),
638 "{key} should be rejected"
639 );
640 }
641 assert!(
643 validate_signed_url_extras(
644 "GCS",
645 &owned_pairs(&[("X-Goog-Signature", "x")]),
646 &HeaderMap::new(),
647 "x-goog-"
648 )
649 .is_err()
650 );
651
652 assert!(
654 validate_signed_url_extras(
655 "S3",
656 &owned_pairs(&[("", "x")]),
657 &HeaderMap::new(),
658 "x-amz-"
659 )
660 .is_err()
661 );
662
663 for name in ["host", "authorization", "content-length", "user-agent"] {
665 let err = validate_signed_url_extras("S3", &[], &header(name), "x-amz-").unwrap_err();
666 assert!(
667 matches!(err, Error::Generic { .. }),
668 "{name} should be rejected"
669 );
670 }
671
672 let mut non_utf8 = HeaderMap::new();
675 non_utf8.insert(
676 HeaderName::from_static("x-custom"),
677 HeaderValue::from_bytes(&[0xFF]).unwrap(),
678 );
679 let err = validate_signed_url_extras("S3", &[], &non_utf8, "x-amz-").unwrap_err();
680 assert!(
681 matches!(err, Error::Generic { .. }),
682 "non-UTF-8 header value should be rejected"
683 );
684 }
685}