pub mod position;
pub use position::DagPosition;
use crate::event::EventKind;
use batpak_macros::Error;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::Arc;
pub const MAX_COORDINATE_COMPONENT_LEN: usize = 1024;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(into = "CoordinateWire")]
pub struct Coordinate {
entity: Arc<str>, scope: Arc<str>, }
#[derive(Serialize, Deserialize)]
struct CoordinateWire {
entity: String,
scope: String,
}
impl From<Coordinate> for CoordinateWire {
fn from(coord: Coordinate) -> Self {
Self {
entity: coord.entity.as_ref().to_owned(),
scope: coord.scope.as_ref().to_owned(),
}
}
}
impl<'de> Deserialize<'de> for Coordinate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let wire = CoordinateWire::deserialize(deserializer)?;
Coordinate::new(&wire.entity, &wire.scope).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum CoordinateError {
#[error("entity cannot be empty")]
EmptyEntity,
#[error("scope cannot be empty")]
EmptyScope,
#[error("entity length {len} exceeds maximum {max}")]
EntityTooLong {
len: usize,
max: usize,
},
#[error("scope length {len} exceeds maximum {max}")]
ScopeTooLong {
len: usize,
max: usize,
},
#[error("coordinate component contains a NUL byte")]
NulByte,
#[error("coordinate component contains a forbidden ASCII control character")]
ControlChar,
#[error("coordinate component contains a forbidden path-traversal substring (`..` or `/`)")]
PathTraversal,
#[error("coordinate component contains a forbidden identity-separator character (`|` or `=`)")]
ForbiddenSeparator,
}
#[derive(Clone, Debug, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum RegionFilterError {
#[error("clock range start {start} exceeds end {end}")]
InvertedClockRange {
start: u32,
end: u32,
},
#[error("event category {category} is out of range (must be < 16)")]
CategoryOutOfRange {
category: u8,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ClockRange {
start: u32,
end: u32,
}
impl ClockRange {
pub fn new(start: u32, end: u32) -> Result<Self, RegionFilterError> {
if start > end {
return Err(RegionFilterError::InvertedClockRange { start, end });
}
Ok(Self { start, end })
}
#[must_use]
pub fn start(&self) -> u32 {
self.start
}
#[must_use]
pub fn end(&self) -> u32 {
self.end
}
pub(crate) fn as_tuple(&self) -> (u32, u32) {
(self.start, self.end)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EventCategory(u8);
impl EventCategory {
pub fn new(category: u8) -> Result<Self, RegionFilterError> {
if category >= 16 {
return Err(RegionFilterError::CategoryOutOfRange { category });
}
Ok(Self(category))
}
#[must_use]
pub fn of_kind(kind: EventKind) -> Self {
Self(kind.category())
}
#[must_use]
pub fn get(&self) -> u8 {
self.0
}
}
#[derive(Clone, Debug, Default)]
pub struct Region {
pub(crate) entity_prefix: Option<Arc<str>>,
pub(crate) scope: Option<Arc<str>>,
pub(crate) fact: Option<KindFilter>,
pub(crate) clock_range: Option<ClockRange>, pub(crate) lane: Option<u32>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum KindFilter {
Exact(EventKind),
Category(u8), Any,
}
impl Coordinate {
pub fn new(entity: impl AsRef<str>, scope: impl AsRef<str>) -> Result<Self, CoordinateError> {
let entity = entity.as_ref();
let scope = scope.as_ref();
Self::validate_parts(entity, scope)?;
Ok(Self {
entity: Arc::from(entity),
scope: Arc::from(scope),
})
}
pub fn entity(&self) -> &str {
&self.entity
}
pub fn scope(&self) -> &str {
&self.scope
}
pub(crate) fn entity_arc(&self) -> Arc<str> {
Arc::clone(&self.entity)
}
pub(crate) fn scope_arc(&self) -> Arc<str> {
Arc::clone(&self.scope)
}
pub(crate) fn from_shared_parts(
entity: Arc<str>,
scope: Arc<str>,
) -> Result<Self, CoordinateError> {
Self::validate_parts(entity.as_ref(), scope.as_ref())?;
Ok(Self { entity, scope })
}
pub fn validate(&self) -> Result<(), CoordinateError> {
Self::validate_parts(self.entity.as_ref(), self.scope.as_ref())
}
fn validate_parts(entity: &str, scope: &str) -> Result<(), CoordinateError> {
if entity.is_empty() {
return Err(CoordinateError::EmptyEntity);
}
if scope.is_empty() {
return Err(CoordinateError::EmptyScope);
}
if entity.len() > MAX_COORDINATE_COMPONENT_LEN {
return Err(CoordinateError::EntityTooLong {
len: entity.len(),
max: MAX_COORDINATE_COMPONENT_LEN,
});
}
if scope.len() > MAX_COORDINATE_COMPONENT_LEN {
return Err(CoordinateError::ScopeTooLong {
len: scope.len(),
max: MAX_COORDINATE_COMPONENT_LEN,
});
}
Self::validate_component_bytes(entity)?;
Self::validate_component_bytes(scope)?;
Ok(())
}
fn validate_component_bytes(value: &str) -> Result<(), CoordinateError> {
for byte in value.bytes() {
if byte == 0 {
return Err(CoordinateError::NulByte);
}
if byte < 0x20 || byte == 0x7F {
return Err(CoordinateError::ControlChar);
}
}
if value.contains('/') || value.contains("..") {
return Err(CoordinateError::PathTraversal);
}
if value.contains('|') || value.contains('=') {
return Err(CoordinateError::ForbiddenSeparator);
}
Ok(())
}
}
impl fmt::Display for Coordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}@{}", self.entity, self.scope)
}
}
impl Region {
pub fn all() -> Self {
Self::default()
}
pub fn entity(prefix: impl AsRef<str>) -> Self {
Self {
entity_prefix: Some(Arc::from(prefix.as_ref())),
..Self::default()
}
}
pub fn scope(scope: impl AsRef<str>) -> Self {
Self {
scope: Some(Arc::from(scope.as_ref())),
..Self::default()
}
}
pub fn with_scope(mut self, scope: impl AsRef<str>) -> Self {
self.scope = Some(Arc::from(scope.as_ref()));
self
}
pub fn with_fact(mut self, filter: KindFilter) -> Self {
self.fact = Some(filter);
self
}
pub fn with_fact_category(mut self, category: EventCategory) -> Self {
self.fact = Some(KindFilter::Category(category.get()));
self
}
pub fn with_clock_range(mut self, range: ClockRange) -> Self {
self.clock_range = Some(range);
self
}
pub fn with_lane(mut self, lane: u32) -> Self {
self.lane = Some(lane);
self
}
pub fn entity_prefix(&self) -> Option<&str> {
self.entity_prefix.as_deref()
}
pub fn scope_value(&self) -> Option<&str> {
self.scope.as_deref()
}
pub fn fact(&self) -> Option<&KindFilter> {
self.fact.as_ref()
}
pub fn clock_range(&self) -> Option<ClockRange> {
self.clock_range
}
pub fn lane(&self) -> Option<u32> {
self.lane
}
#[must_use]
pub(crate) fn matches_entity(&self, entity: &str) -> bool {
match self.entity_prefix.as_deref() {
Some(prefix) => namespace_prefix_matches(prefix, entity),
None => true,
}
}
pub fn matches_event(&self, entity: &str, scope: &str, kind: EventKind) -> bool {
self.matches_event_on_lane(entity, scope, kind, None)
}
pub(crate) fn matches_event_on_lane(
&self,
entity: &str,
scope: &str,
kind: EventKind,
lane: Option<u32>,
) -> bool {
if !self.matches_entity(entity) {
return false;
}
if let Some(expected) = self.lane {
if lane != Some(expected) {
return false;
}
}
if let Some(ref s) = self.scope {
if scope != s.as_ref() {
return false;
}
}
if let Some(ref fact) = self.fact {
match fact {
KindFilter::Exact(k) => {
if kind != *k {
return false;
}
}
KindFilter::Category(c) => {
if kind.category() != *c {
return false;
}
}
KindFilter::Any => {}
}
}
true
}
pub(crate) fn checkpoint_identity(&self) -> String {
let entity = self.entity_prefix.as_deref().unwrap_or("*");
let scope = self.scope.as_deref().unwrap_or("*");
let fact = match self.fact.as_ref() {
Some(KindFilter::Exact(kind)) => {
format!("exact:{:x}:{:x}", kind.category(), kind.type_id())
}
Some(KindFilter::Category(cat)) => format!("category:{cat:x}"),
Some(KindFilter::Any) => "any".to_owned(),
None => "none".to_owned(),
};
let clock = match self.clock_range {
Some(range) => {
let (start, end) = range.as_tuple();
format!("{start}-{end}")
}
None => "*".to_owned(),
};
let base = format!("entity={entity}|scope={scope}|fact={fact}|clock={clock}");
match self.lane {
Some(lane) => format!("{base}|lane={lane}"),
None => base,
}
}
}
#[must_use]
pub(crate) fn namespace_prefix_matches(prefix: &str, candidate: &str) -> bool {
candidate == prefix
|| candidate
.strip_prefix(prefix)
.is_some_and(|suffix| suffix.starts_with(':'))
}
#[cfg(test)]
mod tests {
use super::{namespace_prefix_matches, Coordinate, CoordinateError, Region};
use crate::event::EventKind;
use std::sync::Arc;
#[test]
fn region_filter_error_display_renders_each_variant_detail() {
use super::RegionFilterError;
let inverted = RegionFilterError::InvertedClockRange { start: 9, end: 3 }.to_string();
assert!(
inverted.contains("clock range start 9") && inverted.contains("end 3"),
"InvertedClockRange Display must name its start/end; got {inverted:?}"
);
let out_of_range = RegionFilterError::CategoryOutOfRange { category: 99 }.to_string();
assert!(
out_of_range.contains("99") && out_of_range.contains("out of range"),
"CategoryOutOfRange Display must name the bad category; got {out_of_range:?}"
);
}
#[test]
fn namespace_prefix_matches_exact_and_descendants() {
assert!(namespace_prefix_matches("alice", "alice"));
assert!(namespace_prefix_matches("alice", "alice:child"));
assert!(namespace_prefix_matches("alice", "alice:child:grandchild"));
}
#[test]
fn namespace_prefix_rejects_adjacent_namespaces() {
assert!(!namespace_prefix_matches("alice", "alice2"));
assert!(!namespace_prefix_matches("alpha-a", "alpha-aa"));
assert!(!namespace_prefix_matches("alice", "alice-prod"));
assert!(!namespace_prefix_matches("alice", "alіce"));
}
#[test]
fn region_entity_uses_namespace_matcher() {
let region = Region::entity("alpha:a");
assert!(region.matches_entity("alpha:a"));
assert!(region.matches_entity("alpha:a:child"));
assert!(!region.matches_entity("alpha:aa"));
}
#[test]
fn matches_event_rejects_non_matching_entity_and_scope() {
let region = Region::entity("alpha:a").with_scope("room");
let kind = EventKind::custom(0xF, 1);
assert!(
region.matches_event("alpha:a", "room", kind),
"region must accept events on its own entity prefix and scope"
);
assert!(
region.matches_event("alpha:a:child", "room", kind),
"region must accept descendants of its entity prefix"
);
assert!(
!region.matches_event("beta", "room", kind),
"region must reject events on a foreign entity"
);
assert!(
!region.matches_event("alpha:aa", "room", kind),
"region must reject adjacent entity namespaces"
);
assert!(
!region.matches_event("alpha:a", "lobby", kind),
"region must reject events outside its exact scope"
);
}
#[test]
fn coordinate_rejects_checkpoint_identity_separators() {
assert_eq!(
Coordinate::new("entity|injection", "scope"),
Err(CoordinateError::ForbiddenSeparator)
);
assert_eq!(
Coordinate::new("entity", "scope=injection"),
Err(CoordinateError::ForbiddenSeparator)
);
assert_eq!(
Coordinate::new("entity", "*|fact=any|clock=*"),
Err(CoordinateError::ForbiddenSeparator)
);
}
#[test]
fn coordinate_validate_rejects_internally_forged_separator_values() {
let coord = Coordinate {
entity: Arc::from("entity"),
scope: Arc::from("*|fact=any|clock=*"),
};
assert_eq!(coord.validate(), Err(CoordinateError::ForbiddenSeparator));
}
#[test]
fn coordinate_separator_error_is_displayable_std_error() {
fn assert_error_trait(_: &dyn std::error::Error) {}
let error = CoordinateError::ForbiddenSeparator;
assert_error_trait(&error);
assert!(error.to_string().contains("`|` or `=`"));
}
}