use std::fmt;
use crate::key::Key;
pub const VERSION_CHUNK: &str = "v1";
pub const CLASS_TELEMETRY: &str = "telemetry";
pub const CLASS_STATE: &str = "state";
pub const CLASS_EVENTS: &str = "events";
pub const PLANE_RPC: &str = "@rpc";
pub const PLANE_MEDIA: &str = "@media";
pub const PLANE_BLOB: &str = "@blob";
pub const SERVICE_CATALOG: &str = "@catalog";
pub const BLOB_TIER_ARTIFACT: &str = "artifact";
pub const BLOB_TIER_TREE: &str = "tree";
pub const BLOB_TIER_STORE: &str = "store";
pub const SUBJECT_ALIVE: &str = "alive";
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum KeyError {
#[error("invalid plain chunk {0:?}: must match [a-z0-9]([a-z0-9._-]*[a-z0-9])? (RFC 03 §2)")]
InvalidPlainChunk(String),
#[error("invalid verbatim chunk {0:?}: must match @[a-z0-9][a-z0-9_-]* (RFC 03 §2)")]
InvalidVerbatimChunk(String),
#[error("invalid host origin {0:?}: must match h-[0-9a-f]{{12}} (RFC 03 §1.3)")]
InvalidHostOrigin(String),
#[error("invalid producer {0:?}: {1} (RFC 03 §1.5)")]
InvalidProducer(String, &'static str),
#[error("empty subject: keys need >= 1 subject chunk (RFC 03 §1.6)")]
EmptySubject,
#[error("blob tier token expected (artifact|tree|store), got {0:?} (RFC 03 §1.5)")]
InvalidBlobTier(String),
#[error("reserved token {0:?} may not be used as a {1} (RFC 03 §3)")]
ReservedToken(String, &'static str),
#[error(
"invalid content hash {0:?}: must be lowercase hex, even length, 8..=128 digits (RFC 07 §2.3/§2.4)"
)]
InvalidContentHash(String),
#[error("malformed @blob/{0} key: {1} (RFC 07 §2)")]
MalformedBlobKey(&'static str, &'static str),
#[error("not a v1 key: {0}")]
Parse(String),
#[error("unknown class {chunk:?} — the classes are {} (RFC 04 §1)", Class::chunks().join(", "))]
UnknownClass { chunk: String },
}
pub const fn is_valid_plain_chunk(chunk: &str) -> bool {
const fn alnum(b: u8) -> bool {
b.is_ascii_lowercase() || b.is_ascii_digit()
}
let bytes = chunk.as_bytes();
let n = bytes.len();
if n == 0 || !alnum(bytes[0]) {
return false;
}
if n == 1 {
return true;
}
if !alnum(bytes[n - 1]) {
return false;
}
let mut i = 1;
while i < n - 1 {
let b = bytes[i];
if !(alnum(b) || b == b'.' || b == b'_' || b == b'-') {
return false;
}
i += 1;
}
true
}
pub fn is_valid_verbatim_chunk(chunk: &str) -> bool {
let Some(rest) = chunk.strip_prefix('@') else {
return false;
};
let bytes = rest.as_bytes();
let alnum = |b: u8| b.is_ascii_lowercase() || b.is_ascii_digit();
match bytes {
[] => false,
[first, rest @ ..] => {
alnum(*first) && rest.iter().all(|&b| alnum(b) || b == b'_' || b == b'-')
}
}
}
pub fn is_valid_host_origin(chunk: &str) -> bool {
let Some(hex) = chunk.strip_prefix("h-") else {
return false;
};
hex.len() == 12
&& hex
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Origin {
Host(crate::origin::HostId),
Service(crate::origin::ServiceOrigin),
}
impl Origin {
pub fn catalog() -> Self {
Origin::Service(crate::origin::ServiceOrigin::catalog())
}
pub fn service(name: &str) -> Result<Self, KeyError> {
crate::origin::ServiceOrigin::new(name).map(Origin::Service)
}
pub fn chunk(&self) -> &str {
match self {
Origin::Host(id) => id.as_str(),
Origin::Service(s) => s.as_str(),
}
}
pub fn has_producer_chunk(&self) -> bool {
matches!(self, Origin::Host(_))
}
}
impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.chunk())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Producer {
name: String,
instance: Option<u32>,
}
impl Producer {
pub fn new(name: &str) -> Result<Self, KeyError> {
Self::validate_name(name)?;
Ok(Producer {
name: name.to_string(),
instance: None,
})
}
pub fn with_instance(name: &str, instance: u32) -> Result<Self, KeyError> {
Self::validate_name(name)?;
if instance == 0 {
return Err(KeyError::InvalidProducer(
name.to_string(),
"instance numbers start at 1 (the first instance uses the bare name)",
));
}
Ok(Producer {
name: name.to_string(),
instance: Some(instance),
})
}
fn validate_name(name: &str) -> Result<(), KeyError> {
if !is_valid_plain_chunk(name) {
return Err(KeyError::InvalidProducer(
name.to_string(),
"not a valid plain chunk",
));
}
if Self::split_trailing_int(name).is_some() {
return Err(KeyError::InvalidProducer(
name.to_string(),
"base names must not end in -<int> (reserved for instance suffixes)",
));
}
if name == BLOB_TIER_ARTIFACT || name == BLOB_TIER_TREE || name == BLOB_TIER_STORE {
return Err(KeyError::ReservedToken(name.to_string(), "producer name"));
}
Ok(())
}
fn split_trailing_int(chunk: &str) -> Option<(&str, u32)> {
let (base, tail) = chunk.rsplit_once('-')?;
if base.is_empty() || tail.is_empty() || !tail.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
tail.parse().ok().map(|n| (base, n))
}
pub fn parse_chunk(chunk: &str) -> Result<Self, KeyError> {
if !is_valid_plain_chunk(chunk) {
return Err(KeyError::InvalidProducer(
chunk.to_string(),
"not a valid plain chunk",
));
}
match Self::split_trailing_int(chunk) {
Some((base, n)) if n >= 1 => Ok(Producer {
name: base.to_string(),
instance: Some(n),
}),
_ => Ok(Producer {
name: chunk.to_string(),
instance: None,
}),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn instance(&self) -> Option<u32> {
self.instance
}
pub(crate) fn push_chunk(&self, out: &mut String) {
out.push_str(&self.name);
if let Some(i) = self.instance {
use std::fmt::Write as _;
let _ = write!(out, "-{i}");
}
}
pub fn chunk(&self) -> String {
match self.instance {
None => self.name.clone(),
Some(n) => format!("{}-{n}", self.name),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Class {
Telemetry,
State,
Events,
}
impl Class {
pub const ALL: [Class; 3] = [Class::Telemetry, Class::State, Class::Events];
pub fn chunks() -> [&'static str; 3] {
[CLASS_TELEMETRY, CLASS_STATE, CLASS_EVENTS]
}
pub fn chunk(self) -> &'static str {
match self {
Class::Telemetry => CLASS_TELEMETRY,
Class::State => CLASS_STATE,
Class::Events => CLASS_EVENTS,
}
}
pub fn from_chunk(chunk: &str) -> Option<Self> {
match chunk {
CLASS_TELEMETRY => Some(Class::Telemetry),
CLASS_STATE => Some(Class::State),
CLASS_EVENTS => Some(Class::Events),
_ => None,
}
}
}
impl std::str::FromStr for Class {
type Err = KeyError;
fn from_str(s: &str) -> Result<Self, KeyError> {
Class::from_chunk(s).ok_or_else(|| KeyError::UnknownClass {
chunk: s.to_string(),
})
}
}
impl fmt::Display for Class {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.chunk())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Plane {
Rpc,
Media,
Blob,
}
impl Plane {
pub fn chunk(self) -> &'static str {
match self {
Plane::Rpc => PLANE_RPC,
Plane::Media => PLANE_MEDIA,
Plane::Blob => PLANE_BLOB,
}
}
pub fn from_chunk(chunk: &str) -> Option<Self> {
match chunk {
PLANE_RPC => Some(Plane::Rpc),
PLANE_MEDIA => Some(Plane::Media),
PLANE_BLOB => Some(Plane::Blob),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClassOrPlane {
Class(Class),
Plane(Plane),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlobTier {
Artifact,
Tree,
Store,
}
impl BlobTier {
pub fn chunk(self) -> &'static str {
match self {
BlobTier::Artifact => BLOB_TIER_ARTIFACT,
BlobTier::Tree => BLOB_TIER_TREE,
BlobTier::Store => BLOB_TIER_STORE,
}
}
pub fn from_chunk(chunk: &str) -> Option<Self> {
match chunk {
BLOB_TIER_ARTIFACT => Some(BlobTier::Artifact),
BLOB_TIER_TREE => Some(BlobTier::Tree),
BLOB_TIER_STORE => Some(BlobTier::Store),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContentHash(String);
impl ContentHash {
pub fn parse(s: &str) -> Result<Self, KeyError> {
let ok = (8..=128).contains(&s.len())
&& s.len().is_multiple_of(2)
&& s.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
if ok {
Ok(ContentHash(s.to_string()))
} else {
Err(KeyError::InvalidContentHash(s.to_string()))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ContentHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(feature = "serde")]
mod content_hash_serde {
use super::ContentHash;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
impl Serialize for ContentHash {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ContentHash {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
ContentHash::parse(&raw).map_err(D::Error::custom)
}
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for ContentHash {
fn schema_name() -> std::borrow::Cow<'static, str> {
"ContentHash".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": "^(?:[0-9a-f]{2}){4,64}$"
})
}
}
#[cfg(test)]
mod content_addressing {
use super::*;
fn host() -> Origin {
Origin::Host(crate::origin::HostId::parse("h-3fa9c2d41b7e").unwrap())
}
#[test]
fn a_named_tree_key_is_unspellable() {
for name in ["nightly", "snap-1", "latest", "v2", "my.snapshot"] {
assert!(
blob_key(&host(), BlobTier::Tree, &[name]).is_err(),
"tree/{name} must not be constructible"
);
}
let root = ContentHash::parse(&"ab".repeat(32)).unwrap();
let key = blob_tree_key(&host(), &root).unwrap();
assert!(key.as_str().ends_with(&format!("@blob/tree/{root}")));
assert!(blob_key(&host(), BlobTier::Tree, &[root.as_str(), "x"]).is_err());
}
#[test]
fn store_keys_are_algo_then_hash() {
let hash = ContentHash::parse("ab12cd34ef56").unwrap();
let key = blob_store_key(&host(), "blake3", &hash).unwrap();
assert!(key.as_str().ends_with("@blob/store/blake3/ab12cd34ef56"));
assert!(blob_key(&host(), BlobTier::Store, &["blake3"]).is_err());
assert!(blob_key(&host(), BlobTier::Store, &["blake3", "nightly"]).is_err());
assert!(
blob_key(
&host(),
BlobTier::Store,
&["blake3", &hash.to_string(), "x"]
)
.is_err()
);
}
#[test]
fn artifact_keys_keep_their_id_and_endpoint_tail() {
let key = blob_key(&host(), BlobTier::Artifact, &["01hqxk8f9c2n4p", "manifest"]).unwrap();
assert!(
key.as_str()
.ends_with("@blob/artifact/01hqxk8f9c2n4p/manifest")
);
}
#[test]
fn content_hash_rejects_names_accepts_digests() {
for good in ["ab12cd34ef56", &"0".repeat(64), &"f".repeat(128)] {
ContentHash::parse(good).unwrap_or_else(|e| panic!("{good:?}: {e}"));
}
for bad in [
"", "cafe", "nightly", "AB12CD34EF56", "ab12cd34ef5", "ab12cd34ef5g", &"a".repeat(130), ] {
assert!(ContentHash::parse(bad).is_err(), "{bad:?} must be refused");
}
}
#[cfg(feature = "serde")]
#[test]
fn content_hash_serde_round_trips_through_parse() {
let hash = ContentHash::parse("ab12cd34ef56").unwrap();
let json = serde_json::to_string(&hash).unwrap();
assert_eq!(json, "\"ab12cd34ef56\"");
let back: ContentHash = serde_json::from_str(&json).unwrap();
assert_eq!(back, hash);
for bad in ["\"nightly\"", "\"AB12CD34EF56\"", "\"ab12cd34ef5\""] {
assert!(
serde_json::from_str::<ContentHash>(bad).is_err(),
"{bad} must be refused on deserialize"
);
}
}
#[cfg(feature = "schemars")]
#[test]
fn content_hash_schema_is_a_patterned_string() {
let schema = serde_json::to_value(schemars::schema_for!(ContentHash)).unwrap();
assert_eq!(schema["type"], "string");
assert_eq!(schema["pattern"], "^(?:[0-9a-f]{2}){4,64}$");
}
}
pub fn reject_reserved_chunks(subject: &[&str], what: &'static str) -> Result<(), KeyError> {
if subject.contains(&SUBJECT_ALIVE) {
return Err(KeyError::ReservedToken(SUBJECT_ALIVE.to_string(), what));
}
Ok(())
}
fn validate_subject(subject: &[&str]) -> Result<(), KeyError> {
if subject.is_empty() {
return Err(KeyError::EmptySubject);
}
for chunk in subject {
if !is_valid_plain_chunk(chunk) {
return Err(KeyError::InvalidPlainChunk((*chunk).to_string()));
}
}
Ok(())
}
fn push_key(parts: &mut String, chunk: &str) {
push_key_sep(parts);
parts.push_str(chunk);
}
fn push_key_sep(parts: &mut String) {
if !parts.is_empty() {
parts.push('/');
}
}
pub fn data_key(
origin: &Origin,
class: Class,
producer: Option<&Producer>,
subject: &[&str],
) -> Result<Key, KeyError> {
validate_subject(subject)?;
if origin.has_producer_chunk() != producer.is_some() {
return Err(KeyError::Parse(
"host origins require a producer chunk; service origins forbid one (RFC 03 §1.5)"
.to_string(),
));
}
reject_reserved_chunks(subject, "data subject chunk")?;
let mut key = String::new();
push_key(&mut key, VERSION_CHUNK);
push_key(&mut key, origin.chunk());
push_key(&mut key, class.chunk());
if let Some(p) = producer {
push_key_sep(&mut key);
p.push_chunk(&mut key);
}
for chunk in subject {
push_key(&mut key, chunk);
}
Ok(Key::from_canonical(key))
}
pub fn rpc_key(
origin: &Origin,
producer: Option<&Producer>,
procedure: &[&str],
) -> Result<Key, KeyError> {
validate_subject(procedure)?;
if origin.has_producer_chunk() != producer.is_some() {
return Err(KeyError::Parse(
"host origins require a producer chunk; service origins forbid one (RFC 03 §1.5)"
.to_string(),
));
}
let mut key = String::new();
push_key(&mut key, VERSION_CHUNK);
push_key(&mut key, origin.chunk());
push_key(&mut key, PLANE_RPC);
if let Some(p) = producer {
push_key_sep(&mut key);
p.push_chunk(&mut key);
}
for chunk in procedure {
push_key(&mut key, chunk);
}
Ok(Key::from_canonical(key))
}
pub fn media_key(origin: &Origin, producer: &Producer, stream: &[&str]) -> Result<Key, KeyError> {
validate_subject(stream)?;
let mut key = String::new();
push_key(&mut key, VERSION_CHUNK);
push_key(&mut key, origin.chunk());
push_key(&mut key, PLANE_MEDIA);
push_key_sep(&mut key);
producer.push_chunk(&mut key);
for chunk in stream {
push_key(&mut key, chunk);
}
Ok(Key::from_canonical(key))
}
pub fn blob_key(origin: &Origin, tier: BlobTier, rest: &[&str]) -> Result<Key, KeyError> {
validate_subject(rest)?;
match tier {
BlobTier::Tree => {
if rest.len() != 1 {
return Err(KeyError::MalformedBlobKey(
"tree",
"expected exactly one chunk: the tree's root hash",
));
}
ContentHash::parse(rest[0])?;
}
BlobTier::Store => {
if rest.len() != 2 {
return Err(KeyError::MalformedBlobKey(
"store",
"expected exactly two chunks: <algo>/<hash>",
));
}
ContentHash::parse(rest[1])?;
}
BlobTier::Artifact => {}
}
let mut key = String::new();
push_key(&mut key, VERSION_CHUNK);
push_key(&mut key, origin.chunk());
push_key(&mut key, PLANE_BLOB);
push_key(&mut key, tier.chunk());
for chunk in rest {
push_key(&mut key, chunk);
}
Ok(Key::from_canonical(key))
}
pub fn blob_tier_prefix(origin: &Origin, tier: BlobTier) -> Key {
let mut key = String::new();
push_key(&mut key, VERSION_CHUNK);
push_key(&mut key, origin.chunk());
push_key(&mut key, PLANE_BLOB);
push_key(&mut key, tier.chunk());
Key::from_canonical(key)
}
pub fn blob_tree_key(origin: &Origin, root: &ContentHash) -> Result<Key, KeyError> {
blob_key(origin, BlobTier::Tree, &[root.as_str()])
}
pub fn blob_store_key(origin: &Origin, algo: &str, hash: &ContentHash) -> Result<Key, KeyError> {
blob_key(origin, BlobTier::Store, &[algo, hash.as_str()])
}
pub fn alive_key(origin: &Origin, producer: Option<&Producer>) -> Result<Key, KeyError> {
if origin.has_producer_chunk() != producer.is_some() {
return Err(KeyError::Parse(
"host origins require a producer chunk; service origins forbid one (RFC 03 §1.5)"
.to_string(),
));
}
let mut key = String::new();
push_key(&mut key, VERSION_CHUNK);
push_key(&mut key, origin.chunk());
push_key(&mut key, CLASS_STATE);
if let Some(p) = producer {
push_key_sep(&mut key);
p.push_chunk(&mut key);
}
push_key(&mut key, SUBJECT_ALIVE);
Ok(Key::from_canonical(key))
}
pub fn device_alive_key(
origin: &Origin,
producer: &Producer,
device: &str,
) -> Result<Key, KeyError> {
if !is_valid_plain_chunk(device) {
return Err(KeyError::InvalidPlainChunk(device.to_string()));
}
let mut key = String::new();
push_key(&mut key, VERSION_CHUNK);
push_key(&mut key, origin.chunk());
push_key(&mut key, CLASS_STATE);
push_key_sep(&mut key);
producer.push_chunk(&mut key);
push_key(&mut key, "device");
push_key(&mut key, device);
push_key(&mut key, SUBJECT_ALIVE);
Ok(Key::from_canonical(key))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructuralKey<'k> {
pub origin: Origin,
pub class: ClassOrPlane,
pub position5: Position5,
pub subject: Vec<&'k str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Position5 {
Producer(Producer),
Tier(BlobTier),
Absent,
}
impl Position5 {
pub fn producer(&self) -> Option<&Producer> {
match self {
Position5::Producer(p) => Some(p),
_ => None,
}
}
pub fn blob_tier(&self) -> Option<BlobTier> {
match self {
Position5::Tier(t) => Some(*t),
_ => None,
}
}
pub fn chunk(&self) -> Option<String> {
match self {
Position5::Producer(p) => Some(p.chunk()),
Position5::Tier(t) => Some(t.chunk().to_string()),
Position5::Absent => None,
}
}
}
impl StructuralKey<'_> {
pub fn producer(&self) -> Option<&Producer> {
self.position5.producer()
}
pub fn blob_tier(&self) -> Option<BlobTier> {
self.position5.blob_tier()
}
pub fn remote_origin(&self) -> Option<crate::origin::RemoteOrigin> {
match &self.origin {
Origin::Host(id) => Some(crate::origin::RemoteOrigin::from_host(id.clone())),
Origin::Service(_) => None,
}
}
}
pub fn parse(key: &str) -> Result<StructuralKey<'_>, KeyError> {
let mut chunks = key.split('/');
let version = chunks
.next()
.ok_or_else(|| KeyError::Parse("empty key".into()))?;
if version != VERSION_CHUNK {
return Err(KeyError::Parse(format!(
"expected {VERSION_CHUNK} first, got {version:?}"
)));
}
let origin_chunk = chunks
.next()
.ok_or_else(|| KeyError::Parse("missing origin chunk".into()))?;
let origin = if is_valid_host_origin(origin_chunk) {
Origin::Host(crate::origin::HostId::parse(origin_chunk).expect("validated"))
} else if is_valid_verbatim_chunk(origin_chunk) {
Origin::Service(crate::origin::ServiceOrigin::new(origin_chunk).expect("validated"))
} else {
return Err(KeyError::InvalidHostOrigin(origin_chunk.to_string()));
};
let class_chunk = chunks
.next()
.ok_or_else(|| KeyError::Parse("missing class chunk".into()))?;
let class = if let Some(c) = Class::from_chunk(class_chunk) {
ClassOrPlane::Class(c)
} else if let Some(p) = Plane::from_chunk(class_chunk) {
ClassOrPlane::Plane(p)
} else {
return Err(KeyError::Parse(format!(
"unknown class/plane chunk {class_chunk:?}"
)));
};
let position5 = match (&origin, &class) {
(_, ClassOrPlane::Plane(Plane::Blob)) => {
let tier = chunks
.next()
.ok_or_else(|| KeyError::Parse("missing blob tier".into()))?;
Position5::Tier(
BlobTier::from_chunk(tier)
.ok_or_else(|| KeyError::InvalidBlobTier(tier.to_string()))?,
)
}
(Origin::Host(_), _) => {
let chunk = chunks
.next()
.ok_or_else(|| KeyError::Parse("missing producer chunk".into()))?;
Position5::Producer(Producer::parse_chunk(chunk)?)
}
(Origin::Service(_), _) => Position5::Absent,
};
let subject: Vec<&str> = chunks.collect();
if subject.is_empty() {
return Err(KeyError::EmptySubject);
}
Ok(StructuralKey {
origin,
class,
position5,
subject,
})
}
pub fn with_base(base: &str, key_or_selector: impl AsRef<str>) -> String {
if base.is_empty() {
return key_or_selector.as_ref().to_string();
}
format!("{base}/{}", key_or_selector.as_ref())
}
pub fn strip_base<'k>(base: &str, key: &'k str) -> Option<&'k str> {
if base.is_empty() {
return Some(key);
}
key.strip_prefix(base)?.strip_prefix('/')
}
pub fn parse_full<'k>(base: &str, key: &'k str) -> Option<StructuralKey<'k>> {
parse(strip_base(base, key)?).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::origin::HostId;
fn host() -> Origin {
Origin::Host(HostId::parse("h-3fa9c2d41b7e").unwrap())
}
#[test]
fn plain_chunk_rules() {
for ok in [
"a",
"cpu",
"sys_uptime",
"10-0-0-7",
"sshd.service",
"p95_ms",
"h-3fa9c2d41b7e",
] {
assert!(is_valid_plain_chunk(ok), "{ok}");
}
for bad in ["", "-a", "a-", ".a", "A", "Cpu", "a/b", "a*", "@v1", "é"] {
assert!(!is_valid_plain_chunk(bad), "{bad}");
}
}
#[test]
fn verbatim_chunk_rules() {
for ok in ["@v1", "@rpc", "@catalog", "@adv"] {
assert!(is_valid_verbatim_chunk(ok), "{ok}");
}
for bad in ["@", "@-x", "v1", "@V1", "@a/b"] {
assert!(!is_valid_verbatim_chunk(bad), "{bad}");
}
}
#[test]
fn producer_instance_split_is_unambiguous() {
assert!(Producer::new("snmp").is_ok());
assert!(Producer::new("net-ring").is_ok());
assert!(Producer::new("ipv6-2").is_err());
assert!(Producer::with_instance("snmp", 0).is_err());
let p = Producer::with_instance("snmp", 2).unwrap();
assert_eq!(p.chunk(), "snmp-2");
let back = Producer::parse_chunk("snmp-2").unwrap();
assert_eq!(back.name(), "snmp");
assert_eq!(back.instance(), Some(2));
let bare = Producer::parse_chunk("net-ring").unwrap();
assert_eq!(bare.name(), "net-ring");
assert_eq!(bare.instance(), None);
}
#[test]
fn blob_tiers_are_not_producers() {
assert!(Producer::new("store").is_err());
assert!(Producer::new("tree").is_err());
assert!(Producer::new("artifact").is_err());
}
#[test]
fn position_five_is_exactly_one_of_three_things() {
let producer = parse("v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu/usage").unwrap();
assert!(matches!(producer.position5, Position5::Producer(_)));
assert_eq!(producer.producer().map(Producer::name), Some("sysinfo"));
assert_eq!(producer.blob_tier(), None);
let blob = parse("v1/h-3fa9c2d41b7e/@blob/artifact/01jqz3demo0001/manifest").unwrap();
assert!(matches!(
blob.position5,
Position5::Tier(BlobTier::Artifact)
));
assert_eq!(blob.blob_tier(), Some(BlobTier::Artifact));
assert_eq!(blob.producer(), None);
let service = parse("v1/@catalog/state/entity/h-3fa9c2d41b7e").unwrap();
assert_eq!(service.position5, Position5::Absent);
assert_eq!(service.producer(), None);
assert_eq!(service.blob_tier(), None);
assert_eq!(service.subject, ["entity", "h-3fa9c2d41b7e"]);
assert_eq!(producer.position5.chunk().as_deref(), Some("sysinfo"));
assert_eq!(blob.position5.chunk().as_deref(), Some("artifact"));
assert_eq!(service.position5.chunk(), None);
}
#[test]
fn normative_examples_build_and_roundtrip() {
let p = |n| Producer::new(n).unwrap();
let cases = [
data_key(
&host(),
Class::Telemetry,
Some(&p("sysinfo")),
&["cpu", "usage"],
)
.unwrap(),
data_key(
&host(),
Class::Telemetry,
Some(&p("snmp")),
&["router01", "system", "sys_uptime"],
)
.unwrap(),
data_key(&host(), Class::State, Some(&p("netring")), &["health"]).unwrap(),
data_key(
&host(),
Class::State,
Some(&p("netlink")),
&["alert", "9f2c81ab04d7e3f1"],
)
.unwrap(),
data_key(
&host(),
Class::State,
Some(&p("netring")),
&["evidence", "names", "10-0-0-7"],
)
.unwrap(),
data_key(
&host(),
Class::Events,
Some(&p("netring")),
&["capture", "01jgxqz4yqk8v6txw3m9f2a7cd"],
)
.unwrap(),
rpc_key(&host(), Some(&p("netlink")), &["sockets"]).unwrap(),
media_key(&host(), &p("parallax"), &["cam0", "video", "h264", "high"]).unwrap(),
blob_key(&host(), BlobTier::Store, &["sha256", "ab12cd34ef56"]).unwrap(),
data_key(
&Origin::catalog(),
Class::State,
None,
&["entity", "h-3fa9c2d41b7e"],
)
.unwrap(),
data_key(
&Origin::catalog(),
Class::State,
None,
&["pdns", "93-184-216-34"],
)
.unwrap(),
];
let expected = [
"v1/h-3fa9c2d41b7e/telemetry/sysinfo/cpu/usage",
"v1/h-3fa9c2d41b7e/telemetry/snmp/router01/system/sys_uptime",
"v1/h-3fa9c2d41b7e/state/netring/health",
"v1/h-3fa9c2d41b7e/state/netlink/alert/9f2c81ab04d7e3f1",
"v1/h-3fa9c2d41b7e/state/netring/evidence/names/10-0-0-7",
"v1/h-3fa9c2d41b7e/events/netring/capture/01jgxqz4yqk8v6txw3m9f2a7cd",
"v1/h-3fa9c2d41b7e/@rpc/netlink/sockets",
"v1/h-3fa9c2d41b7e/@media/parallax/cam0/video/h264/high",
"v1/h-3fa9c2d41b7e/@blob/store/sha256/ab12cd34ef56",
"v1/@catalog/state/entity/h-3fa9c2d41b7e",
"v1/@catalog/state/pdns/93-184-216-34",
];
for (built, want) in cases.iter().zip(expected) {
assert_eq!(built, want);
let parsed = parse(built).unwrap();
let subject = &parsed.subject;
let rebuilt = match parsed.class {
ClassOrPlane::Class(c) => {
data_key(&parsed.origin, c, parsed.producer(), subject).unwrap()
}
ClassOrPlane::Plane(Plane::Rpc) => {
rpc_key(&parsed.origin, parsed.producer(), subject).unwrap()
}
ClassOrPlane::Plane(Plane::Media) => {
media_key(&parsed.origin, parsed.producer().unwrap(), subject).unwrap()
}
ClassOrPlane::Plane(Plane::Blob) => {
blob_key(&parsed.origin, parsed.blob_tier().unwrap(), subject).unwrap()
}
};
assert_eq!(&rebuilt, want);
}
}
#[test]
fn alive_is_liveliness_only() {
assert!(
data_key(
&host(),
Class::State,
Some(&Producer::new("netlink").unwrap()),
&["alive"]
)
.is_err()
);
assert!(
data_key(
&host(),
Class::Telemetry,
Some(&Producer::new("netlink").unwrap()),
&["foo", "alive"]
)
.is_err()
);
assert!(
data_key(
&host(),
Class::Events,
Some(&Producer::new("netlink").unwrap()),
&["alive", "01jgxqz4yqk8v6txw3m9f2a7cd"]
)
.is_err()
);
assert_eq!(
alive_key(&host(), Some(&Producer::new("netlink").unwrap())).unwrap(),
"v1/h-3fa9c2d41b7e/state/netlink/alive"
);
assert_eq!(
alive_key(&Origin::catalog(), None).unwrap(),
"v1/@catalog/state/alive"
);
assert_eq!(
device_alive_key(&host(), &Producer::new("snmp").unwrap(), "router01").unwrap(),
"v1/h-3fa9c2d41b7e/state/snmp/device/router01/alive"
);
}
#[test]
fn service_origin_omits_producer() {
assert!(
data_key(
&Origin::catalog(),
Class::State,
Some(&Producer::new("x").unwrap()),
&["entity", "a"]
)
.is_err()
);
assert!(data_key(&host(), Class::State, None, &["health"]).is_err());
}
#[test]
fn no_origin_can_mint_an_illegal_position_2() {
for bad in ["has spaces", "NotVerbatim", "catalog", "@", "@Desired", ""] {
assert!(
Origin::service(bad).is_err(),
"{bad:?} must not become an Origin"
);
assert!(crate::origin::ServiceOrigin::new(bad).is_err(), "{bad:?}");
}
for origin in [
Origin::catalog(),
Origin::service("@desired").unwrap(),
host(),
] {
let key = data_key(
&origin,
Class::State,
origin
.has_producer_chunk()
.then(|| Producer::new("netring").unwrap())
.as_ref(),
&["health"],
)
.unwrap();
let position_2 = key.as_str().split('/').nth(1).unwrap();
assert_eq!(position_2, origin.chunk());
assert!(
is_valid_verbatim_chunk(position_2) || is_valid_host_origin(position_2),
"position 2 of {key} is ungrammatical"
);
}
}
#[test]
fn parse_rejects_foreign_keys() {
assert!(parse("zensight/netlink/host/@/health").is_err());
assert!(parse("@v2/h-3fa9c2d41b7e/state/x/health").is_err());
assert!(parse("v1/h-3fa9c2d41b7e/bogus/x/health").is_err());
assert!(parse("v1/h-3fa9c2d41b7e/@blob/bogus/x").is_err());
}
#[test]
fn empty_base_is_the_identity_for_observers() {
let key = "v1/h-3fa9c2d41b7e/state/sysinfo/alive";
assert_eq!(with_base("", key), key);
assert_eq!(with_base("", "v1/*/**"), "v1/*/**");
assert_eq!(strip_base("", key), Some(key));
let parsed = parse_full("", key).unwrap();
assert_eq!(parsed.origin.chunk(), "h-3fa9c2d41b7e");
assert_eq!(parsed.subject, vec!["alive"]);
assert_eq!(with_base("zs", key), format!("zs/{key}"));
assert_eq!(strip_base("zs", &format!("zs/{key}")), Some(key));
}
}