1mod crc32c;
42mod ranges;
43mod wire;
44
45#[doc(hidden)]
46pub use wire::fuzz as fuzz_wire;
47
48use crate::encoding::{self, ContentEncoding, MAX_INFLATED_BYTES};
49use crate::key::{check_prefix, encode_component};
50use crate::net::{backoff, is_allowed_endpoint, is_retriable_status, transport_error};
51use crate::token::TokenProvider;
52use crate::{Bytes, Cond, Error, Key, Meta, Object, ObjectStore, Result, Stat};
53use std::future::Future;
54use std::ops::Range;
55use std::time::Duration;
56
57type Page<M> = (Vec<M>, Option<String>);
59
60const CHUNK_UNIT: usize = 256 << 10;
62
63#[derive(Debug, Clone)]
71pub struct GcsConfig {
72 pub endpoint: String,
74 pub connect_timeout: Duration,
76 pub request_timeout: Duration,
81 pub idle_timeout: Duration,
84 pub min_throughput: u64,
88 pub max_retries: u32,
91 pub max_object_bytes: u64,
93 pub resumable_threshold: usize,
95 pub upload_chunk: usize,
98}
99
100impl Default for GcsConfig {
101 fn default() -> Self {
102 Self {
103 endpoint: "https://storage.googleapis.com".into(),
104 connect_timeout: Duration::from_secs(10),
105 request_timeout: Duration::from_secs(30),
106 idle_timeout: Duration::from_secs(60),
107 min_throughput: 64 << 10,
108 max_retries: 5,
109 max_object_bytes: 4 << 30,
110 resumable_threshold: 8 << 20,
111 upload_chunk: 8 << 20,
112 }
113 }
114}
115
116impl GcsConfig {
117 fn check(&self) -> Result<()> {
118 let bad = |what| Err(Error::protocol(format!("invalid configuration: {what}")));
119 if [self.connect_timeout, self.request_timeout, self.idle_timeout].contains(&Duration::ZERO)
120 {
121 return bad("timeouts must be positive");
122 }
123 if self.min_throughput == 0 {
124 return bad("min_throughput must be positive");
125 }
126 if self.upload_chunk == 0 || !self.upload_chunk.is_multiple_of(CHUNK_UNIT) {
127 return bad("upload_chunk must be a positive multiple of 256 KiB");
128 }
129 Ok(())
130 }
131
132 fn upload_bound(&self, len: usize) -> Duration {
134 let ms = (len as u128 * 1000) / u128::from(self.min_throughput);
135 self.request_timeout
136 .saturating_add(Duration::from_millis(u64::try_from(ms).unwrap_or(u64::MAX)))
137 }
138}
139
140#[derive(Debug, Clone, Copy)]
142enum Bound {
143 Whole(Duration),
145 Headers(Duration),
148}
149
150enum Session {
152 Done(u16, reqwest::Response),
154 Gone,
156}
157
158#[derive(Debug)]
160pub struct Gcs<T: TokenProvider> {
161 http: reqwest::Client,
162 tokens: T,
163 cfg: GcsConfig,
164 api: String,
166 upload: String,
168 max_inflated: u64,
170}
171
172impl<T: TokenProvider> Gcs<T> {
173 pub fn new(bucket: &str, tokens: T, cfg: GcsConfig) -> Result<Self> {
175 if !is_valid_bucket(bucket) {
176 return Err(Error::InvalidKey { reason: "invalid bucket name" });
177 }
178 let endpoint = cfg.endpoint.trim_end_matches('/').to_owned();
179 if !is_allowed_endpoint(&endpoint) {
180 return Err(Error::protocol(
181 "the endpoint must be https:// (http:// only for a loopback emulator)",
182 ));
183 }
184 cfg.check()?;
185 let http = reqwest::Client::builder()
192 .connect_timeout(cfg.connect_timeout)
193 .redirect(reqwest::redirect::Policy::none())
194 .pool_idle_timeout(Duration::from_secs(90))
195 .user_agent(concat!("objstore-r/", env!("CARGO_PKG_VERSION")))
196 .build()
197 .map_err(|e| Error::unavailable(format!("http client: {e}"), false))?;
198 Ok(Self {
199 http,
200 tokens,
201 api: format!("{endpoint}/storage/v1/b/{bucket}"),
202 upload: format!("{endpoint}/upload/storage/v1/b/{bucket}"),
203 cfg,
204 max_inflated: MAX_INFLATED_BYTES,
205 })
206 }
207
208 #[must_use]
212 pub fn with_max_inflated(mut self, limit: u64) -> Self {
213 self.max_inflated = limit;
214 self
215 }
216
217 pub fn tokens(&self) -> &T {
219 &self.tokens
220 }
221
222 fn object_url(&self, key: &str) -> String {
223 let mut u = String::with_capacity(self.api.len() + key.len() * 3 + 4);
224 u.push_str(&self.api);
225 u.push_str("/o/");
226 encode_component(key, &mut u);
227 u
228 }
229
230 async fn authed(&self, rb: reqwest::RequestBuilder) -> Result<reqwest::RequestBuilder> {
231 let t = self.tokens.token().await?;
232 let mut rb = rb;
233 if !t.secret().is_empty() {
234 rb = rb.bearer_auth(t.secret());
235 }
236 if let Some(q) = self.tokens.quota_project() {
237 rb = rb.header("x-goog-user-project", q);
238 }
239 Ok(rb)
240 }
241
242 fn small(&self) -> Bound {
244 Bound::Whole(self.cfg.request_timeout)
245 }
246
247 fn download(&self) -> Bound {
250 Bound::Headers(self.cfg.request_timeout)
251 }
252
253 async fn dispatch(rb: reqwest::RequestBuilder, bound: Bound) -> Result<reqwest::Response> {
255 match bound {
256 Bound::Whole(d) => rb.timeout(d).send().await.map_err(|e| transport_error(&e)),
257 Bound::Headers(d) => match tokio::time::timeout(d, rb.send()).await {
258 Ok(r) => r.map_err(|e| transport_error(&e)),
259 Err(_) => Err(Error::unavailable("timed out", true)),
260 },
261 }
262 }
263
264 async fn send(
266 &self,
267 bound: Bound,
268 build: impl Fn() -> reqwest::RequestBuilder,
269 ) -> Result<reqwest::Response> {
270 let mut attempt = 0;
271 loop {
272 let rb = self.authed(build()).await?;
275 let outcome = Self::dispatch(rb, bound).await;
276 let retry = match &outcome {
277 Ok(r) => is_retriable_status(r.status().as_u16()),
278 Err(e) => e.is_retriable(),
279 };
280 if !retry || attempt >= self.cfg.max_retries {
281 return outcome;
282 }
283 attempt += 1;
284 tokio::time::sleep(backoff(attempt)).await;
285 }
286 }
287
288 async fn read_into(
292 &self,
293 resp: &mut reqwest::Response,
294 buf: &mut bytes::BytesMut,
295 ) -> Result<()> {
296 let limit = self.cfg.max_object_bytes;
297 loop {
298 let chunk = match tokio::time::timeout(self.cfg.idle_timeout, resp.chunk()).await {
299 Err(_) => {
300 return Err(Error::unavailable(
301 "stalled: no data within the idle timeout",
302 true,
303 ))
304 }
305 Ok(Err(_)) => return Err(Error::unavailable("connection lost mid-body", true)),
306 Ok(Ok(None)) => return Ok(()),
307 Ok(Ok(Some(c))) => c,
308 };
309 if (buf.len() + chunk.len()) as u64 > limit {
310 return Err(Error::TooLarge { limit });
311 }
312 buf.extend_from_slice(&chunk);
313 }
314 }
315
316 fn buffer(&self, len: Option<u64>) -> Result<bytes::BytesMut> {
318 let limit = self.cfg.max_object_bytes;
319 match len {
320 Some(n) if n > limit => Err(Error::TooLarge { limit }),
321 Some(n) => Ok(bytes::BytesMut::with_capacity(usize::try_from(n).unwrap_or(0))),
322 None => Ok(bytes::BytesMut::new()),
323 }
324 }
325
326 async fn body(&self, resp: reqwest::Response) -> Result<Bytes> {
327 let mut buf = self.buffer(resp.content_length())?;
328 let mut resp = resp;
329 self.read_into(&mut resp, &mut buf).await?;
330 Ok(buf.freeze())
331 }
332
333 async fn head_inner(&self, key: &str) -> Result<Option<Meta>> {
334 let url = format!("{}?fields=name,size,generation", self.object_url(key));
335 let resp = self.send(self.small(), || self.http.get(&url)).await?;
336 match resp.status().as_u16() {
337 404 => Ok(None),
338 200 => {
339 let body = self.body(resp).await?;
340 wire::parse_meta(&body).map(Some)
341 }
342 _ => Err(status_error(resp).await),
343 }
344 }
345
346 async fn stat_inner(&self, key: &str) -> Result<Option<Stat>> {
347 let url = format!("{}?fields=name,size,generation,contentEncoding", self.object_url(key));
348 let resp = self.send(self.small(), || self.http.get(&url)).await?;
349 match resp.status().as_u16() {
350 404 => Ok(None),
351 200 => {
352 let body = self.body(resp).await?;
353 wire::parse_stat(&body).map(Some)
354 }
355 _ => Err(status_error(resp).await),
356 }
357 }
358
359 async fn get_inner(&self, key: &str) -> Result<Option<Object>> {
360 let url = format!("{}?alt=media", self.object_url(key));
361 let resp = self.send(self.download(), || stored_bytes(self.http.get(&url))).await?;
362 match resp.status().as_u16() {
363 404 => Ok(None),
364 200 => {
365 let header_gen = generation_header(&resp);
366 let served = served_encoding(&resp)?;
367 let mut bytes = self.read_object(&url, resp, header_gen, served.is_some()).await?;
368 if let Some(e) = served {
369 bytes = encoding::decode(e, &bytes, self.max_inflated)?;
370 }
371 let generation = match header_gen {
372 Some(g) => g,
373 None => self.head_inner(key).await?.map_or(1, |m| m.generation),
375 };
376 Ok(Some(Object { bytes, generation }))
377 }
378 _ => Err(status_error(resp).await),
379 }
380 }
381
382 async fn read_object(
393 &self,
394 url: &str,
395 first: reqwest::Response,
396 generation: Option<u64>,
397 encoded: bool,
398 ) -> Result<Bytes> {
399 let total = first.content_length();
400 let ranged = !encoded && generation.is_some() && total.is_some();
401 let mut sum = stored_crc32c(&first);
402 let mut buf = self.buffer(total)?;
403 let mut resp = first;
404 let mut best = 0;
407 let mut stalls = 0;
408 loop {
409 let failure = match self.read_into(&mut resp, &mut buf).await {
410 Ok(()) if total.is_none_or(|t| buf.len() as u64 == t) => break,
411 Ok(()) => Error::unavailable("body ended early", true),
414 Err(e) if e.is_retriable() => e,
415 Err(e) => return Err(e),
416 };
417 stalls = if ranged && buf.len() > best { 0 } else { stalls + 1 };
418 best = best.max(buf.len());
419 if stalls > self.cfg.max_retries {
420 return Err(failure);
421 }
422 tokio::time::sleep(backoff(stalls.max(1))).await;
423 let from = if ranged { buf.len() as u64 } else { 0 };
424 resp = self.resume(url, generation, from, total).await?;
425 if resp.status().as_u16() == 200 {
426 buf.clear();
428 sum = stored_crc32c(&resp);
429 }
430 }
431 if sum.is_some_and(|want| crc32c::crc32c(&buf) != want) {
432 return Err(Error::corrupt("stored bytes fail their CRC-32C"));
433 }
434 Ok(buf.freeze())
435 }
436
437 async fn resume(
442 &self,
443 url: &str,
444 generation: Option<u64>,
445 from: u64,
446 total: Option<u64>,
447 ) -> Result<reqwest::Response> {
448 let pinned = match generation {
449 Some(g) => format!("{url}&ifGenerationMatch={g}"),
450 None => url.to_owned(),
451 };
452 let range = format!("bytes={from}-");
453 let resp = self
454 .send(self.download(), || {
455 let rb = stored_bytes(self.http.get(&pinned));
456 if from == 0 {
457 rb
458 } else {
459 rb.header("range", &range)
460 }
461 })
462 .await?;
463 let status = resp.status().as_u16();
464 let same = generation.is_none_or(|g| generation_header(&resp).is_none_or(|h| h == g));
465 match status {
466 412 | 404 => Err(Error::Changed),
467 200 | 206 if !same => Err(Error::Changed),
468 200 => Ok(resp),
469 206 => {
470 let span = resp
471 .headers()
472 .get("content-range")
473 .and_then(|v| v.to_str().ok())
474 .and_then(ranges::content_range);
475 match span {
476 Some((first, _, size)) if first == from && total.is_none_or(|t| t == size) => {
477 Ok(resp)
478 }
479 _ => Err(Error::protocol("a resumed read answered with another range")),
480 }
481 }
482 _ => Err(status_error(resp).await),
483 }
484 }
485
486 async fn range_inner(&self, key: &str, range: Range<u64>) -> Result<Bytes> {
487 if range.start >= range.end {
488 return match self.stat_inner(key).await? {
489 Some(Stat { content_encoding: Some(e), .. }) => {
490 Err(Error::EncodedRange { encoding: e.as_str().into() })
491 }
492 Some(_) => Ok(Bytes::new()),
493 None => Err(Error::NotFound),
494 };
495 }
496 let url = format!("{}?alt=media", self.object_url(key));
497 let header = format!("bytes={}-{}", range.start, range.end - 1);
498 let resp = self
501 .send(self.download(), || stored_bytes(self.http.get(&url).header("range", &header)))
502 .await?;
503 if matches!(resp.status().as_u16(), 200 | 206 | 416) {
504 if let Some(token) = stored_encoding(&resp) {
505 return Err(Error::EncodedRange { encoding: token });
506 }
507 }
508 match resp.status().as_u16() {
509 404 => Err(Error::NotFound),
510 416 => match self.stat_inner(key).await? {
513 Some(Stat { content_encoding: Some(e), .. }) => {
514 Err(Error::EncodedRange { encoding: e.as_str().into() })
515 }
516 _ => Ok(Bytes::new()),
517 },
518 206 => self.body(resp).await,
519 200 => {
521 let all = self.body(resp).await?;
522 Ok(all.slice(crate::clip(&range, all.len() as u64)))
523 }
524 _ => Err(status_error(resp).await),
525 }
526 }
527
528 async fn put_inner(
531 &self,
532 key: &str,
533 body: Bytes,
534 stored: Bytes,
535 cond: Cond,
536 encoding: Option<ContentEncoding>,
537 ) -> Result<u64> {
538 let mut query = String::from("name=");
539 encode_component(key, &mut query);
540 if let Some(e) = encoding {
541 query.push_str("&contentEncoding=");
542 query.push_str(e.as_str());
543 }
544 match cond {
545 Cond::None => {}
546 Cond::CreateOnly => query.push_str("&ifGenerationMatch=0"),
547 Cond::IfGeneration(g) => {
548 use std::fmt::Write;
549 let _ = write!(query, "&ifGenerationMatch={g}");
550 }
551 }
552 let (status, resp) = if stored.len() >= self.cfg.resumable_threshold {
553 self.put_resumable(&query, &stored).await?
554 } else {
555 let url = format!("{}/o?uploadType=media&{query}", self.upload);
556 let bound = Bound::Whole(self.cfg.upload_bound(stored.len()));
557 let resp = self
558 .send(bound, || {
559 self.http
560 .post(&url)
561 .header("content-type", "application/octet-stream")
562 .body(stored.clone())
563 })
564 .await?;
565 (resp.status().as_u16(), resp)
566 };
567 match status {
568 200 | 201 => {
569 let b = self.body(resp).await?;
570 wire::parse_meta(&b).map(|m| m.generation)
571 }
572 412 if cond != Cond::None => {
573 if let Some(o) = self.get_inner(key).await? {
577 if o.bytes == body
578 && !matches!(cond, Cond::IfGeneration(g) if g == o.generation)
579 {
580 return Ok(o.generation);
581 }
582 }
583 Err(if cond == Cond::CreateOnly {
584 Error::Conflict
585 } else {
586 Error::PreconditionFailed
587 })
588 }
589 _ => Err(status_error(resp).await),
590 }
591 }
592
593 async fn put_resumable(&self, query: &str, body: &Bytes) -> Result<(u16, reqwest::Response)> {
597 let init = format!("{}/o?uploadType=resumable&{query}", self.upload);
598 let total = body.len().to_string();
599 let mut restarted = false;
600 loop {
601 let resp = self
602 .send(self.small(), || {
603 self.http
604 .post(&init)
605 .header("content-type", "application/json")
606 .header("x-upload-content-type", "application/octet-stream")
607 .header("x-upload-content-length", &total)
608 .body("{}")
609 })
610 .await?;
611 let status = resp.status().as_u16();
612 if status != 200 && status != 201 {
613 return Ok((status, resp));
614 }
615 let session = resp
616 .headers()
617 .get("location")
618 .and_then(|v| v.to_str().ok())
619 .ok_or_else(|| Error::protocol("resumable upload without a session URL"))?;
620 if !self.is_upload_url(session) {
621 return Err(Error::protocol("resumable session URL on an unexpected host"));
622 }
623 match self.upload_chunks(session, body).await? {
624 Session::Done(status, resp) => return Ok((status, resp)),
625 Session::Gone if !restarted => restarted = true,
626 Session::Gone => {
627 return Err(Error::unavailable(
628 "resumable upload session expired again after a restart",
629 true,
630 ))
631 }
632 }
633 }
634 }
635
636 fn is_upload_url(&self, url: &str) -> bool {
639 let after_scheme = self.upload.find("://").map_or(0, |i| i + 3);
643 let origin = self.upload[after_scheme..]
644 .find('/')
645 .map_or(self.upload.as_str(), |i| &self.upload[..after_scheme + i]);
646 url.strip_prefix(origin).is_some_and(|rest| rest.starts_with('/'))
647 }
648
649 async fn upload_chunks(&self, session: &str, body: &Bytes) -> Result<Session> {
658 let total = body.len() as u64;
659 let mut acked = 0u64;
661 let mut stalls = 0u32;
662 let mut failed = false;
663 loop {
664 let resp = if failed {
665 let rb = self
667 .http
668 .put(session)
669 .header("content-range", format!("bytes */{total}"))
670 .body(Bytes::new());
671 Self::dispatch(self.authed(rb).await?, self.small()).await
672 } else {
673 let end = acked.saturating_add(self.cfg.upload_chunk as u64).min(total);
674 let piece = body.slice(span(acked, end));
675 let range = if piece.is_empty() {
676 format!("bytes */{total}")
677 } else {
678 format!("bytes {acked}-{}/{total}", end - 1)
679 };
680 let bound = Bound::Whole(self.cfg.upload_bound(piece.len()));
681 let rb = self.http.put(session).header("content-range", range).body(piece);
682 Self::dispatch(self.authed(rb).await?, bound).await
683 };
684 let error = match resp {
685 Ok(r) => match r.status().as_u16() {
686 308 => {
687 let range = r.headers().get("range").map(|v| v.to_str().unwrap_or(""));
688 let now = ranges::committed(range, total)
689 .filter(|n| *n >= acked)
690 .ok_or_else(|| {
691 Error::protocol("resumable session reported an impossible offset")
692 })?;
693 let progressed = now > acked;
697 acked = now;
698 if progressed {
699 stalls = 0;
700 }
701 if progressed || failed {
702 failed = false;
703 continue;
704 }
705 Error::unavailable("resumable session made no progress", true)
706 }
707 404 | 410 => return Ok(Session::Gone),
708 s if is_retriable_status(s) => status_error(r).await,
709 s => return Ok(Session::Done(s, r)),
710 },
711 Err(e) if e.is_retriable() => e,
712 Err(e) => return Err(e),
713 };
714 stalls += 1;
715 if stalls > self.cfg.max_retries {
716 return Err(error);
717 }
718 failed = true;
719 tokio::time::sleep(backoff(stalls)).await;
720 }
721 }
722
723 async fn list_inner(&self, prefix: &str) -> Result<Vec<Meta>> {
724 self.list_pages(prefix, "items(name,size,generation)", wire::parse_list, |m| &m.key).await
725 }
726
727 async fn list_stat_inner(&self, prefix: &str) -> Result<Vec<Stat>> {
728 self.list_pages(
729 prefix,
730 "items(name,size,generation,contentEncoding)",
731 wire::parse_list_stat,
732 |s| &s.meta.key,
733 )
734 .await
735 }
736
737 async fn list_pages<M: Ord>(
739 &self,
740 prefix: &str,
741 items: &str,
742 parse: fn(&[u8]) -> Result<Page<M>>,
743 key_of: fn(&M) -> &String,
744 ) -> Result<Vec<M>> {
745 let mut out = Vec::new();
746 let mut page: Option<String> = None;
747 loop {
748 let mut url = format!("{}/o?fields={items},nextPageToken&maxResults=1000", self.api);
749 if !prefix.is_empty() {
750 url.push_str("&prefix=");
751 encode_component(prefix, &mut url);
752 }
753 if let Some(p) = &page {
754 url.push_str("&pageToken=");
755 encode_component(p, &mut url);
756 }
757 let resp = self.send(self.small(), || self.http.get(&url)).await?;
758 if resp.status().as_u16() != 200 {
759 return Err(status_error(resp).await);
760 }
761 let body = self.body(resp).await?;
762 let (items, next) = parse(&body)?;
763 out.extend(items.into_iter().filter(|m| {
765 let k = key_of(m);
766 Key::parse(k).is_ok() && k.starts_with(prefix)
767 }));
768 match next {
769 Some(n) if !n.is_empty() => page = Some(n),
770 _ => break,
771 }
772 }
773 out.sort();
774 Ok(out)
775 }
776
777 async fn delete_inner(&self, key: &str) -> Result<()> {
778 let url = self.object_url(key);
779 let resp = self.send(self.small(), || self.http.delete(&url)).await?;
780 match resp.status().as_u16() {
781 200 | 204 | 404 => Ok(()),
782 _ => Err(status_error(resp).await),
783 }
784 }
785}
786
787impl<T: TokenProvider> ObjectStore for Gcs<T> {
788 type Get<'a>
789 = impl Future<Output = Result<Option<Object>>> + 'a
790 where
791 Self: 'a;
792 type GetRange<'a>
793 = impl Future<Output = Result<Bytes>> + 'a
794 where
795 Self: 'a;
796 type Head<'a>
797 = impl Future<Output = Result<Option<Meta>>> + 'a
798 where
799 Self: 'a;
800 type Put<'a>
801 = impl Future<Output = Result<u64>> + 'a
802 where
803 Self: 'a;
804 type List<'a>
805 = impl Future<Output = Result<Vec<Meta>>> + 'a
806 where
807 Self: 'a;
808 type Delete<'a>
809 = impl Future<Output = Result<()>> + 'a
810 where
811 Self: 'a;
812
813 fn get<'a>(&'a self, key: &'a str) -> Self::Get<'a> {
814 async move { self.get_inner(Key::parse(key)?.as_str()).await }
815 }
816
817 fn get_range<'a>(&'a self, key: &'a str, range: Range<u64>) -> Self::GetRange<'a> {
818 async move { self.range_inner(Key::parse(key)?.as_str(), range).await }
819 }
820
821 fn head<'a>(&'a self, key: &'a str) -> Self::Head<'a> {
822 async move { self.head_inner(Key::parse(key)?.as_str()).await }
823 }
824
825 fn put<'a>(&'a self, key: &'a str, body: Bytes, cond: Cond) -> Self::Put<'a> {
826 async move { self.put_inner(Key::parse(key)?.as_str(), body.clone(), body, cond, None).await }
827 }
828
829 fn list<'a>(&'a self, prefix: &'a str) -> Self::List<'a> {
830 async move {
831 check_prefix(prefix)?;
832 self.list_inner(prefix).await
833 }
834 }
835
836 fn delete<'a>(&'a self, key: &'a str) -> Self::Delete<'a> {
837 async move { self.delete_inner(Key::parse(key)?.as_str()).await }
838 }
839
840 async fn put_encoded<'a>(
841 &'a self,
842 key: &'a str,
843 body: Bytes,
844 cond: Cond,
845 encoding: ContentEncoding,
846 ) -> Result<u64> {
847 let key = Key::parse(key)?.as_str();
848 let stored = encoding::encode(encoding, &body)?;
849 self.put_inner(key, body, stored, cond, Some(encoding)).await
850 }
851
852 async fn stat<'a>(&'a self, key: &'a str) -> Result<Option<Stat>> {
853 self.stat_inner(Key::parse(key)?.as_str()).await
854 }
855
856 async fn list_stat<'a>(&'a self, prefix: &'a str) -> Result<Vec<Stat>> {
857 check_prefix(prefix)?;
858 self.list_stat_inner(prefix).await
859 }
860}
861
862fn span(a: u64, b: u64) -> Range<usize> {
864 usize::try_from(a).unwrap_or(usize::MAX)..usize::try_from(b).unwrap_or(usize::MAX)
865}
866
867pub(crate) fn is_valid_bucket(b: &str) -> bool {
870 (3..=222).contains(&b.len())
871 && b.bytes().all(|c| {
872 c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, b'.' | b'_' | b'-')
873 })
874 && b.bytes().next().is_some_and(|c| c.is_ascii_alphanumeric())
875 && b.bytes().last().is_some_and(|c| c.is_ascii_alphanumeric())
876}
877
878fn generation_header(resp: &reqwest::Response) -> Option<u64> {
879 resp.headers()
880 .get("x-goog-generation")
881 .and_then(|v| v.to_str().ok())
882 .and_then(|s| s.parse().ok())
883 .filter(|g| *g != 0)
884}
885
886fn stored_bytes(req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
891 if cfg!(feature = "gzip") {
892 req.header("accept-encoding", "gzip")
893 } else {
894 req
895 }
896}
897
898fn served_encoding(resp: &reqwest::Response) -> Result<Option<ContentEncoding>> {
901 match resp.headers().get("content-encoding") {
902 None => Ok(None),
903 Some(v) => ContentEncoding::parse(
904 v.to_str().map_err(|_| Error::protocol("unreadable content-encoding"))?,
905 ),
906 }
907}
908
909fn stored_encoding(resp: &reqwest::Response) -> Option<String> {
912 ["x-goog-stored-content-encoding", "content-encoding"].iter().find_map(|h| {
913 let v = resp.headers().get(*h)?.to_str().unwrap_or("unreadable").trim();
914 (!v.is_empty() && !v.eq_ignore_ascii_case("identity")).then(|| v.chars().take(64).collect())
915 })
916}
917
918fn stored_crc32c(resp: &reqwest::Response) -> Option<u32> {
920 let h = resp.headers();
921 let served = h.get("content-encoding").and_then(|v| v.to_str().ok());
922 let stored = h.get("x-goog-stored-content-encoding").and_then(|v| v.to_str().ok());
925 if stored.is_some_and(|s| Some(s) != served) {
926 return None;
927 }
928 crc32c::from_goog_hash(h.get_all("x-goog-hash").iter().filter_map(|v| v.to_str().ok()))
929}
930
931async fn status_error(resp: reqwest::Response) -> Error {
932 let status = resp.status().as_u16();
933 let body = resp.bytes().await.unwrap_or_default();
934 crate::net::status_error(status, &body)
935}
936
937#[cfg(test)]
938mod tests {
939 use super::*;
940
941 #[test]
942 fn only_a_build_that_decodes_asks_for_stored_bytes() {
943 let req = stored_bytes(reqwest::Client::new().get("http://127.0.0.1/o")).build().unwrap();
946 let asked = req.headers().get("accept-encoding").and_then(|v| v.to_str().ok());
947 assert_eq!(asked, cfg!(feature = "gzip").then_some("gzip"));
948 }
949
950 #[test]
951 fn session_urls_stay_on_the_upload_origin() {
952 let g = Gcs::new("bkt", crate::token::Static::none(), GcsConfig::default()).unwrap();
953 let ok = "https://storage.googleapis.com/upload/storage/v1/b/bkt/o?uploadType=resumable&upload_id=x";
954 assert!(g.is_upload_url(ok));
955 for bad in [
956 "https://storage.googleapis.com.evil.example/upload/x",
957 "https://storage.googleapis.com@evil.example/upload/x",
958 "https://storage.googleapis.com:8443/upload/x",
959 "http://storage.googleapis.com/upload/x",
960 "https://evil.example/upload/storage/v1/b/bkt/o",
961 "https://storage.googleapis.com",
962 "",
963 ] {
964 assert!(!g.is_upload_url(bad), "{bad}");
965 }
966 }
967
968 #[test]
969 fn upload_bounds_grow_with_the_body() {
970 let c = GcsConfig::default();
971 assert_eq!(c.upload_bound(0), Duration::from_secs(30));
972 assert_eq!(c.upload_bound(8 << 20), Duration::from_secs(158));
974 let floor = GcsConfig { min_throughput: 1, ..GcsConfig::default() };
975 assert!(floor.upload_bound(usize::MAX) > Duration::from_secs(1 << 40), "saturates");
976 }
977
978 #[test]
979 fn bucket_names_and_endpoints() {
980 assert!(is_valid_bucket("my-project-sayasaya-state") && is_valid_bucket("a.b_c"));
981 for bad in ["ab", "-ab", "ab-", "AB", "a/b", &"a".repeat(223)] {
982 assert!(!is_valid_bucket(bad), "{bad}");
983 }
984 let cfg = |e: &str| GcsConfig { endpoint: e.into(), ..GcsConfig::default() };
985 let t = crate::token::Static::none;
986 assert!(
987 Gcs::new("bkt", t(), cfg("http://example.com")).is_err(),
988 "plain http to a remote host is refused"
989 );
990 assert!(Gcs::new("bkt", t(), cfg("http://127.0.0.1:4443")).is_ok());
991 assert!(Gcs::new("bkt", t(), cfg("https://storage.googleapis.com/")).is_ok());
992 }
993}