use bytes::{BufMut, Bytes, BytesMut};
use super::error::StoreError;
use crate::repo::{NamespaceKey, RepoName};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Partition {
Namespace(NamespaceKey),
Coordinator(NamespaceKey),
Ref {
ns: NamespaceKey,
repo: RepoName,
shard_ref: String,
},
RepoIndex {
ns: NamespaceKey,
repo: RepoName,
prefix: u16,
},
RefIndex {
ns: NamespaceKey,
repo: RepoName,
bucket: u16,
},
ContentShard(u16),
}
impl Partition {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Namespace(_) => "namespace",
Self::Coordinator(_) => "coordinator",
Self::Ref { .. } => "ref",
Self::RepoIndex { .. } => "repo_index",
Self::RefIndex { .. } => "ref_index",
Self::ContentShard(_) => "content",
}
}
pub fn encode(&self) -> Result<Bytes, StoreError> {
let (tag, parts): (u8, Vec<String>) = match self {
Self::Namespace(ns) => (b'n', vec![ns.as_str().into()]),
Self::Coordinator(ns) => (b'c', vec![ns.as_str().into()]),
Self::Ref {
ns,
repo,
shard_ref,
} => (
b'r',
vec![ns.as_str().into(), repo.as_str().into(), shard_ref.clone()],
),
Self::RepoIndex { ns, repo, prefix } => (
b'i',
vec![ns.as_str().into(), repo.as_str().into(), prefix.to_string()],
),
Self::RefIndex { ns, repo, bucket } => (
b'x',
vec![ns.as_str().into(), repo.as_str().into(), bucket.to_string()],
),
Self::ContentShard(prefix) => (b's', vec![prefix.to_string()]),
};
let mut buf = BytesMut::new();
buf.put_u8(tag);
for part in parts {
if part.as_bytes().contains(&0) {
return Err(StoreError::Invalid(
"partition component contains 0x00".into(),
));
}
buf.put_slice(part.as_bytes());
buf.put_u8(0);
}
Ok(buf.freeze())
}
pub fn decode(bytes: &[u8]) -> Result<Self, StoreError> {
let corrupt = || StoreError::Corrupt("malformed partition encoding".into());
let (&tag, body) = bytes.split_first().ok_or_else(corrupt)?;
let body = body.strip_suffix(&[0]).ok_or_else(corrupt)?;
let parts = body
.split(|&b| b == 0)
.map(|p| String::from_utf8(p.to_vec()).map_err(|_| corrupt()))
.collect::<Result<Vec<_>, _>>()?;
let ns = |s: &String| NamespaceKey::from_stored(s.clone());
let repo = |s: &String| RepoName::new(s.clone()).map_err(|_| corrupt());
let int = |s: &String| {
s.parse::<u16>()
.ok()
.filter(|n| n.to_string() == *s)
.ok_or_else(corrupt)
};
Ok(match (tag, parts.as_slice()) {
(b'n', [n]) => Self::Namespace(ns(n)),
(b'c', [n]) => Self::Coordinator(ns(n)),
(b'r', [n, r, shard_ref]) => Self::Ref {
ns: ns(n),
repo: repo(r)?,
shard_ref: shard_ref.clone(),
},
(b'i', [n, r, p]) => Self::RepoIndex {
ns: ns(n),
repo: repo(r)?,
prefix: int(p)?,
},
(b'x', [n, r, b]) => Self::RefIndex {
ns: ns(n),
repo: repo(r)?,
bucket: int(b)?,
},
(b's', [p]) => Self::ContentShard(int(p)?),
_ => return Err(corrupt()),
})
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn ns(s: &str) -> NamespaceKey {
NamespaceKey::from_stored(s.into())
}
fn repo(s: &str) -> RepoName {
RepoName::new(s).unwrap()
}
#[test]
fn partition_encoding_golden_bytes() {
let cases: [(Partition, &[u8]); 6] = [
(Partition::Namespace(ns("root")), b"nroot\0"),
(Partition::Coordinator(ns("root")), b"croot\0"),
(
Partition::Ref {
ns: ns("root"),
repo: repo("a"),
shard_ref: "refs/heads/main".into(),
},
b"rroot\0a\0refs/heads/main\0",
),
(
Partition::RepoIndex {
ns: ns("root"),
repo: repo("a"),
prefix: 4095,
},
b"iroot\0a\x004095\0",
),
(
Partition::RefIndex {
ns: ns("root"),
repo: repo("a"),
bucket: 0,
},
b"xroot\0a\x000\0",
),
(Partition::ContentShard(7), b"s7\0"),
];
for (p, golden) in cases {
assert_eq!(p.encode().unwrap().as_ref(), golden);
assert_eq!(Partition::decode(golden).unwrap(), p);
}
let nul = Partition::Ref {
ns: ns("root"),
repo: repo("a"),
shard_ref: "x\0y".into(),
};
assert!(matches!(nul.encode(), Err(StoreError::Invalid(_))));
for bad in [
&b""[..],
b"nroot",
b"s07\0",
b"s65536\0",
b"q1\0",
b"croot\0x\0",
b"ir\0 \x001\0",
] {
assert!(
matches!(Partition::decode(bad), Err(StoreError::Corrupt(_))),
"{bad:?}"
);
}
}
fn partition() -> impl Strategy<Value = Partition> {
let s = "[a-z0-9/._-]{0,8}";
let r = "[!-~]{1,8}";
prop_oneof![
s.prop_map(|n| Partition::Namespace(ns(&n))),
s.prop_map(|n| Partition::Coordinator(ns(&n))),
(s, r, s).prop_map(|(n, rp, sr)| Partition::Ref {
ns: ns(&n),
repo: repo(&rp),
shard_ref: sr,
}),
(s, r, any::<u16>()).prop_map(|(n, rp, prefix)| Partition::RepoIndex {
ns: ns(&n),
repo: repo(&rp),
prefix,
}),
(s, r, any::<u16>()).prop_map(|(n, rp, bucket)| Partition::RefIndex {
ns: ns(&n),
repo: repo(&rp),
bucket,
}),
any::<u16>().prop_map(Partition::ContentShard),
]
}
proptest! {
#[test]
fn partition_encoding_is_injective(a in partition(), b in partition()) {
let (ea, eb) = (a.encode().unwrap(), b.encode().unwrap());
prop_assert_eq!(Partition::decode(&ea).unwrap(), a.clone());
prop_assert_eq!(ea == eb, a == b);
}
}
}