diff --git a/crates/ogar-az/tests/correspond.rs b/crates/ogar-az/tests/correspond.rs index 965e215a..1aa5c67c 100644 --- a/crates/ogar-az/tests/correspond.rs +++ b/crates/ogar-az/tests/correspond.rs @@ -9,8 +9,8 @@ use ogar_ad::AdEntry; use ogar_dir_core::correspond::{ - AdStatus, Binding, Index, Lanes, Output, Planes, Profile, Ruler, Source, flag, fold, - fold_aligned, + AdStatus, Binding, Index, Lanes, NO_ROW, Output, Planes, Profile, Ruler, Source, UNBOUND, flag, + fold, fold_aligned, state, }; use ogar_dir_core::identity::{ BACKSYNC_TO_ENTRA_ID, CONSISTENCY_GUID_TO_IMMUTABLE_ID, ENTRA_ID_TO_EXO_EXTERNAL_ID, @@ -280,6 +280,11 @@ fn run_encoded(e: &Encoded, anchor: IdentityRule, b: &[Binding]) -> Run { let mut out = Output::for_index(&lanes, &ix); fold(&lanes, &ix, &mut out); assert_eq!(out.edges.len(), ix.edges, "the index counts every edge"); + assert_eq!( + out.entra_mailbox, + mailbox_oracle(&lanes), + "the fold's mailbox ordinal is the unique in-scope match" + ); // The same rows on one ruler: the position-wise fold confirms exactly // the slots the join confirms. let ruler = Ruler::from_fold(&lanes, &ix, &out); @@ -297,6 +302,29 @@ fn run_encoded(e: &Encoded, anchor: IdentityRule, b: &[Binding]) -> Run { Run { lanes, out } } +/// Scalar reference for [`Output::entra_mailbox`]: a linear scan, without the +/// index or the merge. Exactly one present, same-scope Exchange Online row +/// naming the Entra id, else `NO_ROW`. +fn mailbox_oracle(l: &Lanes) -> Vec { + (0..l.entra.len()) + .map(|j| { + let scope = l.entra.scope[j]; + if scope == UNBOUND { + return NO_ROW; + } + let mut hits = (0..l.exo.len()).filter(|&x| { + l.exo.scope[x] == scope + && l.exo_external.state[x] == state::PRESENT + && l.exo_external.id[x] == l.entra.owner[j] + }); + match (hits.next(), hits.next()) { + (Some(x), None) => x as u32, + _ => NO_ROW, + } + }) + .collect() +} + fn statuses(r: &Run) -> Vec { r.out.ad.iter().map(|&f| AdStatus::of(f)).collect() } @@ -814,6 +842,7 @@ fn f14_observations_are_not_touched() { ); assert_ne!(r.out.entra[0] & flag::E_EXO_ONE, 0); assert_ne!(r.out.exo[0] & flag::X_ONE, 0); + assert_eq!(r.out.entra_mailbox, [0], "the one mailbox, by row ordinal"); } #[test] @@ -836,12 +865,42 @@ fn exchange_online_dangling_and_duplicate() { }; let r = run(&w, CONSISTENCY_GUID_TO_IMMUTABLE_ID); assert_ne!(r.out.entra[0] & flag::E_EXO_MANY, 0); + assert_eq!(r.out.entra_mailbox, [NO_ROW], "two mailboxes: no choice"); assert_eq!( r.out.exo, [flag::X_ONE, flag::X_ONE, flag::X_DANGLING, flag::X_ABSENT] ); } +/// The mailbox ordinal is scoped: the same external id in another bound +/// tenant names a different Entra row, never this one. +#[test] +fn mailbox_ordinal_does_not_cross_tenants() { + let x = g(3); + let mut there = entra(x, Wire::Absent); + there.tenant = g(OTHER_TENANT); + let exo = |n, tenant| Exo { + guid: g(n), + tenant: g(tenant), + external: Wire::Id(x), + }; + let w = World { + entra: vec![entra(x, Wire::Absent), there], + exo: vec![exo(40, OTHER_TENANT), exo(41, TENANT)], + ..World::default() + }; + let both = [ + bindings()[0], + Binding { + ad_domain: g(OTHER_DOMAIN), + tenant: g(OTHER_TENANT), + }, + ]; + let r = run_encoded(&encode(&w), CONSISTENCY_GUID_TO_IMMUTABLE_ID, &both); + assert_ne!(r.lanes.entra.scope[0], r.lanes.entra.scope[1]); + assert_eq!(r.out.entra_mailbox, [1, 0]); +} + // ---- randomized comparison -------------------------------------------------- struct Rng(u64); diff --git a/crates/ogar-dir-core/src/correspond.rs b/crates/ogar-dir-core/src/correspond.rs index 590c88e8..83b06c9f 100644 --- a/crates/ogar-dir-core/src/correspond.rs +++ b/crates/ogar-dir-core/src/correspond.rs @@ -398,6 +398,9 @@ pub struct Output { pub ad_back: Vec, /// Per Entra row. pub entra: Vec, + /// Per Entra row: the one Exchange Online row whose external id names + /// it, else [`NO_ROW`] (none, or several: a choice is never made). + pub entra_mailbox: Vec, /// Per Entra row: the AD row whose anchor alone claims it, else [`NO_ROW`]. pub entra_claimer: Vec, /// Per Entra row: the AD row whose backsync alone names it, else [`NO_ROW`]. @@ -416,6 +419,7 @@ impl Output { ad_target: vec![NO_ROW; l.ad.len()], ad_back: vec![NO_ROW; l.ad.len()], entra: vec![0; l.entra.len()], + entra_mailbox: vec![NO_ROW; l.entra.len()], entra_claimer: vec![NO_ROW; l.entra.len()], entra_namer: vec![NO_ROW; l.entra.len()], exo: vec![0; l.exo.len()], @@ -585,6 +589,7 @@ pub fn fold(l: &Lanes, ix: &Index, out: &mut Output) { let mbx = count_flag(x.len(), 0, flag::E_EXO_ONE, flag::E_EXO_MANY); for y in e { out.entra[y.row as usize] |= mbx; + out.entra_mailbox[y.row as usize] = sole(x); } }); @@ -735,7 +740,10 @@ impl Ruler { /// failing that exactly one backsync target; exactly one mailbox); /// anything else leaves the cell empty, so ambiguity can never be read /// back as agreement. - pub fn from_fold(l: &Lanes, ix: &Index, out: &Output) -> Self { + /// + /// `_ix` is unused since the mailbox ordinal moved into [`Output`]; it + /// stays so callers keep compiling. + pub fn from_fold(l: &Lanes, _ix: &Index, out: &Output) -> Self { let n = l.ad.len(); let mut r = Self { ad_anchor: vec![Guid128::NIL; n], @@ -747,13 +755,9 @@ impl Ruler { exo_external: vec![Guid128::NIL; n], max_skew_ms: l.max_skew_ms, }; - // The one mailbox of each Entra row, if exactly one. - let mut mailbox = vec![NO_ROW; l.entra.len()]; - merge(&ix.exo_by_external, &ix.entra_by_id, |x, e| { - for y in e { - mailbox[y.row as usize] = sole(x); - } - }); + // The one mailbox of each Entra row comes from the fold; no second + // merge. + let mailbox = &out.entra_mailbox; for i in 0..n { if l.ad_anchor.state[i] == state::PRESENT && out.ad[i] & flag::ANCHOR_SHARED == 0 { r.ad_anchor[i] = l.ad_anchor.id[i];