1use crate::context::Context;
48use crate::dict::DictKey;
49use crate::object::{EntityId, PsObject, PsValue};
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Violation {
54 pub container: EntityId,
56 pub container_desc: String,
59 pub slot: String,
62 pub value_type: &'static str,
64 pub value_entity: EntityId,
66 pub target_reclaimable: bool,
77}
78
79impl std::fmt::Display for Violation {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 write!(
82 f,
83 "{} [{}] -> local {} (entity {}){}",
84 self.container_desc,
85 self.slot,
86 self.value_type,
87 self.value_entity.raw_index(),
88 if self.target_reclaimable {
89 " RECLAIMABLE"
90 } else {
91 " (permanent, sanctioned by PLRM 3.7.5)"
92 }
93 )
94 }
95}
96
97pub fn local_composite_entity(obj: &PsObject) -> Option<EntityId> {
103 let entity = match obj.value {
104 PsValue::String { entity, .. } => entity,
105 PsValue::Array { entity, .. } | PsValue::PackedArray { entity, .. } => entity,
106 PsValue::Dict(entity) => entity,
107 _ => return None,
108 };
109 if entity.is_global() {
110 None
111 } else {
112 Some(entity)
113 }
114}
115
116fn is_reclaimable(ctx: &Context, obj: &PsObject, entity: EntityId) -> bool {
134 let meta = match obj.value {
135 PsValue::String { .. } => ctx.strings.local.entities.get(entity),
136 PsValue::Array { .. } | PsValue::PackedArray { .. } => {
137 ctx.arrays.local.entities.get(entity)
138 }
139 PsValue::Dict(_) => ctx.dicts.local.entities.get(entity),
140 _ => return false,
141 };
142 meta.created_after_save != 0
143}
144
145fn describe_key(ctx: &Context, key: &DictKey) -> String {
147 match key {
148 DictKey::Name(id) => String::from_utf8_lossy(ctx.names.get_bytes(*id)).into_owned(),
149 DictKey::Int(v) => v.to_string(),
150 DictKey::Real(bits) => f64::from_bits(*bits).to_string(),
151 DictKey::Bool(v) => v.to_string(),
152 DictKey::String(bytes) => format!("({})", String::from_utf8_lossy(bytes)),
153 DictKey::Operator(op) => format!("op#{op}"),
154 DictKey::Identity(e, s, l) => format!("identity#{e}+{s}:{l}"),
155 }
156}
157
158pub fn audit_global_vm(ctx: &Context) -> Vec<Violation> {
162 let mut out = Vec::new();
163
164 for (entity, entry) in ctx.dicts.global.iter_entities() {
165 let desc = {
166 let name = String::from_utf8_lossy(&entry.name);
167 if name.is_empty() {
168 format!("dict #{}", entity.raw_index())
169 } else {
170 format!("dict {name}")
171 }
172 };
173 for (key, value) in &entry.entries {
174 if let Some(value_entity) = local_composite_entity(value) {
175 out.push(Violation {
176 container: entity,
177 container_desc: desc.clone(),
178 slot: describe_key(ctx, key),
179 value_type: std::str::from_utf8(value.type_name()).unwrap_or("?"),
180 value_entity,
181 target_reclaimable: is_reclaimable(ctx, value, value_entity),
182 });
183 }
184 }
185 }
186
187 for (entity, elements) in ctx.arrays.global.iter_entities() {
188 for (index, value) in elements.iter().enumerate() {
189 if let Some(value_entity) = local_composite_entity(value) {
190 out.push(Violation {
191 container: entity,
192 container_desc: format!("array #{}", entity.raw_index()),
193 slot: index.to_string(),
194 value_type: std::str::from_utf8(value.type_name()).unwrap_or("?"),
195 value_entity,
196 target_reclaimable: is_reclaimable(ctx, value, value_entity),
197 });
198 }
199 }
200 }
201
202 out
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct DanglingRef {
213 pub holder: String,
215 pub slot: String,
218 pub value_type: &'static str,
220 pub target_index: usize,
222 pub table_len: usize,
224}
225
226impl std::fmt::Display for DanglingRef {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(
229 f,
230 "{} [{}] -> {} entity {} (table len {})",
231 self.holder, self.slot, self.value_type, self.target_index, self.table_len
232 )
233 }
234}
235
236fn table_len_for(ctx: &Context, obj: &PsObject) -> Option<(usize, usize)> {
242 if let PsValue::Gstate(idx) = obj.value {
243 return Some((idx as usize, ctx.gstate_store.len()));
244 }
245 let (entity, len) = match obj.value {
246 PsValue::String { entity, .. } => (
247 entity,
248 if entity.is_global() {
249 ctx.strings.global.entities.len()
250 } else {
251 ctx.strings.local.entities.len()
252 },
253 ),
254 PsValue::Array { entity, .. } | PsValue::PackedArray { entity, .. } => (
255 entity,
256 if entity.is_global() {
257 ctx.arrays.global.entities.len()
258 } else {
259 ctx.arrays.local.entities.len()
260 },
261 ),
262 PsValue::Dict(entity) => (
263 entity,
264 if entity.is_global() {
265 ctx.dicts.global.entities.len()
266 } else {
267 ctx.dicts.local.entities.len()
268 },
269 ),
270 _ => return None,
271 };
272 Some((entity.raw_index(), len))
273}
274
275fn check_color_space(
282 ctx: &Context,
283 cs: &crate::graphics_state::ColorSpace,
284 holder: &str,
285 slot: &str,
286 out: &mut Vec<DanglingRef>,
287) {
288 use crate::graphics_state::ColorSpace as Cs;
289 match cs {
290 Cs::DeviceGray | Cs::DeviceRGB | Cs::DeviceCMYK => {}
291 Cs::Indexed {
292 base, lookup_proc, ..
293 } => {
294 check_color_space(ctx, base, holder, &format!("{slot}.base"), out);
295 if let Some(p) = lookup_proc {
296 check_ref(ctx, p, holder, format!("{slot}.lookup_proc"), out);
297 }
298 }
299 Cs::CIEBasedABC { dict_entity, .. }
300 | Cs::CIEBasedA { dict_entity, .. }
301 | Cs::CIEBasedDEF { dict_entity, .. }
302 | Cs::CIEBasedDEFG { dict_entity, .. }
303 | Cs::ICCBased { dict_entity, .. } => {
304 check_ref(
305 ctx,
306 &PsObject::dict(*dict_entity),
307 holder,
308 format!("{slot}.dict"),
309 out,
310 );
311 }
312 Cs::Separation {
313 alt_space,
314 tint_transform,
315 ..
316 }
317 | Cs::DeviceN {
318 alt_space,
319 tint_transform,
320 ..
321 } => {
322 check_color_space(ctx, alt_space, holder, &format!("{slot}.alt_space"), out);
323 check_ref(
324 ctx,
325 tint_transform,
326 holder,
327 format!("{slot}.tint_transform"),
328 out,
329 );
330 }
331 Cs::Pattern { base } => {
332 if let Some(base) = base {
333 check_color_space(ctx, base, holder, &format!("{slot}.base"), out);
334 }
335 }
336 }
337}
338
339fn check_gstate(
348 ctx: &Context,
349 gs: &crate::graphics_state::GraphicsState,
350 holder: &str,
351 out: &mut Vec<DanglingRef>,
352) {
353 let one = |obj: &Option<PsObject>, slot: &str, out: &mut Vec<DanglingRef>| {
354 if let Some(o) = obj {
355 check_ref(ctx, o, holder, slot.to_string(), out);
356 }
357 };
358 one(&gs.current_font, "current_font", out);
359 one(&gs.root_font, "root_font", out);
360 one(&gs.screen_proc, "screen_proc", out);
361 one(&gs.halftone, "halftone", out);
362 one(&gs.transfer_function, "transfer_function", out);
363 one(&gs.black_generation, "black_generation", out);
364 one(&gs.undercolor_removal, "undercolor_removal", out);
365 one(&gs.color_rendering, "color_rendering", out);
366
367 if let Some(pd) = gs.page_device {
368 check_ref(
369 ctx,
370 &PsObject::dict(pd),
371 holder,
372 "page_device".to_string(),
373 out,
374 );
375 }
376 if let Some(pat) = gs.current_pattern_dict {
377 check_ref(
378 ctx,
379 &PsObject::dict(pat),
380 holder,
381 "current_pattern_dict".to_string(),
382 out,
383 );
384 }
385 if let Some(screens) = &gs.color_screen {
386 for (i, (_, _, proc_obj)) in screens.iter().enumerate() {
387 check_ref(ctx, proc_obj, holder, format!("color_screen[{i}]"), out);
388 }
389 }
390 if let Some(transfers) = &gs.color_transfer {
391 for (i, proc_obj) in transfers.iter().enumerate() {
392 check_ref(ctx, proc_obj, holder, format!("color_transfer[{i}]"), out);
393 }
394 }
395 check_color_space(ctx, &gs.color_space, holder, "color_space", out);
396}
397
398fn check_ref(
400 ctx: &Context,
401 obj: &PsObject,
402 holder: &str,
403 slot: String,
404 out: &mut Vec<DanglingRef>,
405) {
406 if let Some((index, table_len)) = table_len_for(ctx, obj)
407 && index >= table_len
408 {
409 out.push(DanglingRef {
410 holder: holder.to_string(),
411 slot,
412 value_type: std::str::from_utf8(obj.type_name()).unwrap_or("?"),
413 target_index: index,
414 table_len,
415 });
416 }
417}
418
419pub fn audit_dangling_refs(ctx: &Context) -> Vec<DanglingRef> {
449 let mut out = Vec::new();
450
451 for (store, label) in [
452 (&ctx.dicts.local, "local dict"),
453 (&ctx.dicts.global, "global dict"),
454 ] {
455 for (entity, entry) in store.iter_entities() {
456 if store.entities.get(entity).is_cow_backup() {
457 continue;
458 }
459 let name = String::from_utf8_lossy(&entry.name);
460 let holder = if name.is_empty() {
461 format!("{label} #{}", entity.raw_index())
462 } else {
463 format!("{label} {name} #{}", entity.raw_index())
464 };
465 for (key, value) in &entry.entries {
466 check_ref(ctx, value, &holder, describe_key(ctx, key), &mut out);
467 }
468 }
469 }
470
471 for (store, label) in [
472 (&ctx.arrays.local, "local array"),
473 (&ctx.arrays.global, "global array"),
474 ] {
475 for (entity, elements) in store.iter_entities() {
476 if store.entities.get(entity).is_cow_backup() {
477 continue;
478 }
479 let holder = format!("{label} #{}", entity.raw_index());
480 for (index, value) in elements.iter().enumerate() {
481 check_ref(ctx, value, &holder, index.to_string(), &mut out);
482 }
483 }
484 }
485
486 for (index, obj) in ctx.o_stack.as_slice().iter().enumerate() {
487 check_ref(ctx, obj, "operand stack", index.to_string(), &mut out);
488 }
489 for (index, obj) in ctx.e_stack.as_slice().iter().enumerate() {
490 check_ref(ctx, obj, "execution stack", index.to_string(), &mut out);
491 }
492 for (index, entity) in ctx.d_stack.iter().enumerate() {
493 check_ref(
494 ctx,
495 &PsObject::dict(*entity),
496 "dictionary stack",
497 index.to_string(),
498 &mut out,
499 );
500 }
501
502 for (entity, proc) in ctx.files.pending_proc_handles() {
506 check_ref(
507 ctx,
508 &proc,
509 "pending procedure data source",
510 format!("file #{}", entity.raw_index()),
511 &mut out,
512 );
513 }
514
515 for (entity, name) in [
518 (ctx.systemdict, "systemdict"),
519 (ctx.globaldict, "globaldict"),
520 (ctx.userdict, "userdict"),
521 (ctx.errordict, "errordict"),
522 (ctx.dollar_error, "$error"),
523 (ctx.font_directory, "FontDirectory"),
524 (ctx.global_resources, "GlobalResources"),
525 (ctx.local_resources, "LocalResources"),
526 (ctx.category_registry, "CategoryRegistry"),
527 (ctx.user_params, "UserParams"),
528 (ctx.system_params, "SystemParams"),
529 (ctx.internaldict, "internaldict"),
530 ] {
531 check_ref(
532 ctx,
533 &PsObject::dict(entity),
534 "Context handle",
535 name.to_string(),
536 &mut out,
537 );
538 }
539
540 check_gstate(ctx, &ctx.gstate, "graphics state", &mut out);
544 for (index, entry) in ctx.gstate_stack.iter().enumerate() {
545 check_gstate(
546 ctx,
547 &entry.state,
548 &format!("gstate stack #{index}"),
549 &mut out,
550 );
551 }
552 for (index, state) in ctx.gstate_store.iter().enumerate() {
553 check_gstate(ctx, state, &format!("gstate store #{index}"), &mut out);
554 }
555
556 for entity in ctx.glyph_caches.keys() {
559 check_ref(
560 ctx,
561 &PsObject::dict(*entity),
562 "glyph cache",
563 "key".to_string(),
564 &mut out,
565 );
566 }
567 for entity in ctx.form_cache.keys() {
568 check_ref(
569 ctx,
570 &PsObject::dict(*entity),
571 "form cache",
572 "key".to_string(),
573 &mut out,
574 );
575 }
576
577 out
578}
579
580pub fn audit_global_vm_unsafe_only(ctx: &Context) -> Vec<Violation> {
586 audit_global_vm(ctx)
587 .into_iter()
588 .filter(|v| v.target_reclaimable)
589 .collect()
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use crate::object::ObjFlags;
596
597 #[test]
601 fn bootstrap_exception_is_reported_but_not_reclaimable() {
602 let ctx = Context::new();
603 let all = audit_global_vm(&ctx);
604 assert!(!all.is_empty(), "expected the systemdict exception entries");
605 for v in &all {
606 assert_eq!(v.container_desc, "dict systemdict", "{v}");
607 assert!(!v.target_reclaimable, "{v}");
608 }
609 let slots: Vec<&str> = all.iter().map(|v| v.slot.as_str()).collect();
610 for expected in ["userdict", "errordict", "$error", "FontDirectory"] {
611 assert!(slots.contains(&expected), "missing {expected} in {slots:?}");
612 }
613 assert_eq!(audit_global_vm_unsafe_only(&ctx), Vec::new());
614 }
615
616 #[test]
617 fn detects_local_string_in_global_dict() {
618 let mut ctx = Context::new();
619 let gdict = ctx.dicts.allocate_with(4, b"gdict", 0, true, 0);
620 let lstr = ctx.strings.allocate_with(5, 0, false, 1);
622 let key = DictKey::Name(ctx.names.intern(b"k"));
623 ctx.dicts.put(gdict, key, PsObject::string(lstr, 5));
624
625 let found = audit_global_vm_unsafe_only(&ctx);
626 assert_eq!(found.len(), 1, "{found:?}");
627 assert_eq!(found[0].container, gdict);
628 assert_eq!(found[0].slot, "k");
629 assert_eq!(found[0].value_type, "stringtype");
630 assert!(found[0].target_reclaimable);
631 }
632
633 #[test]
634 fn detects_local_array_written_through_get_mut() {
635 let mut ctx = Context::new();
637 let garr = ctx.arrays.allocate_with(2, 0, true, 0);
638 let larr = ctx.arrays.allocate_with(1, 0, false, 1);
639 ctx.arrays.get_mut(garr, 0, 2)[1] = PsObject::array(larr, 1);
640
641 let found = audit_global_vm_unsafe_only(&ctx);
642 assert_eq!(found.len(), 1, "{found:?}");
643 assert_eq!(found[0].slot, "1");
644 assert_eq!(found[0].value_type, "arraytype");
645 }
646
647 #[test]
648 fn global_values_and_simple_values_are_clean() {
649 let mut ctx = Context::new();
650 let gdict = ctx.dicts.allocate_with(4, b"gdict", 0, true, 0);
651 let gstr = ctx.strings.allocate_with(5, 0, true, 0);
652 let k_str = DictKey::Name(ctx.names.intern(b"gs"));
653 let k_int = DictKey::Name(ctx.names.intern(b"n"));
654 ctx.dicts.put(gdict, k_str, PsObject::string(gstr, 5));
655 ctx.dicts.put(gdict, k_int, PsObject::int(7));
656
657 assert_eq!(audit_global_vm_unsafe_only(&ctx), Vec::new());
658 }
659
660 #[test]
661 fn local_container_holding_local_value_is_clean() {
662 let mut ctx = Context::new();
664 let ldict = ctx.dicts.allocate_at_level_zero(4, b"ldict");
665 let lstr = ctx.strings.allocate_with(5, 0, false, 1);
666 let key = DictKey::Name(ctx.names.intern(b"k"));
667 ctx.dicts.put(ldict, key, PsObject::string(lstr, 5));
668
669 assert_eq!(audit_global_vm_unsafe_only(&ctx), Vec::new());
670 }
671
672 #[test]
673 fn local_composite_entity_ignores_simple_objects() {
674 assert_eq!(local_composite_entity(&PsObject::int(3)), None);
675 assert_eq!(local_composite_entity(&PsObject::null()), None);
676 let named = PsObject {
677 value: PsValue::Name(crate::object::NameId(0)),
678 flags: ObjFlags::literal(),
679 };
680 assert_eq!(local_composite_entity(&named), None);
681 }
682
683 fn retired_dict(ctx: &Context) -> PsObject {
686 PsObject::dict(EntityId(ctx.dicts.local.entities.len() as u32))
687 }
688
689 #[test]
690 fn bootstrap_context_has_no_dangling_refs() {
691 assert_eq!(audit_dangling_refs(&Context::new()), Vec::new());
692 }
693
694 #[test]
695 fn gstate_font_reference_is_swept() {
696 let mut ctx = Context::new();
697 ctx.gstate.current_font = Some(retired_dict(&ctx));
698 let found = audit_dangling_refs(&ctx);
699 assert_eq!(found.len(), 1, "{found:?}");
700 assert_eq!(found[0].holder, "graphics state");
701 assert_eq!(found[0].slot, "current_font");
702 }
703
704 #[test]
705 fn gstate_stack_and_gstate_store_are_swept() {
706 let mut ctx = Context::new();
707 let stale = retired_dict(&ctx);
708 ctx.gstate_stack.push(crate::graphics_state::GstateEntry {
709 state: ctx.gstate.clone(),
710 saved_by_save: false,
711 });
712 ctx.gstate_stack[0].state.page_device = match stale.value {
713 PsValue::Dict(e) => Some(e),
714 _ => unreachable!(),
715 };
716 ctx.gstate_store.push(ctx.gstate.clone());
717 ctx.gstate_store[0].root_font = Some(stale);
718
719 let holders: Vec<&str> = audit_dangling_refs(&ctx)
720 .iter()
721 .map(|d| Box::leak(d.holder.clone().into_boxed_str()) as &str)
722 .collect();
723 assert!(holders.contains(&"gstate stack #0"), "{holders:?}");
724 assert!(holders.contains(&"gstate store #0"), "{holders:?}");
725 }
726
727 #[test]
730 fn nested_color_space_reference_is_swept() {
731 use crate::graphics_state::ColorSpace;
732 let mut ctx = Context::new();
733 let stale_entity = EntityId(ctx.dicts.local.entities.len() as u32);
734 ctx.gstate.color_space = ColorSpace::Separation {
735 name: b"Spot".to_vec(),
736 alt_space: Box::new(ColorSpace::ICCBased {
737 dict_entity: stale_entity,
738 n: 4,
739 profile_hash: None,
740 }),
741 tint_transform: PsObject::null(),
742 num_alt_components: 4,
743 };
744 let found = audit_dangling_refs(&ctx);
745 assert_eq!(found.len(), 1, "{found:?}");
746 assert_eq!(found[0].slot, "color_space.alt_space.dict");
747 }
748
749 #[test]
752 fn stale_gstate_object_is_swept() {
753 let mut ctx = Context::new();
754 let stale = PsObject {
755 value: PsValue::Gstate(0),
756 flags: ObjFlags::literal(),
757 };
758 assert_eq!(ctx.gstate_store.len(), 0);
759 ctx.o_stack.push(stale).unwrap();
760 let found = audit_dangling_refs(&ctx);
761 assert_eq!(found.len(), 1, "{found:?}");
762 assert_eq!(found[0].value_type, "gstatetype");
763 }
764
765 #[test]
766 fn entity_keyed_caches_are_swept() {
767 let mut ctx = Context::new();
768 let stale_entity = EntityId(ctx.dicts.local.entities.len() as u32);
769 ctx.form_cache
770 .insert(stale_entity, crate::display_list::DisplayList::new());
771 let found = audit_dangling_refs(&ctx);
772 assert_eq!(found.len(), 1, "{found:?}");
773 assert_eq!(found[0].holder, "form cache");
774 }
775}