use crate::context::Context;
use crate::dict::DictKey;
use crate::object::{EntityId, PsObject, PsValue};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Violation {
pub container: EntityId,
pub container_desc: String,
pub slot: String,
pub value_type: &'static str,
pub value_entity: EntityId,
pub target_reclaimable: bool,
}
impl std::fmt::Display for Violation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} [{}] -> local {} (entity {}){}",
self.container_desc,
self.slot,
self.value_type,
self.value_entity.raw_index(),
if self.target_reclaimable {
" RECLAIMABLE"
} else {
" (permanent, sanctioned by PLRM 3.7.5)"
}
)
}
}
pub fn local_composite_entity(obj: &PsObject) -> Option<EntityId> {
let entity = match obj.value {
PsValue::String { entity, .. } => entity,
PsValue::Array { entity, .. } | PsValue::PackedArray { entity, .. } => entity,
PsValue::Dict(entity) => entity,
_ => return None,
};
if entity.is_global() {
None
} else {
Some(entity)
}
}
fn is_reclaimable(ctx: &Context, obj: &PsObject, entity: EntityId) -> bool {
let meta = match obj.value {
PsValue::String { .. } => ctx.strings.local.entities.get(entity),
PsValue::Array { .. } | PsValue::PackedArray { .. } => {
ctx.arrays.local.entities.get(entity)
}
PsValue::Dict(_) => ctx.dicts.local.entities.get(entity),
_ => return false,
};
meta.created_after_save != 0
}
fn describe_key(ctx: &Context, key: &DictKey) -> String {
match key {
DictKey::Name(id) => String::from_utf8_lossy(ctx.names.get_bytes(*id)).into_owned(),
DictKey::Int(v) => v.to_string(),
DictKey::Real(bits) => f64::from_bits(*bits).to_string(),
DictKey::Bool(v) => v.to_string(),
DictKey::String(bytes) => format!("({})", String::from_utf8_lossy(bytes)),
DictKey::Operator(op) => format!("op#{op}"),
DictKey::Identity(e, s, l) => format!("identity#{e}+{s}:{l}"),
}
}
pub fn audit_global_vm(ctx: &Context) -> Vec<Violation> {
let mut out = Vec::new();
for (entity, entry) in ctx.dicts.global.iter_entities() {
let desc = {
let name = String::from_utf8_lossy(&entry.name);
if name.is_empty() {
format!("dict #{}", entity.raw_index())
} else {
format!("dict {name}")
}
};
for (key, value) in &entry.entries {
if let Some(value_entity) = local_composite_entity(value) {
out.push(Violation {
container: entity,
container_desc: desc.clone(),
slot: describe_key(ctx, key),
value_type: std::str::from_utf8(value.type_name()).unwrap_or("?"),
value_entity,
target_reclaimable: is_reclaimable(ctx, value, value_entity),
});
}
}
}
for (entity, elements) in ctx.arrays.global.iter_entities() {
for (index, value) in elements.iter().enumerate() {
if let Some(value_entity) = local_composite_entity(value) {
out.push(Violation {
container: entity,
container_desc: format!("array #{}", entity.raw_index()),
slot: index.to_string(),
value_type: std::str::from_utf8(value.type_name()).unwrap_or("?"),
value_entity,
target_reclaimable: is_reclaimable(ctx, value, value_entity),
});
}
}
}
out
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DanglingRef {
pub holder: String,
pub slot: String,
pub value_type: &'static str,
pub target_index: usize,
pub table_len: usize,
}
impl std::fmt::Display for DanglingRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} [{}] -> {} entity {} (table len {})",
self.holder, self.slot, self.value_type, self.target_index, self.table_len
)
}
}
fn table_len_for(ctx: &Context, obj: &PsObject) -> Option<(usize, usize)> {
if let PsValue::Gstate(idx) = obj.value {
return Some((idx as usize, ctx.gstate_store.len()));
}
let (entity, len) = match obj.value {
PsValue::String { entity, .. } => (
entity,
if entity.is_global() {
ctx.strings.global.entities.len()
} else {
ctx.strings.local.entities.len()
},
),
PsValue::Array { entity, .. } | PsValue::PackedArray { entity, .. } => (
entity,
if entity.is_global() {
ctx.arrays.global.entities.len()
} else {
ctx.arrays.local.entities.len()
},
),
PsValue::Dict(entity) => (
entity,
if entity.is_global() {
ctx.dicts.global.entities.len()
} else {
ctx.dicts.local.entities.len()
},
),
_ => return None,
};
Some((entity.raw_index(), len))
}
fn check_color_space(
ctx: &Context,
cs: &crate::graphics_state::ColorSpace,
holder: &str,
slot: &str,
out: &mut Vec<DanglingRef>,
) {
use crate::graphics_state::ColorSpace as Cs;
match cs {
Cs::DeviceGray | Cs::DeviceRGB | Cs::DeviceCMYK => {}
Cs::Indexed {
base, lookup_proc, ..
} => {
check_color_space(ctx, base, holder, &format!("{slot}.base"), out);
if let Some(p) = lookup_proc {
check_ref(ctx, p, holder, format!("{slot}.lookup_proc"), out);
}
}
Cs::CIEBasedABC { dict_entity, .. }
| Cs::CIEBasedA { dict_entity, .. }
| Cs::CIEBasedDEF { dict_entity, .. }
| Cs::CIEBasedDEFG { dict_entity, .. }
| Cs::ICCBased { dict_entity, .. } => {
check_ref(
ctx,
&PsObject::dict(*dict_entity),
holder,
format!("{slot}.dict"),
out,
);
}
Cs::Separation {
alt_space,
tint_transform,
..
}
| Cs::DeviceN {
alt_space,
tint_transform,
..
} => {
check_color_space(ctx, alt_space, holder, &format!("{slot}.alt_space"), out);
check_ref(
ctx,
tint_transform,
holder,
format!("{slot}.tint_transform"),
out,
);
}
Cs::Pattern { base } => {
if let Some(base) = base {
check_color_space(ctx, base, holder, &format!("{slot}.base"), out);
}
}
}
}
fn check_gstate(
ctx: &Context,
gs: &crate::graphics_state::GraphicsState,
holder: &str,
out: &mut Vec<DanglingRef>,
) {
let one = |obj: &Option<PsObject>, slot: &str, out: &mut Vec<DanglingRef>| {
if let Some(o) = obj {
check_ref(ctx, o, holder, slot.to_string(), out);
}
};
one(&gs.current_font, "current_font", out);
one(&gs.root_font, "root_font", out);
one(&gs.screen_proc, "screen_proc", out);
one(&gs.halftone, "halftone", out);
one(&gs.transfer_function, "transfer_function", out);
one(&gs.black_generation, "black_generation", out);
one(&gs.undercolor_removal, "undercolor_removal", out);
one(&gs.color_rendering, "color_rendering", out);
if let Some(pd) = gs.page_device {
check_ref(
ctx,
&PsObject::dict(pd),
holder,
"page_device".to_string(),
out,
);
}
if let Some(pat) = gs.current_pattern_dict {
check_ref(
ctx,
&PsObject::dict(pat),
holder,
"current_pattern_dict".to_string(),
out,
);
}
if let Some(screens) = &gs.color_screen {
for (i, (_, _, proc_obj)) in screens.iter().enumerate() {
check_ref(ctx, proc_obj, holder, format!("color_screen[{i}]"), out);
}
}
if let Some(transfers) = &gs.color_transfer {
for (i, proc_obj) in transfers.iter().enumerate() {
check_ref(ctx, proc_obj, holder, format!("color_transfer[{i}]"), out);
}
}
check_color_space(ctx, &gs.color_space, holder, "color_space", out);
}
fn check_ref(
ctx: &Context,
obj: &PsObject,
holder: &str,
slot: String,
out: &mut Vec<DanglingRef>,
) {
if let Some((index, table_len)) = table_len_for(ctx, obj)
&& index >= table_len
{
out.push(DanglingRef {
holder: holder.to_string(),
slot,
value_type: std::str::from_utf8(obj.type_name()).unwrap_or("?"),
target_index: index,
table_len,
});
}
}
pub fn audit_dangling_refs(ctx: &Context) -> Vec<DanglingRef> {
let mut out = Vec::new();
for (store, label) in [
(&ctx.dicts.local, "local dict"),
(&ctx.dicts.global, "global dict"),
] {
for (entity, entry) in store.iter_entities() {
if store.entities.get(entity).is_cow_backup() {
continue;
}
let name = String::from_utf8_lossy(&entry.name);
let holder = if name.is_empty() {
format!("{label} #{}", entity.raw_index())
} else {
format!("{label} {name} #{}", entity.raw_index())
};
for (key, value) in &entry.entries {
check_ref(ctx, value, &holder, describe_key(ctx, key), &mut out);
}
}
}
for (store, label) in [
(&ctx.arrays.local, "local array"),
(&ctx.arrays.global, "global array"),
] {
for (entity, elements) in store.iter_entities() {
if store.entities.get(entity).is_cow_backup() {
continue;
}
let holder = format!("{label} #{}", entity.raw_index());
for (index, value) in elements.iter().enumerate() {
check_ref(ctx, value, &holder, index.to_string(), &mut out);
}
}
}
for (index, obj) in ctx.o_stack.as_slice().iter().enumerate() {
check_ref(ctx, obj, "operand stack", index.to_string(), &mut out);
}
for (index, obj) in ctx.e_stack.as_slice().iter().enumerate() {
check_ref(ctx, obj, "execution stack", index.to_string(), &mut out);
}
for (index, entity) in ctx.d_stack.iter().enumerate() {
check_ref(
ctx,
&PsObject::dict(*entity),
"dictionary stack",
index.to_string(),
&mut out,
);
}
for (entity, proc) in ctx.files.pending_proc_handles() {
check_ref(
ctx,
&proc,
"pending procedure data source",
format!("file #{}", entity.raw_index()),
&mut out,
);
}
for (entity, name) in [
(ctx.systemdict, "systemdict"),
(ctx.globaldict, "globaldict"),
(ctx.userdict, "userdict"),
(ctx.errordict, "errordict"),
(ctx.dollar_error, "$error"),
(ctx.font_directory, "FontDirectory"),
(ctx.global_resources, "GlobalResources"),
(ctx.local_resources, "LocalResources"),
(ctx.category_registry, "CategoryRegistry"),
(ctx.user_params, "UserParams"),
(ctx.system_params, "SystemParams"),
(ctx.internaldict, "internaldict"),
] {
check_ref(
ctx,
&PsObject::dict(entity),
"Context handle",
name.to_string(),
&mut out,
);
}
check_gstate(ctx, &ctx.gstate, "graphics state", &mut out);
for (index, entry) in ctx.gstate_stack.iter().enumerate() {
check_gstate(
ctx,
&entry.state,
&format!("gstate stack #{index}"),
&mut out,
);
}
for (index, state) in ctx.gstate_store.iter().enumerate() {
check_gstate(ctx, state, &format!("gstate store #{index}"), &mut out);
}
for entity in ctx.glyph_caches.keys() {
check_ref(
ctx,
&PsObject::dict(*entity),
"glyph cache",
"key".to_string(),
&mut out,
);
}
for entity in ctx.form_cache.keys() {
check_ref(
ctx,
&PsObject::dict(*entity),
"form cache",
"key".to_string(),
&mut out,
);
}
out
}
pub fn audit_global_vm_unsafe_only(ctx: &Context) -> Vec<Violation> {
audit_global_vm(ctx)
.into_iter()
.filter(|v| v.target_reclaimable)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::ObjFlags;
#[test]
fn bootstrap_exception_is_reported_but_not_reclaimable() {
let ctx = Context::new();
let all = audit_global_vm(&ctx);
assert!(!all.is_empty(), "expected the systemdict exception entries");
for v in &all {
assert_eq!(v.container_desc, "dict systemdict", "{v}");
assert!(!v.target_reclaimable, "{v}");
}
let slots: Vec<&str> = all.iter().map(|v| v.slot.as_str()).collect();
for expected in ["userdict", "errordict", "$error", "FontDirectory"] {
assert!(slots.contains(&expected), "missing {expected} in {slots:?}");
}
assert_eq!(audit_global_vm_unsafe_only(&ctx), Vec::new());
}
#[test]
fn detects_local_string_in_global_dict() {
let mut ctx = Context::new();
let gdict = ctx.dicts.allocate_with(4, b"gdict", 0, true, 0);
let lstr = ctx.strings.allocate_with(5, 0, false, 1);
let key = DictKey::Name(ctx.names.intern(b"k"));
ctx.dicts.put(gdict, key, PsObject::string(lstr, 5));
let found = audit_global_vm_unsafe_only(&ctx);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].container, gdict);
assert_eq!(found[0].slot, "k");
assert_eq!(found[0].value_type, "stringtype");
assert!(found[0].target_reclaimable);
}
#[test]
fn detects_local_array_written_through_get_mut() {
let mut ctx = Context::new();
let garr = ctx.arrays.allocate_with(2, 0, true, 0);
let larr = ctx.arrays.allocate_with(1, 0, false, 1);
ctx.arrays.get_mut(garr, 0, 2)[1] = PsObject::array(larr, 1);
let found = audit_global_vm_unsafe_only(&ctx);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].slot, "1");
assert_eq!(found[0].value_type, "arraytype");
}
#[test]
fn global_values_and_simple_values_are_clean() {
let mut ctx = Context::new();
let gdict = ctx.dicts.allocate_with(4, b"gdict", 0, true, 0);
let gstr = ctx.strings.allocate_with(5, 0, true, 0);
let k_str = DictKey::Name(ctx.names.intern(b"gs"));
let k_int = DictKey::Name(ctx.names.intern(b"n"));
ctx.dicts.put(gdict, k_str, PsObject::string(gstr, 5));
ctx.dicts.put(gdict, k_int, PsObject::int(7));
assert_eq!(audit_global_vm_unsafe_only(&ctx), Vec::new());
}
#[test]
fn local_container_holding_local_value_is_clean() {
let mut ctx = Context::new();
let ldict = ctx.dicts.allocate_at_level_zero(4, b"ldict");
let lstr = ctx.strings.allocate_with(5, 0, false, 1);
let key = DictKey::Name(ctx.names.intern(b"k"));
ctx.dicts.put(ldict, key, PsObject::string(lstr, 5));
assert_eq!(audit_global_vm_unsafe_only(&ctx), Vec::new());
}
#[test]
fn local_composite_entity_ignores_simple_objects() {
assert_eq!(local_composite_entity(&PsObject::int(3)), None);
assert_eq!(local_composite_entity(&PsObject::null()), None);
let named = PsObject {
value: PsValue::Name(crate::object::NameId(0)),
flags: ObjFlags::literal(),
};
assert_eq!(local_composite_entity(&named), None);
}
fn retired_dict(ctx: &Context) -> PsObject {
PsObject::dict(EntityId(ctx.dicts.local.entities.len() as u32))
}
#[test]
fn bootstrap_context_has_no_dangling_refs() {
assert_eq!(audit_dangling_refs(&Context::new()), Vec::new());
}
#[test]
fn gstate_font_reference_is_swept() {
let mut ctx = Context::new();
ctx.gstate.current_font = Some(retired_dict(&ctx));
let found = audit_dangling_refs(&ctx);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].holder, "graphics state");
assert_eq!(found[0].slot, "current_font");
}
#[test]
fn gstate_stack_and_gstate_store_are_swept() {
let mut ctx = Context::new();
let stale = retired_dict(&ctx);
ctx.gstate_stack.push(crate::graphics_state::GstateEntry {
state: ctx.gstate.clone(),
saved_by_save: false,
});
ctx.gstate_stack[0].state.page_device = match stale.value {
PsValue::Dict(e) => Some(e),
_ => unreachable!(),
};
ctx.gstate_store.push(ctx.gstate.clone());
ctx.gstate_store[0].root_font = Some(stale);
let holders: Vec<&str> = audit_dangling_refs(&ctx)
.iter()
.map(|d| Box::leak(d.holder.clone().into_boxed_str()) as &str)
.collect();
assert!(holders.contains(&"gstate stack #0"), "{holders:?}");
assert!(holders.contains(&"gstate store #0"), "{holders:?}");
}
#[test]
fn nested_color_space_reference_is_swept() {
use crate::graphics_state::ColorSpace;
let mut ctx = Context::new();
let stale_entity = EntityId(ctx.dicts.local.entities.len() as u32);
ctx.gstate.color_space = ColorSpace::Separation {
name: b"Spot".to_vec(),
alt_space: Box::new(ColorSpace::ICCBased {
dict_entity: stale_entity,
n: 4,
profile_hash: None,
}),
tint_transform: PsObject::null(),
num_alt_components: 4,
};
let found = audit_dangling_refs(&ctx);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].slot, "color_space.alt_space.dict");
}
#[test]
fn stale_gstate_object_is_swept() {
let mut ctx = Context::new();
let stale = PsObject {
value: PsValue::Gstate(0),
flags: ObjFlags::literal(),
};
assert_eq!(ctx.gstate_store.len(), 0);
ctx.o_stack.push(stale).unwrap();
let found = audit_dangling_refs(&ctx);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].value_type, "gstatetype");
}
#[test]
fn entity_keyed_caches_are_swept() {
let mut ctx = Context::new();
let stale_entity = EntityId(ctx.dicts.local.entities.len() as u32);
ctx.form_cache
.insert(stale_entity, crate::display_list::DisplayList::new());
let found = audit_dangling_refs(&ctx);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].holder, "form cache");
}
}