1use {
8 super::{error::ObjectStoreError, object_store_api as object_store},
9 bytes::{Bytes, BytesMut},
10 futures_util::TryStreamExt as _,
11 object_store::{GetOptions, ObjectStoreExt as _, path::Path},
12 std::sync::Arc,
13};
14
15pub async fn read_capped(
30 store: &Arc<dyn object_store::ObjectStore>,
31 key: &Path,
32 max_bytes: u64,
33) -> Result<Bytes, ObjectStoreError> {
34 let meta = store.head(key).await?;
35 if meta.size > max_bytes {
36 return Err(ObjectStoreError::TooLarge {
37 key: key.clone(),
38 size: meta.size,
39 max_bytes,
40 });
41 }
42 let result = store
43 .get_opts(key, GetOptions {
44 version: meta.version,
45 if_match: meta.e_tag,
46 ..GetOptions::default()
47 })
48 .await?;
49 if result.meta.size > max_bytes {
50 return Err(ObjectStoreError::TooLarge {
51 key: key.clone(),
52 size: result.meta.size,
53 max_bytes,
54 });
55 }
56 let mut stream = result.into_stream();
57 let mut bytes = BytesMut::new();
58 while let Some(chunk) = stream.try_next().await? {
59 let size = u64::try_from(bytes.len())
60 .unwrap_or(u64::MAX)
61 .saturating_add(u64::try_from(chunk.len()).unwrap_or(u64::MAX));
62 if size > max_bytes {
63 return Err(ObjectStoreError::TooLarge {
64 key: key.clone(),
65 size,
66 max_bytes,
67 });
68 }
69 bytes.extend_from_slice(&chunk);
70 }
71 Ok(bytes.freeze())
72}
73
74#[cfg(test)]
75mod tests {
76 use {
77 super::{object_store, *},
78 assert2::assert,
79 futures_util::{StreamExt as _, stream},
80 object_store::{PutPayload, memory::InMemory, path::Path},
81 std::{
82 fmt, iter,
83 sync::{
84 Arc,
85 atomic::{AtomicUsize, Ordering::SeqCst},
86 },
87 },
88 };
89
90 async fn store_with(key: &str, bytes: &'static [u8]) -> Arc<dyn object_store::ObjectStore> {
91 let store: Arc<dyn object_store::ObjectStore> = Arc::new(InMemory::new());
92 store
93 .put(&Path::from(key), PutPayload::from(bytes))
94 .await
95 .unwrap();
96 store
97 }
98
99 #[tokio::test]
100 async fn under_cap_returns_bytes() {
101 let store = store_with("k", b"hello").await;
102 let got = read_capped(&store, &Path::from("k"), 1024).await.unwrap();
103 assert!(&got[..] == b"hello");
104 }
105
106 #[tokio::test]
107 async fn over_cap_returns_too_large() {
108 let store = store_with("k", b"hello world").await;
109 let err = read_capped(&store, &Path::from("k"), 4).await.unwrap_err();
110 assert!(matches!(err, ObjectStoreError::TooLarge {
111 size: 11,
112 max_bytes: 4,
113 ..
114 }));
115 }
116
117 #[tokio::test]
118 async fn missing_object_maps_to_not_found() {
119 let store: Arc<dyn object_store::ObjectStore> = Arc::new(InMemory::new());
120 let err = read_capped(&store, &Path::from("absent"), 1024)
121 .await
122 .unwrap_err();
123 assert!(matches!(err, ObjectStoreError::NotFound(_)));
124 }
125
126 #[derive(Debug, Clone, Copy)]
127 enum Race {
128 Etag,
129 Version,
130 Metadata,
131 Body,
132 }
133
134 #[derive(Debug)]
135 struct ChangingStore {
136 inner: InMemory,
137 race: Race,
138 chunks: Arc<AtomicUsize>,
139 }
140
141 impl fmt::Display for ChangingStore {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("ChangingStore") }
143 }
144
145 #[krabka_macros::object_store_delegate(
146 put_multipart_opts,
147 delete_stream,
148 list,
149 list_with_delimiter,
150 copy_opts
151 )]
152 #[async_trait::async_trait]
153 impl object_store::ObjectStore for ChangingStore {
154 async fn put_opts(
155 &self,
156 location: &Path,
157 payload: PutPayload,
158 opts: object_store::PutOptions,
159 ) -> object_store::Result<object_store::PutResult> {
160 self.inner.put_opts(location, payload, opts).await
161 }
162
163 async fn get_opts(
164 &self,
165 location: &Path,
166 options: GetOptions,
167 ) -> object_store::Result<object_store::GetResult> {
168 if options.head {
169 let mut result = self.inner.get_opts(location, options).await?;
170 if matches!(self.race, Race::Version) {
171 result.meta.version = Some("original".into());
172 } else if !matches!(self.race, Race::Etag) {
173 result.meta.e_tag = None;
174 }
175 return Ok(result);
176 }
177 if matches!(self.race, Race::Version) {
178 assert!(options.version.as_deref() == Some("original"));
179 let original = self
180 .inner
181 .get_opts(location, GetOptions {
182 version: None,
183 ..options
184 })
185 .await?;
186 self.inner
187 .put(location, PutPayload::from_static(b"123456789"))
188 .await?;
189 return Ok(original);
190 }
191 self.inner
192 .put(location, PutPayload::from_static(b"123456789"))
193 .await?;
194 let mut result = self.inner.get_opts(location, options).await?;
195 if matches!(self.race, Race::Body) {
196 result.meta.size = 3;
197 let chunks = Arc::clone(&self.chunks);
198 result.payload = object_store::GetResultPayload::Stream(
199 stream::iter(iter::repeat_with(|| Ok(Bytes::from_static(b"123"))).take(3))
200 .inspect(move |_| {
201 chunks.fetch_add(1, SeqCst);
202 })
203 .boxed(),
204 );
205 }
206 Ok(result)
207 }
208 }
209
210 #[yare::parameterized(
211 etag = { Race::Etag },
212 metadata = { Race::Metadata },
213 body = { Race::Body },
214 )]
215 #[test_macro(tokio::test)]
216 async fn overwrite_between_head_and_get_cannot_bypass_the_cap(race: Race) {
217 let changing = Arc::new(ChangingStore {
218 inner: InMemory::new(),
219 race,
220 chunks: Arc::default(),
221 });
222 let key = Path::from("changing");
223 changing
224 .put(&key, PutPayload::from_static(b"123"))
225 .await
226 .unwrap();
227 let store: Arc<dyn object_store::ObjectStore> = Arc::<ChangingStore>::clone(&changing);
228 let error = read_capped(&store, &key, 5).await.unwrap_err();
229 match race {
230 Race::Etag => assert2::assert!(matches!(error, ObjectStoreError::Precondition { .. })),
231 Race::Version => unreachable!("the versioned case returns the original bytes"),
232 Race::Metadata => {
233 assert2::assert!(matches!(error, ObjectStoreError::TooLarge { size: 9, .. }));
234 }
235 Race::Body => {
236 assert2::assert!(matches!(error, ObjectStoreError::TooLarge { size: 6, .. }));
237 assert2::assert!(changing.chunks.load(SeqCst) == 2);
238 }
239 }
240 }
241
242 #[tokio::test]
243 async fn versioned_get_reads_the_head_version_after_an_overwrite() {
244 let changing = Arc::new(ChangingStore {
245 inner: InMemory::new(),
246 race: Race::Version,
247 chunks: Arc::default(),
248 });
249 let key = Path::from("versioned");
250 changing
251 .put(&key, PutPayload::from_static(b"123"))
252 .await
253 .unwrap();
254 let store: Arc<dyn object_store::ObjectStore> = Arc::<ChangingStore>::clone(&changing);
255 assert!(read_capped(&store, &key, 5).await.unwrap().as_ref() == b"123");
256 assert!(changing.inner.head(&key).await.unwrap().size == 9);
257 }
258}