1use crate::Result;
16use crate::model_ext::{KeyAes256, OpenObjectRequest, ReadRange};
17use crate::object_descriptor::ObjectDescriptor;
18use crate::read_object::ReadObjectResponse;
19use crate::read_resume_policy::ReadResumePolicy;
20use crate::request_options::RequestOptions;
21use std::sync::Arc;
22use std::time::Duration;
23
24#[derive(Clone, Debug)]
43pub struct OpenObject<S = crate::storage::transport::Storage> {
44 stub: Arc<S>,
45 request: OpenObjectRequest,
46 options: RequestOptions,
47}
48
49impl<S> OpenObject<S>
50where
51 S: crate::storage::stub::Storage + 'static,
52{
53 pub async fn send(self) -> Result<ObjectDescriptor> {
67 let (descriptor, _) = self.stub.open_object(self.request, self.options).await?;
68 Ok(descriptor)
69 }
70
71 pub async fn send_and_read(
92 mut self,
93 range: ReadRange,
94 ) -> Result<(ObjectDescriptor, ReadObjectResponse)> {
95 self.request.ranges.push(range);
96 let (descriptor, mut readers) = self.stub.open_object(self.request, self.options).await?;
97 if readers.len() == 1 {
98 return Ok((descriptor, readers.pop().unwrap()));
99 }
100 unreachable!("the stub cannot create more readers")
103 }
104}
105
106impl<S> OpenObject<S> {
107 pub(crate) fn new<B, O>(
108 stub: std::sync::Arc<S>,
109 bucket: B,
110 object: O,
111 options: RequestOptions,
112 ) -> Self
113 where
114 B: Into<String>,
115 O: Into<String>,
116 {
117 let request = OpenObjectRequest::default()
118 .set_bucket(bucket)
119 .set_object(object);
120 Self {
121 request,
122 options,
123 stub,
124 }
125 }
126
127 pub fn compute_md5(mut self) -> Self {
151 self.options.checksum.md5_hash = Some(crate::storage::checksum::details::Md5::default());
152 self
153 }
154
155 pub fn compute_crc32c(mut self, enable: bool) -> Self {
184 self.options.checksum.crc32c =
185 enable.then(crate::storage::checksum::details::Crc32c::default);
186 self
187 }
188
189 pub fn set_generation<T: Into<i64>>(mut self, v: T) -> Self {
205 self.request = self.request.set_generation(v.into());
206 self
207 }
208
209 pub fn set_if_generation_match<T>(mut self, v: T) -> Self
225 where
226 T: Into<i64>,
227 {
228 self.request = self.request.set_if_generation_match(v.into());
229 self
230 }
231
232 pub fn set_if_generation_not_match<T>(mut self, v: T) -> Self
249 where
250 T: Into<i64>,
251 {
252 self.request = self.request.set_if_generation_not_match(v.into());
253 self
254 }
255
256 pub fn set_if_metageneration_match<T>(mut self, v: T) -> Self
272 where
273 T: Into<i64>,
274 {
275 self.request = self.request.set_if_metageneration_match(v.into());
276 self
277 }
278
279 pub fn set_if_metageneration_not_match<T>(mut self, v: T) -> Self
295 where
296 T: Into<i64>,
297 {
298 self.request = self.request.set_if_metageneration_not_match(v.into());
299 self
300 }
301
302 pub fn set_key(mut self, v: KeyAes256) -> Self {
319 self.request = self
320 .request
321 .set_common_object_request_params(crate::model::CommonObjectRequestParams::from(v));
322 self
323 }
324
325 pub fn with_retry_policy<V: Into<google_cloud_gax::retry_policy::RetryPolicyArg>>(
347 mut self,
348 v: V,
349 ) -> Self {
350 self.options.retry_policy = v.into().into();
351 self
352 }
353
354 pub fn with_backoff_policy<V: Into<google_cloud_gax::backoff_policy::BackoffPolicyArg>>(
371 mut self,
372 v: V,
373 ) -> Self {
374 self.options.backoff_policy = v.into().into();
375 self
376 }
377
378 pub fn with_retry_throttler<V: Into<google_cloud_gax::retry_throttler::RetryThrottlerArg>>(
401 mut self,
402 v: V,
403 ) -> Self {
404 self.options.retry_throttler = v.into().into();
405 self
406 }
407
408 pub fn with_read_resume_policy<V>(mut self, v: V) -> Self
428 where
429 V: ReadResumePolicy + 'static,
430 {
431 self.options.set_read_resume_policy(std::sync::Arc::new(v));
432 self
433 }
434
435 pub fn with_attempt_timeout(mut self, v: Duration) -> Self {
459 self.options.set_bidi_attempt_timeout(v);
460 self
461 }
462
463 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
478 self.options.user_agent = Some(user_agent.into());
479 self
480 }
481
482 pub fn with_quota_project(mut self, project: impl Into<String>) -> Self {
502 self.options.set_quota_project(project);
503 self
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use crate::client::Storage;
511 use crate::model::{CommonObjectRequestParams, Object};
512 use crate::model_ext::tests::create_key_helper;
513 use anyhow::Result;
514 use gaxi::grpc::tonic::{Response as TonicResponse, Result as TonicResult};
515 use google_cloud_auth::credentials::anonymous::Builder as Anonymous;
516 use google_cloud_gax::retry_policy::NeverRetry;
517 use http::HeaderValue;
518 use static_assertions::assert_impl_all;
519 use storage_grpc_mock::google::storage::v2::{
520 BidiReadObjectResponse, ChecksummedData, Object as ProtoObject, ObjectRangeData,
521 ReadRange as ProtoRange,
522 };
523 use storage_grpc_mock::{MockStorage, start};
524
525 const BUCKET_NAME: &str = "projects/_/buckets/test-bucket";
526 const OBJECT_NAME: &str = "test-object";
527 const USER_AGENT: &str = "quick_foxes_lazy_dogs/1.2.3";
528 const BIND_ADDRESS: &str = "0.0.0.0:0";
529
530 #[tokio::test]
532 async fn traits() -> Result<()> {
533 assert_impl_all!(OpenObject: Clone, std::fmt::Debug, Send, Sync);
534
535 let client = Storage::builder()
536 .with_credentials(Anonymous::new().build())
537 .build()
538 .await?;
539
540 fn need_send<T: Send>(_val: &T) {}
541 fn need_static<T: 'static>(_val: &T) {}
542
543 let open = client.open_object(BUCKET_NAME, OBJECT_NAME);
544 need_static(&open);
545
546 let fut = client.open_object(BUCKET_NAME, OBJECT_NAME).send();
547 need_send(&fut);
548 need_static(&fut);
549 Ok(())
550 }
551
552 #[tokio::test]
553 async fn open_object_normal() -> Result<()> {
554 let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
555 let initial = BidiReadObjectResponse {
556 metadata: Some(ProtoObject {
557 bucket: BUCKET_NAME.to_string(),
558 name: OBJECT_NAME.to_string(),
559 generation: 123456,
560 size: 42,
561 ..ProtoObject::default()
562 }),
563 ..BidiReadObjectResponse::default()
564 };
565 tx.send(Ok(initial)).await?;
566
567 let mut mock = MockStorage::new();
568 mock.expect_bidi_read_object()
569 .return_once(|_| Ok(TonicResponse::from(rx)));
570 let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
571
572 let client = Storage::builder()
573 .with_endpoint(endpoint)
574 .with_credentials(Anonymous::new().build())
575 .build()
576 .await?;
577 let descriptor = client.open_object(BUCKET_NAME, OBJECT_NAME).send().await?;
578
579 let got = descriptor.object();
580 let want = Object::new()
581 .set_bucket(BUCKET_NAME)
582 .set_name(OBJECT_NAME)
583 .set_generation(123456)
584 .set_size(42);
585 assert_eq!(got, want);
586
587 Ok(())
588 }
589
590 #[tokio::test]
591 async fn attributes() -> Result<()> {
592 let options = RequestOptions::new();
593 let builder = OpenObject::new(
594 Arc::new(StorageStub),
595 BUCKET_NAME.to_string(),
596 OBJECT_NAME.to_string(),
597 options,
598 )
599 .set_generation(123)
600 .set_if_generation_match(234)
601 .set_if_generation_not_match(345)
602 .set_if_metageneration_match(456)
603 .set_if_metageneration_not_match(567);
604 let want = OpenObjectRequest::default()
605 .set_bucket(BUCKET_NAME)
606 .set_object(OBJECT_NAME)
607 .set_generation(123)
608 .set_if_generation_match(234)
609 .set_if_generation_not_match(345)
610 .set_if_metageneration_match(456)
611 .set_if_metageneration_not_match(567);
612 assert_eq!(builder.request, want);
613 Ok(())
614 }
615
616 #[tokio::test]
617 async fn csek() -> Result<()> {
618 let options = RequestOptions::new();
619 let builder = OpenObject::new(
620 Arc::new(StorageStub),
621 BUCKET_NAME.to_string(),
622 OBJECT_NAME.to_string(),
623 options,
624 );
625
626 let (raw_key, _, _, _) = create_key_helper();
627 let key = KeyAes256::new(&raw_key)?;
628 let builder = builder.set_key(key.clone());
629 let want = OpenObjectRequest::default()
630 .set_bucket(BUCKET_NAME)
631 .set_object(OBJECT_NAME)
632 .set_common_object_request_params(CommonObjectRequestParams::from(key));
633 assert_eq!(builder.request, want);
634 Ok(())
635 }
636
637 #[tokio::test]
638 async fn request_options() -> Result<()> {
639 use crate::read_resume_policy::NeverResume;
640 use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
641 use google_cloud_gax::retry_policy::Aip194Strict;
642 use google_cloud_gax::retry_throttler::CircuitBreaker;
643
644 let options = RequestOptions::new();
645 let builder = OpenObject::new(
646 Arc::new(StorageStub),
647 BUCKET_NAME.to_string(),
648 OBJECT_NAME.to_string(),
649 options,
650 )
651 .with_backoff_policy(
652 ExponentialBackoffBuilder::default()
653 .with_scaling(4.0)
654 .build()
655 .expect("expontial backoff builds"),
656 )
657 .with_retry_policy(Aip194Strict)
658 .with_retry_throttler(CircuitBreaker::default())
659 .with_read_resume_policy(NeverResume)
660 .with_attempt_timeout(Duration::from_secs(120))
661 .with_user_agent(USER_AGENT);
662
663 let got = builder.options;
664 assert!(
665 format!("{:?}", got.backoff_policy).contains("ExponentialBackoff"),
666 "{got:?}"
667 );
668 assert!(
669 format!("{:?}", got.retry_policy).contains("Aip194Strict"),
670 "{got:?}"
671 );
672 assert!(
673 format!("{:?}", got.retry_throttler).contains("CircuitBreaker"),
674 "{got:?}"
675 );
676 assert!(
677 format!("{:?}", got.read_resume_policy()).contains("NeverResume"),
678 "{got:?}"
679 );
680 assert_eq!(
681 got.bidi_attempt_timeout,
682 Duration::from_secs(120),
683 "{got:?}"
684 );
685 assert_eq!(got.user_agent.as_deref(), Some(USER_AGENT), "{got:?}");
686
687 Ok(())
688 }
689
690 #[tokio::test]
691 async fn send() -> anyhow::Result<()> {
692 let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
693 let initial = BidiReadObjectResponse {
694 metadata: Some(ProtoObject {
695 bucket: BUCKET_NAME.to_string(),
696 name: OBJECT_NAME.to_string(),
697 generation: 123456,
698 ..ProtoObject::default()
699 }),
700 ..BidiReadObjectResponse::default()
701 };
702 tx.send(Ok(initial)).await?;
703
704 let mut mock = MockStorage::new();
705 mock.expect_bidi_read_object()
706 .return_once(|_| Ok(TonicResponse::from(rx)));
707 let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
708
709 let client = Storage::builder()
710 .with_credentials(Anonymous::new().build())
711 .with_endpoint(endpoint)
712 .build()
713 .await?;
714
715 let descriptor = client.open_object(BUCKET_NAME, OBJECT_NAME).send().await?;
716 let want = Object::new()
717 .set_bucket(BUCKET_NAME)
718 .set_name(OBJECT_NAME)
719 .set_generation(123456);
720 assert_eq!(descriptor.object(), want, "{descriptor:?}");
721 assert_eq!(
722 descriptor.headers().get("content-type"),
723 Some(&HeaderValue::from_static("application/grpc")),
724 "headers={:?}",
725 descriptor.headers()
726 );
727 Ok(())
728 }
729
730 #[tokio::test]
731 async fn send_and_read() -> anyhow::Result<()> {
732 let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
733 let payload = Vec::from_iter((0..32).map(|i| i as u8));
734 let initial = BidiReadObjectResponse {
735 metadata: Some(ProtoObject {
736 bucket: BUCKET_NAME.to_string(),
737 name: OBJECT_NAME.to_string(),
738 generation: 123456,
739 ..ProtoObject::default()
740 }),
741 object_data_ranges: vec![ObjectRangeData {
742 read_range: Some(ProtoRange {
743 read_id: 0_i64,
744 ..ProtoRange::default()
745 }),
746 range_end: true,
747 checksummed_data: Some(ChecksummedData {
748 content: payload.clone(),
749 crc32c: None,
750 }),
751 }],
752 ..BidiReadObjectResponse::default()
753 };
754 tx.send(Ok(initial)).await?;
755
756 let mut mock = MockStorage::new();
757 mock.expect_bidi_read_object()
758 .return_once(|_| Ok(TonicResponse::from(rx)));
759 let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
760
761 let client = Storage::builder()
762 .with_credentials(Anonymous::new().build())
763 .with_endpoint(endpoint)
764 .build()
765 .await?;
766
767 let (descriptor, mut reader) = client
768 .open_object(BUCKET_NAME, OBJECT_NAME)
769 .send_and_read(ReadRange::tail(32))
770 .await?;
771 let want = Object::new()
772 .set_bucket(BUCKET_NAME)
773 .set_name(OBJECT_NAME)
774 .set_generation(123456);
775 assert_eq!(descriptor.object(), want, "{descriptor:?}");
776 assert_eq!(
777 descriptor.headers().get("content-type"),
778 Some(&HeaderValue::from_static("application/grpc")),
779 "headers={:?}",
780 descriptor.headers()
781 );
782
783 let mut got_payload = Vec::new();
784 while let Some(chunk) = reader.next().await.transpose()? {
785 got_payload.extend_from_slice(&chunk);
786 }
787 assert_eq!(got_payload, payload);
788 Ok(())
789 }
790
791 #[tokio::test]
792 async fn send_and_read_checksum_bypassed_due_to_ranged_read() -> anyhow::Result<()> {
793 let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
794 let payload = Vec::from_iter((0..32).map(|i| i as u8));
795 let expected_checksum = crc32c::crc32c(&payload);
796 let corrupted_checksum = expected_checksum + 1; let initial = BidiReadObjectResponse {
798 metadata: Some(ProtoObject {
799 bucket: BUCKET_NAME.to_string(),
800 name: OBJECT_NAME.to_string(),
801 generation: 123456,
802 checksums: Some(storage_grpc_mock::google::storage::v2::ObjectChecksums {
803 crc32c: Some(corrupted_checksum),
804 ..Default::default()
805 }),
806 ..ProtoObject::default()
807 }),
808 object_data_ranges: vec![ObjectRangeData {
809 read_range: Some(ProtoRange {
810 read_id: 0_i64,
811 ..ProtoRange::default()
812 }),
813 range_end: true,
814 checksummed_data: Some(ChecksummedData {
815 content: payload.clone(),
816 crc32c: None,
817 }),
818 }],
819 ..BidiReadObjectResponse::default()
820 };
821 tx.send(Ok(initial)).await?;
822
823 let mut mock = MockStorage::new();
824 mock.expect_bidi_read_object()
825 .return_once(|_| Ok(TonicResponse::from(rx)));
826 let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
827
828 let client = Storage::builder()
829 .with_credentials(Anonymous::new().build())
830 .with_endpoint(endpoint)
831 .build()
832 .await?;
833
834 let (_descriptor, mut reader) = client
835 .open_object(BUCKET_NAME, OBJECT_NAME)
836 .send_and_read(ReadRange::segment(0, 32))
839 .await?;
840
841 let mut got_payload = Vec::new();
842 while let Some(res) = reader.next().await {
843 match res {
844 Ok(chunk) => got_payload.extend_from_slice(&chunk),
845 Err(e) => {
846 panic!("Did not expect error on ranged read: {e}");
847 }
848 }
849 }
850 assert_eq!(got_payload, payload);
851 Ok(())
852 }
853 #[tokio::test]
854 async fn send_and_read_checksum_mismatch() -> anyhow::Result<()> {
855 let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
856 let payload = Vec::from_iter((0..32).map(|i| i as u8));
857 let expected_checksum = crc32c::crc32c(&payload);
858 let corrupted_checksum = expected_checksum + 1; let initial = BidiReadObjectResponse {
860 metadata: Some(ProtoObject {
861 bucket: BUCKET_NAME.to_string(),
862 name: OBJECT_NAME.to_string(),
863 generation: 123456,
864 checksums: Some(storage_grpc_mock::google::storage::v2::ObjectChecksums {
865 crc32c: Some(corrupted_checksum),
866 ..Default::default()
867 }),
868 ..ProtoObject::default()
869 }),
870 object_data_ranges: vec![ObjectRangeData {
871 read_range: Some(ProtoRange {
872 read_id: 0_i64,
873 ..ProtoRange::default()
874 }),
875 range_end: true,
876 checksummed_data: Some(ChecksummedData {
877 content: payload.clone(),
878 crc32c: None,
879 }),
880 }],
881 ..BidiReadObjectResponse::default()
882 };
883 tx.send(Ok(initial)).await?;
884
885 let mut mock = MockStorage::new();
886 mock.expect_bidi_read_object()
887 .return_once(|_| Ok(TonicResponse::from(rx)));
888 let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
889
890 let client = Storage::builder()
891 .with_credentials(Anonymous::new().build())
892 .with_endpoint(endpoint)
893 .build()
894 .await?;
895
896 let (_descriptor, mut reader) = client
897 .open_object(BUCKET_NAME, OBJECT_NAME)
898 .send_and_read(ReadRange::all())
899 .await?;
900
901 let mut got_payload = Vec::new();
902 let mut got_err = false;
903 while let Some(res) = reader.next().await {
904 match res {
905 Ok(chunk) => got_payload.extend_from_slice(&chunk),
906 Err(e) => {
907 use std::error::Error as _;
908 got_err = true;
909 let source = e
910 .source()
911 .and_then(|err| err.downcast_ref::<crate::error::ReadError>());
912 assert!(
913 matches!(source, Some(crate::error::ReadError::ChecksumMismatch(_))),
914 "Expected ChecksumMismatch, got: {source:?}"
915 );
916 }
917 }
918 }
919 assert!(got_err, "Expected a checksum mismatch error");
920 assert_eq!(got_payload, payload);
921 Ok(())
922 }
923
924 #[tokio::test(start_paused = true)]
925 async fn timeout() -> anyhow::Result<()> {
926 let (_tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
927
928 let mut mock = MockStorage::new();
929 mock.expect_bidi_read_object()
930 .return_once(|_| Ok(TonicResponse::from(rx)));
931 let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
932
933 let client = Storage::builder()
934 .with_credentials(Anonymous::new().build())
935 .with_endpoint(endpoint)
936 .with_retry_policy(NeverRetry)
937 .build()
938 .await?;
939
940 let target = Duration::from_secs(120);
942 let start = tokio::time::Instant::now();
943 let err = client
944 .open_object(BUCKET_NAME, OBJECT_NAME)
945 .with_attempt_timeout(target)
946 .send()
947 .await
948 .unwrap_err();
949 assert!(err.is_timeout(), "{err:?}");
950 assert_eq!(start.elapsed(), target);
951
952 Ok(())
953 }
954
955 #[tokio::test]
956 async fn user_agent() -> anyhow::Result<()> {
957 let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
958 let initial = BidiReadObjectResponse {
959 metadata: Some(ProtoObject {
960 bucket: BUCKET_NAME.to_string(),
961 name: OBJECT_NAME.to_string(),
962 generation: 123456,
963 ..ProtoObject::default()
964 }),
965 ..BidiReadObjectResponse::default()
966 };
967 tx.send(Ok(initial)).await?;
968
969 let mut mock = MockStorage::new();
970 mock.expect_bidi_read_object().return_once(|request| {
971 let metadata = request.metadata();
972 let user_agent = metadata
973 .get(http::header::USER_AGENT.as_str())
974 .and_then(|v| v.to_str().ok())
975 .expect("user-agent should be set");
976
977 let got = user_agent.split(' ').any(|s| s == USER_AGENT);
978 assert!(got, "{user_agent:?}");
979
980 Ok(TonicResponse::from(rx))
981 });
982 let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
983
984 let client = Storage::builder()
985 .with_credentials(Anonymous::new().build())
986 .with_endpoint(endpoint)
987 .build()
988 .await?;
989
990 let _descriptor = client
991 .open_object(BUCKET_NAME, OBJECT_NAME)
992 .with_user_agent(USER_AGENT)
993 .send()
994 .await?;
995 Ok(())
996 }
997
998 #[test]
999 fn checksum_toggles() -> Result<()> {
1000 let options = RequestOptions::new();
1001
1002 assert!(options.checksum.crc32c.is_some());
1004 assert!(options.checksum.md5_hash.is_none());
1005
1006 let builder = OpenObject::new(
1007 Arc::new(StorageStub),
1008 BUCKET_NAME.to_string(),
1009 OBJECT_NAME.to_string(),
1010 options,
1011 )
1012 .compute_crc32c(false)
1013 .compute_md5();
1014
1015 let got = builder.options;
1016 assert!(got.checksum.crc32c.is_none());
1017 assert!(got.checksum.md5_hash.is_some());
1018
1019 Ok(())
1020 }
1021
1022 #[tokio::test]
1023 async fn quota_project() -> anyhow::Result<()> {
1024 const PROJECT_NAME: &str = "project_lazy_dog";
1025 let (tx, rx) = tokio::sync::mpsc::channel::<TonicResult<BidiReadObjectResponse>>(1);
1026 let initial = BidiReadObjectResponse {
1027 metadata: Some(ProtoObject {
1028 bucket: BUCKET_NAME.to_string(),
1029 name: OBJECT_NAME.to_string(),
1030 generation: 123456,
1031 ..ProtoObject::default()
1032 }),
1033 ..BidiReadObjectResponse::default()
1034 };
1035 tx.send(Ok(initial)).await?;
1036
1037 let mut mock = MockStorage::new();
1038 mock.expect_bidi_read_object().return_once(|request| {
1039 let user_project = request
1040 .metadata()
1041 .get("x-goog-user-project")
1042 .and_then(|v| v.to_str().ok())
1043 .expect("x-goog-user-project should be set");
1044 assert_eq!(user_project, PROJECT_NAME);
1045 Ok(TonicResponse::from(rx))
1046 });
1047 let (endpoint, _server) = start(BIND_ADDRESS, mock).await?;
1048
1049 let client = Storage::builder()
1050 .with_credentials(Anonymous::new().build())
1051 .with_endpoint(endpoint)
1052 .build()
1053 .await?;
1054
1055 let _descriptor = client
1056 .open_object(BUCKET_NAME, OBJECT_NAME)
1057 .with_quota_project(PROJECT_NAME)
1058 .send()
1059 .await?;
1060 Ok(())
1061 }
1062
1063 #[derive(Debug)]
1064 struct StorageStub;
1065 impl crate::stub::Storage for StorageStub {}
1066}