From 895871e39f1f7f3308d9a16c8ef393682af677e1 Mon Sep 17 00:00:00 2001 From: Bernd Engelhardt Date: Sat, 5 Sep 2026 11:04:32 +0200 Subject: [PATCH] Allow the dynamic PDF-name cache to be cleared and bounded ASAtom keeps a process-wide cache of PDF names that are not one of the predefined constants. It never evicts, so a long-running process that validates documents with many unique names keeps accumulating them for its whole lifetime, with no way to reclaim the memory. Add ASAtom.clearCache() to drop the accumulated dynamic names (the predefined constants are untouched), and an optional upper bound via setMaxCachedNames beyond which new names are returned uncached. Atom equality is by value, so an uncached atom behaves identically to a cached one; no code compares atoms by identity except against the predefined constants, which are unaffected. Default is unbounded with no automatic clearing, so behaviour is unchanged unless a caller opts in. --- src/main/java/org/verapdf/as/ASAtom.java | 49 ++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/verapdf/as/ASAtom.java b/src/main/java/org/verapdf/as/ASAtom.java index 321dd07b..f19a7ab9 100644 --- a/src/main/java/org/verapdf/as/ASAtom.java +++ b/src/main/java/org/verapdf/as/ASAtom.java @@ -38,6 +38,48 @@ public class ASAtom implements Comparable { private static final Map PREDEFINED_PDF_NAMES = Collections.synchronizedMap(new HashMap<>()); private static final Map CACHED_PDF_NAMES = Collections.synchronizedMap(new HashMap<>()); + /** + * Optional upper bound on the number of dynamically encountered PDF names kept in + * {@link #CACHED_PDF_NAMES}. The cache is a process-wide memoization of names that are not one of + * the predefined constants; it never evicts, so a stream of documents with many unique names keeps + * it growing for the lifetime of the process. A value of {@code -1} (the default) keeps the + * historical unbounded behaviour; a non-negative value stops adding new names once the cache holds + * that many, in which case {@link #getASAtom(String)} still returns a correct atom, just an + * uncached one. Equality of atoms is by value ({@link #equals(Object)}), so an uncached atom + * behaves identically to a cached one. + */ + private static volatile int maxCachedNames = -1; + + /** + * Sets the upper bound on the number of cached dynamic PDF names. A negative value removes the + * bound (the default). + * + * @param max maximum number of cached dynamic names, or a negative value for no bound + */ + public static void setMaxCachedNames(int max) { + maxCachedNames = max; + } + + /** + * @return the current upper bound on cached dynamic names, or {@code -1} if unbounded + */ + public static int getMaxCachedNames() { + return maxCachedNames; + } + + /** + * Clears the cache of dynamically encountered PDF names. The predefined name constants are not + * affected. A long-running process that validates untrusted documents can call this between jobs to + * release names accumulated from earlier documents. + */ + public static void clearCache() { + CACHED_PDF_NAMES.clear(); + } + + private static boolean isCacheBelowLimit() { + return maxCachedNames < 0 || CACHED_PDF_NAMES.size() < maxCachedNames; + } + // 3 public static final ASAtom key3D = new ASAtom("3D"); public static final ASAtom key3DD = new ASAtom("3DD"); @@ -696,7 +738,7 @@ private ASAtom(String value, boolean predefinedValue) { if (predefinedValue) { PREDEFINED_PDF_NAMES.put(value, this); } else { - if (!CACHED_PDF_NAMES.containsKey(value)) { + if (isCacheBelowLimit() && !CACHED_PDF_NAMES.containsKey(value)) { CACHED_PDF_NAMES.put(value, this); } } @@ -719,9 +761,8 @@ public static ASAtom getASAtom(String value) { if (CACHED_PDF_NAMES.containsKey(value)) { return CACHED_PDF_NAMES.get(value); } - ASAtom result = new ASAtom(value, false); - CACHED_PDF_NAMES.put(value, result); - return result; + // The constructor is the single caching point; it honours the configured cache limit. + return new ASAtom(value, false); } /**