Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Fix `date` text field formatting emitting invalid JavaScript, so the format was never applied. Fixes #1546
- Fix `indentAllLines` applying the indent again on every paragraph and every page break, and keep it applied across continued text. Fixes #1606
- Fix a hole in a sparse array being skipped entirely, which shifted every later entry down one
- Encrypt strings inside name trees. Fixes #1513

### [v0.19.1] - 2026-06-10

Expand Down
5 changes: 4 additions & 1 deletion lib/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,12 @@ class PDFObject {
// Byte arrays are converted to PDF hex strings
} else if (object instanceof Uint8Array) {
return `<${bytesToHex(object)}>`;
} else if (object instanceof PDFTree) {
// Name trees hold PDF strings as keys, which have to be encrypted like
// any other string in the document
return object.toString(encryptFn);
} else if (
object instanceof PDFAbstractReference ||
object instanceof PDFTree ||
object instanceof SpotColor
) {
return object.toString();
Expand Down
7 changes: 4 additions & 3 deletions lib/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class PDFTree {
return this._items[key];
}

toString() {
toString(encryptFn = null) {
// Needs to be sorted by key
const sortedKeys = Object.keys(this._items).sort((a, b) =>
this._compareKeys(a, b),
Expand All @@ -30,14 +30,15 @@ class PDFTree {
const first = sortedKeys[0],
last = sortedKeys[sortedKeys.length - 1];
out.push(
` /Limits ${PDFObject.convert([this._dataForKey(first), this._dataForKey(last)])}`,
` /Limits ${PDFObject.convert([this._dataForKey(first), this._dataForKey(last)], encryptFn)}`,
);
}
out.push(` /${this._keysName()} [`);
for (let key of sortedKeys) {
out.push(
` ${PDFObject.convert(this._dataForKey(key))} ${PDFObject.convert(
` ${PDFObject.convert(this._dataForKey(key), encryptFn)} ${PDFObject.convert(
this._items[key],
encryptFn,
)}`,
);
}
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/tree.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import PDFDocument from '../../lib/document';
import PDFNameTree from '../../lib/name_tree';
import PDFNumberTree from '../../lib/number_tree';
import PDFObject from '../../lib/object';
import { fromBinaryString, toBinaryString } from '../../lib/binary';
import { collectPdf } from './helpers';

// Stand-in for a real cipher: uppercasing keeps the expected output readable
// while still showing that every string went through it individually
const encryptFn = (bytes) =>
fromBinaryString(toBinaryString(bytes).toUpperCase());

describe.each([
[
'PDFNameTree',
{
Tree: PDFNameTree,
keys: ['a', 'b'],
plain: `<<
/Limits [(a) (b)]
/Names [
(a) (one)
(b) (two)
]
>>`,
// the keys are PDF strings, so they get encrypted just like the values
encrypted: `<<
/Limits [(A) (B)]
/Names [
(A) (ONE)
(B) (TWO)
]
>>`,
},
],
[
'PDFNumberTree',
{
Tree: PDFNumberTree,
keys: [1, 2],
plain: `<<
/Limits [1 2]
/Nums [
1 (one)
2 (two)
]
>>`,
// the keys are numbers rather than strings, so they stay as they are
encrypted: `<<
/Limits [1 2]
/Nums [
1 (ONE)
2 (TWO)
]
>>`,
},
],
])('%s', (_treeName, { Tree, keys, plain, encrypted }) => {
let tree;

beforeEach(() => {
tree = new Tree();
tree.add(keys[0], new String('one'));
tree.add(keys[1], new String('two'));
});

test('is written in plain text without an encryption function', () => {
expect(PDFObject.convert(tree)).toEqual(plain);
});

test('is encrypted with an encryption function', () => {
expect(PDFObject.convert(tree, encryptFn)).toEqual(encrypted);
});
});

describe('name trees in a document', () => {
const names = ['(heading)', '(data.txt)', 'app.alert'];

const writeDocument = (options = {}) => {
const document = new PDFDocument(options);
document.text('link', { destination: 'heading' });
document.file(Buffer.from('example text'), { name: 'data.txt' });
document.addNamedJavaScript('hello', 'app.alert("hi")');
return collectPdf(document);
};

test.each(names)('writes %s in plain text when not encrypted', (name) => {
expect(writeDocument()).toContain(name);
});

test.each(names)('never writes %s when encrypted', (name) => {
expect(writeDocument({ userPassword: 'secret' })).not.toContain(name);
});
});
Loading