pub mod position;
pub use position::DagPosition;
use crate::event::EventKind;
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)]
#[non_exhaustive]
pub enum CoordinateError {
EmptyEntity,
EmptyScope,
EntityTooLong {
len: usize,
max: usize,
},
ScopeTooLong {
len: usize,
max: usize,
},
NulByte,
ControlChar,
PathTraversal,
ForbiddenSeparator,
}
#[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<(u32, 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 fmt::Display for CoordinateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyEntity => write!(f, "entity cannot be empty"),
Self::EmptyScope => write!(f, "scope cannot be empty"),
Self::EntityTooLong { len, max } => {
write!(f, "entity length {len} exceeds maximum {max}")
}
Self::ScopeTooLong { len, max } => {
write!(f, "scope length {len} exceeds maximum {max}")
}
Self::NulByte => write!(f, "coordinate component contains a NUL byte"),
Self::ControlChar => write!(
f,
"coordinate component contains a forbidden ASCII control character"
),
Self::PathTraversal => write!(
f,
"coordinate component contains a forbidden path-traversal substring (`..` or `/`)"
),
Self::ForbiddenSeparator => write!(
f,
"coordinate component contains a forbidden identity-separator character (`|` or `=`)"
),
}
}
}
impl std::error::Error for CoordinateError {}
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, cat: u8) -> Self {
self.fact = Some(KindFilter::Category(cat));
self
}
pub fn with_clock_range(mut self, range: (u32, u32)) -> Self {
self.clock_range = Some(range);
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<(u32, u32)> {
self.clock_range
}
#[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 {
if !self.matches_entity(entity) {
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((start, end)) => format!("{start}-{end}"),
None => "*".to_owned(),
};
format!("entity={entity}|scope={scope}|fact={fact}|clock={clock}")
}
}
#[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 std::sync::Arc;
#[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 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 `=`"));
}
}