1use std::collections::HashMap;
51
52use crate::error::PdfError;
53use crate::objects::{Dict, Object, ObjectId};
54use crate::reader::document::DocumentReader;
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum OcVisibilityPolicy {
60 AllOn,
62 AnyOn,
64 AnyOff,
66 AllOff,
68}
69
70impl OcVisibilityPolicy {
71 pub fn from_name(name: &str) -> Self {
74 match name {
75 "AllOn" => OcVisibilityPolicy::AllOn,
76 "AnyOff" => OcVisibilityPolicy::AnyOff,
77 "AllOff" => OcVisibilityPolicy::AllOff,
78 _ => OcVisibilityPolicy::AnyOn,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum OcBaseState {
86 #[default]
88 On,
89 Off,
91 Unchanged,
96}
97
98impl OcBaseState {
99 fn from_name(name: &str) -> Self {
100 match name {
101 "OFF" => OcBaseState::Off,
102 "Unchanged" => OcBaseState::Unchanged,
103 _ => OcBaseState::On,
105 }
106 }
107}
108
109#[derive(Debug, Clone)]
118pub struct OptionalContentGroup {
119 pub id: ObjectId,
122 pub name: String,
125 pub intents: Vec<String>,
130 pub usage: Option<OcUsage>,
133}
134
135#[derive(Debug, Clone, Default)]
139pub struct OcUsage {
140 pub language: Option<String>,
143 pub language_preferred: Option<bool>,
145 pub zoom_min: Option<f64>,
148 pub zoom_max: Option<f64>,
152 pub print_subtype: Option<String>,
155 pub print_state: Option<bool>,
157 pub view_state: Option<bool>,
159 pub export_state: Option<bool>,
161 pub page_element_subtype: Option<String>,
163}
164
165#[derive(Debug, Clone, Default)]
171pub struct OcConfig {
172 pub name: Option<String>,
174 pub creator: Option<String>,
176 pub base_state: OcBaseState,
178 pub on: Vec<ObjectId>,
181 pub off: Vec<ObjectId>,
184 pub intents: Vec<String>,
187 pub order: Vec<OcOrderItem>,
192 pub list_mode: OcListMode,
194 pub rb_groups: Vec<Vec<ObjectId>>,
197 pub locked: Vec<ObjectId>,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
203pub enum OcListMode {
204 #[default]
206 AllPages,
207 VisiblePages,
209}
210
211#[derive(Debug, Clone)]
213pub enum OcOrderItem {
214 Group(ObjectId),
217 Subtree {
225 label: Option<String>,
228 items: Vec<OcOrderItem>,
230 },
231}
232
233#[derive(Debug, Clone)]
237pub struct OptionalContent {
238 pub groups: Vec<OptionalContentGroup>,
240 pub default_config: OcConfig,
243 pub alternate_configs: Vec<OcConfig>,
245 pub states: HashMap<ObjectId, bool>,
249}
250
251impl OptionalContent {
252 pub fn is_visible(&self, group: ObjectId) -> bool {
257 self.states.get(&group).copied().unwrap_or(false)
258 }
259
260 pub fn states_for_config(&self, config: &OcConfig) -> HashMap<ObjectId, bool> {
264 resolve_states(&self.groups, config)
265 }
266
267 pub fn evaluate_membership(&self, mem: &OcMembership) -> bool {
277 evaluate_membership_with_states(mem, &self.states)
278 }
279}
280
281#[derive(Debug, Clone)]
287pub struct OcMembership {
288 pub groups: Vec<ObjectId>,
290 pub policy: OcVisibilityPolicy,
293 pub visibility_expression: Option<OcVisibilityExpression>,
296}
297
298#[derive(Debug, Clone)]
309pub enum OcVisibilityExpression {
310 And(Vec<OcVisibilityExpression>),
311 Or(Vec<OcVisibilityExpression>),
312 Not(Box<OcVisibilityExpression>),
313 Group(ObjectId),
315}
316
317pub fn optional_content(
321 reader: &mut DocumentReader<'_>,
322) -> Result<Option<OptionalContent>, PdfError> {
323 let root_id = reader.xref().root()?;
324 let catalog = reader.resolve(root_id)?;
325 let Object::Dict(catalog_dict) = catalog else {
326 return Err(PdfError::other(format!(
327 "PDF OCG reader: /Root must be a dict (got {catalog:?})"
328 )));
329 };
330 let ocp_obj = catalog_dict
331 .entries()
332 .iter()
333 .find(|(k, _)| k == "OCProperties")
334 .map(|(_, v)| v.clone());
335 let Some(ocp_obj) = ocp_obj else {
336 return Ok(None);
337 };
338 let ocp_dict = match reader.deref(ocp_obj)? {
339 Object::Dict(d) => d,
340 _ => return Ok(None),
342 };
343
344 let ocgs_array = ocp_dict
346 .entries()
347 .iter()
348 .find(|(k, _)| k == "OCGs")
349 .map(|(_, v)| v.clone());
350 let Some(ocgs_array) = ocgs_array else {
351 return Ok(None);
352 };
353 let ocgs_array = reader.deref(ocgs_array)?;
354 let Object::Array(group_refs) = ocgs_array else {
355 return Ok(None);
356 };
357
358 let mut groups: Vec<OptionalContentGroup> = Vec::with_capacity(group_refs.len());
359 for item in group_refs {
360 let Object::Reference(id) = item else {
361 continue;
362 };
363 let group_obj = match reader.resolve(id) {
364 Ok(o) => o,
365 Err(_) => continue,
366 };
367 let Object::Dict(group_dict) = group_obj else {
368 continue;
369 };
370 let kind = dict_name(&group_dict, "Type");
372 if let Some(k) = kind.as_deref() {
373 if k != "OCG" {
374 continue;
375 }
376 }
377 let name = dict_text(&group_dict, "Name").unwrap_or_default();
378 let intents = decode_intent_array(reader, &group_dict)?;
379 let usage = decode_usage(reader, &group_dict)?;
380 groups.push(OptionalContentGroup {
381 id,
382 name,
383 intents,
384 usage,
385 });
386 }
387
388 let default_config = match ocp_dict
392 .entries()
393 .iter()
394 .find(|(k, _)| k == "D")
395 .map(|(_, v)| v.clone())
396 {
397 Some(o) => decode_config(reader, o)?.unwrap_or_default(),
398 None => OcConfig::default(),
399 };
400
401 let mut alternate_configs: Vec<OcConfig> = Vec::new();
403 if let Some(arr) = ocp_dict
404 .entries()
405 .iter()
406 .find(|(k, _)| k == "Configs")
407 .map(|(_, v)| v.clone())
408 {
409 let arr = reader.deref(arr)?;
410 if let Object::Array(items) = arr {
411 for it in items {
412 if let Some(c) = decode_config(reader, it)? {
413 alternate_configs.push(c);
414 }
415 }
416 }
417 }
418
419 let states = resolve_states(&groups, &default_config);
420
421 Ok(Some(OptionalContent {
422 groups,
423 default_config,
424 alternate_configs,
425 states,
426 }))
427}
428
429pub fn parse_membership(
437 reader: &mut DocumentReader<'_>,
438 dict: &Dict,
439) -> Result<Option<OcMembership>, PdfError> {
440 let kind = dict_name(dict, "Type");
441 if let Some(k) = kind.as_deref() {
442 if k != "OCMD" {
443 return Ok(None);
444 }
445 }
446 let mut groups: Vec<ObjectId> = Vec::new();
447 if let Some(o) = dict
448 .entries()
449 .iter()
450 .find(|(k, _)| k == "OCGs")
451 .map(|(_, v)| v.clone())
452 {
453 let o = reader.deref(o)?;
454 collect_group_refs(reader, o, &mut groups)?;
455 }
456 let policy = dict_name(dict, "P")
457 .map(|s| OcVisibilityPolicy::from_name(&s))
458 .unwrap_or(OcVisibilityPolicy::AnyOn);
459
460 let mut visibility_expression: Option<OcVisibilityExpression> = None;
462 if let Some(ve) = dict
463 .entries()
464 .iter()
465 .find(|(k, _)| k == "VE")
466 .map(|(_, v)| v.clone())
467 {
468 let ve = reader.deref(ve)?;
469 if let Object::Array(items) = ve {
470 visibility_expression = parse_visibility_expression(reader, &items, 0)?;
471 }
472 }
473 Ok(Some(OcMembership {
474 groups,
475 policy,
476 visibility_expression,
477 }))
478}
479
480fn resolve_states(groups: &[OptionalContentGroup], config: &OcConfig) -> HashMap<ObjectId, bool> {
493 let mut states: HashMap<ObjectId, bool> = HashMap::with_capacity(groups.len());
494 let base = match config.base_state {
495 OcBaseState::On => true,
496 OcBaseState::Off => false,
497 OcBaseState::Unchanged => true,
502 };
503 for g in groups {
504 states.insert(g.id, base);
505 }
506 for id in &config.on {
507 if let Some(s) = states.get_mut(id) {
508 *s = true;
509 } else {
510 states.insert(*id, true);
511 }
512 }
513 for id in &config.off {
514 if let Some(s) = states.get_mut(id) {
515 *s = false;
516 } else {
517 states.insert(*id, false);
518 }
519 }
520 states
521}
522
523fn decode_config(
526 reader: &mut DocumentReader<'_>,
527 obj: Object,
528) -> Result<Option<OcConfig>, PdfError> {
529 let dict = match reader.deref(obj)? {
530 Object::Dict(d) => d,
531 _ => return Ok(None),
532 };
533 let mut cfg = OcConfig {
534 name: dict_text(&dict, "Name"),
535 creator: dict_text(&dict, "Creator"),
536 base_state: dict_name(&dict, "BaseState")
537 .map(|s| OcBaseState::from_name(&s))
538 .unwrap_or(OcBaseState::On),
539 ..OcConfig::default()
540 };
541
542 if let Some(o) = dict
543 .entries()
544 .iter()
545 .find(|(k, _)| k == "ON")
546 .map(|(_, v)| v.clone())
547 {
548 let o = reader.deref(o)?;
549 collect_group_refs(reader, o, &mut cfg.on)?;
550 }
551 if let Some(o) = dict
552 .entries()
553 .iter()
554 .find(|(k, _)| k == "OFF")
555 .map(|(_, v)| v.clone())
556 {
557 let o = reader.deref(o)?;
558 collect_group_refs(reader, o, &mut cfg.off)?;
559 }
560 cfg.intents = decode_intent_array(reader, &dict)?;
561 if cfg.intents.is_empty() {
562 cfg.intents.push("View".to_owned());
564 }
565
566 if let Some(o) = dict
567 .entries()
568 .iter()
569 .find(|(k, _)| k == "Order")
570 .map(|(_, v)| v.clone())
571 {
572 let o = reader.deref(o)?;
573 if let Object::Array(items) = o {
574 cfg.order = decode_order_items(reader, &items, 0)?;
575 }
576 }
577
578 cfg.list_mode = match dict_name(&dict, "ListMode").as_deref() {
579 Some("VisiblePages") => OcListMode::VisiblePages,
580 _ => OcListMode::AllPages,
581 };
582
583 if let Some(o) = dict
584 .entries()
585 .iter()
586 .find(|(k, _)| k == "RBGroups")
587 .map(|(_, v)| v.clone())
588 {
589 let o = reader.deref(o)?;
590 if let Object::Array(outer) = o {
591 for inner in outer {
592 let inner = reader.deref(inner)?;
593 if let Object::Array(ids) = inner {
594 let mut group = Vec::with_capacity(ids.len());
595 for it in ids {
596 if let Object::Reference(id) = it {
597 group.push(id);
598 }
599 }
600 if !group.is_empty() {
601 cfg.rb_groups.push(group);
602 }
603 }
604 }
605 }
606 }
607
608 if let Some(o) = dict
609 .entries()
610 .iter()
611 .find(|(k, _)| k == "Locked")
612 .map(|(_, v)| v.clone())
613 {
614 let o = reader.deref(o)?;
615 collect_group_refs(reader, o, &mut cfg.locked)?;
616 }
617
618 Ok(Some(cfg))
619}
620
621fn decode_intent_array(
624 reader: &mut DocumentReader<'_>,
625 dict: &Dict,
626) -> Result<Vec<String>, PdfError> {
627 let Some(o) = dict
628 .entries()
629 .iter()
630 .find(|(k, _)| k == "Intent")
631 .map(|(_, v)| v.clone())
632 else {
633 return Ok(Vec::new());
634 };
635 let o = reader.deref(o)?;
636 Ok(match o {
637 Object::Name(s) => vec![s],
638 Object::Array(items) => items
639 .into_iter()
640 .filter_map(|it| match it {
641 Object::Name(s) => Some(s),
642 _ => None,
643 })
644 .collect(),
645 _ => Vec::new(),
646 })
647}
648
649fn decode_usage(
651 reader: &mut DocumentReader<'_>,
652 group_dict: &Dict,
653) -> Result<Option<OcUsage>, PdfError> {
654 let Some(o) = group_dict
655 .entries()
656 .iter()
657 .find(|(k, _)| k == "Usage")
658 .map(|(_, v)| v.clone())
659 else {
660 return Ok(None);
661 };
662 let usage_dict = match reader.deref(o)? {
663 Object::Dict(d) => d,
664 _ => return Ok(None),
665 };
666 let mut out = OcUsage::default();
667 if let Some(o) = usage_dict
669 .entries()
670 .iter()
671 .find(|(k, _)| k == "Language")
672 .map(|(_, v)| v.clone())
673 {
674 if let Object::Dict(d) = reader.deref(o)? {
675 out.language = dict_text(&d, "Lang");
676 out.language_preferred = dict_name(&d, "Preferred").map(|n| n == "ON");
677 }
678 }
679 if let Some(o) = usage_dict
681 .entries()
682 .iter()
683 .find(|(k, _)| k == "Zoom")
684 .map(|(_, v)| v.clone())
685 {
686 if let Object::Dict(d) = reader.deref(o)? {
687 out.zoom_min = d
688 .entries()
689 .iter()
690 .find(|(k, _)| k == "min")
691 .and_then(|(_, v)| number_to_f64(v));
692 out.zoom_max = d
693 .entries()
694 .iter()
695 .find(|(k, _)| k == "max")
696 .and_then(|(_, v)| number_to_f64(v));
697 }
698 }
699 if let Some(o) = usage_dict
701 .entries()
702 .iter()
703 .find(|(k, _)| k == "Print")
704 .map(|(_, v)| v.clone())
705 {
706 if let Object::Dict(d) = reader.deref(o)? {
707 out.print_subtype = dict_name(&d, "Subtype");
708 out.print_state = dict_name(&d, "PrintState").map(|n| n == "ON");
709 }
710 }
711 if let Some(o) = usage_dict
713 .entries()
714 .iter()
715 .find(|(k, _)| k == "View")
716 .map(|(_, v)| v.clone())
717 {
718 if let Object::Dict(d) = reader.deref(o)? {
719 out.view_state = dict_name(&d, "ViewState").map(|n| n == "ON");
720 }
721 }
722 if let Some(o) = usage_dict
724 .entries()
725 .iter()
726 .find(|(k, _)| k == "Export")
727 .map(|(_, v)| v.clone())
728 {
729 if let Object::Dict(d) = reader.deref(o)? {
730 out.export_state = dict_name(&d, "ExportState").map(|n| n == "ON");
731 }
732 }
733 if let Some(o) = usage_dict
735 .entries()
736 .iter()
737 .find(|(k, _)| k == "PageElement")
738 .map(|(_, v)| v.clone())
739 {
740 if let Object::Dict(d) = reader.deref(o)? {
741 out.page_element_subtype = dict_name(&d, "Subtype");
742 }
743 }
744 Ok(Some(out))
745}
746
747#[allow(clippy::only_used_in_recursion)]
757fn decode_order_items(
758 reader: &mut DocumentReader<'_>,
759 items: &[Object],
760 depth: usize,
761) -> Result<Vec<OcOrderItem>, PdfError> {
762 if depth > 32 {
765 return Ok(Vec::new());
766 }
767 let mut out = Vec::with_capacity(items.len());
768 for it in items {
769 match it.clone() {
770 Object::Reference(id) => out.push(OcOrderItem::Group(id)),
771 Object::Array(nested) => {
772 let mut iter = nested.into_iter();
774 let mut label: Option<String> = None;
775 let mut sub_items: Vec<Object> = Vec::new();
776 let first = iter.next();
777 match first {
778 Some(Object::LiteralString(b)) | Some(Object::HexString(b)) => {
779 label = Some(decode_text_string(&b));
780 sub_items.extend(iter);
781 }
782 Some(other) => {
783 sub_items.push(other);
784 sub_items.extend(iter);
785 }
786 None => {}
787 }
788 let sub = decode_order_items(reader, &sub_items, depth + 1)?;
789 out.push(OcOrderItem::Subtree { label, items: sub });
790 }
791 _ => {} }
793 }
794 Ok(out)
795}
796
797fn parse_visibility_expression(
807 reader: &mut DocumentReader<'_>,
808 items: &[Object],
809 depth: usize,
810) -> Result<Option<OcVisibilityExpression>, PdfError> {
811 if depth > 32 || items.is_empty() {
812 return Ok(None);
813 }
814 let op = match &items[0] {
815 Object::Name(s) => s.as_str(),
816 _ => return Ok(None),
817 };
818 let mut subs: Vec<OcVisibilityExpression> = Vec::new();
819 for it in &items[1..] {
820 let resolved = reader.deref(it.clone())?;
821 match resolved {
822 Object::Reference(id) => subs.push(OcVisibilityExpression::Group(id)),
823 Object::Array(inner) => {
824 if let Some(e) = parse_visibility_expression(reader, &inner, depth + 1)? {
825 subs.push(e);
826 }
827 }
828 _ => {} }
833 }
834 match op {
835 "And" => Ok(Some(OcVisibilityExpression::And(subs))),
836 "Or" => Ok(Some(OcVisibilityExpression::Or(subs))),
837 "Not" => {
838 if let Some(first) = subs.into_iter().next() {
842 Ok(Some(OcVisibilityExpression::Not(Box::new(first))))
843 } else {
844 Ok(None)
845 }
846 }
847 _ => Ok(None),
848 }
849}
850
851fn evaluate_membership_with_states(mem: &OcMembership, states: &HashMap<ObjectId, bool>) -> bool {
854 if let Some(ve) = &mem.visibility_expression {
855 return evaluate_visibility_expression(ve, states);
856 }
857 if mem.groups.is_empty() {
860 return true;
861 }
862 match mem.policy {
863 OcVisibilityPolicy::AllOn => mem
864 .groups
865 .iter()
866 .all(|id| states.get(id).copied().unwrap_or(false)),
867 OcVisibilityPolicy::AnyOn => mem
868 .groups
869 .iter()
870 .any(|id| states.get(id).copied().unwrap_or(false)),
871 OcVisibilityPolicy::AllOff => mem
872 .groups
873 .iter()
874 .all(|id| !states.get(id).copied().unwrap_or(false)),
875 OcVisibilityPolicy::AnyOff => mem
876 .groups
877 .iter()
878 .any(|id| !states.get(id).copied().unwrap_or(false)),
879 }
880}
881
882fn evaluate_visibility_expression(
883 expr: &OcVisibilityExpression,
884 states: &HashMap<ObjectId, bool>,
885) -> bool {
886 match expr {
887 OcVisibilityExpression::And(subs) => subs
888 .iter()
889 .all(|e| evaluate_visibility_expression(e, states)),
890 OcVisibilityExpression::Or(subs) => subs
891 .iter()
892 .any(|e| evaluate_visibility_expression(e, states)),
893 OcVisibilityExpression::Not(inner) => !evaluate_visibility_expression(inner, states),
894 OcVisibilityExpression::Group(id) => states.get(id).copied().unwrap_or(false),
895 }
896}
897
898fn collect_group_refs(
907 _reader: &mut DocumentReader<'_>,
908 obj: Object,
909 out: &mut Vec<ObjectId>,
910) -> Result<(), PdfError> {
911 match obj {
912 Object::Reference(id) => out.push(id),
913 Object::Array(items) => {
914 for it in items {
915 if let Object::Reference(id) = it {
916 out.push(id);
917 }
918 }
919 }
920 _ => {}
921 }
922 Ok(())
923}
924
925fn dict_text(d: &Dict, key: &str) -> Option<String> {
928 d.entries()
929 .iter()
930 .find(|(k, _)| k == key)
931 .and_then(|(_, v)| match v {
932 Object::LiteralString(b) | Object::HexString(b) => Some(decode_text_string(b)),
933 Object::Name(s) => Some(s.clone()),
934 _ => None,
935 })
936}
937
938fn decode_text_string(b: &[u8]) -> String {
939 if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
940 let utf16: Vec<u16> = b[2..]
941 .chunks_exact(2)
942 .map(|c| u16::from_be_bytes([c[0], c[1]]))
943 .collect();
944 String::from_utf16_lossy(&utf16)
945 } else {
946 String::from_utf8_lossy(b).into_owned()
947 }
948}
949
950fn dict_name(d: &Dict, key: &str) -> Option<String> {
951 d.entries()
952 .iter()
953 .find(|(k, _)| k == key)
954 .and_then(|(_, v)| match v {
955 Object::Name(s) => Some(s.clone()),
956 _ => None,
957 })
958}
959
960fn number_to_f64(o: &Object) -> Option<f64> {
961 match o {
962 Object::Integer(n) => Some(*n as f64),
963 Object::Real(f) => Some(*f),
964 _ => None,
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 use super::*;
971 use crate::objects::ObjectId;
972 use std::collections::HashMap;
973
974 fn id(n: u32) -> ObjectId {
975 ObjectId::new(n)
976 }
977
978 fn make_states(pairs: &[(u32, bool)]) -> HashMap<ObjectId, bool> {
979 let mut m = HashMap::new();
980 for (n, s) in pairs {
981 m.insert(id(*n), *s);
982 }
983 m
984 }
985
986 #[test]
987 fn visibility_policy_from_name_defaults_anyon() {
988 assert_eq!(
989 OcVisibilityPolicy::from_name("garbage"),
990 OcVisibilityPolicy::AnyOn
991 );
992 assert_eq!(OcVisibilityPolicy::from_name(""), OcVisibilityPolicy::AnyOn);
993 }
994
995 #[test]
996 fn visibility_policy_recognises_all_four_names() {
997 assert_eq!(
998 OcVisibilityPolicy::from_name("AllOn"),
999 OcVisibilityPolicy::AllOn
1000 );
1001 assert_eq!(
1002 OcVisibilityPolicy::from_name("AnyOn"),
1003 OcVisibilityPolicy::AnyOn
1004 );
1005 assert_eq!(
1006 OcVisibilityPolicy::from_name("AnyOff"),
1007 OcVisibilityPolicy::AnyOff
1008 );
1009 assert_eq!(
1010 OcVisibilityPolicy::from_name("AllOff"),
1011 OcVisibilityPolicy::AllOff
1012 );
1013 }
1014
1015 #[test]
1016 fn base_state_defaults_to_on() {
1017 assert!(matches!(OcBaseState::from_name("garbage"), OcBaseState::On));
1018 assert!(matches!(OcBaseState::from_name("ON"), OcBaseState::On));
1019 assert!(matches!(OcBaseState::from_name("OFF"), OcBaseState::Off));
1020 assert!(matches!(
1021 OcBaseState::from_name("Unchanged"),
1022 OcBaseState::Unchanged
1023 ));
1024 }
1025
1026 #[test]
1027 fn resolve_states_basestate_on_sets_all_on() {
1028 let groups = vec![
1029 OptionalContentGroup {
1030 id: id(10),
1031 name: "L1".into(),
1032 intents: vec!["View".into()],
1033 usage: None,
1034 },
1035 OptionalContentGroup {
1036 id: id(11),
1037 name: "L2".into(),
1038 intents: vec!["View".into()],
1039 usage: None,
1040 },
1041 ];
1042 let cfg = OcConfig {
1043 base_state: OcBaseState::On,
1044 ..OcConfig::default()
1045 };
1046 let s = resolve_states(&groups, &cfg);
1047 assert_eq!(s.get(&id(10)), Some(&true));
1048 assert_eq!(s.get(&id(11)), Some(&true));
1049 }
1050
1051 #[test]
1052 fn resolve_states_basestate_off_sets_all_off() {
1053 let groups = vec![OptionalContentGroup {
1054 id: id(10),
1055 name: "L1".into(),
1056 intents: vec!["View".into()],
1057 usage: None,
1058 }];
1059 let cfg = OcConfig {
1060 base_state: OcBaseState::Off,
1061 ..OcConfig::default()
1062 };
1063 let s = resolve_states(&groups, &cfg);
1064 assert_eq!(s.get(&id(10)), Some(&false));
1065 }
1066
1067 #[test]
1068 fn resolve_states_on_overrides_off_basestate() {
1069 let groups = vec![
1070 OptionalContentGroup {
1071 id: id(10),
1072 name: "L1".into(),
1073 intents: vec![],
1074 usage: None,
1075 },
1076 OptionalContentGroup {
1077 id: id(11),
1078 name: "L2".into(),
1079 intents: vec![],
1080 usage: None,
1081 },
1082 OptionalContentGroup {
1083 id: id(12),
1084 name: "L3".into(),
1085 intents: vec![],
1086 usage: None,
1087 },
1088 ];
1089 let cfg = OcConfig {
1090 base_state: OcBaseState::Off,
1091 on: vec![id(11)],
1092 ..OcConfig::default()
1093 };
1094 let s = resolve_states(&groups, &cfg);
1095 assert_eq!(s.get(&id(10)), Some(&false));
1096 assert_eq!(s.get(&id(11)), Some(&true));
1097 assert_eq!(s.get(&id(12)), Some(&false));
1098 }
1099
1100 #[test]
1101 fn resolve_states_off_overrides_on_basestate() {
1102 let groups = vec![
1103 OptionalContentGroup {
1104 id: id(10),
1105 name: "L1".into(),
1106 intents: vec![],
1107 usage: None,
1108 },
1109 OptionalContentGroup {
1110 id: id(11),
1111 name: "L2".into(),
1112 intents: vec![],
1113 usage: None,
1114 },
1115 ];
1116 let cfg = OcConfig {
1117 base_state: OcBaseState::On,
1118 off: vec![id(10)],
1119 ..OcConfig::default()
1120 };
1121 let s = resolve_states(&groups, &cfg);
1122 assert_eq!(s.get(&id(10)), Some(&false));
1123 assert_eq!(s.get(&id(11)), Some(&true));
1124 }
1125
1126 #[test]
1127 fn evaluate_membership_all_on() {
1128 let states = make_states(&[(10, true), (11, true), (12, false)]);
1129 let mem = OcMembership {
1130 groups: vec![id(10), id(11)],
1131 policy: OcVisibilityPolicy::AllOn,
1132 visibility_expression: None,
1133 };
1134 assert!(evaluate_membership_with_states(&mem, &states));
1135 let mem_with_off = OcMembership {
1136 groups: vec![id(10), id(12)],
1137 policy: OcVisibilityPolicy::AllOn,
1138 visibility_expression: None,
1139 };
1140 assert!(!evaluate_membership_with_states(&mem_with_off, &states));
1141 }
1142
1143 #[test]
1144 fn evaluate_membership_any_on() {
1145 let states = make_states(&[(10, false), (11, false), (12, true)]);
1146 let mem = OcMembership {
1147 groups: vec![id(10), id(11)],
1148 policy: OcVisibilityPolicy::AnyOn,
1149 visibility_expression: None,
1150 };
1151 assert!(!evaluate_membership_with_states(&mem, &states));
1152 let mem_with_on = OcMembership {
1153 groups: vec![id(10), id(12)],
1154 policy: OcVisibilityPolicy::AnyOn,
1155 visibility_expression: None,
1156 };
1157 assert!(evaluate_membership_with_states(&mem_with_on, &states));
1158 }
1159
1160 #[test]
1161 fn evaluate_membership_all_off() {
1162 let states = make_states(&[(10, false), (11, false), (12, true)]);
1163 let mem = OcMembership {
1164 groups: vec![id(10), id(11)],
1165 policy: OcVisibilityPolicy::AllOff,
1166 visibility_expression: None,
1167 };
1168 assert!(evaluate_membership_with_states(&mem, &states));
1169 let mem_with_on = OcMembership {
1170 groups: vec![id(10), id(12)],
1171 policy: OcVisibilityPolicy::AllOff,
1172 visibility_expression: None,
1173 };
1174 assert!(!evaluate_membership_with_states(&mem_with_on, &states));
1175 }
1176
1177 #[test]
1178 fn evaluate_membership_any_off() {
1179 let states = make_states(&[(10, true), (11, true), (12, false)]);
1180 let mem = OcMembership {
1181 groups: vec![id(10), id(11)],
1182 policy: OcVisibilityPolicy::AnyOff,
1183 visibility_expression: None,
1184 };
1185 assert!(!evaluate_membership_with_states(&mem, &states));
1186 let mem_with_off = OcMembership {
1187 groups: vec![id(10), id(12)],
1188 policy: OcVisibilityPolicy::AnyOff,
1189 visibility_expression: None,
1190 };
1191 assert!(evaluate_membership_with_states(&mem_with_off, &states));
1192 }
1193
1194 #[test]
1195 fn evaluate_membership_empty_groups_visible() {
1196 let states = make_states(&[]);
1197 let mem = OcMembership {
1198 groups: vec![],
1199 policy: OcVisibilityPolicy::AllOn,
1200 visibility_expression: None,
1201 };
1202 assert!(evaluate_membership_with_states(&mem, &states));
1203 }
1204
1205 #[test]
1206 fn evaluate_visibility_expression_simple_and() {
1207 let states = make_states(&[(10, true), (11, true), (12, false)]);
1208 let ve = OcVisibilityExpression::And(vec![
1209 OcVisibilityExpression::Group(id(10)),
1210 OcVisibilityExpression::Group(id(11)),
1211 ]);
1212 assert!(evaluate_visibility_expression(&ve, &states));
1213
1214 let ve_fail = OcVisibilityExpression::And(vec![
1215 OcVisibilityExpression::Group(id(10)),
1216 OcVisibilityExpression::Group(id(12)),
1217 ]);
1218 assert!(!evaluate_visibility_expression(&ve_fail, &states));
1219 }
1220
1221 #[test]
1222 fn evaluate_visibility_expression_simple_or() {
1223 let states = make_states(&[(10, false), (11, true), (12, false)]);
1224 let ve = OcVisibilityExpression::Or(vec![
1225 OcVisibilityExpression::Group(id(10)),
1226 OcVisibilityExpression::Group(id(11)),
1227 ]);
1228 assert!(evaluate_visibility_expression(&ve, &states));
1229
1230 let ve_fail = OcVisibilityExpression::Or(vec![
1231 OcVisibilityExpression::Group(id(10)),
1232 OcVisibilityExpression::Group(id(12)),
1233 ]);
1234 assert!(!evaluate_visibility_expression(&ve_fail, &states));
1235 }
1236
1237 #[test]
1238 fn evaluate_visibility_expression_not() {
1239 let states = make_states(&[(10, true)]);
1240 let ve = OcVisibilityExpression::Not(Box::new(OcVisibilityExpression::Group(id(10))));
1241 assert!(!evaluate_visibility_expression(&ve, &states));
1242 let ve_inv = OcVisibilityExpression::Not(Box::new(OcVisibilityExpression::Group(id(11))));
1243 assert!(evaluate_visibility_expression(&ve_inv, &states));
1244 }
1245
1246 #[test]
1247 fn evaluate_visibility_expression_nested() {
1248 let states = make_states(&[
1250 (1, false),
1251 (2, true), (3, true),
1253 (4, true),
1254 (5, true), ]);
1256 let ve = OcVisibilityExpression::Or(vec![
1257 OcVisibilityExpression::Group(id(1)),
1258 OcVisibilityExpression::Not(Box::new(OcVisibilityExpression::Group(id(2)))),
1259 OcVisibilityExpression::And(vec![
1260 OcVisibilityExpression::Group(id(3)),
1261 OcVisibilityExpression::Group(id(4)),
1262 OcVisibilityExpression::Group(id(5)),
1263 ]),
1264 ]);
1265 assert!(evaluate_visibility_expression(&ve, &states));
1267 }
1268
1269 #[test]
1270 fn unknown_group_id_treated_as_off() {
1271 let states = make_states(&[]);
1272 let mem = OcMembership {
1273 groups: vec![id(99)],
1274 policy: OcVisibilityPolicy::AllOn,
1275 visibility_expression: None,
1276 };
1277 assert!(!evaluate_membership_with_states(&mem, &states));
1278 }
1279}