Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/ogar-adapter-surrealql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ pub fn parse_surrealql_ddl(_input: &str) -> Result<Vec<Class>, ParseError> {
generate_warnings: false,
feature_bearer_access: false,
feature_surrealism: false,
// The fork's `main` grows quirk flags; the rest stays at the
// parser's defaults so a new field does not break this build.
..Config::default()
};
let (query, ast) = Parser::enter_parse::<surrealdb_ast::Query>(_input, cfg)
.map_err(|e| ParseError::Parse(format!("{e:?}")))?;
Expand Down
202 changes: 202 additions & 0 deletions crates/ogar-dir-core/src/dn128.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//! The hot hierarchy coordinate: up to 16 levels, one byte per level.
//!
//! ```text
//! DirectoryScope × Dn128
//! (domain/tenant) codes[0..depth] depth (side metadata)
//! ```
//!
//! * A level's code is a byte, `0..=255`: up to **256 distinct children per
//! parent**. Every byte value is a real code — `0` included — so a byte is
//! never stolen as a "no level" marker. How many levels are populated is the
//! explicit [`Dn128::depth`], stored beside the codes.
//! * Bytes past `depth` are zero. That is a canonical form (so equality and
//! ordering are byte comparisons), not a meaning: presence comes from
//! `depth` alone.
//! * The domain or tenant is not a level. It is the [`DirectoryScope`] the
//! codes are interpreted in; a code is only meaningful under its scope and
//! its parent path.
//!
//! Subtree selection is a prefix match on the first `d` bytes plus
//! `depth >= d`: one ternary compare on the 16 bytes and one on the depth.
//!
//! [`OuHhtl`] (`[u16; 8]`) remains the ingress and wire format of the
//! directory records. [`Dn128::from_ou_hhtl`] converts it and fails closed
//! when a parent has more than 256 children; it never hashes or truncates.

use crate::guid::Guid128;
use crate::hhtl::OuHhtl;

/// Maximum hierarchy depth.
pub const DN_LEVELS: usize = 16;

/// Maximum distinct child codes under one parent.
pub const DN_CHILDREN: usize = 256;

/// The directory a [`Dn128`] is interpreted in: an AD domain or an Entra
/// tenant, by its identifier. Codes from two scopes are never comparable.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct DirectoryScope(pub Guid128);

/// 16 one-byte hierarchy levels, root first, plus an explicit depth.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Dn128 {
codes: [u8; DN_LEVELS],
depth: u8,
}

/// Why a [`Dn128`] could not be formed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dn128Error {
/// More than [`DN_LEVELS`] levels.
TooDeep(usize),
/// A parent has more than [`DN_CHILDREN`] children: the source segment
/// id (1-based, as `OuDictionary` allocates it) does not fit a byte.
ChildCodeOverflow {
/// Level (0 = root).
level: usize,
/// The source segment id.
segment: u16,
},
/// The source path has a zero segment before its last level (malformed).
Gap(usize),
}

impl std::fmt::Display for Dn128Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "dn128: {self:?}")
}
}
impl std::error::Error for Dn128Error {}

impl Dn128 {
/// The root (depth 0): an object directly under the naming context.
pub const ROOT: Self = Self {
codes: [0; DN_LEVELS],
depth: 0,
};

/// From root-first level codes.
pub fn new(codes: &[u8]) -> Result<Self, Dn128Error> {
if codes.len() > DN_LEVELS {
return Err(Dn128Error::TooDeep(codes.len()));
}
let mut d = Self::ROOT;
d.codes[..codes.len()].copy_from_slice(codes);
d.depth = codes.len() as u8;
Ok(d)
}

/// Convert an ingress [`OuHhtl`]. Segment `s` (1..=65535, allocated per
/// parent from 1) becomes code `s - 1`; `s > 256` means the parent has
/// more than 256 children and is refused.
pub fn from_ou_hhtl(h: &OuHhtl) -> Result<Self, Dn128Error> {
let depth = h.depth();
if let Some(i) = (depth..h.0.len()).find(|&i| h.0[i] != 0) {
return Err(Dn128Error::Gap(i));
}
let mut codes = [0u8; DN_LEVELS];
for (level, &segment) in h.0[..depth].iter().enumerate() {
codes[level] = u8::try_from(segment - 1)
.map_err(|_| Dn128Error::ChildCodeOverflow { level, segment })?;
}
Self::new(&codes[..depth])
}

/// Number of populated levels.
pub fn depth(&self) -> usize {
usize::from(self.depth)
}

/// The populated codes, root first.
pub fn codes(&self) -> &[u8] {
&self.codes[..self.depth()]
}

/// All 16 code bytes (zero past [`Self::depth`]) — the lane field.
pub fn bytes(&self) -> [u8; DN_LEVELS] {
self.codes
}

/// The first `depth` levels.
pub fn prefix(&self, depth: usize) -> Self {
let d = depth.min(self.depth());
let mut p = Self::ROOT;
p.codes[..d].copy_from_slice(&self.codes[..d]);
p.depth = d as u8;
p
}

/// True if `self` is a (non-strict) ancestor of `other`.
pub fn is_ancestor_of(&self, other: &Self) -> bool {
let d = self.depth();
d <= other.depth() && self.codes[..d] == other.codes[..d]
}

/// The ternary match selecting the subtree of `self`: compare the first
/// `depth` bytes exactly, ignore the rest. Combined with
/// `node.depth >= self.depth` it selects exactly ancestor-or-self.
pub fn subtree_mask(&self) -> ([u8; DN_LEVELS], [u8; DN_LEVELS]) {
let mut care = [0u8; DN_LEVELS];
care[..self.depth()].fill(0xFF);
(self.codes, care)
}
}

