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