Skip to main content

assemblyline_markings/
classification.rs

1//! Classification processing and manipulating tools
2use std::collections::{HashSet, HashMap};
3use std::sync::Arc;
4
5use itertools::Itertools;
6
7use crate::config::{ClassificationConfig, DynamicGroupType, ClassificationLevel, ClassificationMarking, ClassificationSubGroup, ClassificationGroup};
8use crate::errors::Errors;
9
10/// A result that always uses the local error type
11type Result<T> = std::result::Result<T, Errors>;
12
13/// The smallest permitted classification level value
14const MIN_LVL: i32 = 1;
15/// The largest permitted classification level value
16const MAX_LVL: i32 = 10000;
17/// The classification level value used for null values
18const NULL_LVL: i32 = 0;
19/// The classification level value used for invalid values
20const INVALID_LVL: i32 = 10001;
21/// Short and long name used for null classification level
22const NULL_CLASSIFICATION: &str = "NULL";
23/// Short name used with invalid classification level
24const INVALID_SHORT_CLASSIFICATION: &str = "INV";
25/// Long name used with invalid classification level
26const INVALID_CLASSIFICATION: &str = "INVALID";
27
28/// A parser to process classification banners
29#[derive(Default, Debug, PartialEq)]
30pub struct ClassificationParser {
31    /// The config object used to build this parser
32    pub original_definition: ClassificationConfig,
33
34    /// Should this parser enforce access control
35    enforce: bool,
36
37    /// Are dynamic groups allowed
38    dynamic_groups: bool,
39
40    /// What kinds of dynamic groups can be generated
41    dynamic_groups_type: DynamicGroupType,
42
43    /// Classification data by level
44    levels: HashMap<i32, ClassificationLevel>,
45
46    /// Mapping from names and aliases to level
47    levels_scores_map: HashMap<String, i32>,
48
49    /// information about classification markings by all names and aliases
50    access_req: HashMap<String, Arc<ClassificationMarking>>,
51
52    /// Store the details about a group by name and short_name
53    groups: HashMap<String, Arc<ClassificationGroup>>,
54
55    /// Mapping from alias to all groups known by that alias
56    groups_aliases: HashMap<String, HashSet<String>>,
57
58    /// Groups that should automatically be selected and added to all classifications
59    groups_auto_select: Vec<String>,
60
61    /// Groups that should automatically be selected and added to all classifications (short names)
62    groups_auto_select_short: Vec<String>,
63
64    /// Store the details about subgroups by name and short_name
65    subgroups: HashMap<String, Arc<ClassificationSubGroup>>,
66
67    /// Mapping from alias to all subgroups known by that alias
68    subgroups_aliases: HashMap<String, HashSet<String>>,
69
70    /// Subgroups that should automatically by selected in all classifications
71    subgroups_auto_select: Vec<String>,
72
73    /// Subgroups that should automatically by selected in all classifications (short names)
74    subgroups_auto_select_short: Vec<String>,
75
76    /// Description for any given element by name
77    description: HashMap<String, String>,
78
79    /// A flag indicating an invalid classification definition was loaded (not currently used)
80    invalid_mode: bool,
81    // _classification_cache: HashSet<String>,
82    // _classification_cache_short: HashSet<String>,
83
84    /// Classification for minimally controlled data
85    unrestricted: String,
86
87    /// Classification for maximally controlled data
88    restricted: String,
89}
90
91/// A convenience trait that lets you pass true, false, or None for boolean arguments
92pub trait IBool: Into<Option<bool>> + Copy {}
93impl<T: Into<Option<bool>> + Copy> IBool for T {}
94
95impl ClassificationParser {
96
97    /// Load a classification parser from a configuration file
98    pub fn load(path: &std::path::Path) -> Result<Self> {
99        // Open the file
100        let file = std::fs::File::open(path)?;
101        Self::new(serde_yaml::from_reader(file)?)
102    }
103
104    /// Convert a config into a usable parser
105    pub fn new(definition: ClassificationConfig) -> Result<Self> {
106        let mut new = Self {
107            original_definition: definition.clone(),
108            enforce: definition.enforce,
109            dynamic_groups: definition.dynamic_groups,
110            dynamic_groups_type: definition.dynamic_groups_type,
111            ..Default::default()
112        };
113
114        // Add Invalid classification
115        new.insert_level(ClassificationLevel {
116            aliases: vec![],
117            css: Default::default(),
118            description: INVALID_CLASSIFICATION.to_owned(),
119            lvl: INVALID_LVL,
120            name: INVALID_CLASSIFICATION.parse()?,
121            short_name: INVALID_SHORT_CLASSIFICATION.parse()?,
122            is_hidden: false,
123        }, true)?;
124
125        // Add null classification
126        new.insert_level(ClassificationLevel {
127            aliases: vec![],
128            css: Default::default(),
129            description: NULL_CLASSIFICATION.to_owned(),
130            lvl: NULL_LVL,
131            name: NULL_CLASSIFICATION.parse()?,
132            short_name: NULL_CLASSIFICATION.parse()?,
133            is_hidden: false,
134        }, true)?;
135
136        // Convert the levels
137        for level in definition.levels {
138            new.insert_level(level, false)?;
139        }
140
141        for x in definition.required {
142            new.description.insert(x.short_name.to_string(), x.description.clone());
143            new.description.insert(x.name.to_string(), x.description.clone());
144            let x = Arc::new(x);
145
146            for name in x.unique_names() {
147                if let Some(old) = new.access_req.insert(name.to_string(), x.clone()) {
148                    return Err(Errors::InvalidDefinition(format!("Duplicate required name: {}", old.name)))
149                }
150            }
151        }
152
153        for x in definition.groups {
154            for a in &x.aliases {
155                new.groups_aliases.entry(a.to_string()).or_default().insert(x.short_name.to_string());
156            }
157            if let Some(a) = &x.solitary_display_name {
158                new.groups_aliases.entry(a.to_string()).or_default().insert(x.short_name.to_string());
159            }
160            if x.auto_select {
161                new.groups_auto_select.push(x.name.to_string());
162                new.groups_auto_select_short.push(x.short_name.to_string());
163            }
164
165            new.description.insert(x.short_name.to_string(), x.description.to_string());
166            new.description.insert(x.name.to_string(), x.description.to_string());
167
168            let x = Arc::new(x);
169            if x.name != x.short_name {
170                if let Some(old) = new.groups.insert(x.name.to_string(), x.clone()) {
171                    return Err(Errors::InvalidDefinition(format!("Duplicate group name: {}", old.name)))
172                }
173            }
174            if let Some(old) = new.groups.insert(x.short_name.to_string(), x) {
175                return Err(Errors::InvalidDefinition(format!("Duplicate group name: {}", old.short_name)))
176            }
177        }
178
179        for x in definition.subgroups {
180            for a in &x.aliases {
181                new.subgroups_aliases.entry(a.to_string()).or_default().insert(x.short_name.to_string());
182            }
183            // if let Some(a) = &x.solitary_display_name {
184            //     new.subgroups_aliases.entry(a.trim().to_uppercase()).or_default().insert(x.short_name.to_string());
185            // }
186            if x.auto_select {
187                new.subgroups_auto_select.push(x.name.to_string());
188                new.subgroups_auto_select_short.push(x.short_name.to_string());
189            }
190
191            new.description.insert(x.short_name.to_string(), x.description.to_string());
192            new.description.insert(x.name.to_string(), x.description.to_string());
193
194            let x = Arc::new(x);
195            if x.name != x.short_name {
196                if let Some(old) = new.subgroups.insert(x.name.to_string(), x.clone()) {
197                    return Err(Errors::InvalidDefinition(format!("Duplicate subgroup name: {}", old.name)))
198                }
199            }
200            if let Some(old) = new.subgroups.insert(x.short_name.to_string(), x) {
201                return Err(Errors::InvalidDefinition(format!("Duplicate subgroup name: {}", old.short_name)))
202            }
203        }
204
205        if !new.is_valid(&definition.unrestricted) {
206            return Err(Errors::InvalidDefinition("Classification definition's unrestricted classification is invalid.".to_owned()));
207        }
208
209        if !new.is_valid(&definition.restricted) {
210            return Err(Errors::InvalidDefinition("Classification definition's restricted classification is invalid.".to_owned()));
211        }
212
213        new.unrestricted = definition.unrestricted.clone();
214        new.restricted = definition.restricted.clone();
215
216        new.unrestricted = new.normalize_classification(&definition.unrestricted)?;
217        new.restricted = new.normalize_classification(&definition.restricted)?;
218
219        // except Exception as e:
220        //     self.UNRESTRICTED = self.NULL_CLASSIFICATION
221        //     self.RESTRICTED = self.INVALID_CLASSIFICATION
222
223        //     self.invalid_mode = True
224
225        //     log.warning(str(e))
226
227        Ok(new)
228    }
229
230    /// Add a classification level to a classification engine under construction
231    fn insert_level(&mut self, ll: ClassificationLevel, force: bool) -> Result<()> {
232        // Check for bounds and reserved words
233        if !force {
234            if [INVALID_CLASSIFICATION, INVALID_SHORT_CLASSIFICATION, NULL_CLASSIFICATION].contains(&ll.short_name.as_str()) {
235                return Err(Errors::InvalidDefinition("You cannot use reserved words NULL, INVALID or INV in your classification definition.".to_owned()));
236            }
237            if [INVALID_CLASSIFICATION, INVALID_SHORT_CLASSIFICATION, NULL_CLASSIFICATION].contains(&ll.name.as_str()) {
238                return Err(Errors::InvalidDefinition("You cannot use reserved words NULL, INVALID or INV in your classification definition.".to_owned()));
239            }
240
241            if ll.lvl > MAX_LVL {
242                return Err(Errors::InvalidDefinition(format!("Level over maximum classification level of {MAX_LVL}.")))
243            }
244            if ll.lvl < MIN_LVL {
245                return Err(Errors::InvalidDefinition(format!("Level under minimum classification level of {MIN_LVL}.")))
246            }
247        }
248
249        // insert each name
250        for name in ll.unique_names() {
251            if let Some(level) = self.levels_scores_map.insert(name.to_string(), ll.lvl) {
252                return Err(Errors::InvalidDefinition(format!("Name clash between classification levels: {name} on {level} and {}", ll.lvl)))
253            }
254        }
255
256        if let Some(old) = self.levels.insert(ll.lvl, ll) {
257            return Err(Errors::InvalidDefinition(format!("Duplicate classification level: {}", old.lvl)))
258        }
259        return Ok(())
260    }
261
262//     ############################
263//     # Private functions
264//     ############################
265    // fn _build_combinations(items: &HashSet<String>) -> HashSet<String> {
266    //     Self::_build_combinations_options(items, "/", &Default::default())
267    // }
268
269    // /// build the combination string
270    // fn _build_combinations_options(items: &HashSet<String>, separator: &str, solitary_display: &HashMap<String, String>) -> HashSet<String> {
271    //     let mut out = HashSet::<String>::from(["".to_owned()]);
272    //     for i in items {
273    //         let others = items.iter().filter(|x| *x != i).collect_vec();
274    //         for x in 0..=others.len() {
275    //             for c in others.iter().combinations(x) {
276    //                 let mut value = vec![i];
277    //                 value.extend(c);
278    //                 value.sort_unstable();
279    //                 let value = value.into_iter().join(separator);
280    //                 out.insert(solitary_display.get(&value).unwrap_or(&value).clone());
281    //             }
282    //         }
283    //     }
284    //     return out
285    // }
286
287//     @staticmethod
288//     def _list_items_and_aliases(data: List, long_format: bool = True) -> Set:
289//         items = set()
290//         for item in data:
291//             if long_format:
292//                 items.add(item['name'])
293//             else:
294//                 items.add(item['short_name'])
295
296//         return items
297
298    /// From the classification string get the level number
299    fn _get_c12n_level_index(&self, c12n: &str) -> Result<(i32, String)> {
300        // Parse classifications in uppercase mode only
301        let c12n = c12n.trim().to_uppercase();
302
303        let (lvl, remain) = c12n.split_once("//").unwrap_or((&c12n, ""));
304        if let Some(value) = self.levels_scores_map.get(lvl) {
305            return Ok((*value, remain.to_string()))
306        }
307        Err(Errors::InvalidClassification(format!("Classification level '{lvl}' was not found in your classification definition.")))
308    }
309
310    /// Get required section items
311    fn _get_c12n_required(&self, c12n: &str, long_format: impl IBool) -> (Vec<String>, Vec<String>) {
312        let long_format = long_format.into().unwrap_or(true);
313
314        // Parse classifications in uppercase mode only
315        let c12n = c12n.trim().to_uppercase();
316
317        let mut return_set: Vec<String> = vec![];
318        let mut others: Vec<String> = vec![];
319
320        for p in c12n.split('/') {
321            if p.is_empty() {
322                continue
323            }
324
325            if let Some(data) = self.access_req.get(p) {
326                if long_format {
327                    return_set.push(data.name.to_string());
328                } else {
329                    return_set.push(data.short_name.to_string());
330                }
331            } else {
332                others.push(p.to_owned())
333            }
334        }
335
336        return_set.sort_unstable();
337        return_set.dedup();
338        return (return_set, others)
339    }
340
341    /// Get the groups and subgroups for a classification
342    fn _get_c12n_groups(&self, c12n_parts: Vec<String>,
343        long_format: impl IBool,
344        get_dynamic_groups: impl IBool,
345        auto_select: impl IBool
346    ) -> Result<(Vec<String>, Vec<String>, Vec<String>)> {
347        let long_format = long_format.into().unwrap_or(true);
348        let get_dynamic_groups = get_dynamic_groups.into().unwrap_or(true);
349        let auto_select = auto_select.into().unwrap_or(false);
350
351        // Parse classifications in uppercase mode only
352        // let c12n = c12n.trim().to_uppercase();
353
354        let mut g1_set: Vec<&str> = vec![];
355        let mut g2_set: Vec<&str> = vec![];
356        let mut others = vec![];
357
358        let mut groups = vec![];
359        let mut subgroups = vec![];
360        for gp in c12n_parts {
361            if gp.starts_with("REL ") {
362                // Commas may only be used in REL TO controls
363                let gp = gp.replace("REL TO ", "");
364                let gp = gp.replace("REL ", "");
365                for t in gp.split(',') {
366                    groups.extend(t.trim().split('/').map(|x|x.trim().to_owned()));
367                }
368            } else {
369                // Everything else has to be taken as a potential subgroup (or solitary display name of a group)
370                subgroups.push(gp)
371            }
372        }
373
374        for g in &groups {
375            if let Some(data) = self.groups.get(g) {
376                g1_set.push(data.short_name.as_str());
377            } else if let Some(aliases) = self.groups_aliases.get(g) {
378                for a in aliases {
379                    g1_set.push(a)
380                }
381            } else {
382                others.push(g);
383            }
384        }
385
386        for g in &subgroups {
387         if let Some(g) = self.subgroups.get(g) {
388                g2_set.push(g.short_name.as_str());
389            } else if let Some(aliases) = self.subgroups_aliases.get(g) {
390                for a in aliases {
391                    g2_set.push(a)
392                }
393            } else if let Some(aliases) = self.groups_aliases.get(g) {
394                // this alias may be a solitary display names, check for that
395                if aliases.len() != 1 {
396                    return Err(Errors::InvalidClassification(format!("Name used ambiguously: {g}")))
397                }
398                for a in aliases {
399                    g1_set.push(a)
400                }
401            } else {
402                return Err(Errors::InvalidClassification(format!("Unrecognized classification part: {g}")))
403            }
404        }
405
406        let others = if self.dynamic_groups && get_dynamic_groups {
407            g1_set.extend(others.iter().map(|s|s.as_str()));
408            vec![]
409        } else {
410            others.iter().map(|s|s.to_string()).collect()
411        };
412
413        g1_set.sort_unstable();
414        g1_set.dedup();
415        g2_set.sort_unstable();
416        g2_set.dedup();
417
418        // Check if there are any required group assignments
419        for subgroup in &g2_set {
420            match self.subgroups.get(*subgroup) {
421                Some(data) => {
422                    if let Some(limited) = &data.require_group {
423                        g1_set.push(limited.as_str())
424                    }
425                },
426                None => {
427                    return Err(Errors::InvalidClassification(format!("Unknown subgroup: {subgroup}")))
428                }
429            }
430        }
431
432        // Check if there are any forbidden group assignments
433        for subgroup in &g2_set {
434            match self.subgroups.get(*subgroup) {
435                Some(data) => {
436                    if let Some(limited) = &data.limited_to_group {
437                        if g1_set.len() > 1 || (g1_set.len() == 1 && g1_set[0] != limited.as_str()) {
438                            return Err(Errors::InvalidClassification(format!("Subgroup {subgroup} is limited to group {limited} (found: {})", g1_set.join(", "))))
439                        }
440                    }
441                },
442                None => {
443                    return Err(Errors::InvalidClassification(format!("Unknown subgroup: {subgroup}")))
444                }
445            }
446        }
447
448        // Do auto select
449        if auto_select && !g1_set.is_empty() {
450            g1_set.extend(self.groups_auto_select_short.iter().map(String::as_str))
451        }
452        if auto_select && !g2_set.is_empty() {
453            g2_set.extend(self.subgroups_auto_select_short.iter().map(String::as_str))
454        }
455
456        let (mut g1_set, mut g2_set) = if long_format {
457            let g1: Vec<String> = g1_set.into_iter()
458                .map(|r| match self.groups.get(r) {Some(r) => r.name.to_string(), None => r.to_string()})
459                .collect();
460            let g2: Result<Vec<String>> = g2_set.into_iter()
461                .map(|r| self.subgroups.get(r).ok_or(Errors::InvalidClassification("".to_owned())))
462                .map_ok(|r|r.name.to_string())
463                .collect();
464
465            (g1, g2?)
466        } else {
467            (g1_set.into_iter().map(|r|r.to_owned()).collect_vec(), g2_set.into_iter().map(|r| r.to_owned()).collect_vec())
468        };
469
470        g1_set.sort_unstable();
471        g1_set.dedup();
472        g2_set.sort_unstable();
473        g2_set.dedup();
474
475        return Ok((g1_set, g2_set, others))
476    }
477
478    /// check if the user's access controls match the requirements
479    fn _can_see_required(user_req: &Vec<String>, req: &Vec<String>) -> bool {
480        let req: HashSet<&String> = HashSet::from_iter(req);
481        let user_req = HashSet::from_iter(user_req);
482        return req.is_subset(&user_req)
483    }
484
485    /// check if the user is in a group permitted dissemination
486    fn _can_see_groups(user_groups: &Vec<String>, required_groups: &[String]) -> bool {
487        if required_groups.is_empty() {
488            return true
489        }
490
491        for g in user_groups {
492            if required_groups.contains(g) {
493                return true
494            }
495        }
496
497        return false
498    }
499
500    /// Put the given components back togeather into a classification string
501    /// default long_format = true
502    /// default skip_auto_select = false
503    pub fn get_normalized_classification_text(&self, parts: ParsedClassification, long_format: bool, skip_auto_select: bool) -> Result<String> {
504        let ParsedClassification{level: lvl_idx, required: req, mut groups, mut subgroups} = parts;
505
506        let group_delim = if long_format {"REL TO "} else {"REL "};
507
508        // 1. Check for all required items if they need a specific classification lvl
509        let mut required_lvl_idx = 0;
510        for r in &req {
511            if let Some(params) = self.access_req.get(r) {
512                required_lvl_idx = required_lvl_idx.max(params.require_lvl.unwrap_or_default())
513            }
514        }
515        let mut out = self.get_classification_level_text(lvl_idx.max(required_lvl_idx), long_format)?;
516
517        // 2. Check for all required items if they should be shown inside the groups display part
518        let mut req_grp = vec![];
519        for r in &req {
520            if let Some(params) = self.access_req.get(r) {
521                if params.is_required_group {
522                    req_grp.push(r.clone());
523                }
524            }
525        }
526        // req = list(set(req).difference(set(req_grp)))
527        let req = req.into_iter().filter(|item|!req_grp.contains(item)).collect_vec();
528
529        if !req.is_empty() {
530            out += &("//".to_owned() + &req.join("/"));
531        }
532        if !req_grp.is_empty() {
533            req_grp.sort_unstable();
534            out += &("//".to_owned() + &req_grp.join("/"));
535        }
536
537        // 3. Add auto-selected subgroups
538        if long_format {
539            if !subgroups.is_empty() && !self.subgroups_auto_select.is_empty() && !skip_auto_select {
540                // subgroups = sorted(list(set(subgroups).union(set(self.subgroups_auto_select))))
541                subgroups.extend(self.subgroups_auto_select.iter().cloned());
542            }
543        } else {
544            if !subgroups.is_empty() && !self.subgroups_auto_select_short.is_empty() && !skip_auto_select {
545                subgroups.extend(self.subgroups_auto_select_short.iter().cloned())
546                // subgroups = sorted(list(set(subgroups).union(set(self.subgroups_auto_select_short))))
547            }
548        }
549        subgroups.sort_unstable();
550        subgroups.dedup();
551
552        // 4. For every subgroup, check if the subgroup requires or is limited to a specific group
553        let mut temp_groups = vec![];
554        for sg in &subgroups {
555            if let Some(subgroup) = self.subgroups.get(sg) {
556                if let Some(require_group) = &subgroup.require_group {
557                    temp_groups.push(require_group.clone())
558                }
559
560                if let Some(limited_to_group) = &subgroup.limited_to_group {
561                    if temp_groups.contains(limited_to_group) {
562                        temp_groups = vec![limited_to_group.clone()]
563                    } else {
564                        temp_groups.clear()
565                    }
566                }
567            }
568        }
569
570        for g in &temp_groups {
571            if let Some(data) = self.groups.get(g.as_str()) {
572                if long_format {
573                    groups.push(data.name.to_string())
574                } else {
575                    groups.push(data.short_name.to_string())
576                }
577            } else {
578                groups.push(g.to_string())
579            }
580        }
581
582        // 5. Add auto-selected groups
583        if long_format {
584            if !groups.is_empty() && !self.groups_auto_select.is_empty() && !skip_auto_select {
585                groups.extend(self.groups_auto_select.iter().cloned());
586            }
587        } else {
588            if !groups.is_empty() && !self.groups_auto_select_short.is_empty() && !skip_auto_select {
589                groups.extend(self.groups_auto_select_short.iter().cloned());
590            }
591        }
592        groups.sort_unstable();
593        groups.dedup();
594
595        if !groups.is_empty() {
596            out += if req_grp.is_empty() {"//"} else {"/"};
597            if groups.len() == 1 {
598                // 6. If only one group, check if it has a solitary display name.
599                let grp = &groups[0];
600                if let Some(group_data) = self.groups.get(grp) {
601                    if let Some(display_name) = &group_data.solitary_display_name {
602                        out += display_name.as_str();
603                    } else {
604                        out += group_delim;
605                        out += grp;
606                    }
607                }
608            } else {
609                if !long_format {
610                    // 7. In short format mode, check if there is an alias that can replace multiple groups
611                    let group_set: HashSet<String> = groups.iter().cloned().collect();
612                    for (alias, values) in self.groups_aliases.iter() {
613                        if values.len() > 1 && *values == group_set {
614                            groups = vec![alias.clone()]
615                        }
616                    }
617                }
618                out += group_delim;
619                out += &groups.join(", ");
620            }
621        }
622
623        if !subgroups.is_empty() {
624            if groups.is_empty() && req_grp.is_empty() {
625                out += "//"
626            } else {
627                out += "/"
628            }
629            subgroups.sort_unstable();
630            out += &subgroups.join("/");
631        }
632
633        return Ok(out)
634    }
635
636    /// convert a level number to a text form
637    pub fn get_classification_level_text(&self, lvl_idx: i32, long_format: bool) -> Result<String> {
638        if let Some(data) = self.levels.get(&lvl_idx) {
639            if long_format {
640                return Ok(data.name.to_string())
641            } else {
642                return Ok(data.short_name.to_string())
643            }
644        }
645
646        Err(Errors::InvalidClassification(format!("Classification level number '{lvl_idx}' was not found in your classification definition.")))
647    }
648
649    /// Break a classification into its parts
650    pub fn get_classification_parts(&self, c12n: &str, long_format: impl IBool, get_dynamic_groups: impl IBool, auto_select: impl IBool) -> Result<ParsedClassification> {
651        let (level, remain) = self._get_c12n_level_index(c12n)?;
652        let (required, unparsed_required) = self._get_c12n_required(&remain, long_format);
653        let (groups, subgroups, unparsed_groups) = self._get_c12n_groups(unparsed_required, long_format, get_dynamic_groups, auto_select)?;
654
655        if !unparsed_groups.is_empty() {
656            return Err(Errors::InvalidClassification(format!("Unknown parts: {}", unparsed_groups.join(", "))))
657        }
658
659        Ok(ParsedClassification { level, required, groups, subgroups })
660    }
661
662    // /// Listing all classifcation permutations can take a really long time the more the classification
663    // /// definition is complexe. Normalizing each entry makes it even worst. Use only this function if
664    // /// absolutely necessary.
665    // pub fn list_all_classification_combinations(self, long_format: bool = True, normalized: bool = False) -> Set {
666
667    //     combinations = set()
668
669    //     levels = self._list_items_and_aliases(self.original_definition['levels'], long_format=long_format)
670    //     reqs = self._list_items_and_aliases(self.original_definition['required'], long_format=long_format)
671    //     grps = self._list_items_and_aliases(self.original_definition['groups'], long_format=long_format)
672    //     sgrps = self._list_items_and_aliases(self.original_definition['subgroups'], long_format=long_format)
673
674    //     req_cbs = self._build_combinations(reqs)
675    //     if long_format:
676    //         grp_solitary_display = {
677    //             x['name']: x['solitary_display_name'] for x in self.original_definition['groups']
678    //             if 'solitary_display_name' in x
679    //         }
680    //     else:
681    //         grp_solitary_display = {
682    //             x['short_name']: x['solitary_display_name'] for x in self.original_definition['groups']
683    //             if 'solitary_display_name' in x
684    //         }
685    //     solitary_names = [x['solitary_display_name'] for x in self.original_definition['groups']
686    //                       if 'solitary_display_name' in x]
687
688    //     grp_cbs = self._build_combinations(grps, separator=", ", solitary_display=grp_solitary_display)
689    //     sgrp_cbs = self._build_combinations(sgrps)
690
691    //     for p in itertools.product(levels, req_cbs):
692    //         cl = "//".join(p)
693    //         if cl.endswith("//"):
694    //             combinations.add(cl[:-2])
695    //         else:
696    //             combinations.add(cl)
697
698    //     temp_combinations = copy(combinations)
699    //     for p in itertools.product(temp_combinations, grp_cbs):
700    //         cl = "//REL TO ".join(p)
701    //         if cl.endswith("//REL TO "):
702    //             combinations.add(cl[:-9])
703    //         else:
704    //             combinations.add(cl)
705
706    //     for sol_name in solitary_names:
707    //         to_edit = []
708    //         to_find = "REL TO {sol_name}".format(sol_name=sol_name)
709    //         for c in combinations:
710    //             if to_find in c:
711    //                 to_edit.append(c)
712
713    //         for e in to_edit:
714    //             combinations.add(e.replace(to_find, sol_name))
715    //             combinations.remove(e)
716
717    //     temp_combinations = copy(combinations)
718    //     for p in itertools.product(temp_combinations, sgrp_cbs):
719    //         if "//REL TO " in p[0]:
720    //             cl = "/".join(p)
721
722    //             if cl.endswith("/"):
723    //                 combinations.add(cl[:-1])
724    //             else:
725    //                 combinations.add(cl)
726    //         else:
727    //             cl = "//REL TO ".join(p)
728
729    //             if cl.endswith("//REL TO "):
730    //                 combinations.add(cl[:-9])
731    //             else:
732    //                 combinations.add(cl)
733
734    //     if normalized:
735    //         return {self.normalize_classification(x, long_format=long_format) for x in combinations}
736    //     return combinations
737    // }
738
739//     # noinspection PyUnusedLocal
740//     def default_user_classification(self, user: Optional[str] = None, long_format: bool = True) -> str:
741//         """
742//         You can overload this function to specify a way to get the default classification of a user.
743//         By default, this function returns the UNRESTRICTED value of your classification definition.
744
745//         Args:
746//             user: Which user to get the classification for
747//             long_format: Request a long classification format or not
748
749//         Returns:
750//             The classification in the specified format
751//         """
752//         return self.UNRESTRICTED
753
754//     def get_parsed_classification_definition(self) -> Dict:
755//         """
756//         Returns all dictionary of all the variables inside the classification object that will be used
757//         to enforce classification throughout the system.
758//         """
759//         from copy import deepcopy
760//         out = deepcopy(self.__dict__)
761//         out['levels_map'].pop("INV", None)
762//         out['levels_map'].pop(str(self.INVALID_LVL), None)
763//         out['levels_map_stl'].pop("INV", None)
764//         out['levels_map_lts'].pop("INVALID", None)
765//         out['levels_map'].pop("NULL", None)
766//         out['levels_map'].pop(str(self.NULL_LVL), None)
767//         out['levels_map_stl'].pop("NULL", None)
768//         out['levels_map_lts'].pop("NULL", None)
769//         out.pop('_classification_cache', None)
770//         out.pop('_classification_cache_short', None)
771//         return out
772
773    /// Returns a dictionary containing the different access parameters Lucene needs to build it's queries
774    ///
775    /// Args:
776    ///     c12n: The classification to get the parts from
777    ///     user_classification: Is a user classification, (old default = false)
778    pub fn get_access_control_parts(&self, c12n: &str, user_classification: bool) -> Result<serde_json::Value> {
779        let c12n = if !self.enforce || self.invalid_mode {
780            self.unrestricted.clone()
781        } else {
782            c12n.to_owned()
783        };
784
785        let result: Result<serde_json::Value> = (||{
786            // Normalize the classification before gathering the parts
787            let parts = self.get_classification_parts(&c12n, false, true, !user_classification)?;
788
789            return Ok(serde_json::json!({
790                "__access_lvl__": parts.level,
791                "__access_req__": parts.required,
792                "__access_grp1__": if parts.groups.is_empty() { vec!["__EMPTY__".to_owned()] } else { parts.groups },
793                "__access_grp2__": if parts.subgroups.is_empty() { vec!["__EMPTY__".to_owned()] } else { parts.subgroups }
794            }))
795        })();
796
797        if let Err(Errors::InvalidClassification(_)) = &result {
798            if !self.enforce || self.invalid_mode {
799                return Ok(serde_json::json!({
800                    "__access_lvl__": NULL_LVL,
801                    "__access_req__": [],
802                    "__access_grp1__": ["__EMPTY__"],
803                    "__access_grp2__": ["__EMPTY__"]
804                }))
805            }
806        }
807        return result
808    }
809
810//     def get_access_control_req(self) -> Union[KeysView, List]:
811//         """
812//         Returns a list of the different possible REQUIRED parts
813//         """
814//         if not self.enforce or self.invalid_mode:
815//             return []
816
817//         return self.access_req_map_stl.keys()
818
819//     def get_access_control_groups(self) -> Union[KeysView, List]:
820//         """
821//         Returns a list of the different possible GROUPS
822//         """
823//         if not self.enforce or self.invalid_mode:
824//             return []
825
826//         return self.groups_map_stl.keys()
827
828//     def get_access_control_subgroups(self) -> Union[KeysView, List]:
829//         """
830//         Returns a list of the different possible SUBGROUPS
831//         """
832//         if not self.enforce or self.invalid_mode:
833//             return []
834
835//         return self.subgroups_map_stl.keys()
836
837    /// This function intersects two user classification to return the maximum classification
838    /// that both user could see.
839    ///
840    /// Args:
841    ///     user_c12n_1: First user classification
842    ///     user_c12n_2: Second user classification
843    ///     long_format: True/False in long format
844    ///
845    /// Returns:
846    ///     Intersected classification in the desired format
847    pub fn intersect_user_classification(&self, user_c12n_1: &str, user_c12n_2: &str, long_format: impl IBool) -> Result<String> {
848        let long_format = long_format.into().unwrap_or(true);
849        if !self.enforce || self.invalid_mode {
850            return Ok(self.unrestricted.clone())
851        }
852
853        // Normalize classifications before comparing them
854        let parts1 = self.get_classification_parts(user_c12n_1, long_format, None, false)?;
855        let parts2 = self.get_classification_parts(user_c12n_2, long_format, None, false)?;
856
857        let parts = ParsedClassification {
858            level: parts1.level.min(parts2.level),
859            required: intersection(&parts1.required, &parts2.required),
860            groups: intersection(&parts1.groups, &parts2.groups),
861            subgroups: intersection(&parts1.subgroups, &parts2.subgroups),
862        };
863
864        return self.get_normalized_classification_text(parts, long_format, true)
865    }
866
867    /// Given a user classification, check if a user is allow to see a certain classification
868    ///
869    /// Args:
870    ///     user_c12n: Maximum classification for the user
871    ///     c12n: Classification the user which to see
872    /// , ignore_invalid: bool = False
873    /// Returns:
874    ///     True is the user can see the classification
875    pub fn is_accessible(&self, user_c12n: &str, c12n: &str) -> Result<bool> {
876        if self.invalid_mode {
877            return Ok(false)
878        }
879
880        if !self.enforce {
881            return Ok(true)
882        }
883
884        let parts = self.get_classification_parts(c12n, None, None, false)?;
885        let user = self.get_classification_parts(user_c12n, None, None, false)?;
886
887        if user.level >= parts.level {
888            if !Self::_can_see_required(&user.required, &parts.required) {
889                return Ok(false)
890            }
891            if !Self::_can_see_groups(&user.groups, &parts.groups) {
892                return Ok(false)
893            }
894            if !Self::_can_see_groups(&user.subgroups, &parts.subgroups) {
895                return Ok(false)
896            }
897            return Ok(true)
898        }
899        return Ok(false)
900    }
901
902    /// Check if the given classification banner can be interpreted
903    pub fn is_valid(&self, c12n: &str) -> bool {
904        self.is_valid_skip_auto(c12n, false)
905    }
906
907    /// Performs a series of checks against a classification to make sure it is valid in it's current form
908    ///
909    /// Args:
910    ///     c12n: The classification we want to validate
911    ///     skip_auto_select: skip the auto selection phase
912    ///
913    /// Returns:
914    ///     True if the classification is valid
915    pub fn is_valid_skip_auto(&self, c12n: &str, skip_auto_select: bool) -> bool {
916        if !self.enforce {
917            return true;
918        }
919
920        // Classification normalization test
921        let n_c12n = match self.normalize_classification_options(c12n, NormalizeOptions{skip_auto_select, ..Default::default()}) {
922            Ok(n_c12n) => n_c12n,
923            Err(_) => return false,
924        };
925
926        // parse the classification + normal form into parts
927        let ParsedClassification{level: lvl_idx, required: mut req, mut groups, mut subgroups} = match self.get_classification_parts(c12n, None, None, !skip_auto_select) {
928            Ok(row) => row,
929            Err(_) => return false,
930        };
931        let ParsedClassification{level: n_lvl_idx, required: mut n_req, groups: mut n_groups, subgroups: mut n_subgroups} = match self.get_classification_parts(&n_c12n, None, None, !skip_auto_select) {
932            Ok(row) => row,
933            Err(_) => return false,
934        };
935
936        if lvl_idx != n_lvl_idx { return false }
937
938        req.sort_unstable();
939        n_req.sort_unstable();
940        if req != n_req { return false }
941
942        groups.sort_unstable();
943        n_groups.sort_unstable();
944        if groups != n_groups { return false }
945
946        subgroups.sort_unstable();
947        n_subgroups.sort_unstable();
948        if subgroups != n_subgroups { return false; }
949
950        let c12n = c12n.replace("REL TO ", "");
951        let c12n = c12n.replace("REL ", "");
952        let parts = c12n.split("//").collect_vec();
953
954        // There is a maximum of 3 parts
955        if parts.len() > 3 {
956            return false
957        }
958
959
960        // First parts as to be a classification level part
961        let mut parts = parts.iter();
962        let first = *match parts.next() {
963            Some(part) => part,
964            None => return false,
965        };
966        if !self.levels_scores_map.contains_key(first) {
967            return false;
968        }
969
970        let mut check_groups = false;
971        for cur_part in parts {
972            // Can't be two groups sections.
973            if check_groups { return false }
974
975            let mut items = cur_part.split('/').collect_vec();
976            let mut comma_idx = None;
977            for (idx, i) in items.iter().enumerate() {
978                if i.contains(',') {
979                    if comma_idx.is_some() {
980                        return false;
981                    } else {
982                        comma_idx = Some(idx)
983                    }
984                }
985            }
986
987            if let Some(comma_idx) = comma_idx {
988                let value = items.remove(comma_idx);
989                items.extend(value.split(',').map(str::trim))
990            }
991
992            for i in items {
993                if !check_groups {
994                    // If current item not found in access req, we might already be dealing with groups
995                    if !self.access_req.contains_key(i) {
996                        check_groups = true
997                    }
998                }
999
1000                if check_groups && !self.dynamic_groups {
1001                    // If not groups. That stuff does not exists...
1002                    if !self.groups_aliases.contains_key(i) &&
1003                       !self.groups.contains_key(i) &&
1004                       !self.subgroups_aliases.contains_key(i) &&
1005                       !self.subgroups.contains_key(i)
1006                    {
1007                        return false
1008                    }
1009                }
1010            }
1011        }
1012        return true
1013    }
1014
1015    /// Mixes to classification and returns to most restrictive form for them
1016    ///
1017    /// Args:
1018    ///     c12n_1: First classification
1019    ///     c12n_2: Second classification
1020    ///     long_format: True/False in long format, defaulted to true
1021    ///
1022    /// Returns:
1023    ///     The most restrictive classification that we could create out of the two
1024    pub fn max_classification(&self, c12n_1: &str, c12n_2: &str, long_format: impl IBool) -> Result<String> {
1025        let long_format = long_format.into().unwrap_or(true);
1026
1027        if !self.enforce || self.invalid_mode {
1028            return Ok(self.unrestricted.clone())
1029        }
1030
1031        let parts1 = self.get_classification_parts(c12n_1, long_format, None, true)?;
1032        let parts2 = self.get_classification_parts(c12n_2, long_format, None, true)?;
1033
1034        let parts = parts1.max(&parts2)?;
1035
1036        return self.get_normalized_classification_text(parts, long_format, false)
1037    }
1038
1039    /// Mixes to classification and returns to least restrictive form for them
1040    ///
1041    /// Args:
1042    ///     c12n_1: First classification
1043    ///     c12n_2: Second classification
1044    ///     long_format: True/False in long format
1045    ///
1046    /// Returns:
1047    ///     The least restrictive classification that we could create out of the two
1048    pub fn min_classification(&self, c12n_1: &str, c12n_2: &str, long_format: impl IBool) -> Result<String> {
1049        let long_format = long_format.into().unwrap_or(true);
1050
1051        if !self.enforce || self.invalid_mode {
1052            return Ok(self.unrestricted.clone())
1053        }
1054
1055        let parts1 = self.get_classification_parts(c12n_1, long_format, None, true)?;
1056        let parts2 = self.get_classification_parts(c12n_2, long_format, None, true)?;
1057
1058        let parts = parts1.min(&parts2);
1059
1060        return self.get_normalized_classification_text(parts, long_format, false)
1061    }
1062
1063    // pub fn normalize(&self) -> NormalizeBuilder { NormalizeBuilder { ce: self, ..Default::default() }}
1064
1065    /// call normalize_classification_options with default arguments
1066    pub fn normalize_classification(&self, c12n: &str) -> Result<String> {
1067        self.normalize_classification_options(c12n, Default::default())
1068    }
1069
1070    /// Normalize a given classification by applying the rules defined in the classification definition.
1071    /// This function will remove any invalid parts and add missing parts to the classification.
1072    /// It will also ensure that the display of the classification is always done the same way
1073    ///
1074    /// Args:
1075    ///     c12n: Classification to normalize
1076    ///     long_format: True/False in long format
1077    ///     skip_auto_select: True/False skip group auto adding, use True when dealing with user's classifications
1078    ///
1079    /// Returns:
1080    ///     A normalized version of the original classification
1081    pub fn normalize_classification_options(&self, c12n: &str, options: NormalizeOptions) -> Result<String> {
1082        let NormalizeOptions{long_format, skip_auto_select, get_dynamic_groups} = options;
1083
1084        if !self.enforce || self.invalid_mode || c12n.is_empty() {
1085            return Ok(self.unrestricted.clone())
1086        }
1087
1088        // Has the classification has already been normalized before?
1089        // if long_format and c12n in self._classification_cache and get_dynamic_groups:
1090        //     return c12n
1091        // if not long_format and c12n in self._classification_cache_short and get_dynamic_groups:
1092        //     return c12n
1093
1094        let parts = self.get_classification_parts(c12n, long_format, get_dynamic_groups, !skip_auto_select)?;
1095        // println!("{:?}", parts);
1096        let new_c12n = self.get_normalized_classification_text(parts, long_format, skip_auto_select)?;
1097        // if long_format {
1098        //     self._classification_cache.add(new_c12n)
1099        // } else {
1100        //     self._classification_cache_short.add(new_c12n)
1101        // }
1102
1103        return Ok(new_c12n)
1104    }
1105
1106    /// Mixes two classification and return the classification marking that would give access to the most data
1107    ///
1108    /// Args:
1109    ///     c12n_1: First classification
1110    ///     c12n_2: Second classification
1111    ///     long_format: True/False in long format
1112    ///
1113    /// Returns:
1114    ///     The classification that would give access to the most data
1115    pub fn build_user_classification(&self, c12n_1: &str, c12n_2: &str, long_format: impl IBool) -> Result<String> {
1116        let long_format = long_format.into().unwrap_or(true);
1117
1118        if !self.enforce || self.invalid_mode {
1119            return Ok(self.unrestricted.clone())
1120        }
1121
1122        // Normalize classifications before comparing them
1123        let parts1 = self.get_classification_parts(c12n_1, long_format, None, false)?;
1124        let parts2 = self.get_classification_parts(c12n_2, long_format, None, false)?;
1125
1126        let level = parts1.level.max(parts2.level);
1127        let required = union(&parts1.required, &parts2.required);
1128        let groups = union(&parts1.groups, &parts2.groups);
1129        let subgroups = union(&parts1.subgroups, &parts2.subgroups);
1130
1131        return self.get_normalized_classification_text(ParsedClassification { level, required, groups, subgroups }, long_format, true)
1132    }
1133
1134    /// Get all the levels found in this config
1135    pub fn levels(&self) -> &HashMap<i32, ClassificationLevel> {
1136        &self.levels
1137    }
1138
1139    /// Get the classification string predefined as maximally restricted
1140    pub fn restricted(&self) -> &str {
1141        self.restricted.as_str()
1142    }
1143
1144    /// Get the classification string predefined as minimally restricted
1145    pub fn unrestricted(&self) -> &str {
1146        self.unrestricted.as_str()
1147    }
1148}
1149
1150/// values describing a classification string after parsing
1151#[derive(Debug, PartialEq, Default, Clone)]
1152pub struct ParsedClassification {
1153    /// Classification level
1154    pub level: i32,
1155    /// Required access system flags
1156    pub required: Vec<String>,
1157    /// Groups that may be disseminated to
1158    pub groups: Vec<String>,
1159    /// Subgroups
1160    pub subgroups: Vec<String>,
1161}
1162
1163/// Gather the intersection of two string vectors
1164fn intersection(a: &Vec<String>, b: &Vec<String>) -> Vec<String> {
1165    HashSet::<&String>::from_iter(a).intersection(&HashSet::from_iter(b)).map(|&r|r.clone()).collect()
1166}
1167
1168/// Gather the union of two string vectors
1169fn union(a: &[String], b: &[String]) -> Vec<String> {
1170    let mut out = a.to_owned();
1171    out.extend(b.iter().cloned());
1172    out.sort_unstable();
1173    out.dedup();
1174    out
1175}
1176
1177
1178impl ParsedClassification {
1179    /// Calculate the minimum access requirements across two classifications
1180    fn min(&self, other: &Self) -> Self {
1181        let required = intersection(&self.required, &other.required);
1182
1183        let groups = if self.groups.is_empty() || other.groups.is_empty() {
1184            vec![]
1185        } else {
1186            union(&self.groups, &other.groups)
1187        };
1188
1189        let subgroups = if self.subgroups.is_empty() || other.subgroups.is_empty() {
1190            vec![]
1191        } else {
1192            union(&self.subgroups, &other.subgroups)
1193        };
1194
1195        Self {
1196            level: self.level.min(other.level),
1197            required,
1198            groups,
1199            subgroups,
1200        }
1201    }
1202
1203    /// Helper function for max to process groups
1204    fn _max_groups(groups_1: &Vec<String>, groups_2: &Vec<String>) -> Result<Vec<String>> {
1205        let groups = if !groups_1.is_empty() && !groups_2.is_empty() {
1206            intersection(groups_1, groups_2)
1207            // set(groups_1) & set(groups_2)
1208        } else {
1209            union(groups_1, groups_2)
1210            // set(groups_1) | set(groups_2)
1211        };
1212
1213        if !groups_1.is_empty() && !groups_2.is_empty() && groups.is_empty() {
1214            // NOTE: Intersection generated nothing, we will raise an InvalidClassification exception
1215            return Err(Errors::InvalidClassification(format!("Could not find any intersection between the groups. {groups_1:?} & {groups_2:?}")))
1216        }
1217
1218        return Ok(groups)
1219    }
1220
1221    /// Parse the maximally restrictive combination of these values with another set
1222    pub fn max(&self, other: &Self) -> Result<Self> {
1223        let level = self.level.max(other.level);
1224        let required = union(&self.required, &other.required);
1225
1226        let groups = Self::_max_groups(&self.groups, &other.groups)?;
1227        let subgroups = Self::_max_groups(&self.subgroups, &other.subgroups)?;
1228
1229        Ok(Self {
1230            level,
1231            required,
1232            groups,
1233            subgroups,
1234        })
1235    }
1236}
1237
1238/// Parameter struct for the normalize command
1239pub struct NormalizeOptions {
1240    /// Should this normalization output the long format
1241    pub long_format: bool,
1242    /// Should auto select of groups be skipped
1243    pub skip_auto_select: bool,
1244    /// Should dynamic groups be applied
1245    pub get_dynamic_groups: bool
1246}
1247
1248impl Default for NormalizeOptions {
1249    fn default() -> Self {
1250        Self { long_format: true, skip_auto_select: false, get_dynamic_groups: true }
1251    }
1252}
1253
1254impl NormalizeOptions {
1255    /// shortcut to create an options set with the short format
1256    pub fn short() -> Self {
1257        Self{long_format: false, ..Default::default()}
1258    }
1259}
1260
1261// pub struct NormalizeBuilder<'a> {
1262//     ce: &'a ClassificationParser,
1263//     _long_format: bool,
1264//     _skip_auto_select: bool,
1265//     _get_dynamic_groups: bool
1266// }
1267
1268// impl<'a> NormalizeBuilder<'a> {
1269//     pub fn classification(&self, c12n: &str) -> Result<String> {
1270//         self.ce.normalize_classification_options(c12n, options)
1271//     }
1272// }
1273
1274/// Generate a featureful configuration composed of mostly nonsense useful for testing.
1275pub fn sample_config() -> ClassificationConfig {
1276    ClassificationConfig{
1277        enforce: true,
1278        dynamic_groups: false,
1279        dynamic_groups_type: crate::config::DynamicGroupType::All,
1280        levels: vec![
1281            ClassificationLevel::new(1, "L0", "Level 0", vec!["Open"]),
1282            ClassificationLevel::new(5, "L1", "Level 1", vec![]),
1283            ClassificationLevel::new(15, "L2", "Level 2", vec![]),
1284        ],
1285        groups: vec![
1286            ClassificationGroup::new("A", "Group A"),
1287            ClassificationGroup::new("B", "Group B"),
1288            ClassificationGroup::new_solitary("X", "Group X", "XX"),
1289        ],
1290        required: vec![
1291            ClassificationMarking::new("LE", "Legal Department", vec!["Legal"]),
1292            ClassificationMarking::new("AC", "Accounting", vec!["Acc"]),
1293            ClassificationMarking::new_required("orcon", "Originator Controlled"),
1294            ClassificationMarking::new_required("nocon", "No Contractor Access"),
1295        ],
1296        subgroups: vec![
1297            ClassificationSubGroup::new_aliased("R1", "Reserve One", vec!["R0"]),
1298            ClassificationSubGroup::new_with_required("R2", "Reserve Two", "X"),
1299            ClassificationSubGroup::new_with_limited("R3", "Reserve Three", "X"),
1300        ],
1301        restricted: "L2".to_owned(),
1302        unrestricted: "L0".to_owned(),
1303    }
1304}
1305
1306
1307#[cfg(test)]
1308mod test {
1309
1310    // #[test]
1311    // fn defaults() {
1312    //     let option = PartsOptions{long_format: false, ..Default::default()};
1313    //     assert!(!option.long_format);
1314    //     assert!(option.get_dynamic_groups);
1315    // }
1316
1317    use std::path::Path;
1318
1319    use crate::classification::{NormalizeOptions, ParsedClassification};
1320
1321    use super::{sample_config as setup_config, ClassificationParser, Result};
1322
1323    fn setup() -> ClassificationParser {
1324        ClassificationParser::new(setup_config()).unwrap()
1325    }
1326
1327    #[test]
1328    fn load_yaml() {
1329        let yaml = serde_yaml::to_string(&setup_config()).unwrap();
1330        let file = tempfile::NamedTempFile::new().unwrap();
1331        std::fs::write(file.path(), yaml).unwrap();
1332        assert_eq!(ClassificationParser::load(file.path()).unwrap(), setup());
1333    }
1334
1335    #[test]
1336    fn load_json() {
1337        let json = serde_json::to_string(&setup_config()).unwrap();
1338        println!("{json}");
1339        let file = tempfile::NamedTempFile::new().unwrap();
1340        std::fs::write(file.path(), json).unwrap();
1341        assert_eq!(ClassificationParser::load(file.path()).unwrap(), setup());
1342    }
1343
1344    #[test]
1345    fn bad_files() {
1346        assert!(ClassificationParser::load(Path::new("/not-a-file/not-a-file")).is_err());
1347        assert!(ClassificationParser::load(Path::new("/not-a-file/not-a-file")).unwrap_err().to_string().contains("invalid"));
1348        assert!(format!("{:?}", ClassificationParser::load(Path::new("/not-a-file/not-a-file"))).contains("InvalidDefinition"));
1349
1350        let file = tempfile::NamedTempFile::new().unwrap();
1351        std::fs::write(file.path(), "{}").unwrap();
1352        assert!(ClassificationParser::load(file.path()).is_err());
1353        assert!(ClassificationParser::load(file.path()).unwrap_err().to_string().contains("invalid"));
1354    }
1355
1356    #[test]
1357    fn invalid_classifications() {
1358        let mut config = setup_config();
1359
1360        // bad short names
1361        assert!(ClassificationParser::new(config.clone()).is_ok());
1362        config.levels[1].short_name = "INV".parse().unwrap();
1363        assert!(ClassificationParser::new(config.clone()).is_err());
1364        config.levels[1].short_name = "NULL".parse().unwrap();
1365        assert!(ClassificationParser::new(config.clone()).is_err());
1366
1367        // bad long names
1368        let mut config = setup_config();
1369        config.levels[1].name = "INV".parse().unwrap();
1370        assert!(ClassificationParser::new(config.clone()).is_err());
1371        config.levels[1].name = "NULL".parse().unwrap();
1372        assert!(ClassificationParser::new(config.clone()).is_err());
1373
1374        // overlapping level names
1375        let mut config = setup_config();
1376        config.levels[0].short_name = "L0".parse().unwrap();
1377        config.levels[1].short_name = "L0".parse().unwrap();
1378        assert!(ClassificationParser::new(config.clone()).is_err());
1379
1380        // overlapping level
1381        let mut config = setup_config();
1382        config.levels[0].lvl = 100;
1383        config.levels[1].lvl = 100;
1384        assert!(ClassificationParser::new(config.clone()).is_err());
1385
1386        // overlapping required names
1387        let mut config = setup_config();
1388        config.required[0].short_name = "AA".parse().unwrap();
1389        config.required[1].short_name = "AA".parse().unwrap();
1390        assert!(ClassificationParser::new(config.clone()).is_err());
1391
1392        // overlapping required names
1393        let mut config = setup_config();
1394        config.required[0].name = "AA".parse().unwrap();
1395        config.required[1].name = "AA".parse().unwrap();
1396        assert!(ClassificationParser::new(config.clone()).is_err());
1397
1398        // overlapping groups names
1399        let mut config = setup_config();
1400        config.groups[0].short_name = "AA".parse().unwrap();
1401        config.groups[1].short_name = "AA".parse().unwrap();
1402        assert!(ClassificationParser::new(config.clone()).is_err());
1403
1404        // overlapping groups names
1405        let mut config = setup_config();
1406        config.groups[0].name = "AA".parse().unwrap();
1407        config.groups[1].name = "AA".parse().unwrap();
1408        assert!(ClassificationParser::new(config.clone()).is_err());
1409
1410        // overlapping subgroups names
1411        let mut config = setup_config();
1412        config.subgroups[0].short_name = "AA".parse().unwrap();
1413        config.subgroups[1].short_name = "AA".parse().unwrap();
1414        assert!(ClassificationParser::new(config.clone()).is_err());
1415
1416        // overlapping subgroups names
1417        let mut config = setup_config();
1418        config.subgroups[0].name = "AA".parse().unwrap();
1419        config.subgroups[1].name = "AA".parse().unwrap();
1420        assert!(ClassificationParser::new(config.clone()).is_err());
1421
1422        // missing restricted
1423        let mut config = setup_config();
1424        config.restricted = "XF".to_string();
1425        assert!(ClassificationParser::new(config.clone()).is_err());
1426
1427        // missing unrestricted
1428        let mut config = setup_config();
1429        config.unrestricted = "XF".to_string();
1430        assert!(ClassificationParser::new(config.clone()).is_err());
1431
1432        // Use levels outside of range
1433        let mut config = setup_config();
1434        config.levels[0].lvl = 0;
1435        assert!(ClassificationParser::new(config.clone()).is_err());
1436        config.levels[0].lvl = 10002;
1437        assert!(ClassificationParser::new(config.clone()).is_err());
1438    }
1439
1440    #[test]
1441    fn bad_commas() {
1442        let ce = setup();
1443
1444        assert!(ce.is_valid("L1//REL A, B/ORCON/NOCON"));
1445        assert!(!ce.is_valid("L1//REL A, B/ORCON,NOCON"));
1446        assert!(!ce.is_valid("L1//ORCON,NOCON/REL A, B"));
1447
1448        assert_eq!(ce.normalize_classification_options("L1//REL A, B/ORCON/NOCON", NormalizeOptions::short()).unwrap(), "L1//NOCON/ORCON/REL A, B");
1449    }
1450
1451    #[test]
1452    fn typo_errors() {
1453        let ce = setup();
1454        assert!(ce.normalize_classification("L1//REL A, B/ORCON,NOCON").is_err());
1455        assert!(ce.normalize_classification("L1//ORCON,NOCON/REL A, B").is_err());
1456    }
1457
1458    #[test]
1459    fn minimums() {
1460        let ce = setup();
1461
1462        // level only
1463        assert_eq!(ce.min_classification("L0", "L0", false).unwrap(), "L0");
1464        assert_eq!(ce.min_classification("L0", "L0", true).unwrap(), "LEVEL 0");
1465        assert_eq!(ce.min_classification("L0", "L1", false).unwrap(), "L0");
1466        assert_eq!(ce.min_classification("L0", "L1", true).unwrap(), "LEVEL 0");
1467        assert_eq!(ce.min_classification("L0", "L2", false).unwrap(), "L0");
1468        assert_eq!(ce.min_classification("L0", "L2", true).unwrap(), "LEVEL 0");
1469        assert_eq!(ce.min_classification("L1", "L0", false).unwrap(), "L0");
1470        assert_eq!(ce.min_classification("L1", "L0", true).unwrap(), "LEVEL 0");
1471        assert_eq!(ce.min_classification("L1", "L1", false).unwrap(), "L1");
1472        assert_eq!(ce.min_classification("L1", "L1", true).unwrap(), "LEVEL 1");
1473        assert_eq!(ce.min_classification("L1", "L2", false).unwrap(), "L1");
1474        assert_eq!(ce.min_classification("L1", "L2", true).unwrap(), "LEVEL 1");
1475        assert_eq!(ce.min_classification("L2", "L0", false).unwrap(), "L0");
1476        assert_eq!(ce.min_classification("L2", "L0", true).unwrap(), "LEVEL 0");
1477        assert_eq!(ce.min_classification("L2", "L1", false).unwrap(), "L1");
1478        assert_eq!(ce.min_classification("L2", "L1", true).unwrap(), "LEVEL 1");
1479        assert_eq!(ce.min_classification("L2", "L2", false).unwrap(), "L2");
1480        assert_eq!(ce.min_classification("L2", "L2", true).unwrap(), "LEVEL 2");
1481        assert_eq!(ce.min_classification("OPEN", "L2", false).unwrap(), "L0");
1482
1483        // Group operations
1484        assert_eq!(ce.min_classification("L0//REL A, B", "L0", false).unwrap(), "L0");
1485        assert_eq!(ce.min_classification("L0//REL A", "L0", true).unwrap(), "LEVEL 0");
1486        assert_eq!(ce.min_classification("L0", "L2//REL A, B", false).unwrap(), "L0");
1487        assert_eq!(ce.min_classification("L0", "L1//REL A", true).unwrap(), "LEVEL 0");
1488        assert_eq!(ce.min_classification("L0//REL A, B", "L1//REL A, B", false).unwrap(), "L0//REL A, B");
1489        assert_eq!(ce.min_classification("L0//REL A, B", "L0//REL A", true).unwrap(), "LEVEL 0//REL TO GROUP A, GROUP B");
1490        assert_eq!(ce.min_classification("L0//REL B", "L0//REL B, A", true).unwrap(), "LEVEL 0//REL TO GROUP A, GROUP B");
1491
1492        // Subgroups
1493        assert_eq!(ce.min_classification("L0//R1/R2", "L0", false).unwrap(), "L0");
1494        assert_eq!(ce.min_classification("L0//R1", "L0", true).unwrap(), "LEVEL 0");
1495        assert_eq!(ce.min_classification("L0//R1/R2", "L1//R1/R2", false).unwrap(), "L0//XX/R1/R2");
1496        assert_eq!(ce.min_classification("L0//R1/R2", "L0//R1", true).unwrap(), "LEVEL 0//XX/RESERVE ONE/RESERVE TWO");
1497    }
1498
1499    #[test]
1500    fn maximums() {
1501        let ce = setup();
1502
1503        // level only
1504        assert_eq!(ce.max_classification("L0", "L0", false).unwrap(), "L0");
1505        assert_eq!(ce.max_classification("L0", "L0", true).unwrap(), "LEVEL 0");
1506        assert_eq!(ce.max_classification("L0", "L1", false).unwrap(), "L1");
1507        assert_eq!(ce.max_classification("L0", "L1", true).unwrap(), "LEVEL 1");
1508        assert_eq!(ce.max_classification("L0", "L2", false).unwrap(), "L2");
1509        assert_eq!(ce.max_classification("L0", "L2", true).unwrap(), "LEVEL 2");
1510        assert_eq!(ce.max_classification("L1", "L0", false).unwrap(), "L1");
1511        assert_eq!(ce.max_classification("L1", "L0", true).unwrap(), "LEVEL 1");
1512        assert_eq!(ce.max_classification("L1", "L1", false).unwrap(), "L1");
1513        assert_eq!(ce.max_classification("L1", "L1", true).unwrap(), "LEVEL 1");
1514        assert_eq!(ce.max_classification("L1", "L2", false).unwrap(), "L2");
1515        assert_eq!(ce.max_classification("L1", "L2", true).unwrap(), "LEVEL 2");
1516        assert_eq!(ce.max_classification("L2", "L0", false).unwrap(), "L2");
1517        assert_eq!(ce.max_classification("L2", "L0", true).unwrap(), "LEVEL 2");
1518        assert_eq!(ce.max_classification("L2", "L1", false).unwrap(), "L2");
1519        assert_eq!(ce.max_classification("L2", "L1", true).unwrap(), "LEVEL 2");
1520        assert_eq!(ce.max_classification("L2", "L2", false).unwrap(), "L2");
1521        assert_eq!(ce.max_classification("L2", "L2", true).unwrap(), "LEVEL 2");
1522
1523        // Group operations
1524        assert_eq!(ce.max_classification("L0//REL A, B", "L0", false).unwrap(), "L0//REL A, B");
1525        assert_eq!(ce.max_classification("L0//REL A", "L1", true).unwrap(), "LEVEL 1//REL TO GROUP A");
1526        assert_eq!(ce.max_classification("L0", "L2//REL A, B", false).unwrap(), "L2//REL A, B");
1527        assert_eq!(ce.max_classification("L0", "L1//REL A", true).unwrap(), "LEVEL 1//REL TO GROUP A");
1528        assert_eq!(ce.max_classification("L0//REL A, B", "L1//REL A, B", false).unwrap(), "L1//REL A, B");
1529        assert_eq!(ce.max_classification("L0//REL A, B", "L0//REL A", true).unwrap(), "LEVEL 0//REL TO GROUP A");
1530        assert_eq!(ce.max_classification("L0//REL B", "L0//REL B, A", true).unwrap(), "LEVEL 0//REL TO GROUP B");
1531        assert!(ce.max_classification("L0//REL B", "L0//REL A", true).is_err());
1532        assert!(ce.max_classification("L0//REL B", "L0//REL A", false).is_err());
1533
1534        // Subgroups
1535        assert_eq!(ce.max_classification("L0//R1/R2", "L0", false).unwrap(), "L0//XX/R1/R2");
1536        assert_eq!(ce.max_classification("L0//R1", "L0", true).unwrap(), "LEVEL 0//RESERVE ONE");
1537        assert_eq!(ce.max_classification("L0//R1/R2", "L1//R1/R2", false).unwrap(), "L1//XX/R1/R2");
1538        assert_eq!(ce.max_classification("L0//R1/R2", "L0//R1", true).unwrap(), "LEVEL 0//XX/RESERVE ONE");
1539    }
1540
1541    #[test]
1542    fn multi_group_alias() {
1543        let mut config = setup_config();
1544        config.groups[0].aliases.push("Alphabet Gang".parse().unwrap());
1545        config.groups[1].aliases.push("Alphabet Gang".parse().unwrap());
1546        let ce = ClassificationParser::new(config).unwrap();
1547
1548        assert_eq!(ce.normalize_classification_options("L0//REL A", NormalizeOptions::short()).unwrap(), "L0//REL A");
1549        assert_eq!(ce.normalize_classification_options("L0//REL A, B", NormalizeOptions::short()).unwrap(), "L0//REL ALPHABET GANG");
1550        assert!(ce.normalize_classification("L0//ALPHABET GANG").is_err())
1551    }
1552
1553    #[test]
1554    fn auto_select_group() {
1555        let mut config = setup_config();
1556        config.groups[0].auto_select = true;
1557        let ce = ClassificationParser::new(config).unwrap();
1558
1559        assert_eq!(ce.normalize_classification_options("L0", NormalizeOptions::short()).unwrap(), "L0");
1560        assert_eq!(ce.normalize_classification_options("L0//REL A", NormalizeOptions::short()).unwrap(), "L0//REL A");
1561        assert_eq!(ce.normalize_classification_options("L0//REL B", NormalizeOptions::short()).unwrap(), "L0//REL A, B");
1562        assert_eq!(ce.normalize_classification_options("L0//REL A, B", NormalizeOptions::short()).unwrap(), "L0//REL A, B");
1563        assert_eq!(ce.normalize_classification_options("L0", NormalizeOptions::default()).unwrap(), "LEVEL 0");
1564        assert_eq!(ce.normalize_classification_options("L0//REL A", NormalizeOptions::default()).unwrap(), "LEVEL 0//REL TO GROUP A");
1565        assert_eq!(ce.normalize_classification_options("L0//REL B", NormalizeOptions::default()).unwrap(), "LEVEL 0//REL TO GROUP A, GROUP B");
1566        assert_eq!(ce.normalize_classification_options("L0//REL A, B", NormalizeOptions::default()).unwrap(), "LEVEL 0//REL TO GROUP A, GROUP B");
1567        assert_eq!(ce.min_classification("L1", "L0//REL B", false).unwrap(), "L0");
1568        assert_eq!(ce.max_classification("L1", "L0//REL B", false).unwrap(), "L1//REL A, B");
1569    }
1570
1571    #[test]
1572    fn auto_select_subgroup() {
1573        let mut config = setup_config();
1574        config.subgroups[0].auto_select = true;
1575        let ce = ClassificationParser::new(config).unwrap();
1576
1577        assert_eq!(ce.normalize_classification_options("L0", NormalizeOptions::short()).unwrap(), "L0");
1578        assert_eq!(ce.normalize_classification_options("L0//R0", NormalizeOptions::short()).unwrap(), "L0//R1");
1579        assert_eq!(ce.normalize_classification_options("L0//R2", NormalizeOptions::short()).unwrap(), "L0//XX/R1/R2");
1580        assert_eq!(ce.normalize_classification_options("L0//R1/R2", NormalizeOptions::short()).unwrap(), "L0//XX/R1/R2");
1581        assert_eq!(ce.normalize_classification_options("L0", NormalizeOptions::default()).unwrap(), "LEVEL 0");
1582        assert_eq!(ce.normalize_classification_options("L0//R1", NormalizeOptions::default()).unwrap(), "LEVEL 0//RESERVE ONE");
1583        assert_eq!(ce.normalize_classification_options("L0//R2", NormalizeOptions::default()).unwrap(), "LEVEL 0//XX/RESERVE ONE/RESERVE TWO");
1584        assert_eq!(ce.normalize_classification_options("L0//R1/R2", NormalizeOptions::default()).unwrap(), "LEVEL 0//XX/RESERVE ONE/RESERVE TWO");
1585        assert_eq!(ce.min_classification("L1", "L0//R2", false).unwrap(), "L0");
1586        assert_eq!(ce.max_classification("L1", "L0//R2", false).unwrap(), "L1//XX/R1/R2");
1587    }
1588
1589    #[test]
1590    fn parts() {
1591        let ce = setup();
1592
1593        // level only
1594        assert_eq!(ce.get_classification_parts("L0", None, None, None).unwrap(), ParsedClassification{level: 1, ..Default::default()});
1595        assert_eq!(ce.get_classification_parts("LEVEL 0", None, None, None).unwrap(), ParsedClassification{level: 1, ..Default::default()});
1596        assert_eq!(ce.get_classification_parts("L1", None, None, None).unwrap(), ParsedClassification{level: 5, ..Default::default()});
1597        assert_eq!(ce.get_classification_parts("LEVEL 1", None, None, None).unwrap(), ParsedClassification{level: 5, ..Default::default()});
1598        assert_eq!(ce.get_classification_parts("L0", false, None, None).unwrap(), ParsedClassification{level: 1, ..Default::default()});
1599        assert_eq!(ce.get_classification_parts("LEVEL 0", false, None, None).unwrap(), ParsedClassification{level: 1, ..Default::default()});
1600        assert_eq!(ce.get_classification_parts("L1", false, None, None).unwrap(), ParsedClassification{level: 5, ..Default::default()});
1601        assert_eq!(ce.get_classification_parts("LEVEL 1", false, None, None).unwrap(), ParsedClassification{level: 5, ..Default::default()});
1602
1603        // Group operations
1604        assert_eq!(ce.get_classification_parts("L0//REL A", None, None, None).unwrap(), ParsedClassification{level: 1, groups: vec!["GROUP A".to_owned()], ..Default::default()});
1605        assert_eq!(ce.get_classification_parts("LEVEL 0//REL Group A", None, None, None).unwrap(), ParsedClassification{level: 1, groups: vec!["GROUP A".to_owned()], ..Default::default()});
1606        assert_eq!(ce.get_classification_parts("L0//REL A", false, None, None).unwrap(), ParsedClassification{level: 1, groups: vec!["A".to_owned()], ..Default::default()});
1607        assert_eq!(ce.get_classification_parts("LEVEL 0//REL Group A", false, None, None).unwrap(), ParsedClassification{level: 1, groups: vec!["A".to_owned()], ..Default::default()});
1608
1609        // interaction with required groups
1610        for auto in [true, false] {
1611            assert_eq!(ce.get_classification_parts("L0//R1/R2", false, None, auto).unwrap(), ParsedClassification{level: 1, groups: vec!["X".to_owned()], subgroups: vec!["R1".to_owned(), "R2".to_owned()], ..Default::default()});
1612            assert_eq!(ce.get_classification_parts("L0//R1", false, None, auto).unwrap(), ParsedClassification{level: 1, subgroups: vec!["R1".to_owned()], ..Default::default()});
1613        }
1614    }
1615
1616    #[test]
1617    fn normalize() {
1618        let ce = setup();
1619
1620        // level only
1621        assert_eq!(ce.normalize_classification_options("L0", NormalizeOptions::short()).unwrap(), "L0");
1622        assert_eq!(ce.normalize_classification("L1").unwrap(), "LEVEL 1");
1623
1624        // Group operations
1625        assert_eq!(ce.normalize_classification("L0//REL A, B").unwrap(), "LEVEL 0//REL TO GROUP A, GROUP B");
1626        assert_eq!(ce.normalize_classification_options("L0//REL A, B", NormalizeOptions::short()).unwrap(), "L0//REL A, B");
1627        assert_eq!(ce.normalize_classification("L0//REL A").unwrap(), "LEVEL 0//REL TO GROUP A");
1628        assert_eq!(ce.normalize_classification_options("L0//REL A", NormalizeOptions::short()).unwrap(), "L0//REL A");
1629        assert_eq!(ce.normalize_classification("L2//REL A, B").unwrap(), "LEVEL 2//REL TO GROUP A, GROUP B");
1630        assert_eq!(ce.normalize_classification_options("L2//REL A, B", NormalizeOptions::short()).unwrap(), "L2//REL A, B");
1631        assert_eq!(ce.normalize_classification("L1//REL A").unwrap(), "LEVEL 1//REL TO GROUP A");
1632        assert_eq!(ce.normalize_classification_options("L1//REL A", NormalizeOptions::short()).unwrap(), "L1//REL A");
1633        assert_eq!(ce.normalize_classification("L0//REL B").unwrap(), "LEVEL 0//REL TO GROUP B");
1634        assert_eq!(ce.normalize_classification_options("L0//REL B", NormalizeOptions::short()).unwrap(), "L0//REL B");
1635        assert_eq!(ce.normalize_classification("L0//REL B, A").unwrap(), "LEVEL 0//REL TO GROUP A, GROUP B");
1636        assert_eq!(ce.normalize_classification_options("L0//REL B, A", NormalizeOptions::short()).unwrap(), "L0//REL A, B");
1637
1638        //
1639        assert_eq!(ce.normalize_classification("L1//LE").unwrap(), "LEVEL 1//LEGAL DEPARTMENT");
1640
1641        // bad inputs
1642        assert!(ce.normalize_classification("GARBO").is_err());
1643        assert!(ce.normalize_classification("GARBO").unwrap_err().to_string().contains("invalid"));
1644        assert!(ce.normalize_classification("L1//GARBO").is_err());
1645        assert!(ce.normalize_classification("L1//LE//GARBO").is_err());
1646    }
1647
1648    #[test]
1649    fn access_control() -> Result<()> {
1650        let ce = setup();
1651
1652        // Access limits due to level
1653        assert!(ce.is_accessible("L0", "L0")?);
1654        assert!(!ce.is_accessible("L0", "L1")?);
1655        assert!(!ce.is_accessible("L0", "L2")?);
1656        assert!(ce.is_accessible("L1", "L0")?);
1657        assert!(ce.is_accessible("L1", "L1")?);
1658        assert!(!ce.is_accessible("L1", "L2")?);
1659        assert!(ce.is_accessible("L2", "L0")?);
1660        assert!(ce.is_accessible("L2", "L1")?);
1661        assert!(ce.is_accessible("L2", "L2")?);
1662
1663        // Access limits due to control system markings
1664        assert!(!ce.is_accessible("L2", "L0//LE")?);
1665        assert!(ce.is_accessible("L2//LE", "L0//LE")?);
1666
1667        assert!(!ce.is_accessible("L2", "L2//LE/AC")?);
1668        assert!(!ce.is_accessible("L2//LE", "L2//LE/AC")?);
1669        assert!(!ce.is_accessible("L2//AC", "L2//LE/AC")?);
1670        assert!(ce.is_accessible("L2//LE/AC", "L2//LE/AC")?);
1671
1672        // Access limits due to dissemination
1673        assert!(!ce.is_accessible("L2", "L2//ORCON/NOCON")?);
1674        assert!(!ce.is_accessible("L2//ORCON", "L2//ORCON/NOCON")?);
1675        assert!(!ce.is_accessible("L2//NOCON", "L2//ORCON/NOCON")?);
1676        assert!(ce.is_accessible("L2//ORCON/NOCON", "L2//ORCON/NOCON")?);
1677        assert!(!ce.is_accessible("L2//REL A", "L2//REL A,X//R2")?);
1678
1679        // Access limits due to releasability
1680        assert!(!ce.is_accessible("L2", "L2//REL A")?);
1681        assert!(!ce.is_accessible("L2//REL B", "L2//REL A")?);
1682        assert!(ce.is_accessible("L2//REL B", "L2//REL A, B")?);
1683        assert!(ce.is_accessible("L2//REL B", "L2//REL B")?);
1684        assert!(ce.is_accessible("L2//REL B", "L2")?);
1685
1686        Ok(())
1687    }
1688
1689    // Unexpected subcompartment
1690    #[test]
1691    fn unexpected_subcompartment() -> Result<()> {
1692        let ce = setup();
1693        assert_eq!(ce.normalize_classification("L1//LE")?, "LEVEL 1//LEGAL DEPARTMENT");
1694        assert!(ce.normalize_classification("L1//LE-").is_err());
1695        assert!(ce.normalize_classification("L1//LE-O").is_err());
1696        Ok(())
1697    }
1698
1699    // Group names should only be valid inside a REL clause, otherwise
1700    #[test]
1701    fn group_outside_rel() -> Result<()> {
1702        let ce = setup();
1703        assert!(ce.normalize_classification("L1//REL A/G").is_err());
1704        assert!(ce.normalize_classification("L1//REL A/B").is_err());
1705        Ok(())
1706    }
1707
1708    // make sure the bad classification strings are also rejected when dynamic groups are turned on
1709    #[test]
1710    fn dynamic_group_error() -> Result<()> {
1711        let mut config = setup_config();
1712        config.dynamic_groups = true;
1713        let ce = ClassificationParser::new(config)?;
1714
1715        assert!(ce.normalize_classification("GARBO").is_err());
1716        assert!(ce.normalize_classification("GARBO").unwrap_err().to_string().contains("invalid"));
1717        assert!(ce.normalize_classification("L1//GARBO").is_err());
1718        assert!(ce.normalize_classification("L1//LE//GARBO").is_err());
1719
1720        assert!(ce.normalize_classification("L1//REL A, B/ORCON,NOCON").is_err());
1721        assert!(ce.normalize_classification("L1//ORCON,NOCON/REL A, B").is_err());
1722
1723        assert!(ce.normalize_classification("L1//REL A/G").is_err());
1724        assert!(ce.normalize_classification("L1//REL A/B").is_err());
1725
1726        return Ok(())
1727    }
1728
1729    #[test]
1730    fn require_group() -> Result<()> {
1731        let ce = setup();
1732        assert_eq!(ce.normalize_classification("L1//R1")?, "LEVEL 1//RESERVE ONE");
1733        assert_eq!(ce.normalize_classification("L1//R2")?, "LEVEL 1//XX/RESERVE TWO");
1734        Ok(())
1735    }
1736
1737    #[test]
1738    fn limited_to_group() -> Result<()> {
1739        let ce = setup();
1740        assert_eq!(ce.normalize_classification("L1//R3")?, "LEVEL 1//RESERVE THREE");
1741        assert_eq!(ce.normalize_classification("L1//R3/REL X")?, "LEVEL 1//XX/RESERVE THREE");
1742        assert!(ce.normalize_classification("L1//R3/REL A").is_err());
1743        assert!(ce.normalize_classification("L1//R3/REL A, X").is_err());
1744        Ok(())
1745    }
1746
1747    #[test]
1748    fn build_user_classification() -> Result<()> {
1749        let ce = setup();
1750
1751        let class = ce.build_user_classification("L1", "L0//LE", false)?;
1752        assert_eq!(class, "L1//LE");
1753
1754        let class = ce.build_user_classification(&class, "L0//REL A", false)?;
1755        assert_eq!(class, "L1//LE//REL A");
1756
1757        let class = ce.build_user_classification(&class, "L0//XX", false)?;
1758        assert_eq!(class, "L1//LE//REL A, X");
1759
1760        let class = ce.build_user_classification(&class, "L0//AC", false)?;
1761        assert_eq!(class, "L1//AC/LE//REL A, X");
1762
1763        let class = ce.build_user_classification(&class, "L2//R1", false)?;
1764        assert_eq!(class, "L2//AC/LE//REL A, X/R1");
1765
1766        let class = ce.build_user_classification(&class, "L0//R2", false)?;
1767        assert_eq!(class, "L2//AC/LE//REL A, X/R1/R2");
1768
1769        Ok(())
1770    }
1771}