#[cfg(test)]
mod tests {
use super::*;

fn ou(levels: &[u16]) -> OuHhtl {
let mut h = OuHhtl::ROOT;
h.0[..levels.len()].copy_from_slice(levels);
h
}

#[test]
fn code_zero_is_a_level_and_depth_is_explicit() {
let a = Dn128::new(&[0]).unwrap();
assert_eq!(a.depth(), 1);
assert_ne!(a, Dn128::ROOT, "code 0 is a real level, not absence");
let deep = Dn128::new(&[0; 16]).unwrap();
assert_eq!(deep.depth(), 16);
assert!(a.is_ancestor_of(&deep));
assert!(!deep.is_ancestor_of(&a));
}

#[test]
fn deeper_than_sixteen_is_refused() {
assert_eq!(Dn128::new(&[1; 17]), Err(Dn128Error::TooDeep(17)));
}

#[test]
fn ou_hhtl_converts_and_fails_closed_past_256_children() {
let d = Dn128::from_ou_hhtl(&ou(&[1, 256, 7])).unwrap();
assert_eq!(d.codes(), &[0, 255, 6]);
assert_eq!(
Dn128::from_ou_hhtl(&ou(&[1, 257])),
Err(Dn128Error::ChildCodeOverflow {
level: 1,
segment: 257
})
);
assert_eq!(Dn128::from_ou_hhtl(&OuHhtl::ROOT), Ok(Dn128::ROOT));
}

#[test]
fn subtree_mask_compares_only_the_prefix() {
let p = Dn128::new(&[3, 9]).unwrap();
let (pattern, care) = p.subtree_mask();
let matches =
|n: &Dn128| (0..DN_LEVELS).all(|k| (n.bytes()[k] ^ pattern[k]) & care[k] == 0);
assert!(matches(&Dn128::new(&[3, 9, 200, 1]).unwrap()));
assert!(!matches(&Dn128::new(&[3, 8]).unwrap()));
// The byte compare alone also accepts a shallower node whose
// zero-filled bytes happen to equal the prefix; the depth gate is
// what excludes it.
let shallow = Dn128::new(&[3]).unwrap();
let p0 = Dn128::new(&[3, 0]).unwrap();
let (pat0, care0) = p0.subtree_mask();
assert!((0..DN_LEVELS).all(|k| (shallow.bytes()[k] ^ pat0[k]) & care0[k] == 0));
assert!(!p0.is_ancestor_of(&shallow));
}
}
4 changes: 3 additions & 1 deletion crates/ogar-dir-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! | axis | carrier | module |
//! |-------|-------------------------------------------|-------------|
//! | WHO | [`Guid128`] source object id | [`guid`] |
//! | WHERE | scope [`Guid128`] + [`OuHhtl`] | [`hhtl`] |
//! | WHERE | scope [`Guid128`] + [`OuHhtl`] (ingress); [`DirectoryScope`] × [`Dn128`] (execution) | [`hhtl`], [`dn128`] |
//! | WHAT | schema-defined attribute slots + [`ValuePool`] | [`record`], [`pool`], [`schema`] |
//! | LINKS | [`DirEdge`] records, never inline lists | [`edge`] |
//! | WHEN | `observed_at_ms` only (provenance hook) | [`record`] |
Expand All @@ -28,6 +28,7 @@

