use std::collections::HashMap;
use crate::error::PdfError;
use crate::objects::{Dict, Object, ObjectId};
use crate::reader::document::DocumentReader;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcVisibilityPolicy {
AllOn,
AnyOn,
AnyOff,
AllOff,
}
impl OcVisibilityPolicy {
pub fn from_name(name: &str) -> Self {
match name {
"AllOn" => OcVisibilityPolicy::AllOn,
"AnyOff" => OcVisibilityPolicy::AnyOff,
"AllOff" => OcVisibilityPolicy::AllOff,
_ => OcVisibilityPolicy::AnyOn,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OcBaseState {
#[default]
On,
Off,
Unchanged,
}
impl OcBaseState {
fn from_name(name: &str) -> Self {
match name {
"OFF" => OcBaseState::Off,
"Unchanged" => OcBaseState::Unchanged,
_ => OcBaseState::On,
}
}
}
#[derive(Debug, Clone)]
pub struct OptionalContentGroup {
pub id: ObjectId,
pub name: String,
pub intents: Vec<String>,
pub usage: Option<OcUsage>,
}
#[derive(Debug, Clone, Default)]
pub struct OcUsage {
pub language: Option<String>,
pub language_preferred: Option<bool>,
pub zoom_min: Option<f64>,
pub zoom_max: Option<f64>,
pub print_subtype: Option<String>,
pub print_state: Option<bool>,
pub view_state: Option<bool>,
pub export_state: Option<bool>,
pub page_element_subtype: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct OcConfig {
pub name: Option<String>,
pub creator: Option<String>,
pub base_state: OcBaseState,
pub on: Vec<ObjectId>,
pub off: Vec<ObjectId>,
pub intents: Vec<String>,
pub order: Vec<OcOrderItem>,
pub list_mode: OcListMode,
pub rb_groups: Vec<Vec<ObjectId>>,
pub locked: Vec<ObjectId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OcListMode {
#[default]
AllPages,
VisiblePages,
}
#[derive(Debug, Clone)]
pub enum OcOrderItem {
Group(ObjectId),
Subtree {
label: Option<String>,
items: Vec<OcOrderItem>,
},
}
#[derive(Debug, Clone)]
pub struct OptionalContent {
pub groups: Vec<OptionalContentGroup>,
pub default_config: OcConfig,
pub alternate_configs: Vec<OcConfig>,
pub states: HashMap<ObjectId, bool>,
}
impl OptionalContent {
pub fn is_visible(&self, group: ObjectId) -> bool {
self.states.get(&group).copied().unwrap_or(false)
}
pub fn states_for_config(&self, config: &OcConfig) -> HashMap<ObjectId, bool> {
resolve_states(&self.groups, config)
}
pub fn evaluate_membership(&self, mem: &OcMembership) -> bool {
evaluate_membership_with_states(mem, &self.states)
}
}
#[derive(Debug, Clone)]
pub struct OcMembership {
pub groups: Vec<ObjectId>,
pub policy: OcVisibilityPolicy,
pub visibility_expression: Option<OcVisibilityExpression>,
}
#[derive(Debug, Clone)]
pub enum OcVisibilityExpression {
And(Vec<OcVisibilityExpression>),
Or(Vec<OcVisibilityExpression>),
Not(Box<OcVisibilityExpression>),
Group(ObjectId),
}
pub fn optional_content(
reader: &mut DocumentReader<'_>,
) -> Result<Option<OptionalContent>, PdfError> {
let root_id = reader.xref().root()?;
let catalog = reader.resolve(root_id)?;
let Object::Dict(catalog_dict) = catalog else {
return Err(PdfError::other(format!(
"PDF OCG reader: /Root must be a dict (got {catalog:?})"
)));
};
let ocp_obj = catalog_dict
.entries()
.iter()
.find(|(k, _)| k == "OCProperties")
.map(|(_, v)| v.clone());
let Some(ocp_obj) = ocp_obj else {
return Ok(None);
};
let ocp_dict = match reader.deref(ocp_obj)? {
Object::Dict(d) => d,
_ => return Ok(None),
};
let ocgs_array = ocp_dict
.entries()
.iter()
.find(|(k, _)| k == "OCGs")
.map(|(_, v)| v.clone());
let Some(ocgs_array) = ocgs_array else {
return Ok(None);
};
let ocgs_array = reader.deref(ocgs_array)?;
let Object::Array(group_refs) = ocgs_array else {
return Ok(None);
};
let mut groups: Vec<OptionalContentGroup> = Vec::with_capacity(group_refs.len());
for item in group_refs {
let Object::Reference(id) = item else {
continue;
};
let group_obj = match reader.resolve(id) {
Ok(o) => o,
Err(_) => continue,
};
let Object::Dict(group_dict) = group_obj else {
continue;
};
let kind = dict_name(&group_dict, "Type");
if let Some(k) = kind.as_deref() {
if k != "OCG" {
continue;
}
}
let name = dict_text(&group_dict, "Name").unwrap_or_default();
let intents = decode_intent_array(reader, &group_dict)?;
let usage = decode_usage(reader, &group_dict)?;
groups.push(OptionalContentGroup {
id,
name,
intents,
usage,
});
}
let default_config = match ocp_dict
.entries()
.iter()
.find(|(k, _)| k == "D")
.map(|(_, v)| v.clone())
{
Some(o) => decode_config(reader, o)?.unwrap_or_default(),
None => OcConfig::default(),
};
let mut alternate_configs: Vec<OcConfig> = Vec::new();
if let Some(arr) = ocp_dict
.entries()
.iter()
.find(|(k, _)| k == "Configs")
.map(|(_, v)| v.clone())
{
let arr = reader.deref(arr)?;
if let Object::Array(items) = arr {
for it in items {
if let Some(c) = decode_config(reader, it)? {
alternate_configs.push(c);
}
}
}
}
let states = resolve_states(&groups, &default_config);
Ok(Some(OptionalContent {
groups,
default_config,
alternate_configs,
states,
}))
}
pub fn parse_membership(
reader: &mut DocumentReader<'_>,
dict: &Dict,
) -> Result<Option<OcMembership>, PdfError> {
let kind = dict_name(dict, "Type");
if let Some(k) = kind.as_deref() {
if k != "OCMD" {
return Ok(None);
}
}
let mut groups: Vec<ObjectId> = Vec::new();
if let Some(o) = dict
.entries()
.iter()
.find(|(k, _)| k == "OCGs")
.map(|(_, v)| v.clone())
{
let o = reader.deref(o)?;
collect_group_refs(reader, o, &mut groups)?;
}
let policy = dict_name(dict, "P")
.map(|s| OcVisibilityPolicy::from_name(&s))
.unwrap_or(OcVisibilityPolicy::AnyOn);
let mut visibility_expression: Option<OcVisibilityExpression> = None;
if let Some(ve) = dict
.entries()
.iter()
.find(|(k, _)| k == "VE")
.map(|(_, v)| v.clone())
{
let ve = reader.deref(ve)?;
if let Object::Array(items) = ve {
visibility_expression = parse_visibility_expression(reader, &items, 0)?;
}
}
Ok(Some(OcMembership {
groups,
policy,
visibility_expression,
}))
}
fn resolve_states(groups: &[OptionalContentGroup], config: &OcConfig) -> HashMap<ObjectId, bool> {
let mut states: HashMap<ObjectId, bool> = HashMap::with_capacity(groups.len());
let base = match config.base_state {
OcBaseState::On => true,
OcBaseState::Off => false,
OcBaseState::Unchanged => true,
};
for g in groups {
states.insert(g.id, base);
}
for id in &config.on {
if let Some(s) = states.get_mut(id) {
*s = true;
} else {
states.insert(*id, true);
}
}
for id in &config.off {
if let Some(s) = states.get_mut(id) {
*s = false;
} else {
states.insert(*id, false);
}
}
states
}
fn decode_config(
reader: &mut DocumentReader<'_>,
obj: Object,
) -> Result<Option<OcConfig>, PdfError> {
let dict = match reader.deref(obj)? {
Object::Dict(d) => d,
_ => return Ok(None),
};
let mut cfg = OcConfig {
name: dict_text(&dict, "Name"),
creator: dict_text(&dict, "Creator"),
base_state: dict_name(&dict, "BaseState")
.map(|s| OcBaseState::from_name(&s))
.unwrap_or(OcBaseState::On),
..OcConfig::default()
};
if let Some(o) = dict
.entries()
.iter()
.find(|(k, _)| k == "ON")
.map(|(_, v)| v.clone())
{
let o = reader.deref(o)?;
collect_group_refs(reader, o, &mut cfg.on)?;
}
if let Some(o) = dict
.entries()
.iter()
.find(|(k, _)| k == "OFF")
.map(|(_, v)| v.clone())
{
let o = reader.deref(o)?;
collect_group_refs(reader, o, &mut cfg.off)?;
}
cfg.intents = decode_intent_array(reader, &dict)?;
if cfg.intents.is_empty() {
cfg.intents.push("View".to_owned());
}
if let Some(o) = dict
.entries()
.iter()
.find(|(k, _)| k == "Order")
.map(|(_, v)| v.clone())
{
let o = reader.deref(o)?;
if let Object::Array(items) = o {
cfg.order = decode_order_items(reader, &items, 0)?;
}
}
cfg.list_mode = match dict_name(&dict, "ListMode").as_deref() {
Some("VisiblePages") => OcListMode::VisiblePages,
_ => OcListMode::AllPages,
};
if let Some(o) = dict
.entries()
.iter()
.find(|(k, _)| k == "RBGroups")
.map(|(_, v)| v.clone())
{
let o = reader.deref(o)?;
if let Object::Array(outer) = o {
for inner in outer {
let inner = reader.deref(inner)?;
if let Object::Array(ids) = inner {
let mut group = Vec::with_capacity(ids.len());
for it in ids {
if let Object::Reference(id) = it {
group.push(id);
}
}
if !group.is_empty() {
cfg.rb_groups.push(group);
}
}
}
}
}
if let Some(o) = dict
.entries()
.iter()
.find(|(k, _)| k == "Locked")
.map(|(_, v)| v.clone())
{
let o = reader.deref(o)?;
collect_group_refs(reader, o, &mut cfg.locked)?;
}
Ok(Some(cfg))
}
fn decode_intent_array(
reader: &mut DocumentReader<'_>,
dict: &Dict,
) -> Result<Vec<String>, PdfError> {
let Some(o) = dict
.entries()
.iter()
.find(|(k, _)| k == "Intent")
.map(|(_, v)| v.clone())
else {
return Ok(Vec::new());
};
let o = reader.deref(o)?;
Ok(match o {
Object::Name(s) => vec![s],
Object::Array(items) => items
.into_iter()
.filter_map(|it| match it {
Object::Name(s) => Some(s),
_ => None,
})
.collect(),
_ => Vec::new(),
})
}
fn decode_usage(
reader: &mut DocumentReader<'_>,
group_dict: &Dict,
) -> Result<Option<OcUsage>, PdfError> {
let Some(o) = group_dict
.entries()
.iter()
.find(|(k, _)| k == "Usage")
.map(|(_, v)| v.clone())
else {
return Ok(None);
};
let usage_dict = match reader.deref(o)? {
Object::Dict(d) => d,
_ => return Ok(None),
};
let mut out = OcUsage::default();
if let Some(o) = usage_dict
.entries()
.iter()
.find(|(k, _)| k == "Language")
.map(|(_, v)| v.clone())
{
if let Object::Dict(d) = reader.deref(o)? {
out.language = dict_text(&d, "Lang");
out.language_preferred = dict_name(&d, "Preferred").map(|n| n == "ON");
}
}
if let Some(o) = usage_dict
.entries()
.iter()
.find(|(k, _)| k == "Zoom")
.map(|(_, v)| v.clone())
{
if let Object::Dict(d) = reader.deref(o)? {
out.zoom_min = d
.entries()
.iter()
.find(|(k, _)| k == "min")
.and_then(|(_, v)| number_to_f64(v));
out.zoom_max = d
.entries()
.iter()
.find(|(k, _)| k == "max")
.and_then(|(_, v)| number_to_f64(v));
}
}
if let Some(o) = usage_dict
.entries()
.iter()
.find(|(k, _)| k == "Print")
.map(|(_, v)| v.clone())
{
if let Object::Dict(d) = reader.deref(o)? {
out.print_subtype = dict_name(&d, "Subtype");
out.print_state = dict_name(&d, "PrintState").map(|n| n == "ON");
}
}
if let Some(o) = usage_dict
.entries()
.iter()
.find(|(k, _)| k == "View")
.map(|(_, v)| v.clone())
{
if let Object::Dict(d) = reader.deref(o)? {
out.view_state = dict_name(&d, "ViewState").map(|n| n == "ON");
}
}
if let Some(o) = usage_dict
.entries()
.iter()
.find(|(k, _)| k == "Export")
.map(|(_, v)| v.clone())
{
if let Object::Dict(d) = reader.deref(o)? {
out.export_state = dict_name(&d, "ExportState").map(|n| n == "ON");
}
}
if let Some(o) = usage_dict
.entries()
.iter()
.find(|(k, _)| k == "PageElement")
.map(|(_, v)| v.clone())
{
if let Object::Dict(d) = reader.deref(o)? {
out.page_element_subtype = dict_name(&d, "Subtype");
}
}
Ok(Some(out))
}
#[allow(clippy::only_used_in_recursion)]
fn decode_order_items(
reader: &mut DocumentReader<'_>,
items: &[Object],
depth: usize,
) -> Result<Vec<OcOrderItem>, PdfError> {
if depth > 32 {
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(items.len());
for it in items {
match it.clone() {
Object::Reference(id) => out.push(OcOrderItem::Group(id)),
Object::Array(nested) => {
let mut iter = nested.into_iter();
let mut label: Option<String> = None;
let mut sub_items: Vec<Object> = Vec::new();
let first = iter.next();
match first {
Some(Object::LiteralString(b)) | Some(Object::HexString(b)) => {
label = Some(decode_text_string(&b));
sub_items.extend(iter);
}
Some(other) => {
sub_items.push(other);
sub_items.extend(iter);
}
None => {}
}
let sub = decode_order_items(reader, &sub_items, depth + 1)?;
out.push(OcOrderItem::Subtree { label, items: sub });
}
_ => {} }
}
Ok(out)
}
fn parse_visibility_expression(
reader: &mut DocumentReader<'_>,
items: &[Object],
depth: usize,
) -> Result<Option<OcVisibilityExpression>, PdfError> {
if depth > 32 || items.is_empty() {
return Ok(None);
}
let op = match &items[0] {
Object::Name(s) => s.as_str(),
_ => return Ok(None),
};
let mut subs: Vec<OcVisibilityExpression> = Vec::new();
for it in &items[1..] {
let resolved = reader.deref(it.clone())?;
match resolved {
Object::Reference(id) => subs.push(OcVisibilityExpression::Group(id)),
Object::Array(inner) => {
if let Some(e) = parse_visibility_expression(reader, &inner, depth + 1)? {
subs.push(e);
}
}
_ => {} }
}
match op {
"And" => Ok(Some(OcVisibilityExpression::And(subs))),
"Or" => Ok(Some(OcVisibilityExpression::Or(subs))),
"Not" => {
if let Some(first) = subs.into_iter().next() {
Ok(Some(OcVisibilityExpression::Not(Box::new(first))))
} else {
Ok(None)
}
}
_ => Ok(None),
}
}
fn evaluate_membership_with_states(mem: &OcMembership, states: &HashMap<ObjectId, bool>) -> bool {
if let Some(ve) = &mem.visibility_expression {
return evaluate_visibility_expression(ve, states);
}
if mem.groups.is_empty() {
return true;
}
match mem.policy {
OcVisibilityPolicy::AllOn => mem
.groups
.iter()
.all(|id| states.get(id).copied().unwrap_or(false)),
OcVisibilityPolicy::AnyOn => mem
.groups
.iter()
.any(|id| states.get(id).copied().unwrap_or(false)),
OcVisibilityPolicy::AllOff => mem
.groups
.iter()
.all(|id| !states.get(id).copied().unwrap_or(false)),
OcVisibilityPolicy::AnyOff => mem
.groups
.iter()
.any(|id| !states.get(id).copied().unwrap_or(false)),
}
}
fn evaluate_visibility_expression(
expr: &OcVisibilityExpression,
states: &HashMap<ObjectId, bool>,
) -> bool {
match expr {
OcVisibilityExpression::And(subs) => subs
.iter()
.all(|e| evaluate_visibility_expression(e, states)),
OcVisibilityExpression::Or(subs) => subs
.iter()
.any(|e| evaluate_visibility_expression(e, states)),
OcVisibilityExpression::Not(inner) => !evaluate_visibility_expression(inner, states),
OcVisibilityExpression::Group(id) => states.get(id).copied().unwrap_or(false),
}
}
fn collect_group_refs(
_reader: &mut DocumentReader<'_>,
obj: Object,
out: &mut Vec<ObjectId>,
) -> Result<(), PdfError> {
match obj {
Object::Reference(id) => out.push(id),
Object::Array(items) => {
for it in items {
if let Object::Reference(id) = it {
out.push(id);
}
}
}
_ => {}
}
Ok(())
}
fn dict_text(d: &Dict, key: &str) -> Option<String> {
d.entries()
.iter()
.find(|(k, _)| k == key)
.and_then(|(_, v)| match v {
Object::LiteralString(b) | Object::HexString(b) => Some(decode_text_string(b)),
Object::Name(s) => Some(s.clone()),
_ => None,
})
}
fn decode_text_string(b: &[u8]) -> String {
if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
let utf16: Vec<u16> = b[2..]
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&utf16)
} else {
String::from_utf8_lossy(b).into_owned()
}
}
fn dict_name(d: &Dict, key: &str) -> Option<String> {
d.entries()
.iter()
.find(|(k, _)| k == key)
.and_then(|(_, v)| match v {
Object::Name(s) => Some(s.clone()),
_ => None,
})
}
fn number_to_f64(o: &Object) -> Option<f64> {
match o {
Object::Integer(n) => Some(*n as f64),
Object::Real(f) => Some(*f),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::objects::ObjectId;
use std::collections::HashMap;
fn id(n: u32) -> ObjectId {
ObjectId::new(n)
}
fn make_states(pairs: &[(u32, bool)]) -> HashMap<ObjectId, bool> {
let mut m = HashMap::new();
for (n, s) in pairs {
m.insert(id(*n), *s);
}
m
}
#[test]
fn visibility_policy_from_name_defaults_anyon() {
assert_eq!(
OcVisibilityPolicy::from_name("garbage"),
OcVisibilityPolicy::AnyOn
);
assert_eq!(OcVisibilityPolicy::from_name(""), OcVisibilityPolicy::AnyOn);
}
#[test]
fn visibility_policy_recognises_all_four_names() {
assert_eq!(
OcVisibilityPolicy::from_name("AllOn"),
OcVisibilityPolicy::AllOn
);
assert_eq!(
OcVisibilityPolicy::from_name("AnyOn"),
OcVisibilityPolicy::AnyOn
);
assert_eq!(
OcVisibilityPolicy::from_name("AnyOff"),
OcVisibilityPolicy::AnyOff
);
assert_eq!(
OcVisibilityPolicy::from_name("AllOff"),
OcVisibilityPolicy::AllOff
);
}
#[test]
fn base_state_defaults_to_on() {
assert!(matches!(OcBaseState::from_name("garbage"), OcBaseState::On));
assert!(matches!(OcBaseState::from_name("ON"), OcBaseState::On));
assert!(matches!(OcBaseState::from_name("OFF"), OcBaseState::Off));
assert!(matches!(
OcBaseState::from_name("Unchanged"),
OcBaseState::Unchanged
));
}
#[test]
fn resolve_states_basestate_on_sets_all_on() {
let groups = vec![
OptionalContentGroup {
id: id(10),
name: "L1".into(),
intents: vec!["View".into()],
usage: None,
},
OptionalContentGroup {
id: id(11),
name: "L2".into(),
intents: vec!["View".into()],
usage: None,
},
];
let cfg = OcConfig {
base_state: OcBaseState::On,
..OcConfig::default()
};
let s = resolve_states(&groups, &cfg);
assert_eq!(s.get(&id(10)), Some(&true));
assert_eq!(s.get(&id(11)), Some(&true));
}
#[test]
fn resolve_states_basestate_off_sets_all_off() {
let groups = vec![OptionalContentGroup {
id: id(10),
name: "L1".into(),
intents: vec!["View".into()],
usage: None,
}];
let cfg = OcConfig {
base_state: OcBaseState::Off,
..OcConfig::default()
};
let s = resolve_states(&groups, &cfg);
assert_eq!(s.get(&id(10)), Some(&false));
}
#[test]
fn resolve_states_on_overrides_off_basestate() {
let groups = vec![
OptionalContentGroup {
id: id(10),
name: "L1".into(),
intents: vec![],
usage: None,
},
OptionalContentGroup {
id: id(11),
name: "L2".into(),
intents: vec![],
usage: None,
},
OptionalContentGroup {
id: id(12),
name: "L3".into(),
intents: vec![],
usage: None,
},
];
let cfg = OcConfig {
base_state: OcBaseState::Off,
on: vec![id(11)],
..OcConfig::default()
};
let s = resolve_states(&groups, &cfg);
assert_eq!(s.get(&id(10)), Some(&false));
assert_eq!(s.get(&id(11)), Some(&true));
assert_eq!(s.get(&id(12)), Some(&false));
}
#[test]
fn resolve_states_off_overrides_on_basestate() {
let groups = vec![
OptionalContentGroup {
id: id(10),
name: "L1".into(),
intents: vec![],
usage: None,
},
OptionalContentGroup {
id: id(11),
name: "L2".into(),
intents: vec![],
usage: None,
},
];
let cfg = OcConfig {
base_state: OcBaseState::On,
off: vec![id(10)],
..OcConfig::default()
};
let s = resolve_states(&groups, &cfg);
assert_eq!(s.get(&id(10)), Some(&false));
assert_eq!(s.get(&id(11)), Some(&true));
}
#[test]
fn evaluate_membership_all_on() {
let states = make_states(&[(10, true), (11, true), (12, false)]);
let mem = OcMembership {
groups: vec![id(10), id(11)],
policy: OcVisibilityPolicy::AllOn,
visibility_expression: None,
};
assert!(evaluate_membership_with_states(&mem, &states));
let mem_with_off = OcMembership {
groups: vec![id(10), id(12)],
policy: OcVisibilityPolicy::AllOn,
visibility_expression: None,
};
assert!(!evaluate_membership_with_states(&mem_with_off, &states));
}
#[test]
fn evaluate_membership_any_on() {
let states = make_states(&[(10, false), (11, false), (12, true)]);
let mem = OcMembership {
groups: vec![id(10), id(11)],
policy: OcVisibilityPolicy::AnyOn,
visibility_expression: None,
};
assert!(!evaluate_membership_with_states(&mem, &states));
let mem_with_on = OcMembership {
groups: vec![id(10), id(12)],
policy: OcVisibilityPolicy::AnyOn,
visibility_expression: None,
};
assert!(evaluate_membership_with_states(&mem_with_on, &states));
}
#[test]
fn evaluate_membership_all_off() {
let states = make_states(&[(10, false), (11, false), (12, true)]);
let mem = OcMembership {
groups: vec![id(10), id(11)],
policy: OcVisibilityPolicy::AllOff,
visibility_expression: None,
};
assert!(evaluate_membership_with_states(&mem, &states));
let mem_with_on = OcMembership {
groups: vec![id(10), id(12)],
policy: OcVisibilityPolicy::AllOff,
visibility_expression: None,
};
assert!(!evaluate_membership_with_states(&mem_with_on, &states));
}
#[test]
fn evaluate_membership_any_off() {
let states = make_states(&[(10, true), (11, true), (12, false)]);
let mem = OcMembership {
groups: vec![id(10), id(11)],
policy: OcVisibilityPolicy::AnyOff,
visibility_expression: None,
};
assert!(!evaluate_membership_with_states(&mem, &states));
let mem_with_off = OcMembership {
groups: vec![id(10), id(12)],
policy: OcVisibilityPolicy::AnyOff,
visibility_expression: None,
};
assert!(evaluate_membership_with_states(&mem_with_off, &states));
}
#[test]
fn evaluate_membership_empty_groups_visible() {
let states = make_states(&[]);
let mem = OcMembership {
groups: vec![],
policy: OcVisibilityPolicy::AllOn,
visibility_expression: None,
};
assert!(evaluate_membership_with_states(&mem, &states));
}
#[test]
fn evaluate_visibility_expression_simple_and() {
let states = make_states(&[(10, true), (11, true), (12, false)]);
let ve = OcVisibilityExpression::And(vec![
OcVisibilityExpression::Group(id(10)),
OcVisibilityExpression::Group(id(11)),
]);
assert!(evaluate_visibility_expression(&ve, &states));
let ve_fail = OcVisibilityExpression::And(vec![
OcVisibilityExpression::Group(id(10)),
OcVisibilityExpression::Group(id(12)),
]);
assert!(!evaluate_visibility_expression(&ve_fail, &states));
}
#[test]
fn evaluate_visibility_expression_simple_or() {
let states = make_states(&[(10, false), (11, true), (12, false)]);
let ve = OcVisibilityExpression::Or(vec![
OcVisibilityExpression::Group(id(10)),
OcVisibilityExpression::Group(id(11)),
]);
assert!(evaluate_visibility_expression(&ve, &states));
let ve_fail = OcVisibilityExpression::Or(vec![
OcVisibilityExpression::Group(id(10)),
OcVisibilityExpression::Group(id(12)),
]);
assert!(!evaluate_visibility_expression(&ve_fail, &states));
}
#[test]
fn evaluate_visibility_expression_not() {
let states = make_states(&[(10, true)]);
let ve = OcVisibilityExpression::Not(Box::new(OcVisibilityExpression::Group(id(10))));
assert!(!evaluate_visibility_expression(&ve, &states));
let ve_inv = OcVisibilityExpression::Not(Box::new(OcVisibilityExpression::Group(id(11))));
assert!(evaluate_visibility_expression(&ve_inv, &states));
}
#[test]
fn evaluate_visibility_expression_nested() {
let states = make_states(&[
(1, false),
(2, true), (3, true),
(4, true),
(5, true), ]);
let ve = OcVisibilityExpression::Or(vec![
OcVisibilityExpression::Group(id(1)),
OcVisibilityExpression::Not(Box::new(OcVisibilityExpression::Group(id(2)))),
OcVisibilityExpression::And(vec![
OcVisibilityExpression::Group(id(3)),
OcVisibilityExpression::Group(id(4)),
OcVisibilityExpression::Group(id(5)),
]),
]);
assert!(evaluate_visibility_expression(&ve, &states));
}
#[test]
fn unknown_group_id_treated_as_off() {
let states = make_states(&[]);
let mem = OcMembership {
groups: vec![id(99)],
policy: OcVisibilityPolicy::AllOn,
visibility_expression: None,
};
assert!(!evaluate_membership_with_states(&mem, &states));
}
}