Skip to main content

acyclic_objects/
grpc.rs

1//! Idiomatic gRPC client for the public Objects contract.
2
3use crate::{Condition, wire};
4use bytes::Bytes;
5use futures::{Stream, StreamExt, stream::iter};
6use prost::Message;
7use thiserror::Error;
8use tonic::{
9    Request,
10    metadata::{Ascii, MetadataValue},
11    transport::{Certificate, Channel, ClientTlsConfig, Endpoint},
12};
13
14const BODY_FRAME_BYTES: usize = 64 * 1024;
15/// Maximum caller-supplied private CA bundle accepted by the client.
16pub const MAX_CA_CERTIFICATE_BYTES: usize = 64 * 1024;
17
18/// Errors returned while constructing or using a client.
19#[derive(Debug, Error)]
20pub enum Error {
21    /// The configured endpoint was not a valid URI.
22    #[error("invalid Objects endpoint: {0}")]
23    InvalidEndpoint(#[from] tonic::transport::Error),
24    /// Customer endpoints must use authenticated TLS.
25    #[error("Objects customer endpoints must use https")]
26    InsecureEndpoint,
27    /// The supplied bearer credential could not be encoded as request metadata.
28    #[error("invalid Objects bearer credential")]
29    InvalidCredential,
30    /// The caller-supplied private CA bundle was empty or exceeded its fixed bound.
31    #[error("invalid Objects CA certificate bundle")]
32    InvalidCaCertificate,
33    /// The service rejected a request with a canonical public semantic code.
34    #[error("Objects request rejected ({code:?}, request {request_id}): {message}")]
35    Rejected {
36        /// Stable public error category.
37        code: wire::ErrorCode,
38        /// Request identity supplied by the service when available.
39        request_id: String,
40        /// Human-readable diagnostic, not suitable for control flow.
41        message: String,
42    },
43    /// The transport failed without a valid canonical Objects error detail.
44    #[error("Objects transport failed: {0}")]
45    Transport(tonic::Status),
46    /// A successful response violated the versioned protocol contract.
47    #[error("invalid Objects protocol response: {0}")]
48    Protocol(&'static str),
49}
50
51impl From<tonic::Status> for Error {
52    fn from(status: tonic::Status) -> Self {
53        if let Ok(detail) = wire::ErrorDetail::decode(status.details())
54            && let Ok(code) = wire::ErrorCode::try_from(detail.code)
55            && code != wire::ErrorCode::Unspecified
56        {
57            return Self::Rejected {
58                code,
59                request_id: detail.request_id,
60                message: status.message().to_owned(),
61            };
62        }
63        Self::Transport(status)
64    }
65}
66
67/// Result type used by this SDK.
68pub type Result<T> = std::result::Result<T, Error>;
69
70/// A configured Objects account client.
71#[derive(Clone)]
72pub struct Client {
73    channel: Channel,
74    authorization: MetadataValue<Ascii>,
75}
76
77impl Client {
78    /// Connect to an account endpoint using its account-bound bearer credential.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error when the endpoint/credential is invalid or the TLS connection fails.
83    pub async fn connect(endpoint: impl AsRef<str>, bearer_token: impl AsRef<str>) -> Result<Self> {
84        Self::connect_with_tls(endpoint, bearer_token, None).await
85    }
86
87    /// Connect to an account endpoint augmented by one caller-supplied private CA bundle.
88    ///
89    /// The certificate augments the client's roots for this connection only. HTTPS remains
90    /// mandatory, and the process-wide trust store is never changed.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error when the endpoint, credential, or bounded CA bundle is invalid, or when
95    /// the authenticated TLS connection fails.
96    pub async fn connect_with_ca_certificate(
97        endpoint: impl AsRef<str>,
98        bearer_token: impl AsRef<str>,
99        certificate_pem: impl AsRef<[u8]>,
100    ) -> Result<Self> {
101        let certificate_pem = bounded_ca_certificate(certificate_pem.as_ref())?;
102        Self::connect_with_tls(endpoint, bearer_token, Some(certificate_pem)).await
103    }
104
105    async fn connect_with_tls(
106        endpoint: impl AsRef<str>,
107        bearer_token: impl AsRef<str>,
108        certificate_pem: Option<&[u8]>,
109    ) -> Result<Self> {
110        let mut endpoint = Endpoint::new(endpoint.as_ref().to_owned())?;
111        if endpoint.uri().scheme_str() != Some("https") {
112            return Err(Error::InsecureEndpoint);
113        }
114        if let Some(certificate_pem) = certificate_pem {
115            endpoint = endpoint.tls_config(
116                ClientTlsConfig::new()
117                    .with_enabled_roots()
118                    .ca_certificate(Certificate::from_pem(certificate_pem)),
119            )?;
120        }
121        let authorization = format!("Bearer {}", bearer_token.as_ref())
122            .parse::<MetadataValue<Ascii>>()
123            .map_err(|_| Error::InvalidCredential)?;
124        let channel = endpoint.connect().await?;
125        Ok(Self {
126            channel,
127            authorization,
128        })
129    }
130
131    /// Create a permanently versioned bucket.
132    ///
133    /// # Errors
134    ///
135    /// Returns an error when the service rejects or cannot complete the request.
136    pub async fn create_bucket(
137        &self,
138        name: impl Into<String>,
139        options: MutationOptions,
140    ) -> Result<Bucket> {
141        let mut client =
142            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
143        let response = client
144            .create_bucket(authenticated(
145                &self.authorization,
146                wire::CreateBucketRequest {
147                    name: name.into(),
148                    mutation: options.wire(),
149                },
150            ))
151            .await?
152            .into_inner();
153        let reference = response
154            .bucket
155            .ok_or(Error::Protocol("missing bucket identity"))?;
156        Ok(Bucket::new(self, reference))
157    }
158
159    /// Validate and open one exact persisted bucket identity.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error when the identity is stale, mismatched, or unavailable.
164    pub async fn open_bucket(&self, reference: wire::BucketRef) -> Result<Bucket> {
165        let mut client =
166            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
167        let response = client
168            .head_bucket(authenticated(
169                &self.authorization,
170                wire::HeadBucketRequest {
171                    bucket: Some(reference.clone()),
172                },
173            ))
174            .await?
175            .into_inner();
176        if response.bucket.as_ref() != Some(&reference) {
177            return Err(Error::Protocol("bucket identity changed"));
178        }
179        Ok(Bucket::new(self, reference))
180    }
181
182    /// Reconstruct a read-only snapshot handle from its exact persisted identity.
183    #[must_use]
184    pub fn snapshot(&self, reference: wire::SnapshotRef) -> Snapshot {
185        Snapshot::new(self, reference)
186    }
187}
188
189fn bounded_ca_certificate(certificate_pem: &[u8]) -> Result<&[u8]> {
190    if certificate_pem.is_empty() || certificate_pem.len() > MAX_CA_CERTIFICATE_BYTES {
191        return Err(Error::InvalidCaCertificate);
192    }
193    Ok(certificate_pem)
194}
195
196/// Options common to retryable mutations.
197#[derive(Clone, Debug, Default)]
198pub struct MutationOptions {
199    idempotency_key: String,
200}
201
202impl MutationOptions {
203    /// Attach an account-scoped idempotency key retained for seven days.
204    #[must_use]
205    pub fn idempotency_key(mut self, value: impl Into<String>) -> Self {
206        self.idempotency_key = value.into();
207        self
208    }
209
210    fn wire(&self) -> Option<wire::MutationIdentity> {
211        (!self.idempotency_key.is_empty()).then(|| wire::MutationIdentity {
212            idempotency_key: self.idempotency_key.clone(),
213        })
214    }
215}
216
217/// Options for a single object put.
218#[derive(Clone, Debug, Default)]
219pub struct PutOptions {
220    metadata: wire::ObjectMetadata,
221    mutation: MutationOptions,
222    condition: Option<Condition>,
223}
224
225impl PutOptions {
226    /// Attach an account-scoped idempotency key retained for seven days.
227    #[must_use]
228    pub fn idempotency_key(mut self, value: impl Into<String>) -> Self {
229        self.mutation = self.mutation.idempotency_key(value);
230        self
231    }
232
233    /// Set the immutable content type for this version.
234    #[must_use]
235    pub fn content_type(mut self, value: impl Into<String>) -> Self {
236        self.metadata.content_type = value.into();
237        self
238    }
239
240    /// Set the immutable content encoding for this version.
241    #[must_use]
242    pub fn content_encoding(mut self, value: impl Into<String>) -> Self {
243        self.metadata.content_encoding = value.into();
244        self
245    }
246
247    /// Set the immutable cache-control value for this version.
248    #[must_use]
249    pub fn cache_control(mut self, value: impl Into<String>) -> Self {
250        self.metadata.cache_control = value.into();
251        self
252    }
253
254    /// Set the immutable content disposition for this version.
255    #[must_use]
256    pub fn content_disposition(mut self, value: impl Into<String>) -> Self {
257        self.metadata.content_disposition = value.into();
258        self
259    }
260
261    /// Set the immutable content language for this version.
262    #[must_use]
263    pub fn content_language(mut self, value: impl Into<String>) -> Self {
264        self.metadata.content_language = value.into();
265        self
266    }
267
268    /// Set the immutable HTTP expiry time as Unix seconds.
269    #[must_use]
270    pub fn expires_unix_seconds(mut self, value: i64) -> Self {
271        self.metadata.expires_unix_seconds = Some(value);
272        self
273    }
274
275    /// Add immutable user metadata to this version.
276    #[must_use]
277    pub fn user_metadata(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
278        self.metadata.user.insert(name.into(), value.into());
279        self
280    }
281
282    /// Attach the sole current-version condition.
283    #[must_use]
284    pub fn condition(mut self, value: Condition) -> Self {
285        self.condition = Some(value);
286        self
287    }
288}
289
290/// Options for an object read or head.
291#[derive(Clone, Debug)]
292pub struct GetOptions {
293    version_id: String,
294    range: Option<(u64, Option<u64>)>,
295    if_match: String,
296    if_none_match: String,
297    maximum_bytes: u64,
298}
299
300impl Default for GetOptions {
301    fn default() -> Self {
302        Self {
303            version_id: String::new(),
304            range: None,
305            if_match: String::new(),
306            if_none_match: String::new(),
307            maximum_bytes: crate::limits::OBJECT_BYTES,
308        }
309    }
310}
311
312impl GetOptions {
313    /// Read one exact immutable version.
314    #[must_use]
315    pub fn version(mut self, value: impl Into<String>) -> Self {
316        self.version_id = value.into();
317        self
318    }
319
320    /// Read one inclusive byte range.
321    #[must_use]
322    pub fn range(mut self, start: u64, end_inclusive: Option<u64>) -> Self {
323        self.range = Some((start, end_inclusive));
324        self
325    }
326
327    /// Require one opaque `ETag`.
328    #[must_use]
329    pub fn if_match(mut self, value: impl Into<String>) -> Self {
330        self.if_match = value.into();
331        self
332    }
333
334    /// Reject one opaque `ETag`.
335    #[must_use]
336    pub fn if_none_match(mut self, value: impl Into<String>) -> Self {
337        self.if_none_match = value.into();
338        self
339    }
340
341    /// Bound the selected response body before any body frame is decoded.
342    #[must_use]
343    pub fn maximum_bytes(mut self, value: u64) -> Self {
344        self.maximum_bytes = value;
345        self
346    }
347}
348
349/// Options for object deletion.
350#[derive(Clone, Debug, Default)]
351pub struct DeleteOptions {
352    version_id: String,
353    mutation: MutationOptions,
354    condition: Option<Condition>,
355}
356
357impl DeleteOptions {
358    /// Delete one exact immutable version rather than publishing a marker.
359    #[must_use]
360    pub fn version(mut self, value: impl Into<String>) -> Self {
361        self.version_id = value.into();
362        self
363    }
364
365    /// Attach an account-scoped idempotency key.
366    #[must_use]
367    pub fn idempotency_key(mut self, value: impl Into<String>) -> Self {
368        self.mutation = self.mutation.idempotency_key(value);
369        self
370    }
371
372    /// Attach the sole current-version condition for marker publication.
373    #[must_use]
374    pub fn condition(mut self, value: Condition) -> Self {
375        self.condition = Some(value);
376        self
377    }
378}
379
380/// Options for one stable listing page.
381#[derive(Clone, Debug)]
382pub struct ListOptions {
383    /// Prefix filter.
384    pub prefix: String,
385    /// Optional delimiter.
386    pub delimiter: String,
387    /// Include every version instead of live current versions.
388    pub versions: bool,
389    /// Maximum combined objects and common prefixes.
390    pub page_size: u32,
391    /// Opaque continuation token.
392    pub continuation: String,
393}
394
395impl Default for ListOptions {
396    fn default() -> Self {
397        Self {
398            prefix: String::new(),
399            delimiter: String::new(),
400            versions: false,
401            page_size: 1_000,
402            continuation: String::new(),
403        }
404    }
405}
406
407/// One typed stable listing page.
408#[derive(Clone, Debug)]
409pub struct ListPage {
410    /// Ordered object/version entries.
411    pub entries: Vec<wire::ListEntry>,
412    /// Ordered delimiter-derived prefixes.
413    pub common_prefixes: Vec<String>,
414    /// Opaque continuation token, absent on the final page.
415    pub continuation: Option<String>,
416}
417
418/// A handle bound to one exact bucket identity.
419#[derive(Clone)]
420pub struct Bucket {
421    channel: Channel,
422    authorization: MetadataValue<Ascii>,
423    reference: wire::BucketRef,
424}
425
426impl Bucket {
427    fn new(client: &Client, reference: wire::BucketRef) -> Self {
428        Self {
429            channel: client.channel.clone(),
430            authorization: client.authorization.clone(),
431            reference,
432        }
433    }
434
435    /// The opaque bucket identity. Name reuse never revives it.
436    #[must_use]
437    pub fn id(&self) -> &str {
438        &self.reference.bucket_id
439    }
440
441    /// The account-scoped bucket name.
442    #[must_use]
443    pub fn name(&self) -> &str {
444        &self.reference.name
445    }
446
447    /// Copy the complete opaque bucket identity for durable application state.
448    #[must_use]
449    pub fn reference(&self) -> wire::BucketRef {
450        self.reference.clone()
451    }
452
453    /// Delete this exact bucket when it has no versions, markers, or multipart uploads.
454    ///
455    /// # Errors
456    ///
457    /// Returns an error when the bucket is not empty or the service cannot complete the request.
458    pub async fn destroy(&self, options: MutationOptions) -> Result<bool> {
459        let mut client =
460            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
461        Ok(client
462            .delete_bucket(authenticated(
463                &self.authorization,
464                wire::DeleteBucketRequest {
465                    bucket: Some(self.reference.clone()),
466                    mutation: options.wire(),
467                },
468            ))
469            .await?
470            .into_inner()
471            .existed)
472    }
473
474    /// Create one immutable version from buffered bytes.
475    ///
476    /// # Errors
477    ///
478    /// Returns an error when the service rejects or cannot complete the request.
479    pub async fn put(
480        &self,
481        object_key: impl Into<String>,
482        body: Bytes,
483        options: PutOptions,
484    ) -> Result<wire::ObjectVersion> {
485        self.put_stream(object_key, iter([body]), options).await
486    }
487
488    /// Stream one immutable version without buffering it in the SDK.
489    ///
490    /// # Errors
491    ///
492    /// Returns an error when the service rejects or cannot complete the request.
493    pub async fn put_stream<S>(
494        &self,
495        object_key: impl Into<String>,
496        body: S,
497        options: PutOptions,
498    ) -> Result<wire::ObjectVersion>
499    where
500        S: Stream<Item = Bytes> + Send + 'static,
501    {
502        let header = wire::PutObjectRequest {
503            frame: Some(wire::put_object_request::Frame::Header(
504                wire::PutObjectHeader {
505                    bucket: Some(self.reference.clone()),
506                    object_key: object_key.into(),
507                    metadata: Some(options.metadata),
508                    preconditions: options.condition.map(Condition::wire),
509                    mutation: options.mutation.wire(),
510                },
511            )),
512        };
513        let frames = iter([header]).chain(body.flat_map(|chunk| {
514            iter(
515                chunk
516                    .chunks(BODY_FRAME_BYTES)
517                    .map(|frame| wire::PutObjectRequest {
518                        frame: Some(wire::put_object_request::Frame::Body(frame.to_vec())),
519                    })
520                    .collect::<Vec<_>>(),
521            )
522        }));
523        let mut client =
524            wire::objects_service_client::ObjectsServiceClient::new(self.channel.clone());
525        Ok(client
526            .put_object(authenticated(&self.authorization, frames))
527            .await?
528            .into_inner())
529    }
530
531    /// Open a streaming object read.
532    ///
533    /// # Errors
534    ///
535    /// Returns an error when the service rejects the read or violates framing.
536    pub async fn get(
537        &self,
538        object_key: impl Into<String>,
539        options: GetOptions,
540    ) -> Result<StoredObject> {
541        get_from(
542            &self.channel,
543            &self.authorization,
544            bucket_target(&self.reference),
545            object_key.into(),
546            options,
547        )
548        .await
549    }
550
551    /// Read immutable object metadata without transferring its body.
552    ///
553    /// # Errors
554    ///
555    /// Returns an error when the service rejects or cannot complete the request.
556    pub async fn head(
557        &self,
558        object_key: impl Into<String>,
559        options: GetOptions,
560    ) -> Result<wire::ObjectVersion> {
561        head_from(
562            &self.channel,
563            &self.authorization,
564            bucket_target(&self.reference),
565            object_key.into(),
566            options,
567        )
568        .await
569    }
570
571    /// Publish a delete marker or delete one exact immutable version.
572    ///
573    /// # Errors
574    ///
575    /// Returns an error when the service rejects or cannot complete the request.
576    pub async fn delete(
577        &self,
578        object_key: impl Into<String>,
579        options: DeleteOptions,
580    ) -> Result<wire::DeleteObjectResponse> {
581        let mut client =
582            wire::objects_service_client::ObjectsServiceClient::new(self.channel.clone());
583        Ok(client
584            .delete_object(authenticated(
585                &self.authorization,
586                wire::DeleteObjectRequest {
587                    bucket: Some(self.reference.clone()),
588                    object_key: object_key.into(),
589                    version_id: options.version_id,
590                    preconditions: options.condition.map(Condition::wire),
591                    mutation: options.mutation.wire(),
592                },
593            ))
594            .await?
595            .into_inner())
596    }
597
598    /// Read one page from a fixed 24-hour listing view.
599    ///
600    /// # Errors
601    ///
602    /// Returns an error when the service rejects or cannot complete the request.
603    pub async fn list_page(&self, options: ListOptions) -> Result<ListPage> {
604        list_from(
605            &self.channel,
606            &self.authorization,
607            bucket_target(&self.reference),
608            options,
609        )
610        .await
611    }
612
613    /// Capture an immutable whole-bucket snapshot.
614    ///
615    /// # Errors
616    ///
617    /// Returns an error when the service rejects or cannot complete the request.
618    pub async fn snapshot(&self, options: MutationOptions) -> Result<Snapshot> {
619        let mut client =
620            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
621        let response = client
622            .create_snapshot(authenticated(
623                &self.authorization,
624                wire::CreateSnapshotRequest {
625                    bucket: Some(self.reference.clone()),
626                    mutation: options.wire(),
627                },
628            ))
629            .await?
630            .into_inner();
631        let reference = response
632            .snapshot
633            .ok_or(Error::Protocol("missing snapshot identity"))?;
634        Ok(Snapshot {
635            channel: self.channel.clone(),
636            authorization: self.authorization.clone(),
637            reference,
638        })
639    }
640
641    /// Atomically fork the current complete bucket state.
642    ///
643    /// # Errors
644    ///
645    /// Returns an error when the service rejects or cannot complete the request.
646    pub async fn fork_current(
647        &self,
648        destination_name: impl Into<String>,
649        options: MutationOptions,
650    ) -> Result<Self> {
651        let mut client =
652            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
653        let response = client
654            .fork_bucket(authenticated(
655                &self.authorization,
656                wire::ForkBucketRequest {
657                    source: Some(self.reference.clone()),
658                    destination_name: destination_name.into(),
659                    mutation: options.wire(),
660                },
661            ))
662            .await?
663            .into_inner();
664        let reference = response
665            .bucket
666            .ok_or(Error::Protocol("missing fork bucket identity"))?;
667        Ok(Self {
668            channel: self.channel.clone(),
669            authorization: self.authorization.clone(),
670            reference,
671        })
672    }
673
674    /// Begin a multipart upload.
675    ///
676    /// # Errors
677    ///
678    /// Returns an error when the service rejects or cannot complete the request.
679    pub async fn create_multipart(
680        &self,
681        object_key: impl Into<String>,
682        options: PutOptions,
683    ) -> Result<MultipartUpload> {
684        let object_key = object_key.into();
685        let mut client =
686            wire::multipart_service_client::MultipartServiceClient::new(self.channel.clone());
687        let response = client
688            .create_multipart(authenticated(
689                &self.authorization,
690                wire::CreateMultipartRequest {
691                    bucket: Some(self.reference.clone()),
692                    object_key: object_key.clone(),
693                    metadata: Some(options.metadata),
694                    preconditions: options.condition.map(Condition::wire),
695                    mutation: options.mutation.wire(),
696                },
697            ))
698            .await?
699            .into_inner();
700        Ok(MultipartUpload {
701            bucket: self.clone(),
702            object_key,
703            upload_id: response.upload_id,
704        })
705    }
706}
707
708/// Immutable snapshot read handle.
709#[derive(Clone)]
710pub struct Snapshot {
711    channel: Channel,
712    authorization: MetadataValue<Ascii>,
713    reference: wire::SnapshotRef,
714}
715
716impl Snapshot {
717    fn new(client: &Client, reference: wire::SnapshotRef) -> Self {
718        Self {
719            channel: client.channel.clone(),
720            authorization: client.authorization.clone(),
721            reference,
722        }
723    }
724
725    /// Opaque snapshot identity.
726    #[must_use]
727    pub fn id(&self) -> &str {
728        &self.reference.snapshot_id
729    }
730
731    /// Copy the complete opaque snapshot identity for durable application state.
732    #[must_use]
733    pub fn reference(&self) -> wire::SnapshotRef {
734        self.reference.clone()
735    }
736
737    /// Open a streaming immutable object read.
738    ///
739    /// # Errors
740    ///
741    /// Returns an error when the service rejects the read or violates framing.
742    pub async fn get(
743        &self,
744        object_key: impl Into<String>,
745        options: GetOptions,
746    ) -> Result<StoredObject> {
747        get_from(
748            &self.channel,
749            &self.authorization,
750            snapshot_target(&self.reference),
751            object_key.into(),
752            options,
753        )
754        .await
755    }
756
757    /// Read one page from this immutable snapshot.
758    ///
759    /// # Errors
760    ///
761    /// Returns an error when the service rejects or cannot complete the request.
762    pub async fn list_page(&self, options: ListOptions) -> Result<ListPage> {
763        list_from(
764            &self.channel,
765            &self.authorization,
766            snapshot_target(&self.reference),
767            options,
768        )
769        .await
770    }
771
772    /// Atomically publish an independent bucket from this snapshot.
773    ///
774    /// # Errors
775    ///
776    /// Returns an error when the service rejects or cannot complete the request.
777    pub async fn fork(
778        &self,
779        destination_name: impl Into<String>,
780        options: MutationOptions,
781    ) -> Result<Bucket> {
782        let mut client =
783            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
784        let response = client
785            .fork_snapshot(authenticated(
786                &self.authorization,
787                wire::ForkSnapshotRequest {
788                    snapshot: Some(self.reference.clone()),
789                    destination_name: destination_name.into(),
790                    mutation: options.wire(),
791                },
792            ))
793            .await?
794            .into_inner();
795        let reference = response
796            .bucket
797            .ok_or(Error::Protocol("missing fork bucket identity"))?;
798        Ok(Bucket {
799            channel: self.channel.clone(),
800            authorization: self.authorization.clone(),
801            reference,
802        })
803    }
804
805    /// Destroy this snapshot identity.
806    ///
807    /// # Errors
808    ///
809    /// Returns an error when the service rejects or cannot complete the request.
810    pub async fn destroy(&self, options: MutationOptions) -> Result<bool> {
811        let mut client =
812            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
813        Ok(client
814            .destroy_snapshot(authenticated(
815                &self.authorization,
816                wire::DestroySnapshotRequest {
817                    snapshot: Some(self.reference.clone()),
818                    mutation: options.wire(),
819                },
820            ))
821            .await?
822            .into_inner()
823            .existed)
824    }
825}
826
827/// Streaming object response after its immutable descriptor has been validated.
828pub struct StoredObject {
829    /// Immutable version descriptor.
830    pub version: wire::ObjectVersion,
831    stream: tonic::Streaming<wire::GetObjectResponse>,
832    remaining: u64,
833}
834
835impl StoredObject {
836    /// Read the next body chunk.
837    ///
838    /// # Errors
839    ///
840    /// Returns an error for transport failure or invalid response framing.
841    pub async fn next_chunk(&mut self) -> Result<Option<Bytes>> {
842        let Some(response) = self.stream.message().await? else {
843            return if self.remaining == 0 {
844                Ok(None)
845            } else {
846                Err(Error::Protocol("object body ended before declared size"))
847            };
848        };
849        match response.frame {
850            Some(wire::get_object_response::Frame::Body(body)) => {
851                let size = u64::try_from(body.len())
852                    .map_err(|_| Error::Protocol("object body frame is too large"))?;
853                self.remaining = self
854                    .remaining
855                    .checked_sub(size)
856                    .ok_or(Error::Protocol("object body exceeded declared size"))?;
857                Ok(Some(Bytes::from(body)))
858            }
859            Some(wire::get_object_response::Frame::Version(_)) => {
860                Err(Error::Protocol("duplicate object version frame"))
861            }
862            None => Err(Error::Protocol("empty object response frame")),
863        }
864    }
865}
866
867/// Typed multipart upload handle.
868#[derive(Clone)]
869pub struct MultipartUpload {
870    bucket: Bucket,
871    object_key: String,
872    upload_id: String,
873}
874
875impl MultipartUpload {
876    /// Opaque upload identity.
877    #[must_use]
878    pub fn id(&self) -> &str {
879        &self.upload_id
880    }
881
882    /// Upload or replace one buffered part.
883    ///
884    /// # Errors
885    ///
886    /// Returns an error when the service rejects or cannot complete the request.
887    pub async fn upload_part(
888        &self,
889        part_number: u32,
890        body: Bytes,
891        options: MutationOptions,
892    ) -> Result<wire::UploadedPart> {
893        let header = wire::UploadPartRequest {
894            frame: Some(wire::upload_part_request::Frame::Header(
895                wire::UploadPartHeader {
896                    bucket: Some(self.bucket.reference.clone()),
897                    object_key: self.object_key.clone(),
898                    upload_id: self.upload_id.clone(),
899                    part_number,
900                    mutation: options.wire(),
901                },
902            )),
903        };
904        let content = body
905            .chunks(BODY_FRAME_BYTES)
906            .map(|frame| wire::UploadPartRequest {
907                frame: Some(wire::upload_part_request::Frame::Body(frame.to_vec())),
908            })
909            .collect::<Vec<_>>();
910        let mut client = wire::multipart_service_client::MultipartServiceClient::new(
911            self.bucket.channel.clone(),
912        );
913        Ok(client
914            .upload_part(authenticated(
915                &self.bucket.authorization,
916                iter([header]).chain(iter(content)),
917            ))
918            .await?
919            .into_inner())
920    }
921
922    /// List staged parts in ascending part-number order.
923    ///
924    /// # Errors
925    ///
926    /// Returns an error when the service rejects or cannot complete the request.
927    pub async fn list_parts(&self) -> Result<Vec<wire::UploadedPart>> {
928        let mut client = wire::multipart_service_client::MultipartServiceClient::new(
929            self.bucket.channel.clone(),
930        );
931        Ok(client
932            .list_parts(authenticated(
933                &self.bucket.authorization,
934                wire::ListPartsRequest {
935                    bucket: Some(self.bucket.reference.clone()),
936                    object_key: self.object_key.clone(),
937                    upload_id: self.upload_id.clone(),
938                },
939            ))
940            .await?
941            .into_inner()
942            .parts)
943    }
944
945    /// Atomically publish one immutable version from exact part receipts.
946    ///
947    /// # Errors
948    ///
949    /// Returns an error when the service rejects or cannot complete the request.
950    pub async fn complete(
951        &self,
952        parts: Vec<wire::UploadedPart>,
953        options: MutationOptions,
954    ) -> Result<wire::ObjectVersion> {
955        let mut client = wire::multipart_service_client::MultipartServiceClient::new(
956            self.bucket.channel.clone(),
957        );
958        Ok(client
959            .complete_multipart(authenticated(
960                &self.bucket.authorization,
961                wire::CompleteMultipartRequest {
962                    bucket: Some(self.bucket.reference.clone()),
963                    object_key: self.object_key.clone(),
964                    upload_id: self.upload_id.clone(),
965                    parts,
966                    mutation: options.wire(),
967                },
968            ))
969            .await?
970            .into_inner())
971    }
972
973    /// Abort this exact upload.
974    ///
975    /// # Errors
976    ///
977    /// Returns an error when the service rejects or cannot complete the request.
978    pub async fn abort(&self, options: MutationOptions) -> Result<bool> {
979        let mut client = wire::multipart_service_client::MultipartServiceClient::new(
980            self.bucket.channel.clone(),
981        );
982        Ok(client
983            .abort_multipart(authenticated(
984                &self.bucket.authorization,
985                wire::AbortMultipartRequest {
986                    bucket: Some(self.bucket.reference.clone()),
987                    object_key: self.object_key.clone(),
988                    upload_id: self.upload_id.clone(),
989                    mutation: options.wire(),
990                },
991            ))
992            .await?
993            .into_inner()
994            .existed)
995    }
996}
997
998async fn get_from(
999    channel: &Channel,
1000    authorization: &MetadataValue<Ascii>,
1001    target: wire::ReadTarget,
1002    object_key: String,
1003    options: GetOptions,
1004) -> Result<StoredObject> {
1005    let requested_range = options.range;
1006    let (range_start, range_end_inclusive) = requested_range.unwrap_or((0, None));
1007    let frame_body_limit = usize::try_from(options.maximum_bytes.min(BODY_FRAME_BYTES as u64))
1008        .map_err(|_| Error::Protocol("object response bound is not representable"))?;
1009    let decoding_limit = frame_body_limit
1010        .checked_add(256)
1011        .ok_or(Error::Protocol("object response bound overflowed"))?;
1012    let mut client = wire::objects_service_client::ObjectsServiceClient::new(channel.clone())
1013        .max_decoding_message_size(decoding_limit);
1014    let mut stream = client
1015        .get_object(authenticated(
1016            authorization,
1017            wire::GetObjectRequest {
1018                target: Some(target),
1019                object_key,
1020                version_id: options.version_id,
1021                range_start,
1022                range_end_inclusive,
1023                if_match: options.if_match,
1024                if_none_match: options.if_none_match,
1025            },
1026        ))
1027        .await?
1028        .into_inner();
1029    let first = stream
1030        .message()
1031        .await?
1032        .and_then(|response| response.frame)
1033        .ok_or(Error::Protocol("missing object version frame"))?;
1034    let wire::get_object_response::Frame::Version(version) = first else {
1035        return Err(Error::Protocol("object version frame must be first"));
1036    };
1037    if version.delete_marker {
1038        return Err(Error::Protocol("get returned a delete marker"));
1039    }
1040    let remaining = match requested_range {
1041        None => version.size,
1042        Some((start, end)) => {
1043            let end = end.unwrap_or_else(|| version.size.saturating_sub(1));
1044            if start > end || end >= version.size {
1045                return Err(Error::Protocol("response cannot satisfy requested range"));
1046            }
1047            end - start + 1
1048        }
1049    };
1050    if remaining > options.maximum_bytes {
1051        return Err(Error::Protocol("object body exceeds requested bound"));
1052    }
1053    Ok(StoredObject {
1054        version,
1055        stream,
1056        remaining,
1057    })
1058}
1059
1060async fn head_from(
1061    channel: &Channel,
1062    authorization: &MetadataValue<Ascii>,
1063    target: wire::ReadTarget,
1064    object_key: String,
1065    options: GetOptions,
1066) -> Result<wire::ObjectVersion> {
1067    let mut client = wire::objects_service_client::ObjectsServiceClient::new(channel.clone());
1068    client
1069        .head_object(authenticated(
1070            authorization,
1071            wire::HeadObjectRequest {
1072                target: Some(target),
1073                object_key,
1074                version_id: options.version_id,
1075                if_match: options.if_match,
1076                if_none_match: options.if_none_match,
1077            },
1078        ))
1079        .await?
1080        .into_inner()
1081        .version
1082        .ok_or(Error::Protocol("missing object version"))
1083}
1084
1085async fn list_from(
1086    channel: &Channel,
1087    authorization: &MetadataValue<Ascii>,
1088    target: wire::ReadTarget,
1089    options: ListOptions,
1090) -> Result<ListPage> {
1091    let mut client = wire::objects_service_client::ObjectsServiceClient::new(channel.clone());
1092    let response = client
1093        .list_objects(authenticated(
1094            authorization,
1095            wire::ListObjectsRequest {
1096                target: Some(target),
1097                prefix: options.prefix,
1098                delimiter: options.delimiter,
1099                mode: if options.versions {
1100                    wire::ListingMode::Versions as i32
1101                } else {
1102                    wire::ListingMode::Current as i32
1103                },
1104                page_size: options.page_size,
1105                continuation_token: options.continuation,
1106            },
1107        ))
1108        .await?
1109        .into_inner();
1110    Ok(ListPage {
1111        entries: response.entries,
1112        common_prefixes: response.common_prefixes,
1113        continuation: (!response.continuation_token.is_empty())
1114            .then_some(response.continuation_token),
1115    })
1116}
1117
1118fn authenticated<T>(authorization: &MetadataValue<Ascii>, message: T) -> Request<T> {
1119    let mut request = Request::new(message);
1120    request
1121        .metadata_mut()
1122        .insert("authorization", authorization.clone());
1123    request
1124}
1125
1126fn bucket_target(reference: &wire::BucketRef) -> wire::ReadTarget {
1127    wire::ReadTarget {
1128        target: Some(wire::read_target::Target::Bucket(reference.clone())),
1129    }
1130}
1131
1132fn snapshot_target(reference: &wire::SnapshotRef) -> wire::ReadTarget {
1133    wire::ReadTarget {
1134        target: Some(wire::read_target::Target::Snapshot(reference.clone())),
1135    }
1136}
1137
1138fn mutation_options(
1139    idempotency_key: Option<String>,
1140) -> std::result::Result<MutationOptions, crate::ObjectsError> {
1141    if idempotency_key
1142        .as_ref()
1143        .is_some_and(|key| key.is_empty() || key.len() > 256)
1144    {
1145        return Err(crate::ObjectsError::Invalid("invalid idempotency key"));
1146    }
1147    Ok(MutationOptions {
1148        idempotency_key: idempotency_key.unwrap_or_default(),
1149    })
1150}
1151
1152fn validate_bucket(
1153    bucket: wire::Bucket,
1154    expected: Option<&wire::BucketRef>,
1155    expected_name: Option<&str>,
1156) -> std::result::Result<wire::Bucket, crate::ObjectsError> {
1157    let reference = bucket
1158        .bucket
1159        .as_ref()
1160        .ok_or(crate::ObjectsError::Unavailable)?;
1161    if reference.bucket_id.is_empty()
1162        || reference.name.is_empty()
1163        || expected.is_some_and(|value| value != reference)
1164        || expected_name.is_some_and(|value| value != reference.name)
1165        || bucket.created_at.is_none()
1166    {
1167        return Err(crate::ObjectsError::Unavailable);
1168    }
1169    Ok(bucket)
1170}
1171
1172fn validate_snapshot(
1173    snapshot: wire::Snapshot,
1174    source: &wire::BucketRef,
1175) -> std::result::Result<wire::Snapshot, crate::ObjectsError> {
1176    let reference = snapshot
1177        .snapshot
1178        .as_ref()
1179        .ok_or(crate::ObjectsError::Unavailable)?;
1180    if reference.snapshot_id.is_empty()
1181        || reference.source_bucket_id != source.bucket_id
1182        || snapshot.created_at.is_none()
1183    {
1184        return Err(crate::ObjectsError::Unavailable);
1185    }
1186    Ok(snapshot)
1187}
1188
1189fn provider_target(target: crate::ReadTarget) -> wire::ReadTarget {
1190    match target {
1191        crate::ReadTarget::Bucket(reference) => bucket_target(&reference),
1192        crate::ReadTarget::Snapshot(reference) => snapshot_target(&reference),
1193    }
1194}
1195
1196fn provider_error(error: Error) -> crate::ObjectsError {
1197    match error {
1198        Error::Rejected { code, .. } => match code {
1199            wire::ErrorCode::InvalidArgument => {
1200                crate::ObjectsError::Invalid("service rejected the request")
1201            }
1202            wire::ErrorCode::TokenExpired => crate::ObjectsError::Unauthorized,
1203            wire::ErrorCode::NotFound => crate::ObjectsError::NotFound,
1204            wire::ErrorCode::AlreadyExists => crate::ObjectsError::AlreadyExists,
1205            wire::ErrorCode::PreconditionFailed => crate::ObjectsError::PreconditionFailed,
1206            wire::ErrorCode::IdempotencyMismatch => crate::ObjectsError::IdempotencyMismatch,
1207            wire::ErrorCode::QuotaExceeded => crate::ObjectsError::Capacity,
1208            wire::ErrorCode::Unsupported => crate::ObjectsError::Unsupported,
1209            wire::ErrorCode::Unavailable | wire::ErrorCode::Unspecified => {
1210                crate::ObjectsError::Unavailable
1211            }
1212        },
1213        Error::InvalidEndpoint(_)
1214        | Error::InsecureEndpoint
1215        | Error::InvalidCredential
1216        | Error::InvalidCaCertificate => {
1217            crate::ObjectsError::Invalid("invalid provider configuration")
1218        }
1219        Error::Transport(status)
1220            if matches!(
1221                status.code(),
1222                tonic::Code::Unauthenticated | tonic::Code::PermissionDenied
1223            ) =>
1224        {
1225            crate::ObjectsError::Unauthorized
1226        }
1227        Error::Transport(_) | Error::Protocol(_) => crate::ObjectsError::Unavailable,
1228    }
1229}
1230
1231#[async_trait::async_trait]
1232impl crate::ObjectsProvider for Client {
1233    async fn create_bucket(
1234        &self,
1235        name: String,
1236        idempotency_key: Option<String>,
1237    ) -> std::result::Result<wire::Bucket, crate::ObjectsError> {
1238        let expected_name = name.clone();
1239        let mut client =
1240            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
1241        let bucket = client
1242            .create_bucket(authenticated(
1243                &self.authorization,
1244                wire::CreateBucketRequest {
1245                    name,
1246                    mutation: mutation_options(idempotency_key)?.wire(),
1247                },
1248            ))
1249            .await
1250            .map(tonic::Response::into_inner)
1251            .map_err(Error::from)
1252            .map_err(provider_error)?;
1253        validate_bucket(bucket, None, Some(&expected_name))
1254    }
1255
1256    async fn head_bucket(
1257        &self,
1258        bucket: &wire::BucketRef,
1259    ) -> std::result::Result<wire::Bucket, crate::ObjectsError> {
1260        let mut client =
1261            wire::buckets_service_client::BucketsServiceClient::new(self.channel.clone());
1262        let response = client
1263            .head_bucket(authenticated(
1264                &self.authorization,
1265                wire::HeadBucketRequest {
1266                    bucket: Some(bucket.clone()),
1267                },
1268            ))
1269            .await
1270            .map(tonic::Response::into_inner)
1271            .map_err(Error::from)
1272            .map_err(provider_error)?;
1273        validate_bucket(response, Some(bucket), None)
1274    }
1275
1276    async fn delete_bucket(
1277        &self,
1278        bucket: &wire::BucketRef,
1279        idempotency_key: Option<String>,
1280    ) -> std::result::Result<bool, crate::ObjectsError> {
1281        Bucket::new(self, bucket.clone())
1282            .destroy(mutation_options(idempotency_key)?)
1283            .await
1284            .map_err(provider_error)
1285    }
1286
1287    async fn put(
1288        &self,
1289        request: crate::PutRequest,
1290    ) -> std::result::Result<wire::ObjectVersion, crate::ObjectsError> {
1291        let bucket = Bucket::new(self, request.bucket);
1292        bucket
1293            .put(
1294                request.object_key,
1295                request.body,
1296                PutOptions {
1297                    metadata: request.metadata,
1298                    mutation: mutation_options(request.idempotency_key)?,
1299                    condition: request.condition,
1300                },
1301            )
1302            .await
1303            .map_err(provider_error)
1304    }
1305
1306    async fn get(
1307        &self,
1308        request: crate::GetRequest,
1309    ) -> std::result::Result<crate::BufferedObject, crate::ObjectsError> {
1310        let mut object = get_from(
1311            &self.channel,
1312            &self.authorization,
1313            provider_target(request.target),
1314            request.object_key,
1315            GetOptions {
1316                version_id: request.version_id.unwrap_or_default(),
1317                range: request.range,
1318                if_match: request.if_match.unwrap_or_default(),
1319                if_none_match: request.if_none_match.unwrap_or_default(),
1320                maximum_bytes: request.maximum_bytes,
1321            },
1322        )
1323        .await
1324        .map_err(provider_error)?;
1325        let version = object.version.clone();
1326        if object.remaining > request.maximum_bytes {
1327            return Err(crate::ObjectsError::Capacity);
1328        }
1329        let capacity =
1330            usize::try_from(object.remaining).map_err(|_| crate::ObjectsError::Capacity)?;
1331        let mut body = Vec::with_capacity(capacity);
1332        while let Some(chunk) = object.next_chunk().await.map_err(provider_error)? {
1333            body.extend_from_slice(&chunk);
1334        }
1335        Ok(crate::BufferedObject {
1336            version,
1337            body: Bytes::from(body),
1338        })
1339    }
1340
1341    async fn delete(
1342        &self,
1343        bucket: wire::BucketRef,
1344        object_key: String,
1345        version_id: Option<String>,
1346        condition: Option<Condition>,
1347        idempotency_key: Option<String>,
1348    ) -> std::result::Result<crate::DeleteResult, crate::ObjectsError> {
1349        let response = Bucket::new(self, bucket)
1350            .delete(
1351                object_key,
1352                DeleteOptions {
1353                    version_id: version_id.unwrap_or_default(),
1354                    mutation: mutation_options(idempotency_key)?,
1355                    condition,
1356                },
1357            )
1358            .await
1359            .map_err(provider_error)?;
1360        Ok(crate::DeleteResult {
1361            existed: response.existed,
1362            marker: response.version,
1363        })
1364    }
1365
1366    async fn list(
1367        &self,
1368        target: crate::ReadTarget,
1369        prefix: String,
1370        delimiter: Option<String>,
1371        versions: bool,
1372        page_size: u32,
1373        continuation: Option<String>,
1374    ) -> std::result::Result<crate::ProviderListPage, crate::ObjectsError> {
1375        let page = list_from(
1376            &self.channel,
1377            &self.authorization,
1378            provider_target(target),
1379            ListOptions {
1380                prefix,
1381                delimiter: delimiter.unwrap_or_default(),
1382                versions,
1383                page_size,
1384                continuation: continuation.unwrap_or_default(),
1385            },
1386        )
1387        .await
1388        .map_err(provider_error)?;
1389        Ok(crate::ProviderListPage {
1390            entries: page.entries,
1391            common_prefixes: page.common_prefixes,
1392            continuation: page.continuation,
1393        })
1394    }
1395
1396    async fn snapshot(
1397        &self,
1398        bucket: wire::BucketRef,
1399        idempotency_key: Option<String>,
1400    ) -> std::result::Result<wire::Snapshot, crate::ObjectsError> {
1401        let mut client =
1402            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
1403        let snapshot = client
1404            .create_snapshot(authenticated(
1405                &self.authorization,
1406                wire::CreateSnapshotRequest {
1407                    bucket: Some(bucket.clone()),
1408                    mutation: mutation_options(idempotency_key)?.wire(),
1409                },
1410            ))
1411            .await
1412            .map(tonic::Response::into_inner)
1413            .map_err(Error::from)
1414            .map_err(provider_error)?;
1415        validate_snapshot(snapshot, &bucket)
1416    }
1417
1418    async fn destroy_snapshot(
1419        &self,
1420        snapshot: wire::SnapshotRef,
1421        idempotency_key: Option<String>,
1422    ) -> std::result::Result<bool, crate::ObjectsError> {
1423        Snapshot::new(self, snapshot)
1424            .destroy(mutation_options(idempotency_key)?)
1425            .await
1426            .map_err(provider_error)
1427    }
1428
1429    async fn fork(
1430        &self,
1431        source: crate::ReadTarget,
1432        destination_name: String,
1433        idempotency_key: Option<String>,
1434    ) -> std::result::Result<wire::Bucket, crate::ObjectsError> {
1435        let mut client =
1436            wire::snapshots_service_client::SnapshotsServiceClient::new(self.channel.clone());
1437        let mutation = mutation_options(idempotency_key)?.wire();
1438        let expected_name = destination_name.clone();
1439        let response = match source {
1440            crate::ReadTarget::Bucket(source) => {
1441                client
1442                    .fork_bucket(authenticated(
1443                        &self.authorization,
1444                        wire::ForkBucketRequest {
1445                            source: Some(source),
1446                            destination_name: destination_name.clone(),
1447                            mutation,
1448                        },
1449                    ))
1450                    .await
1451            }
1452            crate::ReadTarget::Snapshot(snapshot) => {
1453                client
1454                    .fork_snapshot(authenticated(
1455                        &self.authorization,
1456                        wire::ForkSnapshotRequest {
1457                            snapshot: Some(snapshot),
1458                            destination_name,
1459                            mutation,
1460                        },
1461                    ))
1462                    .await
1463            }
1464        };
1465        let bucket = response
1466            .map(tonic::Response::into_inner)
1467            .map_err(Error::from)
1468            .map_err(provider_error)?;
1469        validate_bucket(bucket, None, Some(&expected_name))
1470    }
1471
1472    async fn create_multipart(
1473        &self,
1474        bucket: wire::BucketRef,
1475        object_key: String,
1476        metadata: wire::ObjectMetadata,
1477        condition: Option<Condition>,
1478        idempotency_key: Option<String>,
1479    ) -> std::result::Result<wire::MultipartUpload, crate::ObjectsError> {
1480        let upload = Bucket::new(self, bucket)
1481            .create_multipart(
1482                object_key,
1483                PutOptions {
1484                    metadata,
1485                    mutation: mutation_options(idempotency_key)?,
1486                    condition,
1487                },
1488            )
1489            .await
1490            .map_err(provider_error)?;
1491        Ok(wire::MultipartUpload {
1492            upload_id: upload.upload_id,
1493        })
1494    }
1495
1496    async fn upload_part(
1497        &self,
1498        bucket: wire::BucketRef,
1499        object_key: String,
1500        upload_id: String,
1501        part_number: u32,
1502        body: Bytes,
1503        idempotency_key: Option<String>,
1504    ) -> std::result::Result<wire::UploadedPart, crate::ObjectsError> {
1505        MultipartUpload {
1506            bucket: Bucket::new(self, bucket),
1507            object_key,
1508            upload_id,
1509        }
1510        .upload_part(part_number, body, mutation_options(idempotency_key)?)
1511        .await
1512        .map_err(provider_error)
1513    }
1514
1515    async fn list_parts(
1516        &self,
1517        bucket: wire::BucketRef,
1518        object_key: String,
1519        upload_id: String,
1520    ) -> std::result::Result<Vec<wire::UploadedPart>, crate::ObjectsError> {
1521        MultipartUpload {
1522            bucket: Bucket::new(self, bucket),
1523            object_key,
1524            upload_id,
1525        }
1526        .list_parts()
1527        .await
1528        .map_err(provider_error)
1529    }
1530
1531    async fn complete_multipart(
1532        &self,
1533        bucket: wire::BucketRef,
1534        object_key: String,
1535        upload_id: String,
1536        parts: Vec<wire::UploadedPart>,
1537        idempotency_key: Option<String>,
1538    ) -> std::result::Result<wire::ObjectVersion, crate::ObjectsError> {
1539        MultipartUpload {
1540            bucket: Bucket::new(self, bucket),
1541            object_key,
1542            upload_id,
1543        }
1544        .complete(parts, mutation_options(idempotency_key)?)
1545        .await
1546        .map_err(provider_error)
1547    }
1548
1549    async fn abort_multipart(
1550        &self,
1551        bucket: wire::BucketRef,
1552        object_key: String,
1553        upload_id: String,
1554        idempotency_key: Option<String>,
1555    ) -> std::result::Result<bool, crate::ObjectsError> {
1556        MultipartUpload {
1557            bucket: Bucket::new(self, bucket),
1558            object_key,
1559            upload_id,
1560        }
1561        .abort(mutation_options(idempotency_key)?)
1562        .await
1563        .map_err(provider_error)
1564    }
1565}
1566
1567#[cfg(test)]
1568mod tests {
1569    use super::*;
1570    use rcgen::generate_simple_self_signed;
1571    use tokio::net::TcpListener;
1572    use tokio_stream::wrappers::TcpListenerStream;
1573    use tonic::{
1574        Response, Status,
1575        transport::{Identity, Server, ServerTlsConfig},
1576    };
1577    use wire::buckets_service_server::{BucketsService, BucketsServiceServer};
1578
1579    struct AuthenticatedBuckets;
1580
1581    #[tonic::async_trait]
1582    impl BucketsService for AuthenticatedBuckets {
1583        async fn create_bucket(
1584            &self,
1585            request: Request<wire::CreateBucketRequest>,
1586        ) -> std::result::Result<Response<wire::Bucket>, Status> {
1587            if request
1588                .metadata()
1589                .get("authorization")
1590                .and_then(|value| value.to_str().ok())
1591                != Some("Bearer exact-token")
1592            {
1593                return Err(Status::unauthenticated("missing exact bearer credential"));
1594            }
1595            let name = request.into_inner().name;
1596            Ok(Response::new(wire::Bucket {
1597                bucket: Some(wire::BucketRef {
1598                    bucket_id: "bucket-1".into(),
1599                    name,
1600                }),
1601                created_at: Some(prost_types::Timestamp::default()),
1602            }))
1603        }
1604
1605        async fn head_bucket(
1606            &self,
1607            _request: Request<wire::HeadBucketRequest>,
1608        ) -> std::result::Result<Response<wire::Bucket>, Status> {
1609            Err(Status::unimplemented("fixture"))
1610        }
1611
1612        async fn delete_bucket(
1613            &self,
1614            _request: Request<wire::DeleteBucketRequest>,
1615        ) -> std::result::Result<Response<wire::DeleteBucketResponse>, Status> {
1616            Err(Status::unimplemented("fixture"))
1617        }
1618    }
1619
1620    fn assert_provider<T: crate::ObjectsProvider>() {}
1621
1622    #[test]
1623    fn remote_client_implements_the_public_provider_boundary() {
1624        assert_provider::<Client>();
1625    }
1626
1627    #[test]
1628    fn provider_idempotency_keys_match_memory_validation() {
1629        assert!(matches!(
1630            mutation_options(Some(String::new())),
1631            Err(crate::ObjectsError::Invalid(_))
1632        ));
1633        assert!(matches!(
1634            mutation_options(Some("x".repeat(257))),
1635            Err(crate::ObjectsError::Invalid(_))
1636        ));
1637        assert!(mutation_options(None).is_ok());
1638        assert!(mutation_options(Some("key".into())).is_ok());
1639    }
1640
1641    #[test]
1642    fn provider_rejects_malformed_and_substituted_identities() {
1643        let expected = wire::BucketRef {
1644            bucket_id: "bucket-1".into(),
1645            name: "expected".into(),
1646        };
1647        assert!(matches!(
1648            validate_bucket(wire::Bucket::default(), Some(&expected), None),
1649            Err(crate::ObjectsError::Unavailable)
1650        ));
1651        let substituted = wire::Bucket {
1652            bucket: Some(wire::BucketRef {
1653                bucket_id: "bucket-2".into(),
1654                name: "expected".into(),
1655            }),
1656            created_at: Some(prost_types::Timestamp::default()),
1657        };
1658        assert!(matches!(
1659            validate_bucket(substituted, Some(&expected), None),
1660            Err(crate::ObjectsError::Unavailable)
1661        ));
1662    }
1663
1664    #[tokio::test]
1665    async fn plaintext_customer_endpoint_fails_before_connecting() {
1666        assert!(matches!(
1667            Client::connect("http://127.0.0.1:1", "token").await,
1668            Err(Error::InsecureEndpoint)
1669        ));
1670        assert!(matches!(
1671            Client::connect_with_ca_certificate("http://127.0.0.1:1", "token", b"certificate")
1672                .await,
1673            Err(Error::InsecureEndpoint)
1674        ));
1675    }
1676
1677    #[tokio::test]
1678    async fn private_ca_connection_rejects_unrepresentable_bearer_before_network_io() {
1679        assert!(matches!(
1680            Client::connect_with_ca_certificate(
1681                "https://127.0.0.1:1",
1682                "invalid\nbearer",
1683                b"certificate",
1684            )
1685            .await,
1686            Err(Error::InvalidCredential)
1687        ));
1688    }
1689
1690    #[test]
1691    fn private_ca_bundle_is_exactly_bounded() {
1692        assert!(matches!(
1693            bounded_ca_certificate(&[]),
1694            Err(Error::InvalidCaCertificate)
1695        ));
1696        assert!(matches!(
1697            bounded_ca_certificate(&vec![b'x'; MAX_CA_CERTIFICATE_BYTES]),
1698            Ok(certificate) if certificate.len() == MAX_CA_CERTIFICATE_BYTES
1699        ));
1700        assert!(matches!(
1701            bounded_ca_certificate(&vec![b'x'; MAX_CA_CERTIFICATE_BYTES + 1]),
1702            Err(Error::InvalidCaCertificate)
1703        ));
1704    }
1705
1706    #[tokio::test]
1707    async fn private_ca_https_connection_preserves_exact_bearer_authentication()
1708    -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
1709        let certified = generate_simple_self_signed(["localhost".to_owned()])?;
1710        let certificate_pem = certified.cert.pem();
1711        let private_key_pem = certified.signing_key.serialize_pem();
1712        let listener = TcpListener::bind("127.0.0.1:0").await?;
1713        let address = listener.local_addr()?;
1714        let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
1715        let server_certificate_pem = certificate_pem.clone();
1716        let server = tokio::spawn(async move {
1717            Server::builder()
1718                .tls_config(
1719                    ServerTlsConfig::new()
1720                        .identity(Identity::from_pem(server_certificate_pem, private_key_pem)),
1721                )?
1722                .add_service(BucketsServiceServer::new(AuthenticatedBuckets))
1723                .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async {
1724                    let _ = shutdown_rx.await;
1725                })
1726                .await
1727        });
1728
1729        let endpoint = format!("https://localhost:{}", address.port());
1730        assert!(Client::connect(&endpoint, "exact-token").await.is_err());
1731        let client =
1732            Client::connect_with_ca_certificate(endpoint, "exact-token", certificate_pem).await?;
1733        let bucket = client
1734            .create_bucket("fixture", MutationOptions::default())
1735            .await?;
1736        assert_eq!(bucket.id(), "bucket-1");
1737        assert_eq!(bucket.name(), "fixture");
1738
1739        let _ = shutdown_tx.send(());
1740        server.await??;
1741        Ok(())
1742    }
1743
1744    #[test]
1745    fn default_mutation_does_not_emit_an_empty_identity() {
1746        assert!(MutationOptions::default().wire().is_none());
1747    }
1748
1749    #[test]
1750    fn canonical_error_details_remain_typed() {
1751        let detail = wire::ErrorDetail {
1752            code: wire::ErrorCode::IdempotencyMismatch as i32,
1753            request_id: "request-7".into(),
1754        }
1755        .encode_to_vec();
1756        let error = Error::from(tonic::Status::with_details(
1757            tonic::Code::AlreadyExists,
1758            "idempotency mismatch",
1759            detail.into(),
1760        ));
1761        assert!(matches!(
1762            error,
1763            Error::Rejected {
1764                code: wire::ErrorCode::IdempotencyMismatch,
1765                request_id,
1766                ..
1767            } if request_id == "request-7"
1768        ));
1769    }
1770}