use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use pdfrum_object::{Dict, Name, Object, Resolve};
use crate::names;
#[must_use]
pub fn field_attr<R: Resolve>(
dict: &Dict,
key: &Name,
r: &R,
limits: &Limits,
diags: &mut Diagnostics,
) -> Option<Object> {
let mut level = Some(dict.clone());
let mut depth = 0;
while let Some(node) = level {
if depth > limits.max_name_tree_depth {
diags.record(Severity::Suspicious, DiagKind::TreeDepthExceeded, None);
return None;
}
if let Some(found) = node.get(key, r) {
return Some(found.get().clone());
}
level = node.dict(names::PARENT, r);
depth += 1;
}
None
}
#[must_use]
pub fn full_name<R: Resolve>(dict: &Dict, r: &R) -> String {
let mut full = String::new();
let mut visited: Vec<Dict> = Vec::new();
let mut level = Some(dict.clone());
while let Some(node) = level {
visited.push(node.clone());
let short = node.text(names::T, r).unwrap_or_default();
if !short.is_empty() {
full = if full.is_empty() {
short
} else {
format!("{short}.{full}")
};
}
level = node.dict(names::PARENT, r);
if let Some(next) = &level
&& visited.contains(next)
{
break;
}
}
full
}
#[cfg(test)]
mod tests {
use super::{field_attr, full_name};
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Name, NoResolve, Object, PdfString};
fn dict(pairs: &[(&str, Object)]) -> Dict {
Dict::from_pairs(
pairs
.iter()
.map(|(k, v)| (Name::from(*k), v.clone()))
.collect::<Vec<_>>(),
)
}
fn named(text: &str) -> Object {
Object::Str(PdfString::literal(text.as_bytes()))
}
#[test]
fn an_attribute_is_found_on_an_ancestor() {
let parent = dict(&[("FT", Object::Name(Name::from("Btn")))]);
let child = dict(&[("Parent", Object::Dict(parent))]);
let (l, mut d) = (Limits::default(), Diagnostics::default());
assert_eq!(
field_attr(&child, &Name::from("FT"), &NoResolve, &l, &mut d),
Some(Object::Name(Name::from("Btn")))
);
assert_eq!(
field_attr(&child, &Name::from("Ff"), &NoResolve, &l, &mut d),
None
);
}
#[test]
fn the_fields_own_value_wins_over_its_parents() {
let parent = dict(&[("Ff", Object::Int(1))]);
let child = dict(&[("Ff", Object::Int(2)), ("Parent", Object::Dict(parent))]);
let (l, mut d) = (Limits::default(), Diagnostics::default());
assert_eq!(
field_attr(&child, &Name::from("Ff"), &NoResolve, &l, &mut d),
Some(Object::Int(2))
);
}
#[test]
fn a_non_dictionary_parent_ends_the_walk() {
let child = dict(&[("Parent", Object::Int(4))]);
let (l, mut d) = (Limits::default(), Diagnostics::default());
assert_eq!(
field_attr(&child, &Name::from("FT"), &NoResolve, &l, &mut d),
None
);
}
#[test]
fn a_level_with_no_name_is_skipped_without_a_separator() {
let grandparent = dict(&[("T", named("a"))]);
let parent = dict(&[("Parent", Object::Dict(grandparent))]);
let child = dict(&[("T", named("b")), ("Parent", Object::Dict(parent))]);
assert_eq!(full_name(&child, &NoResolve), "a.b");
}
#[test]
fn a_field_with_no_ancestors_is_its_own_name() {
assert_eq!(full_name(&dict(&[("T", named("foo"))]), &NoResolve), "foo");
assert_eq!(full_name(&Dict::new(), &NoResolve), "");
}
#[test]
fn a_two_level_name_reads_root_first() {
let parent = dict(&[("T", named("bar"))]);
let child = dict(&[("T", named("foo")), ("Parent", Object::Dict(parent))]);
assert_eq!(full_name(&child, &NoResolve), "bar.foo");
}
#[test]
fn a_cycle_is_cut_after_the_entry_node_is_seen_once_more() {
let root_again = dict(&[("T", named("foo"))]);
let d3 = dict(&[("T", named("qux")), ("Parent", Object::Dict(root_again))]);
let d2 = dict(&[("Parent", Object::Dict(d3))]);
let d1 = dict(&[("T", named("bar")), ("Parent", Object::Dict(d2))]);
let root = dict(&[("T", named("foo")), ("Parent", Object::Dict(d1))]);
assert_eq!(full_name(&root, &NoResolve), "foo.qux.bar.foo");
}
}