Skip to content

Commit a7121d8

Browse files
committed
feat: add SanityCheckLayer to detect unexpected list responses and expose SanityCheckLayer in mod.rs (#6646)
1 parent 42545a5 commit a7121d8

File tree

2 files changed

+89
-0
lines changed

2 files changed

+89
-0
lines changed

core/src/layers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,5 @@ pub use capability_check::CapabilityCheckLayer;
132132

133133
mod http_client;
134134
pub use http_client::HttpClientLayer;
135+
mod sanity_check;
136+
pub use sanity_check::SanityCheckLayer;

core/src/layers/sanity_check.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use crate::raw::*;
2+
use crate::{Error, ErrorKind, Result};
3+
4+
/// SanityCheckLayer adds a simple sanity check for list operations.
5+
/// It ensures that the paths returned by underlying storage start with
6+
/// the requested prefix. If an unexpected path is returned, it will return
7+
/// an Unexpected error.
8+
///
9+
/// This layer can detect misbehaving services that return responses for
10+
/// unrelated keys (see issue #6646).
11+
#[derive(Default)]
12+
pub struct SanityCheckLayer;
13+
14+
impl<A: Access> Layer<A> for SanityCheckLayer {
15+
type LayeredAccess = SanityCheckAccessor<A>;
16+
17+
fn layer(&self, inner: A) -> Self::LayeredAccess {
18+
SanityCheckAccessor {
19+
inner,
20+
}
21+
}
22+
}
23+
24+
#[derive(Debug, Clone)]
25+
pub struct SanityCheckAccessor<A: Access> {
26+
inner: A,
27+
}
28+
29+
impl<A: Access> LayeredAccess for SanityCheckAccessor<A> {
30+
type Inner = A;
31+
type Reader = A::Reader;
32+
type Writer = A::Writer;
33+
type Lister = SanityCheckLister<A::Lister>;
34+
type Deleter = A::Deleter;
35+
36+
fn inner(&self) -> &Self::Inner {
37+
&self.inner
38+
}
39+
40+
async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
41+
self.inner.read(path, args).await
42+
}
43+
44+
async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, Self::Writer)> {
45+
self.inner.write(path, args).await
46+
}
47+
48+
async fn delete(&self) -> Result<(RpDelete, Self::Deleter)> {
49+
self.inner.delete().await
50+
}
51+
52+
async fn list(&self, path: &str, args: OpList) -> Result<(RpList, Self::Lister)> {
53+
let (rp, lister) = self.inner.list(path, args).await?;
54+
let prefix = path.to_string();
55+
let checker = SanityCheckLister {
56+
inner: lister,
57+
prefix,
58+
};
59+
Ok((rp, checker))
60+
}
61+
}
62+
63+
pub struct SanityCheckLister<L: oio::List> {
64+
inner: L,
65+
prefix: String,
66+
}
67+
68+
impl<L: oio::List> oio::List for SanityCheckLister<L> {
69+
async fn next(&mut self) -> Result<Option<oio::Entry>> {
70+
match self.inner.next().await? {
71+
Some(entry) => {
72+
let p = entry.path();
73+
if !p.starts_with(&self.prefix) {
74+
return Err(Error::new(
75+
ErrorKind::Unexpected,
76+
&format!(
77+
"sanity check failed: entry {} is outside prefix {}",
78+
p, self.prefix
79+
),
80+
));
81+
}
82+
Ok(Some(entry))
83+
}
84+
None => Ok(None),
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)