1use crate::{Bytes, Cond, Error, ObjectStore};
17
18#[allow(clippy::too_many_lines)] pub async fn conformance<S: ObjectStore>(s: &S, prefix: &str) -> Result<(), String> {
22 macro_rules! ensure {
23 ($c:expr, $($t:tt)*) => { if !$c { return Err(format!($($t)*)); } };
24 }
25 let k = |name: &str| format!("{prefix}/{name}");
26 let e = |r: Error| format!("unexpected error: {r}");
27
28 ensure!(s.get(&k("missing")).await.map_err(e)?.is_none(), "get of a missing key must be None");
30 ensure!(
31 s.head(&k("missing")).await.map_err(e)?.is_none(),
32 "head of a missing key must be None"
33 );
34 ensure!(
35 matches!(s.get_range(&k("missing"), 0..4).await, Err(Error::NotFound)),
36 "range of a missing key must be NotFound"
37 );
38 s.delete(&k("missing")).await.map_err(e)?;
39
40 let body = Bytes::from_static(b"hello, object store");
42 let g1 = s.put(&k("a"), body.clone(), Cond::CreateOnly).await.map_err(e)?;
43 ensure!(g1 != 0, "generations are never zero");
44 ensure!(
45 matches!(
46 s.put(&k("a"), Bytes::from_static(b"x"), Cond::CreateOnly).await,
47 Err(Error::Conflict)
48 ),
49 "create-only over an existing key must be Conflict"
50 );
51 let got = s.get(&k("a")).await.map_err(e)?.ok_or("object vanished")?;
52 ensure!(
53 got.bytes == body && got.generation == g1,
54 "get must return the bytes and generation written"
55 );
56 let h = s.head(&k("a")).await.map_err(e)?.ok_or("head of an existing key")?;
57 ensure!(
58 h.size == body.len() as u64 && h.generation == g1 && h.key == k("a"),
59 "head must report size, generation and key"
60 );
61
62 ensure!(
64 matches!(
65 s.put(&k("a"), Bytes::from_static(b"y"), Cond::IfGeneration(g1 + 1)).await,
66 Err(Error::PreconditionFailed)
67 ),
68 "a stale generation must fail the precondition"
69 );
70 let g2 = s.put(&k("a"), Bytes::from_static(b"v2"), Cond::IfGeneration(g1)).await.map_err(e)?;
71 ensure!(g2 != g1, "a write must change the generation");
72 ensure!(
73 matches!(
74 s.put(&k("a"), Bytes::from_static(b"v3"), Cond::IfGeneration(g1)).await,
75 Err(Error::PreconditionFailed)
76 ),
77 "the old generation must no longer match"
78 );
79 ensure!(
80 matches!(
81 s.put(&k("nope"), Bytes::from_static(b"v"), Cond::IfGeneration(g1)).await,
82 Err(Error::PreconditionFailed)
83 ),
84 "IfGeneration on a missing key must fail"
85 );
86 let g3 = s.put(&k("a"), Bytes::from_static(b"v3"), Cond::None).await.map_err(e)?;
87 ensure!(g3 != g2, "an unconditional write changes the generation");
88
89 s.put(&k("r"), Bytes::from_static(b"0123456789"), Cond::None).await.map_err(e)?;
91 let cases: [(u64, u64, &[u8]); 5] =
92 [(0, 4, b"0123"), (6, 10, b"6789"), (8, 100, b"89"), (10, 12, b""), (50, 60, b"")];
93 for (a, b, want) in cases {
94 let got = s.get_range(&k("r"), a..b).await.map_err(e)?;
95 ensure!(got.as_ref() == want, "range {a}..{b} returned {:?}", got);
96 }
97
98 s.put(&k("empty"), Bytes::new(), Cond::CreateOnly).await.map_err(e)?;
100 ensure!(
101 s.get(&k("empty")).await.map_err(e)?.is_some_and(|o| o.bytes.is_empty()),
102 "empty objects round-trip"
103 );
104 let big: Bytes = (0..(1u32 << 20))
105 .map(|i| (i.wrapping_mul(2_654_435_761) >> 24) as u8)
106 .collect::<Vec<u8>>()
107 .into();
108 s.put(&k("big"), big.clone(), Cond::CreateOnly).await.map_err(e)?;
109 ensure!(
110 s.get(&k("big")).await.map_err(e)?.is_some_and(|o| o.bytes == big),
111 "a 1 MiB object round-trips exactly"
112 );
113 ensure!(
114 s.get_range(&k("big"), 1000..1010).await.map_err(e)? == big.slice(1000..1010),
115 "ranges of a large object"
116 );
117
118 let huge: Bytes = (0..(9u32 << 20))
121 .map(|i| (i.wrapping_mul(2_246_822_519) >> 25) as u8)
122 .collect::<Vec<u8>>()
123 .into();
124 s.put(&k("huge"), huge.clone(), Cond::CreateOnly).await.map_err(e)?;
125 ensure!(
126 s.head(&k("huge")).await.map_err(e)?.is_some_and(|m| m.size == huge.len() as u64),
127 "a 9 MiB object reports its size"
128 );
129 let edge = (8u64 << 20) - 5;
130 ensure!(
131 s.get_range(&k("huge"), edge..edge + 10).await.map_err(e)?
132 == huge.slice(edge as usize..edge as usize + 10),
133 "a range across the 8 MiB boundary"
134 );
135 ensure!(
136 s.get(&k("huge")).await.map_err(e)?.is_some_and(|o| o.bytes == huge),
137 "a 9 MiB object round-trips exactly"
138 );
139
140 s.put(&k("dir/x"), Bytes::from_static(b"1"), Cond::None).await.map_err(e)?;
142 s.put(&k("dir/y/z"), Bytes::from_static(b"22"), Cond::None).await.map_err(e)?;
143 s.put(&k("dirt"), Bytes::from_static(b"3"), Cond::None).await.map_err(e)?;
144 let listed: Vec<String> =
145 s.list(&k("dir/")).await.map_err(e)?.into_iter().map(|m| m.key).collect();
146 ensure!(listed == [k("dir/x"), k("dir/y/z")], "list(\"dir/\") returned {listed:?}");
147 let listed: Vec<String> =
148 s.list(&k("dir")).await.map_err(e)?.into_iter().map(|m| m.key).collect();
149 ensure!(listed == [k("dir/x"), k("dir/y/z"), k("dirt")], "list(\"dir\") returned {listed:?}");
150 let all = s.list(&format!("{prefix}/")).await.map_err(e)?;
151 ensure!(all.windows(2).all(|w| w[0].key < w[1].key), "list output must be sorted");
152 ensure!(all.iter().any(|m| m.key == k("dir/y/z") && m.size == 2), "list reports sizes");
153
154 for bad in ["", "../x", "a//b", ".hidden", "sp ace"] {
156 ensure!(
157 matches!(s.put(bad, Bytes::new(), Cond::None).await, Err(Error::InvalidKey { .. })),
158 "key {bad:?} must be rejected"
159 );
160 }
161
162 s.delete(&k("a")).await.map_err(e)?;
164 ensure!(s.get(&k("a")).await.map_err(e)?.is_none(), "deleted objects are gone");
165 let g4 = s.put(&k("a"), Bytes::from_static(b"again"), Cond::CreateOnly).await.map_err(e)?;
166 ensure!(g4 != g3, "a recreated object gets a new generation");
167
168 for m in s.list(&format!("{prefix}/")).await.map_err(e)? {
170 s.delete(&m.key).await.map_err(e)?;
171 }
172 ensure!(
173 s.list(&format!("{prefix}/")).await.map_err(e)?.is_empty(),
174 "cleanup left objects behind"
175 );
176 Ok(())
177}
178
179#[allow(clippy::too_many_lines)] pub async fn conformance_encoded<S: ObjectStore>(s: &S, prefix: &str) -> Result<(), String> {
186 use crate::ContentEncoding::Gzip;
187 macro_rules! ensure {
188 ($c:expr, $($t:tt)*) => { if !$c { return Err(format!($($t)*)); } };
189 }
190 let k = |name: &str| format!("{prefix}/{name}");
191 let e = |r: Error| format!("unexpected error: {r}");
192 let text: Bytes = b"a line of text that compresses, ".repeat(4096).into();
193
194 let g1 = s.put_encoded(&k("enc"), text.clone(), Cond::CreateOnly, Gzip).await.map_err(e)?;
196 let got = s.get(&k("enc")).await.map_err(e)?.ok_or("encoded object vanished")?;
197 ensure!(got.bytes == text && got.generation == g1, "get must decode to the bytes written");
198 let st = s.stat(&k("enc")).await.map_err(e)?.ok_or("stat of an encoded object")?;
199 ensure!(st.content_encoding == Some(Gzip), "stat must report gzip, got {st:?}");
200 ensure!(
201 st.meta.generation == g1 && st.meta.size < text.len() as u64 / 4,
202 "stat reports the generation and the stored (compressed) size: {st:?}"
203 );
204 let head = s.head(&k("enc")).await.map_err(e)?.ok_or("head of an encoded object")?;
205 ensure!(head == st.meta, "head and stat agree: {head:?} vs {st:?}");
206
207 for r in [0..4, 0..u64::MAX, 3..3, 1_000_000..1_000_010] {
209 ensure!(
210 matches!(s.get_range(&k("enc"), r.clone()).await, Err(Error::EncodedRange { .. })),
211 "a range {r:?} of an encoded object must be EncodedRange"
212 );
213 }
214
215 s.put(&k("plain"), Bytes::from_static(b"0123456789"), Cond::CreateOnly).await.map_err(e)?;
217 ensure!(
218 s.get_range(&k("plain"), 2..5).await.map_err(e)? == b"234"[..],
219 "plain ranges are unchanged"
220 );
221 let pst = s.stat(&k("plain")).await.map_err(e)?.ok_or("stat of a plain object")?;
222 ensure!(pst.content_encoding.is_none() && pst.meta.size == 10, "plain stat: {pst:?}");
223
224 let other: Bytes = b"other text that also compresses, ".repeat(512).into();
227 ensure!(
228 matches!(
229 s.put_encoded(&k("enc"), other.clone(), Cond::CreateOnly, Gzip).await,
230 Err(Error::Conflict)
231 ),
232 "create-only over an encoded object must be Conflict"
233 );
234 ensure!(
235 matches!(
236 s.put_encoded(&k("enc"), other.clone(), Cond::IfGeneration(g1 + 1), Gzip).await,
237 Err(Error::PreconditionFailed)
238 ),
239 "a stale generation must fail the precondition"
240 );
241 ensure!(
242 matches!(
243 s.put_encoded(&k("none"), other.clone(), Cond::IfGeneration(g1), Gzip).await,
244 Err(Error::PreconditionFailed)
245 ),
246 "IfGeneration on a missing key must fail"
247 );
248
249 let g2 = s.put(&k("enc"), Bytes::from_static(b"now plain"), Cond::IfGeneration(g1)).await;
251 let g2 = g2.map_err(e)?;
252 ensure!(g2 != g1, "a rewrite changes the generation");
253 ensure!(
254 s.get(&k("enc")).await.map_err(e)?.is_some_and(|o| o.bytes == b"now plain"[..]),
255 "a plain rewrite of an encoded key reads plain"
256 );
257 ensure!(
258 s.get_range(&k("enc"), 0..3).await.map_err(e)? == b"now"[..],
259 "and its ranges work again"
260 );
261 let small: Bytes = Bytes::from_static(b"tiny");
262 let g3 = s.put_encoded(&k("enc"), small.clone(), Cond::IfGeneration(g2), Gzip).await;
263 let g3 = g3.map_err(e)?;
264 ensure!(
265 s.get(&k("enc")).await.map_err(e)?.is_some_and(|o| o.bytes == small && o.generation == g3),
266 "an encoded rewrite of a plain key decodes"
267 );
268 let empty = s.put_encoded(&k("empty"), Bytes::new(), Cond::None, Gzip).await.map_err(e)?;
269 ensure!(
270 s.get(&k("empty"))
271 .await
272 .map_err(e)?
273 .is_some_and(|o| o.bytes.is_empty() && o.generation == empty),
274 "an encoded empty body round-trips"
275 );
276
277 let listed = s.list_stat(&format!("{prefix}/")).await.map_err(e)?;
279 let seen: Vec<(String, Option<crate::ContentEncoding>)> =
280 listed.iter().map(|st| (st.meta.key.clone(), st.content_encoding)).collect();
281 ensure!(
282 seen == [(k("empty"), Some(Gzip)), (k("enc"), Some(Gzip)), (k("plain"), None)],
283 "list_stat returned {seen:?}"
284 );
285 let plain_list: Vec<_> = s.list(&format!("{prefix}/")).await.map_err(e)?;
286 ensure!(
287 plain_list == listed.iter().map(|st| st.meta.clone()).collect::<Vec<_>>(),
288 "list and list_stat agree"
289 );
290
291 ensure!(
293 matches!(
294 s.put_encoded("../x", Bytes::new(), Cond::None, Gzip).await,
295 Err(Error::InvalidKey { .. })
296 ),
297 "an invalid key is refused before encoding"
298 );
299
300 s.delete(&k("enc")).await.map_err(e)?;
302 ensure!(s.stat(&k("enc")).await.map_err(e)?.is_none(), "deleted objects have no stat");
303 s.put(&k("enc"), Bytes::from_static(b"fresh"), Cond::CreateOnly).await.map_err(e)?;
304 ensure!(
305 s.stat(&k("enc")).await.map_err(e)?.is_some_and(|st| st.content_encoding.is_none()),
306 "a key recreated plain carries no stale encoding"
307 );
308 clean(s, prefix).await
309}
310
311#[must_use]
313pub fn unique(tag: &str) -> String {
314 let nanos = std::time::SystemTime::now()
315 .duration_since(std::time::UNIX_EPOCH)
316 .map_or(0, |d| d.as_nanos());
317 format!("{tag}-{}-{nanos}", std::process::id())
318}
319
320pub async fn clean<S: ObjectStore>(s: &S, prefix: &str) -> Result<(), String> {
322 let dir = format!("{}/", prefix.trim_end_matches('/'));
323 for m in s.list(&dir).await.map_err(|e| e.to_string())? {
324 s.delete(&m.key).await.map_err(|e| e.to_string())?;
325 }
326 if s.list(&dir).await.map_err(|e| e.to_string())?.is_empty() {
327 Ok(())
328 } else {
329 Err(format!("{dir} not empty after cleanup"))
330 }
331}
332
333#[cfg(feature = "gcs")]
336#[must_use]
337pub fn live_bucket() -> Option<crate::Gcs<crate::token::Adc>> {
338 let bucket = std::env::var("OBJSTORE_LIVE_BUCKET").ok()?;
339 let adc = crate::token::Adc::discover().expect("credentials for the live bucket");
340 Some(crate::Gcs::new(&bucket, adc, crate::GcsConfig::default()).expect("live bucket name"))
341}
342
343#[cfg(feature = "gcs")]
347pub async fn emulator_bucket(
348 resumable_threshold: usize,
349) -> Option<crate::Gcs<crate::token::Static>> {
350 let endpoint = std::env::var("OBJSTORE_FAKE_GCS").ok()?;
351 let bucket = unique("objstore-r").replace('_', "-");
352 let resp = reqwest::Client::new()
353 .post(format!("{endpoint}/storage/v1/b?project=test"))
354 .header("content-type", "application/json")
355 .body(format!(r#"{{"name":"{bucket}"}}"#))
356 .send()
357 .await
358 .expect("fake-gcs-server reachable");
359 assert!(resp.status().is_success(), "bucket creation: {}", resp.status());
360 let cfg = crate::GcsConfig { endpoint, resumable_threshold, ..crate::GcsConfig::default() };
361 Some(crate::Gcs::new(&bucket, crate::token::Static::none(), cfg).expect("emulator bucket"))
362}