1mod crc32c;
33mod wire;
34
35#[doc(hidden)]
36pub use wire::fuzz as fuzz_wire;
37
38use crate::encoding::{self, ContentEncoding, MAX_INFLATED_BYTES};
39use crate::key::{check_prefix, encode_component};
40use crate::net::{backoff, is_allowed_endpoint, is_retriable_status, transport_error};
41use crate::token::TokenProvider;
42use crate::{Bytes, Cond, Error, Key, Meta, Object, ObjectStore, Result, Stat};
43use std::future::Future;
44use std::ops::Range;
45use std::time::Duration;
46
47type Page<M> = (Vec<M>, Option<String>);
49
50#[derive(Debug, Clone)]
52pub struct GcsConfig {
53 pub endpoint: String,
55 pub timeout: Duration,
57 pub max_retries: u32,
59 pub max_object_bytes: u64,
61 pub resumable_threshold: usize,
63}
64
65impl Default for GcsConfig {
66 fn default() -> Self {
67 Self {
68 endpoint: "https://storage.googleapis.com".into(),
69 timeout: Duration::from_secs(60),
70 max_retries: 5,
71 max_object_bytes: 4 << 30,
72 resumable_threshold: 8 << 20,
73 }
74 }
75}
76
77#[derive(Debug)]
79pub struct Gcs<T: TokenProvider> {
80 http: reqwest::Client,
81 tokens: T,
82 cfg: GcsConfig,
83 api: String,
85 upload: String,
87 max_inflated: u64,
89}
90
91impl<T: TokenProvider> Gcs<T> {
92 pub fn new(bucket: &str, tokens: T, cfg: GcsConfig) -> Result<Self> {
94 if !is_valid_bucket(bucket) {
95 return Err(Error::InvalidKey { reason: "invalid bucket name" });
96 }
97 let endpoint = cfg.endpoint.trim_end_matches('/').to_owned();
98 if !is_allowed_endpoint(&endpoint) {
99 return Err(Error::protocol(
100 "the endpoint must be https:// (http:// only for a loopback emulator)",
101 ));
102 }
103 let http = reqwest::Client::builder()
104 .timeout(cfg.timeout)
105 .redirect(reqwest::redirect::Policy::none())
106 .pool_idle_timeout(Duration::from_secs(90))
107 .user_agent(concat!("objstore-r/", env!("CARGO_PKG_VERSION")))
108 .build()
109 .map_err(|e| Error::unavailable(format!("http client: {e}"), false))?;
110 Ok(Self {
111 http,
112 tokens,
113 api: format!("{endpoint}/storage/v1/b/{bucket}"),
114 upload: format!("{endpoint}/upload/storage/v1/b/{bucket}"),
115 cfg,
116 max_inflated: MAX_INFLATED_BYTES,
117 })
118 }
119
120 #[must_use]
124 pub fn with_max_inflated(mut self, limit: u64) -> Self {
125 self.max_inflated = limit;
126 self
127 }
128
129 pub fn tokens(&self) -> &T {
131 &self.tokens
132 }
133
134 fn object_url(&self, key: &str) -> String {
135 let mut u = String::with_capacity(self.api.len() + key.len() * 3 + 4);
136 u.push_str(&self.api);
137 u.push_str("/o/");
138 encode_component(key, &mut u);
139 u
140 }
141
142 async fn authed(&self, rb: reqwest::RequestBuilder) -> Result<reqwest::RequestBuilder> {
143 let t = self.tokens.token().await?;
144 let mut rb = rb;
145 if !t.secret().is_empty() {
146 rb = rb.bearer_auth(t.secret());
147 }
148 if let Some(q) = self.tokens.quota_project() {
149 rb = rb.header("x-goog-user-project", q);
150 }
151 Ok(rb)
152 }
153
154 async fn send(&self, build: impl Fn() -> reqwest::RequestBuilder) -> Result<reqwest::Response> {
156 let mut attempt = 0;
157 loop {
158 let rb = self.authed(build()).await?;
159 let outcome = rb.send().await;
160 let retry = match &outcome {
161 Ok(r) => is_retriable_status(r.status().as_u16()),
162 Err(e) => e.is_timeout() || e.is_connect() || e.is_request(),
163 };
164 if !retry || attempt >= self.cfg.max_retries {
165 return outcome.map_err(|e| transport_error(&e));
166 }
167 attempt += 1;
168 tokio::time::sleep(backoff(attempt)).await;
169 }
170 }
171
172 async fn body(&self, resp: reqwest::Response) -> Result<Bytes> {
173 if resp.content_length().is_some_and(|n| n > self.cfg.max_object_bytes) {
174 return Err(Error::TooLarge { limit: self.cfg.max_object_bytes });
175 }
176 let mut resp = resp;
177 let mut buf = bytes::BytesMut::new();
178 while let Some(chunk) = resp.chunk().await.map_err(|e| transport_error(&e))? {
179 if (buf.len() + chunk.len()) as u64 > self.cfg.max_object_bytes {
180 return Err(Error::TooLarge { limit: self.cfg.max_object_bytes });
181 }
182 buf.extend_from_slice(&chunk);
183 }
184 Ok(buf.freeze())
185 }
186
187 async fn head_inner(&self, key: &str) -> Result<Option<Meta>> {
188 let url = format!("{}?fields=name,size,generation", self.object_url(key));
189 let resp = self.send(|| self.http.get(&url)).await?;
190 match resp.status().as_u16() {
191 404 => Ok(None),
192 200 => {
193 let body = self.body(resp).await?;
194 wire::parse_meta(&body).map(Some)
195 }
196 _ => Err(status_error(resp).await),
197 }
198 }
199
200 async fn stat_inner(&self, key: &str) -> Result<Option<Stat>> {
201 let url = format!("{}?fields=name,size,generation,contentEncoding", self.object_url(key));
202 let resp = self.send(|| self.http.get(&url)).await?;
203 match resp.status().as_u16() {
204 404 => Ok(None),
205 200 => {
206 let body = self.body(resp).await?;
207 wire::parse_stat(&body).map(Some)
208 }
209 _ => Err(status_error(resp).await),
210 }
211 }
212
213 async fn get_inner(&self, key: &str) -> Result<Option<Object>> {
214 let url = format!("{}?alt=media", self.object_url(key));
215 let resp = self.send(|| stored_bytes(self.http.get(&url))).await?;
216 match resp.status().as_u16() {
217 404 => Ok(None),
218 200 => {
219 let header_gen = generation_header(&resp);
220 let served = served_encoding(&resp)?;
221 let sum = served.and_then(|_| stored_crc32c(&resp));
222 let mut bytes = self.body(resp).await?;
223 if let Some(e) = served {
224 if sum.is_some_and(|want| crc32c::crc32c(&bytes) != want) {
227 return Err(Error::corrupt(format!(
228 "{key}: stored bytes fail their CRC-32C"
229 )));
230 }
231 bytes = encoding::decode(e, &bytes, self.max_inflated)?;
232 }
233 let generation = match header_gen {
234 Some(g) => g,
235 None => self.head_inner(key).await?.map_or(1, |m| m.generation),
237 };
238 Ok(Some(Object { bytes, generation }))
239 }
240 _ => Err(status_error(resp).await),
241 }
242 }
243
244 async fn range_inner(&self, key: &str, range: Range<u64>) -> Result<Bytes> {
245 if range.start >= range.end {
246 return match self.stat_inner(key).await? {
247 Some(Stat { content_encoding: Some(e), .. }) => {
248 Err(Error::EncodedRange { encoding: e.as_str().into() })
249 }
250 Some(_) => Ok(Bytes::new()),
251 None => Err(Error::NotFound),
252 };
253 }
254 let url = format!("{}?alt=media", self.object_url(key));
255 let header = format!("bytes={}-{}", range.start, range.end - 1);
256 let resp = self.send(|| stored_bytes(self.http.get(&url).header("range", &header))).await?;
259 if matches!(resp.status().as_u16(), 200 | 206 | 416) {
260 if let Some(token) = stored_encoding(&resp) {
261 return Err(Error::EncodedRange { encoding: token });
262 }
263 }
264 match resp.status().as_u16() {
265 404 => Err(Error::NotFound),
266 416 => match self.stat_inner(key).await? {
269 Some(Stat { content_encoding: Some(e), .. }) => {
270 Err(Error::EncodedRange { encoding: e.as_str().into() })
271 }
272 _ => Ok(Bytes::new()),
273 },
274 206 => self.body(resp).await,
275 200 => {
277 let all = self.body(resp).await?;
278 Ok(all.slice(crate::clip(&range, all.len() as u64)))
279 }
280 _ => Err(status_error(resp).await),
281 }
282 }
283
284 async fn put_inner(
287 &self,
288 key: &str,
289 body: Bytes,
290 stored: Bytes,
291 cond: Cond,
292 encoding: Option<ContentEncoding>,
293 ) -> Result<u64> {
294 let mut query = String::from("name=");
295 encode_component(key, &mut query);
296 if let Some(e) = encoding {
297 query.push_str("&contentEncoding=");
298 query.push_str(e.as_str());
299 }
300 match cond {
301 Cond::None => {}
302 Cond::CreateOnly => query.push_str("&ifGenerationMatch=0"),
303 Cond::IfGeneration(g) => {
304 use std::fmt::Write;
305 let _ = write!(query, "&ifGenerationMatch={g}");
306 }
307 }
308 let (status, resp) = if stored.len() >= self.cfg.resumable_threshold {
309 self.put_resumable(&query, &stored).await?
310 } else {
311 let url = format!("{}/o?uploadType=media&{query}", self.upload);
312 let resp = self
313 .send(|| {
314 self.http
315 .post(&url)
316 .header("content-type", "application/octet-stream")
317 .body(stored.clone())
318 })
319 .await?;
320 (resp.status().as_u16(), resp)
321 };
322 match status {
323 200 | 201 => {
324 let b = self.body(resp).await?;
325 wire::parse_meta(&b).map(|m| m.generation)
326 }
327 412 if cond != Cond::None => {
328 if let Some(o) = self.get_inner(key).await? {
332 if o.bytes == body
333 && !matches!(cond, Cond::IfGeneration(g) if g == o.generation)
334 {
335 return Ok(o.generation);
336 }
337 }
338 Err(if cond == Cond::CreateOnly {
339 Error::Conflict
340 } else {
341 Error::PreconditionFailed
342 })
343 }
344 _ => Err(status_error(resp).await),
345 }
346 }
347
348 async fn put_resumable(&self, query: &str, body: &Bytes) -> Result<(u16, reqwest::Response)> {
349 let init = format!("{}/o?uploadType=resumable&{query}", self.upload);
350 let resp = self
351 .send(|| {
352 self.http
353 .post(&init)
354 .header("content-type", "application/json")
355 .header("x-upload-content-type", "application/octet-stream")
356 .header("x-upload-content-length", body.len().to_string())
357 .body("{}")
358 })
359 .await?;
360 let status = resp.status().as_u16();
361 if status != 200 && status != 201 {
362 return Ok((status, resp));
363 }
364 let session = resp
365 .headers()
366 .get("location")
367 .and_then(|v| v.to_str().ok())
368 .map(str::to_owned)
369 .ok_or_else(|| Error::protocol("resumable upload without a session URL"))?;
370 if !(session.starts_with(&self.upload) || session.starts_with("https://")) {
371 return Err(Error::protocol("resumable session URL on an unexpected host"));
372 }
373 let resp = self.send(|| self.http.put(&session).body(body.clone())).await?;
374 Ok((resp.status().as_u16(), resp))
375 }
376
377 async fn list_inner(&self, prefix: &str) -> Result<Vec<Meta>> {
378 self.list_pages(prefix, "items(name,size,generation)", wire::parse_list, |m| &m.key).await
379 }
380
381 async fn list_stat_inner(&self, prefix: &str) -> Result<Vec<Stat>> {
382 self.list_pages(
383 prefix,
384 "items(name,size,generation,contentEncoding)",
385 wire::parse_list_stat,
386 |s| &s.meta.key,
387 )
388 .await
389 }
390
391 async fn list_pages<M: Ord>(
393 &self,
394 prefix: &str,
395 items: &str,
396 parse: fn(&[u8]) -> Result<Page<M>>,
397 key_of: fn(&M) -> &String,
398 ) -> Result<Vec<M>> {
399 let mut out = Vec::new();
400 let mut page: Option<String> = None;
401 loop {
402 let mut url = format!("{}/o?fields={items},nextPageToken&maxResults=1000", self.api);
403 if !prefix.is_empty() {
404 url.push_str("&prefix=");
405 encode_component(prefix, &mut url);
406 }
407 if let Some(p) = &page {
408 url.push_str("&pageToken=");
409 encode_component(p, &mut url);
410 }
411 let resp = self.send(|| self.http.get(&url)).await?;
412 if resp.status().as_u16() != 200 {
413 return Err(status_error(resp).await);
414 }
415 let body = self.body(resp).await?;
416 let (items, next) = parse(&body)?;
417 out.extend(items.into_iter().filter(|m| {
419 let k = key_of(m);
420 Key::parse(k).is_ok() && k.starts_with(prefix)
421 }));
422 match next {
423 Some(n) if !n.is_empty() => page = Some(n),
424 _ => break,
425 }
426 }
427 out.sort();
428 Ok(out)
429 }
430
431 async fn delete_inner(&self, key: &str) -> Result<()> {
432 let url = self.object_url(key);
433 let resp = self.send(|| self.http.delete(&url)).await?;
434 match resp.status().as_u16() {
435 200 | 204 | 404 => Ok(()),
436 _ => Err(status_error(resp).await),
437 }
438 }
439}
440
441impl<T: TokenProvider> ObjectStore for Gcs<T> {
442 type Get<'a>
443 = impl Future<Output = Result<Option<Object>>> + 'a
444 where
445 Self: 'a;
446 type GetRange<'a>
447 = impl Future<Output = Result<Bytes>> + 'a
448 where
449 Self: 'a;
450 type Head<'a>
451 = impl Future<Output = Result<Option<Meta>>> + 'a
452 where
453 Self: 'a;
454 type Put<'a>
455 = impl Future<Output = Result<u64>> + 'a
456 where
457 Self: 'a;
458 type List<'a>
459 = impl Future<Output = Result<Vec<Meta>>> + 'a
460 where
461 Self: 'a;
462 type Delete<'a>
463 = impl Future<Output = Result<()>> + 'a
464 where
465 Self: 'a;
466
467 fn get<'a>(&'a self, key: &'a str) -> Self::Get<'a> {
468 async move { self.get_inner(Key::parse(key)?.as_str()).await }
469 }
470
471 fn get_range<'a>(&'a self, key: &'a str, range: Range<u64>) -> Self::GetRange<'a> {
472 async move { self.range_inner(Key::parse(key)?.as_str(), range).await }
473 }
474
475 fn head<'a>(&'a self, key: &'a str) -> Self::Head<'a> {
476 async move { self.head_inner(Key::parse(key)?.as_str()).await }
477 }
478
479 fn put<'a>(&'a self, key: &'a str, body: Bytes, cond: Cond) -> Self::Put<'a> {
480 async move { self.put_inner(Key::parse(key)?.as_str(), body.clone(), body, cond, None).await }
481 }
482
483 fn list<'a>(&'a self, prefix: &'a str) -> Self::List<'a> {
484 async move {
485 check_prefix(prefix)?;
486 self.list_inner(prefix).await
487 }
488 }
489
490 fn delete<'a>(&'a self, key: &'a str) -> Self::Delete<'a> {
491 async move { self.delete_inner(Key::parse(key)?.as_str()).await }
492 }
493
494 async fn put_encoded<'a>(
495 &'a self,
496 key: &'a str,
497 body: Bytes,
498 cond: Cond,
499 encoding: ContentEncoding,
500 ) -> Result<u64> {
501 let key = Key::parse(key)?.as_str();
502 let stored = encoding::encode(encoding, &body)?;
503 self.put_inner(key, body, stored, cond, Some(encoding)).await
504 }
505
506 async fn stat<'a>(&'a self, key: &'a str) -> Result<Option<Stat>> {
507 self.stat_inner(Key::parse(key)?.as_str()).await
508 }
509
510 async fn list_stat<'a>(&'a self, prefix: &'a str) -> Result<Vec<Stat>> {
511 check_prefix(prefix)?;
512 self.list_stat_inner(prefix).await
513 }
514}
515
516pub(crate) fn is_valid_bucket(b: &str) -> bool {
519 (3..=222).contains(&b.len())
520 && b.bytes().all(|c| {
521 c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, b'.' | b'_' | b'-')
522 })
523 && b.bytes().next().is_some_and(|c| c.is_ascii_alphanumeric())
524 && b.bytes().last().is_some_and(|c| c.is_ascii_alphanumeric())
525}
526
527fn generation_header(resp: &reqwest::Response) -> Option<u64> {
528 resp.headers()
529 .get("x-goog-generation")
530 .and_then(|v| v.to_str().ok())
531 .and_then(|s| s.parse().ok())
532 .filter(|g| *g != 0)
533}
534
535fn stored_bytes(req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
540 if cfg!(feature = "gzip") {
541 req.header("accept-encoding", "gzip")
542 } else {
543 req
544 }
545}
546
547fn served_encoding(resp: &reqwest::Response) -> Result<Option<ContentEncoding>> {
550 match resp.headers().get("content-encoding") {
551 None => Ok(None),
552 Some(v) => ContentEncoding::parse(
553 v.to_str().map_err(|_| Error::protocol("unreadable content-encoding"))?,
554 ),
555 }
556}
557
558fn stored_encoding(resp: &reqwest::Response) -> Option<String> {
561 ["x-goog-stored-content-encoding", "content-encoding"].iter().find_map(|h| {
562 let v = resp.headers().get(*h)?.to_str().unwrap_or("unreadable").trim();
563 (!v.is_empty() && !v.eq_ignore_ascii_case("identity")).then(|| v.chars().take(64).collect())
564 })
565}
566
567fn stored_crc32c(resp: &reqwest::Response) -> Option<u32> {
569 let h = resp.headers();
570 let served = h.get("content-encoding").and_then(|v| v.to_str().ok());
571 let stored = h.get("x-goog-stored-content-encoding").and_then(|v| v.to_str().ok());
574 if stored.is_some_and(|s| Some(s) != served) {
575 return None;
576 }
577 crc32c::from_goog_hash(h.get_all("x-goog-hash").iter().filter_map(|v| v.to_str().ok()))
578}
579
580async fn status_error(resp: reqwest::Response) -> Error {
581 let status = resp.status().as_u16();
582 let body = resp.bytes().await.unwrap_or_default();
583 crate::net::status_error(status, &body)
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589
590 #[test]
591 fn only_a_build_that_decodes_asks_for_stored_bytes() {
592 let req = stored_bytes(reqwest::Client::new().get("http://127.0.0.1/o")).build().unwrap();
595 let asked = req.headers().get("accept-encoding").and_then(|v| v.to_str().ok());
596 assert_eq!(asked, cfg!(feature = "gzip").then_some("gzip"));
597 }
598
599 #[test]
600 fn bucket_names_and_endpoints() {
601 assert!(is_valid_bucket("my-project-sayasaya-state") && is_valid_bucket("a.b_c"));
602 for bad in ["ab", "-ab", "ab-", "AB", "a/b", &"a".repeat(223)] {
603 assert!(!is_valid_bucket(bad), "{bad}");
604 }
605 let cfg = |e: &str| GcsConfig { endpoint: e.into(), ..GcsConfig::default() };
606 let t = crate::token::Static::none;
607 assert!(
608 Gcs::new("bkt", t(), cfg("http://example.com")).is_err(),
609 "plain http to a remote host is refused"
610 );
611 assert!(Gcs::new("bkt", t(), cfg("http://127.0.0.1:4443")).is_ok());
612 assert!(Gcs::new("bkt", t(), cfg("https://storage.googleapis.com/")).is_ok());
613 }
614}