Skip to content

Commit 0fca64f

Browse files
committed
Check for having Ref before adding them in a RefSet (bug 2023106)
1 parent b7698d6 commit 0fca64f

6 files changed

Lines changed: 200 additions & 7 deletions

File tree

src/core/editor/pdf_editor.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,13 +1390,16 @@ class PDFEditor {
13901390
let parent = parentRef;
13911391
let lastNonNullParent = parentRef;
13921392
while (true) {
1393-
parent = xref.fetchIfRef(parent)?.get("Parent") || null;
1393+
parent = xref.fetchIfRef(parent)?.getRaw("Parent") || null;
13941394
if (!parent) {
13951395
break;
13961396
}
13971397
lastNonNullParent = parent;
13981398
}
1399-
if (!processed.has(lastNonNullParent)) {
1399+
if (
1400+
lastNonNullParent instanceof Ref &&
1401+
!processed.has(lastNonNullParent)
1402+
) {
14001403
newFields.push(lastNonNullParent);
14011404
processed.put(lastNonNullParent);
14021405
}

src/core/name_number_tree.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16-
import { Dict, RefSet } from "./primitives.js";
16+
import { Dict, Ref, RefSet } from "./primitives.js";
1717
import { FormatError, unreachable, warn } from "../shared/util.js";
1818

1919
/**
@@ -42,7 +42,9 @@ class NameOrNumberTree {
4242
const xref = this.xref;
4343
// Reading Name/Number tree.
4444
const processed = new RefSet();
45-
processed.put(this.root);
45+
if (this.root instanceof Ref) {
46+
processed.put(this.root);
47+
}
4648
const queue = [this.root];
4749
while (queue.length > 0) {
4850
const obj = xref.fetchIfRef(queue.shift());
@@ -55,11 +57,13 @@ class NameOrNumberTree {
5557
continue;
5658
}
5759
for (const kid of kids) {
58-
if (processed.has(kid)) {
59-
throw new FormatError(`Duplicate entry in "${this._type}" tree.`);
60+
if (kid instanceof Ref) {
61+
if (processed.has(kid)) {
62+
throw new FormatError(`Duplicate entry in "${this._type}" tree.`);
63+
}
64+
processed.put(kid);
6065
}
6166
queue.push(kid);
62-
processed.put(kid);
6367
}
6468
continue;
6569
}

src/core/primitives.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,24 @@ class RefSet {
377377
}
378378

379379
has(ref) {
380+
if (
381+
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
382+
!(ref instanceof Ref) &&
383+
typeof ref !== "string"
384+
) {
385+
unreachable('RefSet: Invalid "ref" value in has.');
386+
}
380387
return this._set.has(ref.toString());
381388
}
382389

383390
put(ref) {
391+
if (
392+
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
393+
!(ref instanceof Ref) &&
394+
typeof ref !== "string"
395+
) {
396+
unreachable('RefSet: Invalid "ref" value in put.');
397+
}
384398
this._set.add(ref.toString());
385399
}
386400

test/unit/clitests.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"message_handler_spec.js",
3232
"metadata_spec.js",
3333
"murmurhash3_spec.js",
34+
"name_number_tree_spec.js",
3435
"network_utils_spec.js",
3536
"node_stream_spec.js",
3637
"obj_bin_transform_spec.js",

test/unit/jasmine-boot.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ async function initializePDFJS(callback) {
7474
"pdfjs-test/unit/message_handler_spec.js",
7575
"pdfjs-test/unit/metadata_spec.js",
7676
"pdfjs-test/unit/murmurhash3_spec.js",
77+
"pdfjs-test/unit/name_number_tree_spec.js",
7778
"pdfjs-test/unit/network_spec.js",
7879
"pdfjs-test/unit/network_utils_spec.js",
7980
"pdfjs-test/unit/obj_bin_transform_spec.js",

test/unit/name_number_tree_spec.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/* Copyright 2026 Mozilla Foundation
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import { Dict, Ref } from "../../src/core/primitives.js";
17+
import { NameTree, NumberTree } from "../../src/core/name_number_tree.js";
18+
import { XRefMock } from "./test_utils.js";
19+
20+
describe("NameOrNumberTree", function () {
21+
describe("NameTree", function () {
22+
it("should return an empty map when root is null", function () {
23+
const xref = new XRefMock([]);
24+
const tree = new NameTree(null, xref);
25+
expect(tree.getAll().size).toEqual(0);
26+
});
27+
28+
it("should collect all entries from a flat tree", function () {
29+
const root = new Dict();
30+
root.set("Names", ["alpha", "value_a", "beta", "value_b"]);
31+
32+
const xref = new XRefMock([]);
33+
const tree = new NameTree(root, xref);
34+
const map = tree.getAll();
35+
36+
expect(map.size).toEqual(2);
37+
expect(map.get("alpha")).toEqual("value_a");
38+
expect(map.get("beta")).toEqual("value_b");
39+
});
40+
41+
it("should collect all entries from a tree with Ref-based Kids", function () {
42+
const leafRef = Ref.get(10, 0);
43+
const leaf = new Dict();
44+
leaf.set("Names", ["key1", "val1", "key2", "val2"]);
45+
46+
const root = new Dict();
47+
root.set("Kids", [leafRef]);
48+
49+
const xref = new XRefMock([{ ref: leafRef, data: leaf }]);
50+
const tree = new NameTree(root, xref);
51+
const map = tree.getAll();
52+
53+
expect(map.size).toEqual(2);
54+
expect(map.get("key1")).toEqual("val1");
55+
expect(map.get("key2")).toEqual("val2");
56+
});
57+
58+
it("should handle Kids containing inline (non-Ref) Dict nodes without throwing", function () {
59+
// Regression test: before the fix, processed.put() was called on non-Ref
60+
// Dict objects, causing an error in TESTING mode because RefSet only
61+
// accepts Ref instances or ref strings.
62+
const inlineLeaf = new Dict();
63+
inlineLeaf.set("Names", ["key1", "val1", "key2", "val2"]);
64+
65+
const root = new Dict();
66+
root.set("Kids", [inlineLeaf]);
67+
68+
const xref = new XRefMock([]);
69+
const tree = new NameTree(root, xref);
70+
71+
// Should not throw even though the kid is an inline Dict (not a Ref).
72+
const map = tree.getAll();
73+
expect(map.size).toEqual(2);
74+
expect(map.get("key1")).toEqual("val1");
75+
expect(map.get("key2")).toEqual("val2");
76+
});
77+
78+
it("should throw on duplicate Ref entries in Kids", function () {
79+
const leafRef = Ref.get(20, 0);
80+
const leaf = new Dict();
81+
leaf.set("Names", ["a", "b"]);
82+
83+
const root = new Dict();
84+
root.set("Kids", [leafRef, leafRef]);
85+
86+
const xref = new XRefMock([{ ref: leafRef, data: leaf }]);
87+
const tree = new NameTree(root, xref);
88+
89+
expect(() => tree.getAll()).toThrow(
90+
new Error('Duplicate entry in "Names" tree.')
91+
);
92+
});
93+
94+
it("should resolve Ref values when isRaw is false", function () {
95+
const valRef = Ref.get(30, 0);
96+
const valData = "resolved_value";
97+
98+
const root = new Dict();
99+
root.set("Names", ["mykey", valRef]);
100+
101+
const xref = new XRefMock([{ ref: valRef, data: valData }]);
102+
const tree = new NameTree(root, xref);
103+
104+
const map = tree.getAll();
105+
expect(map.get("mykey")).toEqual("resolved_value");
106+
});
107+
108+
it("should keep raw Ref values when isRaw is true", function () {
109+
const valRef = Ref.get(31, 0);
110+
111+
const root = new Dict();
112+
root.set("Names", ["mykey", valRef]);
113+
114+
const xref = new XRefMock([{ ref: valRef, data: "resolved_value" }]);
115+
const tree = new NameTree(root, xref);
116+
117+
const map = tree.getAll(/* isRaw = */ true);
118+
expect(map.get("mykey")).toBe(valRef);
119+
});
120+
});
121+
122+
describe("NumberTree", function () {
123+
it("should collect all entries from a flat tree", function () {
124+
const root = new Dict();
125+
root.set("Nums", [1, "one", 2, "two"]);
126+
127+
const xref = new XRefMock([]);
128+
const tree = new NumberTree(root, xref);
129+
const map = tree.getAll();
130+
131+
expect(map.size).toEqual(2);
132+
expect(map.get(1)).toEqual("one");
133+
expect(map.get(2)).toEqual("two");
134+
});
135+
136+
it("should handle Kids containing inline (non-Ref) Dict nodes without throwing", function () {
137+
// Same regression as NameTree: non-Ref kids must not be passed to
138+
// RefSet.put().
139+
const inlineLeaf = new Dict();
140+
inlineLeaf.set("Nums", [0, "zero", 1, "one"]);
141+
142+
const root = new Dict();
143+
root.set("Kids", [inlineLeaf]);
144+
145+
const xref = new XRefMock([]);
146+
const tree = new NumberTree(root, xref);
147+
148+
const map = tree.getAll();
149+
expect(map.size).toEqual(2);
150+
expect(map.get(0)).toEqual("zero");
151+
expect(map.get(1)).toEqual("one");
152+
});
153+
154+
it("should throw on duplicate Ref entries in Kids", function () {
155+
const leafRef = Ref.get(40, 0);
156+
const leaf = new Dict();
157+
leaf.set("Nums", [5, "five"]);
158+
159+
const root = new Dict();
160+
root.set("Kids", [leafRef, leafRef]);
161+
162+
const xref = new XRefMock([{ ref: leafRef, data: leaf }]);
163+
const tree = new NumberTree(root, xref);
164+
165+
expect(() => tree.getAll()).toThrow(
166+
new Error('Duplicate entry in "Nums" tree.')
167+
);
168+
});
169+
});
170+
});

0 commit comments

Comments
 (0)