use crate::attrs::{
AeaMarking, Classification, CountryCode, DeclassExemption, DissemControl, FgiMarker,
IsmAttributes, MarkingClassification, NonIcDissem, SarCompartment, SarIndicator, SarMarking,
SarProgram, SciCompartment, SciControl, SciControlSystem, SciMarking,
};
use crate::date::IsmDate;
pub fn sar_sort_key(s: &str) -> (bool, u64, &str) {
let prefix_len = s.bytes().take_while(|b| b.is_ascii_digit()).count();
if prefix_len == 0 {
(true, 0, s)
} else {
let n: u64 = s[..prefix_len].parse().unwrap_or(u64::MAX);
(false, n, &s[prefix_len..])
}
}
#[derive(Debug, Clone, Default)]
pub struct PageContext {
portions: Vec<IsmAttributes>,
}
impl PageContext {
pub fn new() -> Self {
Self::default()
}
pub fn add_portion(&mut self, attrs: IsmAttributes) {
self.portions.push(attrs);
}
pub fn portion_count(&self) -> usize {
self.portions.len()
}
pub fn is_empty(&self) -> bool {
self.portions.is_empty()
}
pub fn portions(&self) -> &[IsmAttributes] {
&self.portions
}
pub fn expected_classification(&self) -> Option<Classification> {
self.portions
.iter()
.filter_map(|a| a.classification.as_ref().map(|c| c.effective_level()))
.max()
}
pub fn expected_sci_controls(&self) -> Vec<SciControl> {
let mut seen = std::collections::BTreeSet::new();
seen.extend(
self.portions
.iter()
.flat_map(|attrs| attrs.sci_controls.iter().copied()),
);
seen.into_iter().collect()
}
pub fn expected_sci_markings(&self) -> Box<[SciMarking]> {
let mut acc: std::collections::BTreeMap<
SystemKey,
std::collections::BTreeMap<String, std::collections::BTreeSet<String>>,
> = std::collections::BTreeMap::new();
for attrs in &self.portions {
for marking in attrs.sci_markings.iter() {
let key = SystemKey::from_system(&marking.system);
let comp_map = acc.entry(key).or_default();
for comp in marking.compartments.iter() {
let sub_set = comp_map.entry(comp.identifier.to_string()).or_default();
sub_set.extend(comp.sub_compartments.iter().map(ToString::to_string));
}
}
}
let mut systems: Vec<(SystemKey, _)> = acc.into_iter().collect();
systems.sort_by(|a, b| sar_sort_key(a.0.text()).cmp(&sar_sort_key(b.0.text())));
let mut out: Vec<SciMarking> = Vec::with_capacity(systems.len());
for (sys_key, comp_map) in systems {
let mut comps: Vec<(String, std::collections::BTreeSet<String>)> =
comp_map.into_iter().collect();
comps.sort_by(|a, b| sar_sort_key(&a.0).cmp(&sar_sort_key(&b.0)));
let compartments: Vec<SciCompartment> = comps
.into_iter()
.map(|(id, sub_set)| {
let mut subs: Vec<String> = sub_set.into_iter().collect();
subs.sort_by(|a, b| sar_sort_key(a).cmp(&sar_sort_key(b)));
let sub_boxes: Box<[Box<str>]> = subs
.into_iter()
.map(|s| s.into_boxed_str())
.collect::<Vec<_>>()
.into_boxed_slice();
SciCompartment::new(id.into_boxed_str(), sub_boxes)
})
.collect();
out.push(SciMarking::new(
sys_key.into_system(),
compartments.into_boxed_slice(),
None,
));
}
out.into_boxed_slice()
}
pub fn expected_sar_marking(&self) -> Option<SarMarking> {
use std::collections::{BTreeMap, BTreeSet};
let mut programs: BTreeMap<String, BTreeMap<String, BTreeSet<String>>> = BTreeMap::new();
for attrs in &self.portions {
let Some(sar) = attrs.sar_markings.as_ref() else {
continue;
};
for prog in sar.programs.iter() {
let comps = programs.entry(prog.identifier.to_string()).or_default();
for comp in prog.compartments.iter() {
let subs = comps.entry(comp.identifier.to_string()).or_default();
subs.extend(comp.sub_compartments.iter().map(ToString::to_string));
}
}
}
if programs.is_empty() {
return None;
}
let mut prog_keys: Vec<String> = programs.keys().cloned().collect();
prog_keys.sort_by(|a, b| sar_sort_key(a).cmp(&sar_sort_key(b)));
let built_programs: Vec<SarProgram> = prog_keys
.into_iter()
.map(|pid| {
let comp_map = programs.remove(&pid).expect("key enumerated above");
let mut comp_keys: Vec<String> = comp_map.keys().cloned().collect();
comp_keys.sort_by(|a, b| sar_sort_key(a).cmp(&sar_sort_key(b)));
let built_compartments: Vec<SarCompartment> = comp_keys
.into_iter()
.map(|cid| {
let subs = comp_map.get(&cid).expect("key enumerated above");
let mut sub_vec: Vec<String> = subs.iter().cloned().collect();
sub_vec.sort_by(|a, b| sar_sort_key(a).cmp(&sar_sort_key(b)));
let boxed: Box<[Box<str>]> = sub_vec
.into_iter()
.map(|s| s.into_boxed_str())
.collect::<Vec<_>>()
.into_boxed_slice();
SarCompartment::new(cid.into_boxed_str(), boxed)
})
.collect();
SarProgram::new(pid.into_boxed_str(), built_compartments.into_boxed_slice())
})
.collect();
Some(SarMarking::new(
SarIndicator::Abbrev,
built_programs.into_boxed_slice(),
))
}
pub fn expected_dissem_controls(&self) -> Vec<DissemControl> {
let classified = self.is_classified();
let mut seen = std::collections::BTreeSet::new();
seen.extend(
self.portions
.iter()
.flat_map(|attrs| attrs.dissem_controls.iter().copied()),
);
if seen.contains(&DissemControl::OcUsgov) {
let oc_portions: Vec<_> = self
.portions
.iter()
.filter(|a| a.dissem_controls.contains(&DissemControl::Oc))
.collect();
if !oc_portions.is_empty() {
let all_have_usgov = oc_portions
.iter()
.all(|a| a.dissem_controls.contains(&DissemControl::OcUsgov));
if !all_have_usgov {
seen.remove(&DissemControl::OcUsgov);
}
}
}
let dsen_present = seen.contains(&DissemControl::Dsen);
if seen.contains(&DissemControl::Fouo) && (classified || dsen_present) {
seen.remove(&DissemControl::Fouo);
}
let (_, needs_nf) = self.expected_non_ic_dissem();
if needs_nf {
seen.insert(DissemControl::Nf);
}
seen.into_iter().collect()
}
pub fn expected_rel_to(&self) -> Vec<CountryCode> {
let any_noforn = self.portions.iter().any(|a| {
a.dissem_controls
.iter()
.any(|d| matches!(d, DissemControl::Nf))
});
if any_noforn {
return vec![];
}
let (_, needs_nf) = self.expected_non_ic_dissem();
if needs_nf {
return vec![];
}
let rel_to_portions: Vec<_> = self
.portions
.iter()
.filter(|a| !a.rel_to.is_empty())
.collect();
if rel_to_portions.is_empty() {
return vec![];
}
let expanded: Vec<std::collections::BTreeSet<&str>> = rel_to_portions
.iter()
.map(|a| {
let mut set = std::collections::BTreeSet::new();
for t in a.rel_to.iter() {
let s = t.as_str();
if let Some(members) = expand_tetragraph(s) {
for &m in members {
set.insert(m);
}
} else {
set.insert(s);
}
}
set
})
.collect();
let mut result: std::collections::BTreeSet<&str> = expanded[0].clone();
for set in &expanded[1..] {
result = result.intersection(set).copied().collect();
}
let mut codes: Vec<CountryCode> = result
.iter()
.filter_map(|s| CountryCode::try_new(s.as_bytes()))
.collect();
if let Some(pos) = codes.iter().position(|c| *c == CountryCode::USA) {
if pos != 0 {
let usa = codes.remove(pos);
codes.insert(0, usa);
}
}
codes
}
pub fn expected_declassify_on(&self) -> Option<&IsmDate> {
self.portions
.iter()
.filter_map(|a| a.declassify_on.as_ref())
.max_by(|a, b| a.end_cmp(b))
}
pub fn expected_declass_exemption(&self) -> Option<DeclassExemption> {
self.portions
.iter()
.filter_map(|a| a.declass_exemption)
.next_back()
}
pub fn is_classified(&self) -> bool {
self.expected_classification()
.is_some_and(|c| c > Classification::Unclassified)
}
pub fn expected_aea_markings(&self) -> Vec<AeaMarking> {
let classified = self.is_classified();
let mut has_rd = false;
let mut rd_cnwdi = false;
let mut rd_sigma: std::collections::BTreeSet<u8> = std::collections::BTreeSet::new();
let mut has_frd = false;
let mut frd_sigma: std::collections::BTreeSet<u8> = std::collections::BTreeSet::new();
let mut has_tfni = false;
let mut has_dod_ucni = false;
let mut has_doe_ucni = false;
for attrs in &self.portions {
for aea in attrs.aea_markings.iter() {
match aea {
AeaMarking::Rd(rd) => {
has_rd = true;
if rd.cnwdi {
rd_cnwdi = true;
}
rd_sigma.extend(rd.sigma.iter().copied());
}
AeaMarking::Frd(frd) => {
has_frd = true;
frd_sigma.extend(frd.sigma.iter().copied());
}
AeaMarking::Tfni => has_tfni = true,
AeaMarking::DodUcni => has_dod_ucni = true,
AeaMarking::DoeUcni => has_doe_ucni = true,
}
}
}
let mut result = Vec::new();
if has_rd {
let all_sigma: Vec<u8> = rd_sigma.union(&frd_sigma).copied().collect();
result.push(AeaMarking::Rd(crate::attrs::RdBlock {
cnwdi: rd_cnwdi,
sigma: all_sigma.into(),
}));
}
if has_frd && !has_rd {
result.push(AeaMarking::Frd(crate::attrs::FrdBlock {
sigma: frd_sigma.into_iter().collect::<Vec<_>>().into(),
}));
}
if has_tfni && !has_rd {
result.push(AeaMarking::Tfni);
}
if !classified {
if has_dod_ucni {
result.push(AeaMarking::DodUcni);
}
if has_doe_ucni {
result.push(AeaMarking::DoeUcni);
}
}
result
}
pub fn expected_fgi_marker(&self) -> Option<FgiMarker> {
let mut has_any_fgi = false;
let mut has_source_concealed = false;
let mut countries = std::collections::BTreeSet::new();
for attrs in &self.portions {
if let Some(marker) = &attrs.fgi_marker {
has_any_fgi = true;
if marker.countries.is_empty() {
has_source_concealed = true;
} else {
countries.extend(marker.countries.iter().map(|c| c.as_str().to_owned()));
}
}
match &attrs.classification {
Some(MarkingClassification::Fgi(fgi)) => {
has_any_fgi = true;
if fgi.countries.is_empty() {
has_source_concealed = true;
} else {
for c in fgi.countries.iter() {
countries.insert(c.as_str().to_owned());
}
}
}
Some(MarkingClassification::Nato(_)) => {
has_any_fgi = true;
countries.insert("NATO".to_owned());
}
Some(MarkingClassification::Joint(j)) => {
has_any_fgi = true;
for c in j.countries.iter() {
if c.as_str() != "USA" {
countries.insert(c.as_str().to_owned());
}
}
}
_ => {}
}
}
if !has_any_fgi {
return None;
}
if has_source_concealed {
return Some(FgiMarker {
countries: Box::new([]),
});
}
let codes: Vec<CountryCode> = countries
.iter()
.filter_map(|s| CountryCode::try_new(s.as_bytes()))
.collect();
Some(FgiMarker {
countries: codes.into(),
})
}
pub fn expected_non_ic_dissem(&self) -> (Vec<NonIcDissem>, bool) {
let classified = self.is_classified();
let mut seen = std::collections::BTreeSet::new();
let mut needs_nf_from_split = false;
seen.extend(
self.portions
.iter()
.flat_map(|attrs| attrs.non_ic_dissem.iter().copied()),
);
if classified {
if seen.remove(&NonIcDissem::SbuNf) {
seen.insert(NonIcDissem::Sbu);
needs_nf_from_split = true;
}
if seen.remove(&NonIcDissem::LesNf) {
seen.insert(NonIcDissem::Les);
needs_nf_from_split = true;
}
}
(seen.into_iter().collect(), needs_nf_from_split)
}
pub fn render_expected_banner(&self) -> Option<String> {
if self.portions.is_empty() {
return None;
}
let classification = self
.expected_classification()
.map(|c| c.banner_str().to_owned())
.unwrap_or_else(|| "UNCLASSIFIED".to_owned());
let mut blocks: Vec<String> = vec![classification];
let sci_markings = self.expected_sci_markings();
if !sci_markings.is_empty() {
blocks.push(render_sci_markings_block(&sci_markings));
} else {
let sci = self.expected_sci_controls();
if !sci.is_empty() {
blocks.push(sci.iter().map(|s| s.as_str()).collect::<Vec<_>>().join("/"));
}
}
if let Some(sar) = self.expected_sar_marking() {
blocks.push(render_sar_block(&sar));
}
let aea = self.expected_aea_markings();
if !aea.is_empty() {
blocks.push(
aea.iter()
.map(|a| a.banner_str())
.collect::<Vec<_>>()
.join("/"),
);
}
let rel_to = self.expected_rel_to();
let (non_ic, needs_nf_from_non_ic) = self.expected_non_ic_dissem();
let dissem = self.expected_dissem_controls();
let mut dissem_parts: Vec<String> = Vec::new();
for d in &dissem {
let portion = d.as_str();
let banner = crate::marking_forms::portion_to_banner(portion).unwrap_or(portion);
if banner == "REL" && !rel_to.is_empty() {
continue;
}
dissem_parts.push(banner.to_owned());
}
if needs_nf_from_non_ic && !dissem_parts.iter().any(|p| p == "NOFORN") {
dissem_parts.push("NOFORN".to_owned());
}
if !rel_to.is_empty() {
let trigraphs = rel_to
.iter()
.map(|t| t.as_str())
.collect::<Vec<_>>()
.join(", ");
dissem_parts.push(format!("REL TO {trigraphs}"));
}
if !dissem_parts.is_empty() {
blocks.push(dissem_parts.join("/"));
}
if !non_ic.is_empty() {
blocks.push(
non_ic
.iter()
.map(|n| n.banner_str())
.collect::<Vec<_>>()
.join("/"),
);
}
Some(blocks.join("//"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum SystemKey {
Published(crate::attrs::SciControlBare),
Custom(String),
}
impl SystemKey {
fn from_system(sys: &SciControlSystem) -> Self {
match sys {
SciControlSystem::Published(b) => SystemKey::Published(*b),
SciControlSystem::Custom(s) => SystemKey::Custom(s.to_string()),
}
}
fn text(&self) -> &str {
match self {
SystemKey::Published(b) => b.as_str(),
SystemKey::Custom(s) => s.as_str(),
}
}
fn into_system(self) -> SciControlSystem {
match self {
SystemKey::Published(b) => SciControlSystem::Published(b),
SystemKey::Custom(s) => SciControlSystem::Custom(s.into_boxed_str()),
}
}
}
fn render_sci_markings_block(markings: &[SciMarking]) -> String {
let mut systems: Vec<String> = Vec::with_capacity(markings.len());
for m in markings {
let sys_text = match &m.system {
SciControlSystem::Published(b) => b.as_str().to_owned(),
SciControlSystem::Custom(s) => s.to_string(),
};
if m.compartments.is_empty() {
systems.push(sys_text);
continue;
}
let mut rendered = sys_text;
for comp in m.compartments.iter() {
rendered.push('-');
rendered.push_str(&comp.identifier);
for sub in comp.sub_compartments.iter() {
rendered.push(' ');
rendered.push_str(sub);
}
}
systems.push(rendered);
}
systems.join("/")
}
fn render_sar_block(sar: &SarMarking) -> String {
let mut out = String::from("SAR-");
let mut first_prog = true;
for prog in sar.programs.iter() {
if !first_prog {
out.push('/');
}
first_prog = false;
out.push_str(&prog.identifier);
for comp in prog.compartments.iter() {
out.push('-');
out.push_str(&comp.identifier);
for sub in comp.sub_compartments.iter() {
out.push(' ');
out.push_str(sub);
}
}
}
out
}
fn expand_tetragraph(code: &str) -> Option<&'static [&'static str]> {
crate::lookup_tetragraph_members(code)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::attrs::{Classification, MarkingClassification};
use crate::date::IsmDate;
fn attrs_with_classification(c: Classification) -> IsmAttributes {
IsmAttributes {
classification: Some(MarkingClassification::Us(c)),
..Default::default()
}
}
#[test]
fn expected_classification_returns_max() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_classification(Classification::Secret));
ctx.add_portion(attrs_with_classification(Classification::Confidential));
assert_eq!(ctx.expected_classification(), Some(Classification::Secret));
}
#[test]
fn expected_classification_empty_returns_none() {
assert_eq!(PageContext::new().expected_classification(), None);
}
#[test]
fn nato_secret_contributes_to_max_classification() {
use crate::attrs::NatoClassification::NatoSecret;
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_classification(Classification::Confidential));
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Nato(NatoSecret)),
..Default::default()
});
assert_eq!(
ctx.expected_classification(),
Some(Classification::Secret),
"NS (NATO SECRET) should drive banner to SECRET"
);
}
#[test]
fn fgi_secret_contributes_to_max_classification() {
use crate::attrs::{CountryCode, FgiClassification};
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_classification(Classification::Confidential));
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Fgi(FgiClassification {
level: Classification::Secret,
countries: vec![CountryCode::try_new(b"DEU").unwrap()].into(),
})),
..Default::default()
});
assert_eq!(
ctx.expected_classification(),
Some(Classification::Secret),
"DEU S (FGI SECRET) should drive banner to SECRET"
);
}
#[test]
fn joint_secret_contributes_to_max_classification() {
use crate::attrs::JointClassification;
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_classification(Classification::Confidential));
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Joint(JointClassification {
level: Classification::Secret,
countries: Box::new([]),
})),
..Default::default()
});
assert_eq!(
ctx.expected_classification(),
Some(Classification::Secret),
"JOINT SECRET should drive banner to SECRET"
);
}
#[test]
fn expected_sci_controls_union() {
use crate::attrs::SciControl;
let mut ctx = PageContext::new();
let a1 = IsmAttributes {
sci_controls: vec![SciControl::Si].into_boxed_slice(),
..Default::default()
};
let a2 = IsmAttributes {
sci_controls: vec![SciControl::Tk].into_boxed_slice(),
..Default::default()
};
ctx.add_portion(a1);
ctx.add_portion(a2);
let expected = ctx.expected_sci_controls();
assert!(expected.contains(&SciControl::Si));
assert!(expected.contains(&SciControl::Tk));
assert_eq!(expected.len(), 2);
}
#[test]
fn expected_rel_to_intersection() {
use crate::attrs::CountryCode;
let mut ctx = PageContext::new();
let a1 = IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"GBR").unwrap()]
.into_boxed_slice(),
..Default::default()
};
let a2 = IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"DEU").unwrap()]
.into_boxed_slice(),
..Default::default()
};
ctx.add_portion(a1);
ctx.add_portion(a2);
let rel = ctx.expected_rel_to();
assert_eq!(rel, vec![CountryCode::USA]);
}
#[test]
fn noforn_supersedes_rel_to() {
use crate::attrs::{CountryCode, DissemControl};
let mut ctx = PageContext::new();
let a1 = IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"GBR").unwrap()]
.into_boxed_slice(),
..Default::default()
};
let a2 = IsmAttributes {
dissem_controls: vec![DissemControl::Nf].into_boxed_slice(),
..Default::default()
};
ctx.add_portion(a1);
ctx.add_portion(a2);
assert!(ctx.expected_rel_to().is_empty());
}
#[test]
fn expected_declassify_on_max() {
let a1 = IsmAttributes {
declassify_on: Some(IsmDate::Date(2035, 1, 1)),
..Default::default()
};
let a2 = IsmAttributes {
declassify_on: Some(IsmDate::Date(2048, 12, 31)),
..Default::default()
};
let mut ctx = PageContext::new();
ctx.add_portion(a1);
ctx.add_portion(a2);
assert_eq!(
ctx.expected_declassify_on(),
Some(&IsmDate::Date(2048, 12, 31))
);
}
#[test]
fn is_classified_true_for_secret() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_classification(Classification::Secret));
assert!(ctx.is_classified());
}
#[test]
fn is_classified_false_for_unclassified() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_classification(Classification::Unclassified));
assert!(!ctx.is_classified());
}
#[test]
fn aea_rd_union_across_portions() {
use crate::attrs::{AeaMarking, RdBlock};
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
aea_markings: vec![AeaMarking::Rd(RdBlock::default())].into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::TopSecret)),
aea_markings: vec![AeaMarking::Rd(RdBlock {
cnwdi: false,
sigma: vec![18].into(),
})]
.into(),
..Default::default()
});
let aea = ctx.expected_aea_markings();
assert_eq!(aea.len(), 1);
match &aea[0] {
AeaMarking::Rd(rd) => {
assert!(!rd.cnwdi);
assert_eq!(&*rd.sigma, &[18]);
}
other => panic!("expected Rd, got: {other:?}"),
}
}
#[test]
fn aea_sigma_aggregated_sorted() {
use crate::attrs::{AeaMarking, RdBlock};
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
aea_markings: vec![AeaMarking::Rd(RdBlock {
cnwdi: false,
sigma: vec![20, 14].into(),
})]
.into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
aea_markings: vec![AeaMarking::Rd(RdBlock {
cnwdi: false,
sigma: vec![18].into(),
})]
.into(),
..Default::default()
});
let aea = ctx.expected_aea_markings();
match &aea[0] {
AeaMarking::Rd(rd) => {
assert_eq!(&*rd.sigma, &[14, 18, 20]);
}
other => panic!("expected Rd, got: {other:?}"),
}
}
#[test]
fn aea_ucni_drops_in_classified() {
use crate::attrs::AeaMarking;
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
aea_markings: vec![AeaMarking::DodUcni].into(),
..Default::default()
});
let aea = ctx.expected_aea_markings();
assert!(aea.is_empty(), "UCNI should drop in classified: {aea:?}");
}
#[test]
fn aea_ucni_kept_in_unclassified() {
use crate::attrs::AeaMarking;
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Unclassified)),
aea_markings: vec![AeaMarking::DodUcni].into(),
..Default::default()
});
let aea = ctx.expected_aea_markings();
assert_eq!(aea.len(), 1);
assert_eq!(aea[0], AeaMarking::DodUcni);
}
#[test]
fn non_ic_sbu_nf_splits_in_classified() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
non_ic_dissem: vec![NonIcDissem::SbuNf].into(),
..Default::default()
});
let (non_ic, needs_nf) = ctx.expected_non_ic_dissem();
assert!(non_ic.contains(&NonIcDissem::Sbu));
assert!(!non_ic.contains(&NonIcDissem::SbuNf));
assert!(needs_nf, "NF should be added to dissem from SBU-NF split");
}
#[test]
fn non_ic_sbu_nf_kept_in_unclassified() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Unclassified)),
non_ic_dissem: vec![NonIcDissem::SbuNf].into(),
..Default::default()
});
let (non_ic, needs_nf) = ctx.expected_non_ic_dissem();
assert!(non_ic.contains(&NonIcDissem::SbuNf));
assert!(!needs_nf);
}
#[test]
fn fgi_source_concealed_supersedes_open() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
fgi_marker: Some(FgiMarker {
countries: Box::new([]),
}),
..Default::default()
});
ctx.add_portion(IsmAttributes {
fgi_marker: Some(FgiMarker {
countries: vec![CountryCode::try_new(b"GBR").unwrap()].into(),
}),
..Default::default()
});
let marker = ctx.expected_fgi_marker().expect("should have FGI marker");
assert!(
marker.countries.is_empty(),
"source-concealed should supersede: {:?}",
marker.countries,
);
}
#[test]
fn fgi_open_union_of_countries() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
fgi_marker: Some(FgiMarker {
countries: vec![CountryCode::try_new(b"GBR").unwrap()].into(),
}),
..Default::default()
});
ctx.add_portion(IsmAttributes {
fgi_marker: Some(FgiMarker {
countries: vec![CountryCode::try_new(b"DEU").unwrap()].into(),
}),
..Default::default()
});
let marker = ctx.expected_fgi_marker().unwrap();
assert_eq!(marker.countries.len(), 2);
}
#[test]
fn rel_to_fvey_expansion_intersects_correctly() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
rel_to: vec![
CountryCode::USA,
CountryCode::try_new(b"AUS").unwrap(),
CountryCode::try_new(b"CAN").unwrap(),
CountryCode::try_new(b"GBR").unwrap(),
CountryCode::try_new(b"NZL").unwrap(),
]
.into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
rel_to: vec![
CountryCode::USA,
CountryCode::try_new(b"AUS").unwrap(),
CountryCode::try_new(b"CAN").unwrap(),
]
.into(),
..Default::default()
});
let rel = ctx.expected_rel_to();
assert_eq!(rel.len(), 3);
assert_eq!(rel[0], CountryCode::USA); assert_eq!(rel[1].as_str(), "AUS");
assert_eq!(rel[2].as_str(), "CAN");
}
#[test]
fn rel_to_empty_intersection_returns_empty() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"AUS").unwrap()].into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"GBR").unwrap()].into(),
..Default::default()
});
let rel = ctx.expected_rel_to();
assert_eq!(rel.len(), 1);
assert_eq!(rel[0], CountryCode::USA);
}
#[test]
fn rel_to_intersection_expands_fvey_into_constituent_trigraphs() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"FVEY").unwrap()].into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"NZL").unwrap()].into(),
..Default::default()
});
let rel = ctx.expected_rel_to();
let codes: Vec<&str> = rel.iter().map(|c| c.as_str()).collect();
assert_eq!(codes, vec!["USA", "NZL"]);
}
#[test]
fn rel_to_opaque_tetragraph_in_one_portion_drops_from_intersection() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"KFOR").unwrap()].into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"GBR").unwrap()].into(),
..Default::default()
});
let rel = ctx.expected_rel_to();
let codes: Vec<&str> = rel.iter().map(|c| c.as_str()).collect();
assert_eq!(codes, vec!["USA"]);
}
#[test]
fn rel_to_opaque_tetragraph_in_every_portion_survives_intersection() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"KFOR").unwrap()].into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"KFOR").unwrap()].into(),
..Default::default()
});
let rel = ctx.expected_rel_to();
let codes: Vec<&str> = rel.iter().map(|c| c.as_str()).collect();
assert_eq!(codes, vec!["USA", "KFOR"]);
}
#[test]
fn rel_to_fvey_intersected_with_acgu_yields_acgu_members() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"FVEY").unwrap()].into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
rel_to: vec![CountryCode::USA, CountryCode::try_new(b"ACGU").unwrap()].into(),
..Default::default()
});
let rel = ctx.expected_rel_to();
let codes: Vec<&str> = rel.iter().map(|c| c.as_str()).collect();
assert_eq!(codes, vec!["USA", "AUS", "CAN", "GBR"]);
}
#[test]
fn expand_tetragraph_reads_canonical_table_for_fvey_acgu() {
assert_eq!(
super::expand_tetragraph("FVEY"),
Some(crate::lookup_tetragraph_members("FVEY").unwrap()),
"page_context::expand_tetragraph must defer to the \
canonical marque_ism::lookup_tetragraph_members for FVEY"
);
assert_eq!(
super::expand_tetragraph("ACGU"),
Some(crate::lookup_tetragraph_members("ACGU").unwrap()),
);
}
#[test]
fn expand_tetragraph_returns_none_for_opaque_and_unknown() {
assert!(super::expand_tetragraph("EU").is_none());
assert!(super::expand_tetragraph("KFOR").is_none());
assert!(super::expand_tetragraph("RSMA").is_none());
assert!(super::expand_tetragraph("ISAF").is_none());
assert!(super::expand_tetragraph("USA").is_none());
assert!(super::expand_tetragraph("XYZW").is_none());
}
#[test]
fn dissem_fouo_drops_in_classified() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
dissem_controls: vec![DissemControl::Fouo].into(),
..Default::default()
});
let dissem = ctx.expected_dissem_controls();
assert!(
!dissem.contains(&DissemControl::Fouo),
"FOUO should drop in classified doc: {dissem:?}"
);
}
#[test]
fn dissem_fouo_kept_in_unclassified() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Unclassified)),
dissem_controls: vec![DissemControl::Fouo].into(),
..Default::default()
});
let dissem = ctx.expected_dissem_controls();
assert!(
dissem.contains(&DissemControl::Fouo),
"FOUO should stay in unclassified: {dissem:?}"
);
}
#[test]
fn dissem_fouo_drops_when_dsen_present_unclassified() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Unclassified)),
dissem_controls: vec![DissemControl::Dsen, DissemControl::Fouo].into(),
..Default::default()
});
let dissem = ctx.expected_dissem_controls();
assert!(
!dissem.contains(&DissemControl::Fouo),
"FOUO should drop when DSEN is present, even unclassified: {dissem:?}"
);
assert!(
dissem.contains(&DissemControl::Dsen),
"DSEN should be retained: {dissem:?}"
);
}
#[test]
fn dissem_oc_usgov_drops_when_not_on_all_oc_portions() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
dissem_controls: vec![DissemControl::Oc, DissemControl::OcUsgov].into(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
dissem_controls: vec![DissemControl::Oc].into(),
..Default::default()
});
let dissem = ctx.expected_dissem_controls();
assert!(dissem.contains(&DissemControl::Oc));
assert!(
!dissem.contains(&DissemControl::OcUsgov),
"OC-USGOV should drop when not on all OC portions: {dissem:?}"
);
}
#[test]
fn dissem_nf_injected_from_sbu_nf_split() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
non_ic_dissem: vec![NonIcDissem::SbuNf].into(),
..Default::default()
});
let dissem = ctx.expected_dissem_controls();
assert!(
dissem.contains(&DissemControl::Nf),
"NF should be injected from SBU-NF split: {dissem:?}"
);
}
#[test]
fn render_banner_empty_returns_none() {
assert_eq!(PageContext::new().render_expected_banner(), None);
}
#[test]
fn render_banner_ts_si_tk_noforn() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::TopSecret)),
sci_controls: vec![SciControl::Si, SciControl::Tk].into_boxed_slice(),
dissem_controls: vec![DissemControl::Nf].into_boxed_slice(),
..Default::default()
});
assert_eq!(
ctx.render_expected_banner().as_deref(),
Some("TOP SECRET//SI/TK//NOFORN")
);
}
#[test]
fn render_banner_rollup_from_multiple_portions() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::TopSecret)),
sci_controls: vec![SciControl::Si, SciControl::Tk].into_boxed_slice(),
dissem_controls: vec![DissemControl::Nf].into_boxed_slice(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
dissem_controls: vec![DissemControl::Nf].into_boxed_slice(),
..Default::default()
});
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Unclassified)),
..Default::default()
});
assert_eq!(
ctx.render_expected_banner().as_deref(),
Some("TOP SECRET//SI/TK//NOFORN")
);
}
#[test]
fn render_banner_secret_noforn() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Secret)),
dissem_controls: vec![DissemControl::Nf].into_boxed_slice(),
..Default::default()
});
assert_eq!(
ctx.render_expected_banner().as_deref(),
Some("SECRET//NOFORN")
);
}
#[test]
fn render_banner_unclassified_with_no_dissem() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::Unclassified)),
..Default::default()
});
assert_eq!(
ctx.render_expected_banner().as_deref(),
Some("UNCLASSIFIED")
);
}
use crate::attrs::{SciCompartment, SciControlBare, SciControlSystem, SciMarking};
fn sci_sys_pub(b: SciControlBare) -> SciControlSystem {
SciControlSystem::Published(b)
}
fn sci_sys_custom(s: &str) -> SciControlSystem {
SciControlSystem::Custom(s.to_owned().into_boxed_str())
}
fn comp(id: &str, subs: &[&str]) -> SciCompartment {
let sub_box: Box<[Box<str>]> = subs
.iter()
.map(|s| (*s).to_owned().into_boxed_str())
.collect::<Vec<_>>()
.into_boxed_slice();
SciCompartment::new(id.to_owned().into_boxed_str(), sub_box)
}
fn attrs_with_sci_markings(markings: Vec<SciMarking>) -> IsmAttributes {
IsmAttributes {
sci_markings: markings.into_boxed_slice(),
..Default::default()
}
}
use crate::attrs::{SarCompartment, SarIndicator, SarMarking, SarProgram};
fn sar_prog(id: &str, comps: Vec<SarCompartment>) -> SarProgram {
SarProgram::new(id.into(), comps.into_boxed_slice())
}
fn sar_comp(id: &str, subs: &[&str]) -> SarCompartment {
let subs: Box<[Box<str>]> = subs
.iter()
.map(|s| (*s).into())
.collect::<Vec<_>>()
.into_boxed_slice();
SarCompartment::new(id.into(), subs)
}
fn attrs_with_sar(sar: SarMarking) -> IsmAttributes {
IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::TopSecret)),
sar_markings: Some(sar),
..Default::default()
}
}
#[test]
fn sci_markings_single_portion_identity() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([comp("G", &["ABCD"])]),
None,
)]));
let out = ctx.expected_sci_markings();
assert_eq!(out.len(), 1);
assert_eq!(out[0].system, sci_sys_pub(SciControlBare::Si));
assert_eq!(out[0].compartments.len(), 1);
assert_eq!(&*out[0].compartments[0].identifier, "G");
assert_eq!(out[0].compartments[0].sub_compartments.len(), 1);
assert_eq!(&*out[0].compartments[0].sub_compartments[0], "ABCD");
assert_eq!(out[0].canonical_enum, None);
}
#[test]
fn sci_markings_merge_subs_within_same_system_same_compartment() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([comp("G", &["ABCD"])]),
None,
)]));
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([comp("G", &["DEFG"])]),
None,
)]));
let out = ctx.expected_sci_markings();
assert_eq!(out.len(), 1);
assert_eq!(out[0].compartments.len(), 1);
let subs: Vec<&str> = out[0].compartments[0]
.sub_compartments
.iter()
.map(|s| s.as_ref())
.collect();
assert_eq!(subs, vec!["ABCD", "DEFG"]);
}
#[test]
fn sci_markings_two_distinct_systems_sorted_alpha() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([]),
None,
)]));
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_pub(SciControlBare::Hcs),
Box::new([]),
None,
)]));
let out = ctx.expected_sci_markings();
assert_eq!(out.len(), 2);
assert_eq!(out[0].system, sci_sys_pub(SciControlBare::Hcs));
assert_eq!(out[1].system, sci_sys_pub(SciControlBare::Si));
}
#[test]
fn sci_markings_numeric_sorts_before_alpha() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([]),
None,
)]));
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_custom("123"),
Box::new([]),
None,
)]));
let out = ctx.expected_sci_markings();
assert_eq!(out.len(), 2);
assert_eq!(out[0].system, sci_sys_custom("123"));
assert_eq!(out[1].system, sci_sys_pub(SciControlBare::Si));
}
#[test]
fn sci_markings_sub_compartments_sorted() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([comp("G", &["DEFG", "ABCD"])]),
None,
)]));
let out = ctx.expected_sci_markings();
let subs: Vec<&str> = out[0].compartments[0]
.sub_compartments
.iter()
.map(|s| s.as_ref())
.collect();
assert_eq!(subs, vec!["ABCD", "DEFG"]);
}
#[test]
fn sci_markings_canonical_enum_never_populated_on_rollup() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sci_markings(vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([comp("G", &[])]),
Some(SciControl::SiG),
)]));
let out = ctx.expected_sci_markings();
assert_eq!(out[0].canonical_enum, None);
}
#[test]
fn render_banner_uses_structural_sci_block_bare() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::TopSecret)),
sci_markings: vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([]),
None,
)]
.into_boxed_slice(),
..Default::default()
});
assert_eq!(
ctx.render_expected_banner().as_deref(),
Some("TOP SECRET//SI")
);
}
#[test]
fn render_banner_uses_structural_sci_block_with_compartments() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::TopSecret)),
sci_markings: vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([comp("G", &["ABCD", "DEFG"])]),
None,
)]
.into_boxed_slice(),
..Default::default()
});
assert_eq!(
ctx.render_expected_banner().as_deref(),
Some("TOP SECRET//SI-G ABCD DEFG")
);
}
#[test]
fn render_banner_structural_sci_multi_compartment() {
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::TopSecret)),
sci_markings: vec![SciMarking::new(
sci_sys_pub(SciControlBare::Si),
Box::new([comp("G", &["ABCD", "DEFG"]), comp("MMM", &["AACD"])]),
None,
)]
.into_boxed_slice(),
..Default::default()
});
assert_eq!(
ctx.render_expected_banner().as_deref(),
Some("TOP SECRET//SI-G ABCD DEFG-MMM AACD")
);
}
#[test]
fn rollup_empty_portions_returns_none() {
assert!(PageContext::new().expected_sar_marking().is_none());
}
#[test]
fn rollup_no_sar_portions_returns_none() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_classification(Classification::Secret));
assert!(ctx.expected_sar_marking().is_none());
}
#[test]
fn rollup_single_program_no_compartments() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sar(SarMarking::new(
SarIndicator::Abbrev,
vec![sar_prog("BP", vec![])].into_boxed_slice(),
)));
let got = ctx.expected_sar_marking().expect("one program");
assert_eq!(got.indicator, SarIndicator::Abbrev);
assert_eq!(got.programs.len(), 1);
assert_eq!(&*got.programs[0].identifier, "BP");
assert!(got.programs[0].compartments.is_empty());
}
#[test]
fn rollup_two_portions_merge_programs() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sar(SarMarking::new(
SarIndicator::Abbrev,
vec![sar_prog("BP", vec![])].into_boxed_slice(),
)));
ctx.add_portion(attrs_with_sar(SarMarking::new(
SarIndicator::Full, vec![sar_prog("CD", vec![])].into_boxed_slice(),
)));
let got = ctx.expected_sar_marking().expect("merged");
assert_eq!(got.indicator, SarIndicator::Abbrev);
let ids: Vec<&str> = got.programs.iter().map(|p| &*p.identifier).collect();
assert_eq!(ids, vec!["BP", "CD"]);
}
#[test]
fn rollup_merges_compartments_under_same_program() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sar(SarMarking::new(
SarIndicator::Abbrev,
vec![sar_prog("BP", vec![sar_comp("J12", &[])])].into_boxed_slice(),
)));
ctx.add_portion(attrs_with_sar(SarMarking::new(
SarIndicator::Abbrev,
vec![sar_prog("BP", vec![sar_comp("K15", &[])])].into_boxed_slice(),
)));
let got = ctx.expected_sar_marking().expect("merged");
assert_eq!(got.programs.len(), 1);
let prog = &got.programs[0];
assert_eq!(&*prog.identifier, "BP");
let comps: Vec<&str> = prog.compartments.iter().map(|c| &*c.identifier).collect();
assert_eq!(comps, vec!["J12", "K15"]);
}
#[test]
fn rollup_numeric_before_alpha() {
let mut ctx = PageContext::new();
ctx.add_portion(attrs_with_sar(SarMarking::new(
SarIndicator::Abbrev,
vec![sar_prog("BP", vec![])].into_boxed_slice(),
)));
ctx.add_portion(attrs_with_sar(SarMarking::new(
SarIndicator::Abbrev,
vec![sar_prog("AC", vec![sar_comp("12A", &[])])].into_boxed_slice(),
)));
ctx.add_portion(attrs_with_sar(SarMarking::new(
SarIndicator::Abbrev,
vec![sar_prog("99", vec![])].into_boxed_slice(),
)));
let got = ctx.expected_sar_marking().expect("merged");
let ids: Vec<&str> = got.programs.iter().map(|p| &*p.identifier).collect();
assert_eq!(ids, vec!["99", "AC", "BP"]);
}
#[test]
fn render_expected_banner_with_sar_between_sci_and_aea() {
use crate::attrs::{DissemControl, SciControl};
let mut ctx = PageContext::new();
ctx.add_portion(IsmAttributes {
classification: Some(MarkingClassification::Us(Classification::TopSecret)),
sci_controls: vec![SciControl::Si].into_boxed_slice(),
sar_markings: Some(SarMarking::new(
SarIndicator::Abbrev,
vec![sar_prog("BP", vec![sar_comp("J12", &["J54"])])].into_boxed_slice(),
)),
dissem_controls: vec![DissemControl::Nf].into_boxed_slice(),
..Default::default()
});
assert_eq!(
ctx.render_expected_banner().as_deref(),
Some("TOP SECRET//SI//SAR-BP-J12 J54//NOFORN")
);
}
#[test]
fn render_sar_block_canonical_example() {
let sar = SarMarking::new(
SarIndicator::Abbrev,
vec![
sar_prog("BP", vec![sar_comp("J12", &["J54"]), sar_comp("K15", &[])]),
sar_prog("CD", vec![sar_comp("YYY", &["456", "689"])]),
sar_prog("XR", vec![sar_comp("XRA", &["RB"])]),
]
.into_boxed_slice(),
);
assert_eq!(
render_sar_block(&sar),
"SAR-BP-J12 J54-K15/CD-YYY 456 689/XR-XRA RB"
);
}
#[test]
fn sar_sort_key_numeric_before_alpha() {
assert!(sar_sort_key("99") < sar_sort_key("AC"));
assert!(sar_sort_key("12A") < sar_sort_key("AC"));
assert!(sar_sort_key("AC") < sar_sort_key("BP"));
assert!(sar_sort_key("2") < sar_sort_key("10"));
}
}