pub mod base64;
pub mod dn;
pub mod dn128;
pub mod edge;
pub mod guid;
pub mod hhtl;
Expand All @@ -36,6 +37,7 @@ pub mod record;
pub mod schema;

pub use dn::{Dn, DnError, Rdn};
pub use dn128::{DN_CHILDREN, DN_LEVELS, DirectoryScope, Dn128, Dn128Error};
pub use edge::{DirEdge, EdgeEvidence, EdgeKind};
pub use guid::{Guid128, GuidParseError};
pub use hhtl::{HhtlError, OU_LEVELS, OuDictionary, OuHhtl};
Expand Down
52 changes: 40 additions & 12 deletions crates/ogar-dir-sim/src/change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,34 @@
//! diff *reports* them. An attribute change carries the value it expects to
//! replace (`from`): applying it where `from` no longer holds is refused, and
//! the same expectation becomes a plan operation's [`Precondition`](crate::Precondition).
//!
//! ## No strings past ingress
//!
//! ```text
//! external String ──► ingress (normalize) ──► cold label/value store
//! │ stable ValueId / KeyId
//! ════════════════════ numeric substrate ════════╧═══════════════════════
//! Change · NodeState · Violation · ExecutionPlan carry ids only
//! ═══════════════════════════════════════════════════════════════════════
//! actuator / UI / evidence formatting ──► resolve id ──► String
//! ```
//!
//! A [`ValueId`] names one exact raw value; a [`KeyId`] names its comparison
//! form ([`normalize`]). Both are issued by the store that owns the
//! observations, append-only, so an id keeps its meaning from observation
//! through simulation, the desired version, reconciliation and the plan. They
//! are not snapshot ordinals.

use ogar_dir_core::{Guid128, OuHhtl};
use ogar_dir_core::{Dn128, Guid128};

/// Stable identity of one exact raw attribute value (case and spacing kept).
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ValueId(pub u32);

/// Stable identity of a value's comparison form ([`normalize`]): two values
/// the directory treats as equal share one key.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct KeyId(pub u32);

/// Kind of a directory node.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand All @@ -29,12 +55,13 @@ pub struct NodeState {
pub kind: NodeKind,
/// Enabled. Users only: groups have no enabled flag and carry `true`.
pub active: bool,
/// Raw UPN.
pub upn: Option<String>,
/// Raw primary SMTP.
pub primary_smtp: Option<String>,
/// OU location (numeric HHTL, never a DN), if known.
pub ou: Option<OuHhtl>,
/// UPN.
pub upn: Option<ValueId>,
/// Primary SMTP.
pub primary_smtp: Option<ValueId>,
/// Hierarchy location (numeric, within the store's directory scope;
/// never a DN), if known.
pub dn: Option<Dn128>,
Comment thread
AdaWorldAPI marked this conversation as resolved.
}

/// Attribute a change can set.
Expand Down Expand Up @@ -86,10 +113,10 @@ pub enum Change {
node: Guid128,
/// Which attribute.
attribute: Attribute,
/// Value the change expects to replace (raw, as observed).
from: Option<String>,
/// New value (raw).
to: Option<String>,
/// Value the change expects to replace (as observed).
from: Option<ValueId>,
/// New value.
to: Option<ValueId>,
},
/// A node that did not exist comes into existence.
CreateNode {
Expand All @@ -107,7 +134,8 @@ pub enum Change {
},
}

/// Comparison form of a UPN / SMTP address: trimmed and lowercased with
/// Comparison form of a UPN / SMTP address, computed once at ingress (the
/// store maps each [`ValueId`] to its [`KeyId`]); never during execution: trimmed and lowercased with
/// Unicode case mapping (Exchange and Entra compare these
/// case-insensitively, and neither restricts them to ASCII).
///
Expand Down
2 changes: 1 addition & 1 deletion crates/ogar-dir-sim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub mod plan;
pub mod provenance;
pub mod violation;

pub use change::{Attribute, Change, NodeKind, NodeState, normalize};
pub use change::{Attribute, Change, KeyId, NodeKind, NodeState, ValueId, normalize};
pub use plan::{ExecutionPlan, Operation, PlanError, PlannedOp, Precondition};
pub use provenance::{EvidenceRef, Origin, RuleId, TAG_DESIRED, TAG_OBSERVED, Version, VersionId};
pub use violation::{Endpoint, Violation};
Loading
Loading