Skip to main content

marque_ism/
marking_forms.rs

1//! Single source of truth for banner ↔ portion marking form mappings.
2//!
3//! The CAPCO Register defines three columns per marking:
4//! - **Marking Title** (full descriptive name, e.g., "NOT RELEASABLE TO FOREIGN NATIONALS")
5//! - **Banner Line Abbreviation** (e.g., "NOFORN")
6//! - **Portion Mark** (e.g., "NF")
7//!
8//! For most markings, banner and portion forms are identical (e.g., HCS, FISA,
9//! RELIDO). This module only tracks entries where the forms *differ*, since
10//! those are the ones E001 (banner uses portion abbreviation) and E009 (portion
11//! uses banner expansion) need to detect and correct.
12//!
13//! Classification levels (TOP SECRET ↔ TS, etc.) are handled separately by
14//! [`crate::Classification::banner_str`] / [`crate::Classification::portion_str`]
15//! because they follow a different structural pattern (banners use full words
16//! with no abbreviation, not a shortened form).
17//!
18//! # Maintenance
19//!
20//! This table is hand-maintained from the CAPCO Register. The ODNI CVE XML
21//! schemas only carry the portion-form codes; banner abbreviations are a CAPCO
22//! marking convention not encoded in the XML. When ODNI publishes a new
23//! register, update this table and bump the schema version in
24//! `crates/marque-ism/Cargo.toml`.
25
26/// A marking where the banner-line abbreviation differs from the portion mark.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct MarkingForm {
29    /// Text used in banner marking lines (e.g., "NOFORN", "ORCON").
30    pub banner: &'static str,
31    /// Text used in portion markings (e.g., "NF", "OC").
32    pub portion: &'static str,
33}
34
35/// All markings where banner abbreviation ≠ portion mark.
36///
37/// Source: CAPCO Register (Implementation Manual for the IC, current edition).
38///
39/// Sections covered:
40/// - §8  Dissemination Control Markings
41/// - §9  Non-IC Dissemination Control Markings
42/// - §6  Atomic Energy Act Information Markings (subset with differing forms)
43///
44/// Markings where banner = portion (e.g., FOUO, FISA, RELIDO, HCS, TK) are
45/// intentionally omitted — they don't need form correction.
46pub static MARKING_FORMS: &[MarkingForm] = &[
47    // §8 Dissemination Control Markings
48    MarkingForm {
49        banner: "NOFORN",
50        portion: "NF",
51    },
52    MarkingForm {
53        banner: "ORCON-USGOV",
54        portion: "OC-USGOV",
55    },
56    MarkingForm {
57        banner: "ORCON",
58        portion: "OC",
59    },
60    MarkingForm {
61        banner: "IMCON",
62        portion: "IMC",
63    },
64    MarkingForm {
65        banner: "PROPIN",
66        portion: "PR",
67    },
68    MarkingForm {
69        banner: "RSEN",
70        portion: "RS",
71    },
72    MarkingForm {
73        banner: "DEA SENSITIVE",
74        portion: "DSEN",
75    },
76    // §9 Non-IC Dissemination Control Markings
77    MarkingForm {
78        banner: "LIMDIS",
79        portion: "DS",
80    },
81    MarkingForm {
82        banner: "EXDIS",
83        portion: "XD",
84    },
85    MarkingForm {
86        banner: "NODIS",
87        portion: "ND",
88    },
89    MarkingForm {
90        banner: "SBU NOFORN",
91        portion: "SBU-NF",
92    },
93    MarkingForm {
94        banner: "LES NOFORN",
95        portion: "LES-NF",
96    },
97    // §6 Atomic Energy Act Information Markings (differing forms only)
98    MarkingForm {
99        banner: "DOD UCNI",
100        portion: "DCNI",
101    },
102    MarkingForm {
103        banner: "DOE UCNI",
104        portion: "UCNI",
105    },
106    // Note: SIGMA [##] ↔ SG [##] is parametric and handled separately
107    // by the parser's pattern-matching path, not this static table.
108];
109
110/// Look up the portion-form abbreviation for a banner-form string.
111///
112/// Used by:
113/// - E009 (portion-abbreviation): detects banner forms in portions, suggests abbreviation
114/// - Parser (`parse_dissem_full_form`): accepts banner-form input and maps to CVE code
115///
116/// Returns `None` if the input is not a known banner form (i.e., it's already
117/// the portion form, or it's not a recognized marking).
118pub fn banner_to_portion(banner: &str) -> Option<&'static str> {
119    MARKING_FORMS
120        .iter()
121        .find(|f| f.banner == banner)
122        .map(|f| f.portion)
123}
124
125/// Look up the banner-form expansion for a portion-form abbreviation.
126///
127/// Used by:
128/// - E001 (banner-abbreviation): detects portion abbreviations in banners, suggests expansion
129///
130/// Returns `None` if the input is not a known portion form that has a distinct
131/// banner form (i.e., it's already the banner form, or banner = portion).
132pub fn portion_to_banner(portion: &str) -> Option<&'static str> {
133    MARKING_FORMS
134        .iter()
135        .find(|f| f.portion == portion)
136        .map(|f| f.banner)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn banner_to_portion_known_entries() {
145        assert_eq!(banner_to_portion("NOFORN"), Some("NF"));
146        assert_eq!(banner_to_portion("ORCON"), Some("OC"));
147        assert_eq!(banner_to_portion("IMCON"), Some("IMC"));
148        assert_eq!(banner_to_portion("DEA SENSITIVE"), Some("DSEN"));
149        assert_eq!(banner_to_portion("PROPIN"), Some("PR"));
150        assert_eq!(banner_to_portion("RSEN"), Some("RS"));
151        assert_eq!(banner_to_portion("LIMDIS"), Some("DS"));
152        assert_eq!(banner_to_portion("EXDIS"), Some("XD"));
153        assert_eq!(banner_to_portion("NODIS"), Some("ND"));
154        assert_eq!(banner_to_portion("SBU NOFORN"), Some("SBU-NF"));
155        assert_eq!(banner_to_portion("LES NOFORN"), Some("LES-NF"));
156        assert_eq!(banner_to_portion("DOD UCNI"), Some("DCNI"));
157        assert_eq!(banner_to_portion("DOE UCNI"), Some("UCNI"));
158    }
159
160    #[test]
161    fn portion_to_banner_known_entries() {
162        assert_eq!(portion_to_banner("NF"), Some("NOFORN"));
163        assert_eq!(portion_to_banner("OC"), Some("ORCON"));
164        assert_eq!(portion_to_banner("IMC"), Some("IMCON"));
165        assert_eq!(portion_to_banner("DSEN"), Some("DEA SENSITIVE"));
166        assert_eq!(portion_to_banner("PR"), Some("PROPIN"));
167        assert_eq!(portion_to_banner("RS"), Some("RSEN"));
168        assert_eq!(portion_to_banner("DS"), Some("LIMDIS"));
169        assert_eq!(portion_to_banner("XD"), Some("EXDIS"));
170        // spellchecker:ignore-next-line
171        assert_eq!(portion_to_banner("ND"), Some("NODIS"));
172        assert_eq!(portion_to_banner("SBU-NF"), Some("SBU NOFORN"));
173        assert_eq!(portion_to_banner("LES-NF"), Some("LES NOFORN"));
174        assert_eq!(portion_to_banner("DCNI"), Some("DOD UCNI"));
175        assert_eq!(portion_to_banner("UCNI"), Some("DOE UCNI"));
176    }
177
178    #[test]
179    fn banner_to_portion_returns_none_for_unknown() {
180        assert_eq!(banner_to_portion("BANANAPHONE"), None);
181    }
182
183    #[test]
184    fn portion_to_banner_returns_none_for_unknown() {
185        assert_eq!(portion_to_banner("BANANAPHONE"), None);
186    }
187
188    #[test]
189    fn banner_to_portion_returns_none_for_portion_form() {
190        // Passing a portion form to banner_to_portion should not match.
191        assert_eq!(banner_to_portion("NF"), None);
192        assert_eq!(banner_to_portion("OC"), None);
193    }
194
195    #[test]
196    fn portion_to_banner_returns_none_for_banner_form() {
197        // Passing a banner form to portion_to_banner should not match.
198        assert_eq!(portion_to_banner("NOFORN"), None);
199        assert_eq!(portion_to_banner("ORCON"), None);
200    }
201
202    #[test]
203    fn no_duplicate_banner_entries() {
204        for (i, a) in MARKING_FORMS.iter().enumerate() {
205            for (j, b) in MARKING_FORMS.iter().enumerate() {
206                if i != j {
207                    assert_ne!(a.banner, b.banner, "duplicate banner entry: {:?}", a.banner);
208                }
209            }
210        }
211    }
212
213    #[test]
214    fn no_duplicate_portion_entries() {
215        for (i, a) in MARKING_FORMS.iter().enumerate() {
216            for (j, b) in MARKING_FORMS.iter().enumerate() {
217                if i != j {
218                    assert_ne!(
219                        a.portion, b.portion,
220                        "duplicate portion entry: {:?}",
221                        a.portion
222                    );
223                }
224            }
225        }
226    }
227
228    #[test]
229    fn banner_and_portion_never_equal() {
230        for f in MARKING_FORMS {
231            assert_ne!(
232                f.banner, f.portion,
233                "marking form has identical banner and portion: {:?}",
234                f.banner
235            );
236        }
237    }
238}