1use 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;
15pub const MAX_CA_CERTIFICATE_BYTES: usize = 64 * 1024;
17
18#[derive(Debug, Error)]
20pub enum Error {
21 #[error("invalid Objects endpoint: {0}")]
23 InvalidEndpoint(#[from] tonic::transport::Error),
24 #[error("Objects customer endpoints must use https")]
26 InsecureEndpoint,
27 #[error("invalid Objects bearer credential")]
29 InvalidCredential,
30 #[error("invalid Objects CA certificate bundle")]
32 InvalidCaCertificate,
33 #[error("Objects request rejected ({code:?}, request {request_id}): {message}")]
35 Rejected {
36 code: wire::ErrorCode,
38 request_id: String,
40 message: String,
42 },
43 #[error("Objects transport failed: {0}")]
45 Transport(tonic::Status),
46 #[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
67pub type Result<T> = std::result::Result<T, Error>;
69
70#[derive(Clone)]
72pub struct Client {
73 channel: Channel,
74 authorization: MetadataValue<Ascii>,
75}
76
77impl Client {
78 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 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 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 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 #[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#[derive(Clone, Debug, Default)]
198pub struct MutationOptions {
199 idempotency_key: String,
200}
201
202impl MutationOptions {
203 #[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#[derive(Clone, Debug, Default)]
219pub struct PutOptions {
220 metadata: wire::ObjectMetadata,
221 mutation: MutationOptions,
222 condition: Option<Condition>,
223}
224
225impl PutOptions {
226 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
284 pub fn condition(mut self, value: Condition) -> Self {
285 self.condition = Some(value);
286 self
287 }
288}
289
290#[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 #[must_use]
315 pub fn version(mut self, value: impl Into<String>) -> Self {
316 self.version_id = value.into();
317 self
318 }
319
320 #[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 #[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 #[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 #[must_use]
343 pub fn maximum_bytes(mut self, value: u64) -> Self {
344 self.maximum_bytes = value;
345 self
346 }
347}
348
349#[derive(Clone, Debug, Default)]
351pub struct DeleteOptions {
352 version_id: String,
353 mutation: MutationOptions,
354 condition: Option<Condition>,
355}
356
357impl DeleteOptions {
358 #[must_use]
360 pub fn version(mut self, value: impl Into<String>) -> Self {
361 self.version_id = value.into();
362 self
363 }
364
365 #[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 #[must_use]
374 pub fn condition(mut self, value: Condition) -> Self {
375 self.condition = Some(value);
376 self
377 }
378}
379
380#[derive(Clone, Debug)]
382pub struct ListOptions {
383 pub prefix: String,
385 pub delimiter: String,
387 pub versions: bool,
389 pub page_size: u32,
391 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#[derive(Clone, Debug)]
409pub struct ListPage {
410 pub entries: Vec<wire::ListEntry>,
412 pub common_prefixes: Vec<String>,
414 pub continuation: Option<String>,
416}
417
418#[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 #[must_use]
437 pub fn id(&self) -> &str {
438 &self.reference.bucket_id
439 }
440
441 #[must_use]
443 pub fn name(&self) -> &str {
444 &self.reference.name
445 }
446
447 #[must_use]
449 pub fn reference(&self) -> wire::BucketRef {
450 self.reference.clone()
451 }
452
453 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 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 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 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 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 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 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 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 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 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#[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 #[must_use]
727 pub fn id(&self) -> &str {
728 &self.reference.snapshot_id
729 }
730
731 #[must_use]
733 pub fn reference(&self) -> wire::SnapshotRef {
734 self.reference.clone()
735 }
736
737 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 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 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 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
827pub struct StoredObject {
829 pub version: wire::ObjectVersion,
831 stream: tonic::Streaming<wire::GetObjectResponse>,
832 remaining: u64,
833}
834
835impl StoredObject {
836 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#[derive(Clone)]
869pub struct MultipartUpload {
870 bucket: Bucket,
871 object_key: String,
872 upload_id: String,
873}
874
875impl MultipartUpload {
876 #[must_use]
878 pub fn id(&self) -> &str {
879 &self.upload_id
880 }
881
882 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 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 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 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}