From f02ea646529cb3631226e5d97ac296ff38ac16b1 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Fri, 10 Nov 2023 18:32:42 +0200 Subject: [PATCH 01/15] Parsing the xref should not relate to the EOF --- .../src/__snapshots__/readRefTable.test.js.snap | 2 -- packages/placeholder-plain/src/readRefTable.test.js | 12 ------------ 2 files changed, 14 deletions(-) diff --git a/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap b/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap index 3d1517e2..48716abd 100644 --- a/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap +++ b/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap @@ -1,7 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getXref Throws an error when next EOF is not found 1`] = `"Expected EOF after xref and trailer but could not find one."`; - exports[`getXref Throws an error when size has unexpected value 1`] = `"Failed to parse size of xref table."`; exports[`getXref Throws an error when size is not found 1`] = `"Size not found in xref table."`; diff --git a/packages/placeholder-plain/src/readRefTable.test.js b/packages/placeholder-plain/src/readRefTable.test.js index a2450e20..b563ecac 100644 --- a/packages/placeholder-plain/src/readRefTable.test.js +++ b/packages/placeholder-plain/src/readRefTable.test.js @@ -51,18 +51,6 @@ describe(getXref, () => { expect(e.message).toMatchSnapshot(); } }); - it('Throws an error when next EOF is not found', () => { - const pdf = Buffer.from('xref\n then /Size 123 and then but no EOF'); - const position = 0; - try { - getXref(pdf, position); - expect('here').not.toBe('here'); - } catch (e) { - expect(e instanceof SignPdfError).toBe(true); - expect(e.type).toBe(SignPdfError.TYPE_PARSE); - expect(e.message).toMatchSnapshot(); - } - }); }); describe(readRefTable, () => { From 3f67d7bf4c34c4559c41aacf3cc52974a0788357 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Fri, 10 Nov 2023 18:34:31 +0200 Subject: [PATCH 02/15] =?UTF-8?q?=F0=9F=A7=AA=20Add=20a=20failing=20test?= =?UTF-8?q?=20to=20parse=20the=20resource=20from=20#79?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeholder-plain/src/readRefTable.test.js | 3 +++ resources/issue-79-test.pdf | Bin 0 -> 1770 bytes 2 files changed, 3 insertions(+) create mode 100644 resources/issue-79-test.pdf diff --git a/packages/placeholder-plain/src/readRefTable.test.js b/packages/placeholder-plain/src/readRefTable.test.js index b563ecac..a0a8c457 100644 --- a/packages/placeholder-plain/src/readRefTable.test.js +++ b/packages/placeholder-plain/src/readRefTable.test.js @@ -3,6 +3,9 @@ import {SignPdfError} from '@signpdf/utils'; import readRefTable, {getXref} from './readRefTable'; describe(getXref, () => { + it('works for #79', () => { + expect(getXref(readTestResource('issue-79-test.pdf'))).toBeTruthy(); + }); it('Throws an error when xref is not found at position', () => { const pdf = Buffer.from('Not containing an X R E F.'); const position = 0; diff --git a/resources/issue-79-test.pdf b/resources/issue-79-test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..739c7774e8b7daffc4257119943fd34eeaef950c GIT binary patch literal 1770 zcmaJ>dr%a09LHBQrzWLLIg`2`gM@jvkJ~%$*5gRLI}kRvK~faHu)mg6H+Ytm7R95D?w@`10sn97-IOe&HY+dB|Q$^GlQ-{1H9`F_67 z^gDg1gWH|-Mu#+PdU38LGi<=9)-0~G&t!I14_D9MWQK!5oMlKY@{S{EJa4~V2Ng| zaNkqZic-6$h;ldi0fT6j;DP`63Tw(F#Z5Rc*Hrvx@@tddJ@TQSXN7r+8F0 zUUIp5(-HjoSP}J<$ipF0Qd2=-o_{QxKG&x^s%fh|a=8e*NCl#$nQ6BL0xV59j20ti zo|dLV(;P+}?m(QnOtTqLe^I9qLr}ZPVYI?e_(stPmq@criCvv|8-`$r4l(L5Yz}HB z5EEg*bqG!%NX)mtWp|bOW&eNWP#ZFEPU?eN3`2CPRFTM&MLIwM*)}aRbHM`0$qFDJ zkTj4XRAv4jiveQCnxhV;+P&T}w)L%nx>4yV!ITW!09$5wY|C;z#RUov=Rj%dHIV%Z zgd+o-g-3gbBUs_<%Erqg*|?F@&Z0sX#kOk+jOHnO2QuiQ~nu`_An z@vf_dn`a%|Ut5*Z@YnOzRrT)cw?B5C{O$|0ZP_U++Jciwk(C>EwGXb&!Y9HHE}O-v z;gKCVqgS_!YfC0fm|pQ8uHu^-@Jct$oUbSTfeP{9qpxu_^8IS9h>iuZ@CdED@017jGRwi zegW*<=Ps`g%^DFI)-dU_+?v*NUGa;yx9<9C#EHG!i~oF@b^F3KCBE5U8h!7>MPIvX zUVH1OlZq*jS9h&_^@hDz=CXTp8*6Xw==!CrEhr4V@#@BkaV@pye!W}SiZ`DbGNp-) z9B%FiE*v>!Z|AYWmB=1tfPVM8`?0G!KOZn~0b2?nG!34&5QQ)Zb;J3hsHhSG<-;=t zMuehMqKfu~uN;KDN2M^Au_q-f;pm+&t5ToTxSuvK7ES+wp^DVcb88ssgqXn|24YD1 zmUsCes6KJcm6yhvmUph7;*l&)lj)hb$?=1HPv+xSNLdNVfYG2v9MdExJ93@>0uMc- A$^ZZW literal 0 HcmV?d00001 From 661e8ff1ef5a526962dfcab0531248f76eece4a0 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Fri, 10 Nov 2023 18:41:39 +0200 Subject: [PATCH 03/15] =?UTF-8?q?=F0=9F=92=A9=20Prepare=20to=20implement?= =?UTF-8?q?=20readXrefStreamAt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-config/eslint.js | 10 ++- .../__snapshots__/readRefTable.test.js.snap | 4 +- .../placeholder-plain/src/readRefTable.js | 68 +++++++++++++------ 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/packages/eslint-config/eslint.js b/packages/eslint-config/eslint.js index 7aca630a..db7f01fe 100644 --- a/packages/eslint-config/eslint.js +++ b/packages/eslint-config/eslint.js @@ -34,7 +34,15 @@ module.exports = { "jest/no-conditional-expect": "off", "import/prefer-default-export": "off", "no-bitwise": "off", - "jest/valid-title": ["error", {"ignoreTypeOfDescribeName": true}] + "jest/valid-title": ["error", {"ignoreTypeOfDescribeName": true}], + "no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] }, settings: { "import/resolver": { diff --git a/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap b/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap index 48716abd..73d421c9 100644 --- a/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap +++ b/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap @@ -4,9 +4,9 @@ exports[`getXref Throws an error when size has unexpected value 1`] = `"Failed t exports[`getXref Throws an error when size is not found 1`] = `"Size not found in xref table."`; -exports[`getXref Throws an error when xref is not at its expected position 1`] = `"Expected xref at 2 but found other content."`; +exports[`getXref Throws an error when xref is not at its expected position 1`] = `"Cross-Reference Streams not yet implemented."`; -exports[`getXref Throws an error when xref is not found at position 1`] = `"Could not find xref anywhere at or after 0."`; +exports[`getXref Throws an error when xref is not found at position 1`] = `"Cross-Reference Streams not yet implemented."`; exports[`readRefTable Expects to merge correctly the refTable of resources 1`] = ` Object { diff --git a/packages/placeholder-plain/src/readRefTable.js b/packages/placeholder-plain/src/readRefTable.js index 5bded0d9..4102d932 100644 --- a/packages/placeholder-plain/src/readRefTable.js +++ b/packages/placeholder-plain/src/readRefTable.js @@ -12,35 +12,26 @@ export const getLastTrailerPosition = (pdf) => { return parseInt(xRefPosition); }; -export const getXref = (pdf, position) => { - let refTable = pdf.slice(position); // slice starting from where xref starts +/** + * @param {Buffer} pdfSlice + * @param {number} position + * @returns {GetXRefReturnType | null} + */ +const readXrefTableAt = (pdfSlice, position) => { + let refTable = pdfSlice.subarray(position); // slice starting from where xref starts const realPosition = refTable.indexOf(Buffer.from('xref', 'utf8')); if (realPosition === -1) { - throw new SignPdfError( - `Could not find xref anywhere at or after ${position}.`, - SignPdfError.TYPE_PARSE, - ); + return null; } if (realPosition > 0) { - const prefix = refTable.slice(0, realPosition); + const prefix = refTable.subarray(0, realPosition); if (prefix.toString().replace(/\s*/g, '') !== '') { - throw new SignPdfError( - `Expected xref at ${position} but found other content.`, - SignPdfError.TYPE_PARSE, - ); + return null; } } - const nextEofPosition = refTable.indexOf(Buffer.from('%%EOF', 'utf8')); - if (nextEofPosition === -1) { - throw new SignPdfError( - 'Expected EOF after xref and trailer but could not find one.', - SignPdfError.TYPE_PARSE, - ); - } - refTable = refTable.slice(0, nextEofPosition); - refTable = refTable.slice(realPosition + 4); // move ahead with the "xref" - refTable = refTable.slice(refTable.indexOf('\n') + 1); // move after the next new line + // move ahead with the "xref\n" + refTable = refTable.subarray(realPosition + 5); // extract the size let size = refTable.toString().split('/Size')[1]; @@ -81,6 +72,41 @@ export const getXref = (pdf, position) => { }; }; +/** + * @param {Buffer} _pdfSlice + * @param {number} _position + * @returns {GetXRefReturnType | null} + */ +const readXrefStreamAt = (_pdfSlice, _position) => { + throw new SignPdfError( + 'Cross-Reference Streams not yet implemented.', + SignPdfError.TYPE_PARSE, + ); +}; + +/** + * @typedef {object} GetXRefReturnType + * // TODO + */ + +/** + * @param {Buffer} pdf + * @param {number} position + * @returns {GetXRefReturnType} + * @throws {SignPdfError} + */ +export const getXref = (pdf, position) => { + const table = readXrefTableAt(pdf, position) + || readXrefStreamAt(pdf, position); + if (!table) { + throw new SignPdfError( + `Could not find xref anywhere at or after startxref position ${position}.`, + SignPdfError.TYPE_PARSE, + ); + } + return table; +}; + /** * @typedef {Map<*, *>} GetFullXrefTableReturnType */ From 1c9e4c3f36212e02286d31be7737c26b4ae6fe28 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Sat, 11 Nov 2023 00:26:00 +0200 Subject: [PATCH 04/15] Separate findObjectAt out of findObject to allow searching by position --- packages/placeholder-plain/src/findObject.js | 39 ++++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/placeholder-plain/src/findObject.js b/packages/placeholder-plain/src/findObject.js index b1ff9691..0d564b2b 100644 --- a/packages/placeholder-plain/src/findObject.js +++ b/packages/placeholder-plain/src/findObject.js @@ -1,5 +1,36 @@ import getIndexFromRef from './getIndexFromRef'; +/** + * @typedef {object} FindObjectAtReturnType + * @property {Buffer} dictionary + * @property {Buffer} stream + */ + +/** + * @param {Buffer} pdf + * @param {number} position + * @returns {FindObjectAtReturnType} + */ +export const findObjectAt = (pdf, position) => { + let slice = pdf.subarray(position); + slice = slice.subarray(0, slice.indexOf('endobj', 'utf8') - 1); + // ^ Buffer from the start position until the first endobj (included). + + const dictionary = slice.subarray( + slice.indexOf('<<', 'utf8') + 2, + slice.indexOf('>>', 'utf8') - 1, + ); + const stream = slice.subarray( + slice.indexOf('stream', 'utf8') + 6, + slice.indexOf('endstream', 'utf8') - 1, + ); + + return { + dictionary, + stream, + }; +}; + /** * @param {Buffer} pdf * @param {Map} refTable @@ -9,13 +40,7 @@ const findObject = (pdf, refTable, ref) => { const index = getIndexFromRef(refTable, ref); const offset = refTable.offsets.get(index); - let slice = pdf.slice(offset); - slice = slice.slice(0, slice.indexOf('endobj', 'utf8')); - - // FIXME: What if it is a stream? - slice = slice.slice(slice.indexOf('<<', 'utf8') + 2); - slice = slice.slice(0, slice.lastIndexOf('>>', 'utf8')); - return slice; + return findObjectAt(pdf, offset).stream; }; export default findObject; From 0f1bf61e5c3e2a73620117070749be4ead7cfb02 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Sat, 11 Nov 2023 00:27:33 +0200 Subject: [PATCH 05/15] Fetch the cross-reference stream object --- .../placeholder-plain/src/readRefTable.js | 37 +++++++++++++------ .../src/readRefTable.test.js | 9 +++-- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/placeholder-plain/src/readRefTable.js b/packages/placeholder-plain/src/readRefTable.js index 4102d932..6fcea5a7 100644 --- a/packages/placeholder-plain/src/readRefTable.js +++ b/packages/placeholder-plain/src/readRefTable.js @@ -1,15 +1,28 @@ import {SignPdfError} from '@signpdf/utils'; import xrefToRefMap from './xrefToRefMap'; +import {findObjectAt} from './findObject'; -export const getLastTrailerPosition = (pdf) => { - const trailerStart = pdf.lastIndexOf(Buffer.from('trailer', 'utf8')); - const trailer = pdf.slice(trailerStart, pdf.length - 6); - - const xRefPosition = trailer - .slice(trailer.lastIndexOf(Buffer.from('startxref', 'utf8')) + 10) - .toString(); - - return parseInt(xRefPosition); +/** + * @param {Buffer} pdf + * @returns {number} + */ +export const getLastXrefPosition = (pdf) => { + const xRefPosition = pdf + .subarray( + pdf.lastIndexOf(Buffer.from('startxref', 'utf8')) + 10, + pdf.lastIndexOf(Buffer.from('%%EOF', 'utf8')), + ) + .toString() + .trim(); + + const lastXrefPosition = parseInt(xRefPosition); + if (`${lastXrefPosition}` !== xRefPosition) { + throw new SignPdfError( + `Expected an integer startxref position but got ${xRefPosition} instead.`, + SignPdfError.TYPE_PARSE, + ); + } + return lastXrefPosition; }; /** @@ -77,7 +90,9 @@ const readXrefTableAt = (pdfSlice, position) => { * @param {number} _position * @returns {GetXRefReturnType | null} */ -const readXrefStreamAt = (_pdfSlice, _position) => { +const readXrefStreamAt = (pdfSlice, position) => { + const {dictionary: _d, stream: _s} = findObjectAt(pdfSlice, position); + throw new SignPdfError( 'Cross-Reference Streams not yet implemented.', SignPdfError.TYPE_PARSE, @@ -116,7 +131,7 @@ export const getXref = (pdf, position) => { * @returns {GetFullXrefTableReturnType} */ export const getFullXrefTable = (pdf) => { - const lastTrailerPosition = getLastTrailerPosition(pdf); + const lastTrailerPosition = getLastXrefPosition(pdf); const lastXrefTable = getXref(pdf, lastTrailerPosition); if (lastXrefTable.prev === undefined) { diff --git a/packages/placeholder-plain/src/readRefTable.test.js b/packages/placeholder-plain/src/readRefTable.test.js index a0a8c457..96c771ac 100644 --- a/packages/placeholder-plain/src/readRefTable.test.js +++ b/packages/placeholder-plain/src/readRefTable.test.js @@ -1,11 +1,14 @@ import {readTestResource} from '@signpdf/internal-utils'; import {SignPdfError} from '@signpdf/utils'; -import readRefTable, {getXref} from './readRefTable'; +import readRefTable, {getFullXrefTable, getXref} from './readRefTable'; -describe(getXref, () => { +describe(getFullXrefTable, () => { it('works for #79', () => { - expect(getXref(readTestResource('issue-79-test.pdf'))).toBeTruthy(); + expect(getFullXrefTable(readTestResource('issue-79-test.pdf'))).toBeTruthy(); }); +}); + +describe(getXref, () => { it('Throws an error when xref is not found at position', () => { const pdf = Buffer.from('Not containing an X R E F.'); const position = 0; From 7ff6a320d744021665191a36cd492a3b2c203df2 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Sat, 11 Nov 2023 00:54:25 +0200 Subject: [PATCH 06/15] Split getValue out of readPdf to reuse it for reading dictionaries --- packages/placeholder-plain/src/getValue.js | 19 +++++++++++++++++ .../src/{readPdf.test.js => getValue.test.js} | 2 +- packages/placeholder-plain/src/readPdf.js | 21 +------------------ 3 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 packages/placeholder-plain/src/getValue.js rename packages/placeholder-plain/src/{readPdf.test.js => getValue.test.js} (94%) diff --git a/packages/placeholder-plain/src/getValue.js b/packages/placeholder-plain/src/getValue.js new file mode 100644 index 00000000..801b7b9d --- /dev/null +++ b/packages/placeholder-plain/src/getValue.js @@ -0,0 +1,19 @@ +/** + * @param {Buffer} trailer + * @param {string} key + * @returns {string} + */ +export const getValue = (trailer, key) => { + let index = trailer.indexOf(key); + + if (index === -1) { + return undefined; + } + + const slice = trailer.subarray(index); + index = slice.indexOf('/', 1); + if (index === -1) { + index = slice.indexOf('>', 1); + } + return slice.subarray(key.length + 1, index).toString().trim(); // key + at least one space +}; diff --git a/packages/placeholder-plain/src/readPdf.test.js b/packages/placeholder-plain/src/getValue.test.js similarity index 94% rename from packages/placeholder-plain/src/readPdf.test.js rename to packages/placeholder-plain/src/getValue.test.js index 30b8f550..3689a3c0 100644 --- a/packages/placeholder-plain/src/readPdf.test.js +++ b/packages/placeholder-plain/src/getValue.test.js @@ -1,4 +1,4 @@ -import {getValue} from './readPdf'; +import {getValue} from './getValue'; describe(getValue, () => { it('matches snapshots', () => { diff --git a/packages/placeholder-plain/src/readPdf.js b/packages/placeholder-plain/src/readPdf.js index ebbebba9..4997908c 100644 --- a/packages/placeholder-plain/src/readPdf.js +++ b/packages/placeholder-plain/src/readPdf.js @@ -1,25 +1,6 @@ import readRefTable from './readRefTable'; import findObject from './findObject'; - -/** - * @param {Buffer} trailer - * @param {string} key - * @returns {string} - */ -export const getValue = (trailer, key) => { - let index = trailer.indexOf(key); - - if (index === -1) { - return undefined; - } - - const slice = trailer.slice(index); - index = slice.indexOf('/', 1); - if (index === -1) { - index = slice.indexOf('>', 1); - } - return slice.slice(key.length + 1, index).toString().trim(); // key + at least one space -}; +import {getValue} from './getValue'; /** * @typedef {object} ReadPdfReturnType From 0d45df1ee4ad6062cec2ca101039abc7ac1c804a Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Mon, 13 Nov 2023 10:57:28 +0200 Subject: [PATCH 07/15] =?UTF-8?q?=F0=9F=92=A9=20Should=20I=20parse=20dicti?= =?UTF-8?q?onaries=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/placeholder-plain/src/getValue.js | 12 ++++++++++++ packages/placeholder-plain/src/readRefTable.js | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/placeholder-plain/src/getValue.js b/packages/placeholder-plain/src/getValue.js index 801b7b9d..2c637281 100644 --- a/packages/placeholder-plain/src/getValue.js +++ b/packages/placeholder-plain/src/getValue.js @@ -2,6 +2,18 @@ * @param {Buffer} trailer * @param {string} key * @returns {string} + * + * FIXME: + * 0 00 000 Null (NUL) + * 9 09 011 Tab (HT) + * 10 0A 012 Line feed (LF) + * 12 0C 014 Form feed (FF) + * 13 0D 015 Carriage return (CR) + * 32 20 040 Space (SP) + * + * The delimiter characters (, ), <, >, [, ], {, }, /, and % + * + * ^ All are terminators */ export const getValue = (trailer, key) => { let index = trailer.indexOf(key); diff --git a/packages/placeholder-plain/src/readRefTable.js b/packages/placeholder-plain/src/readRefTable.js index 6fcea5a7..577a21bc 100644 --- a/packages/placeholder-plain/src/readRefTable.js +++ b/packages/placeholder-plain/src/readRefTable.js @@ -1,6 +1,7 @@ import {SignPdfError} from '@signpdf/utils'; import xrefToRefMap from './xrefToRefMap'; import {findObjectAt} from './findObject'; +// import {getValue} from './getValue'; /** * @param {Buffer} pdf @@ -93,6 +94,17 @@ const readXrefTableAt = (pdfSlice, position) => { const readXrefStreamAt = (pdfSlice, position) => { const {dictionary: _d, stream: _s} = findObjectAt(pdfSlice, position); + // const parsed = { + // size: getValue(_d, '/Size'), + // root: getValue(_d, '/Root'), + // info: getValue(_d, '/Info'), + // filter: getValue(_d, '/Filter'), + // length: getValue(_d, '/Length'), + // }; + + // console.log(_d.toString(), parsed); + // console.log(zlib.deflateSync(stream)); + throw new SignPdfError( 'Cross-Reference Streams not yet implemented.', SignPdfError.TYPE_PARSE, From 59c9e4c54d46bda51eeea5242f6f0093f6d6afcb Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Wed, 29 Nov 2023 17:20:34 +0200 Subject: [PATCH 08/15] previousXrefs not ever used --- packages/placeholder-plain/dist/readPdf.d.ts | 2 -- packages/placeholder-plain/dist/readPdf.js | 2 -- packages/placeholder-plain/src/createBufferTrailer.test.js | 1 - packages/placeholder-plain/src/readPdf.js | 2 -- 4 files changed, 7 deletions(-) diff --git a/packages/placeholder-plain/dist/readPdf.d.ts b/packages/placeholder-plain/dist/readPdf.d.ts index b5d2a226..27e24705 100644 --- a/packages/placeholder-plain/dist/readPdf.d.ts +++ b/packages/placeholder-plain/dist/readPdf.d.ts @@ -6,7 +6,6 @@ export type ReadPdfReturnType = { root: Buffer; infoRef: string; trailerStart: number; - previousXrefs: any[]; xRefPosition: number; }; /** @@ -16,7 +15,6 @@ export type ReadPdfReturnType = { * @prop {Buffer} root * @prop {string} infoRef * @prop {number} trailerStart - * @prop {*[]} previousXrefs * @prop {number} xRefPosition */ /** diff --git a/packages/placeholder-plain/dist/readPdf.js b/packages/placeholder-plain/dist/readPdf.js index 1ecee0b1..51dcbb33 100644 --- a/packages/placeholder-plain/dist/readPdf.js +++ b/packages/placeholder-plain/dist/readPdf.js @@ -32,7 +32,6 @@ const getValue = (trailer, key) => { * @prop {Buffer} root * @prop {string} infoRef * @prop {number} trailerStart - * @prop {*[]} previousXrefs * @prop {number} xRefPosition */ @@ -63,7 +62,6 @@ const readPdf = pdfBuffer => { root, infoRef, trailerStart, - previousXrefs: [], xRefPosition }; }; diff --git a/packages/placeholder-plain/src/createBufferTrailer.test.js b/packages/placeholder-plain/src/createBufferTrailer.test.js index 52c8d3c7..ebe6197c 100644 --- a/packages/placeholder-plain/src/createBufferTrailer.test.js +++ b/packages/placeholder-plain/src/createBufferTrailer.test.js @@ -19,7 +19,6 @@ describe(createBufferTrailer, () => { root: '\n/Type /Catalog\n/Pages 1 0 R\n/AcroForm 9 0 R\n', infoRef: '2 0 R', trailerStart: 4510, - previousXrefs: [], xRefPosition: 4220, }; const addedReferences = new Map(); diff --git a/packages/placeholder-plain/src/readPdf.js b/packages/placeholder-plain/src/readPdf.js index 4997908c..b76d974e 100644 --- a/packages/placeholder-plain/src/readPdf.js +++ b/packages/placeholder-plain/src/readPdf.js @@ -9,7 +9,6 @@ import {getValue} from './getValue'; * @prop {Buffer} root * @prop {string} infoRef * @prop {number} trailerStart - * @prop {*[]} previousXrefs * @prop {number} xRefPosition */ @@ -44,7 +43,6 @@ const readPdf = (pdfBuffer) => { root, infoRef, trailerStart, - previousXrefs: [], xRefPosition, }; }; From a15209872459e5c155425278085638f066352bb9 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Thu, 30 Nov 2023 12:45:31 +0200 Subject: [PATCH 09/15] =?UTF-8?q?=F0=9F=9A=A8=20Allow=20`continue`=20to=20?= =?UTF-8?q?be=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/eslint-config/eslint.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-config/eslint.js b/packages/eslint-config/eslint.js index db7f01fe..9139b3dd 100644 --- a/packages/eslint-config/eslint.js +++ b/packages/eslint-config/eslint.js @@ -34,6 +34,7 @@ module.exports = { "jest/no-conditional-expect": "off", "import/prefer-default-export": "off", "no-bitwise": "off", + "no-continue": "off", "jest/valid-title": ["error", {"ignoreTypeOfDescribeName": true}], "no-unused-vars": [ "error", From 9d95fad7e942c89af9c628d7c051c94c3e7ea71b Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Thu, 30 Nov 2023 12:53:39 +0200 Subject: [PATCH 10/15] Read xref table according to its line number statement --- .../src/readRefTable.test.js | 30 +++++++++++++++++++ .../placeholder-plain/src/xrefToRefMap.js | 30 ++++++++++++++----- .../src/xrefToRefMap.test.js | 15 ++++------ 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/placeholder-plain/src/readRefTable.test.js b/packages/placeholder-plain/src/readRefTable.test.js index 96c771ac..a473f995 100644 --- a/packages/placeholder-plain/src/readRefTable.test.js +++ b/packages/placeholder-plain/src/readRefTable.test.js @@ -6,6 +6,36 @@ describe(getFullXrefTable, () => { it('works for #79', () => { expect(getFullXrefTable(readTestResource('issue-79-test.pdf'))).toBeTruthy(); }); + it('skips unreferenced xref tables', () => { + const pdf = Buffer.from(`xref +0 3 +0000000000 65535 f +0000000123 00000 n +0000000234 00000 n + +xref +2 1 +0000000555 00000 n + +xref +0 1 +0000000666 00000 n + +trailer +<< +/Size 3 +/Root 1 0 R +>> +startxref +0 +%%EOF +`); + + expect(getFullXrefTable(pdf)).toEqual(new Map([ + [1, 123], + [2, 234], + ])); + }); }); describe(getXref, () => { diff --git a/packages/placeholder-plain/src/xrefToRefMap.js b/packages/placeholder-plain/src/xrefToRefMap.js index cec63ee7..a99042fa 100644 --- a/packages/placeholder-plain/src/xrefToRefMap.js +++ b/packages/placeholder-plain/src/xrefToRefMap.js @@ -1,29 +1,42 @@ import {SignPdfError} from '@signpdf/utils'; +/** + * @param {string} xrefString + * @returns {Map} + */ const xrefToRefMap = (xrefString) => { const lines = xrefString.split('\n').filter((l) => l !== ''); let index = 0; let expectedLines = 0; + + /** + * @type {Map} + */ const xref = new Map(); - lines.forEach((line) => { + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; const split = line.split(' '); if (split.length === 2) { + // We've found a line that states the next index and number of expected lines. + // e.g.: + // 39 35 # <--- this is the line + // 0000655893 00000 n + // 0000656101 00000 n index = parseInt(split[0]); expectedLines = parseInt(split[1]); - return; + continue; } if (expectedLines <= 0) { - throw new SignPdfError( - 'Too many lines in xref table.', - SignPdfError.TYPE_PARSE, - ); + // We don't expect any more lines. Whatever is left is not part of the xref table. + break; } expectedLines -= 1; const [offset, , inUse] = split; if (inUse.trim() === 'f') { + // unused object index += 1; - return; + continue; } if (inUse.trim() !== 'n') { throw new SignPdfError( @@ -37,10 +50,11 @@ const xrefToRefMap = (xrefString) => { SignPdfError.TYPE_PARSE, ); } + const storeOffset = parseInt(offset.trim()); xref.set(index, storeOffset); index += 1; - }); + } return xref; }; diff --git a/packages/placeholder-plain/src/xrefToRefMap.test.js b/packages/placeholder-plain/src/xrefToRefMap.test.js index c4cbcd05..68260b9a 100644 --- a/packages/placeholder-plain/src/xrefToRefMap.test.js +++ b/packages/placeholder-plain/src/xrefToRefMap.test.js @@ -85,23 +85,18 @@ const xrefStrings = [ 0000673269 00000 n`, ]; -describe('xrefToRefMap', () => { +describe(xrefToRefMap, () => { it('Predefined xrefs match their snapshots', () => { xrefStrings.forEach((xref) => { expect(xrefToRefMap(xref)).toMatchSnapshot(); }); }); - it('Throws an error when number of items does not match statement', () => { - try { - xrefToRefMap(`0 1 + it('Only reads the stated number of lines', () => { + const result = xrefToRefMap(`0 2 000 000 f 001 000 n -002 100 n`); - expect('here').not.toBe('here'); - } catch (e) { - expect(e instanceof SignPdfError).toBe(true); - expect(e.type).toBe(SignPdfError.TYPE_PARSE); - } +002 100 n`); // the third line is ignored + expect(result.size).toBe(1); }); it('Throws an error when unknown in-use flag is uses', () => { try { From 0360b34c678c7b5d48bc2810c8696c2b319e5078 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Thu, 30 Nov 2023 13:03:11 +0200 Subject: [PATCH 11/15] Confirm it reads the correct details from a ready resource --- packages/placeholder-plain/src/readPdf.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/placeholder-plain/src/readPdf.test.js diff --git a/packages/placeholder-plain/src/readPdf.test.js b/packages/placeholder-plain/src/readPdf.test.js new file mode 100644 index 00000000..ac683ad0 --- /dev/null +++ b/packages/placeholder-plain/src/readPdf.test.js @@ -0,0 +1,13 @@ +import {readTestResource} from '@signpdf/internal-utils'; +import readPdf from './readPdf'; + +describe(readPdf, () => { + it('reads contributing.pdf', () => { + const pdfBuffer = readTestResource('contributing.pdf'); + const result = readPdf(pdfBuffer); + + expect(result.xRefPosition).toBe(72203); + expect(result.rootRef).toBe('12 0 R'); + expect(result.infoRef).toBe('1 0 R'); + }); +}); From 823af74e215e0bfae5b3334be84802b9365a25b4 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Thu, 30 Nov 2023 13:04:14 +0200 Subject: [PATCH 12/15] =?UTF-8?q?=F0=9F=A7=AA=20Expect=20it=20to=20success?= =?UTF-8?q?fully=20parse=20a=20streamed=20resource=20that=20does=20not=20h?= =?UTF-8?q?ave=20the=20`trailer`=20keyword?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/placeholder-plain/src/readPdf.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/placeholder-plain/src/readPdf.test.js b/packages/placeholder-plain/src/readPdf.test.js index ac683ad0..4811494f 100644 --- a/packages/placeholder-plain/src/readPdf.test.js +++ b/packages/placeholder-plain/src/readPdf.test.js @@ -10,4 +10,12 @@ describe(readPdf, () => { expect(result.rootRef).toBe('12 0 R'); expect(result.infoRef).toBe('1 0 R'); }); + it('reads issue-79-test.pdf', () => { + const pdfBuffer = readTestResource('issue-79-test.pdf'); + const result = readPdf(pdfBuffer); + + expect(result.xRefPosition).toBe(1542); + expect(result.rootRef).toBe('2 0 R'); + expect(result.infoRef).toBe('3 0 R'); + }); }); From 2d4015e6b6042088e2314c33bfa12daadf1028d4 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Fri, 1 Dec 2023 12:02:52 +0200 Subject: [PATCH 13/15] =?UTF-8?q?=E2=9C=A8=20=F0=9F=A7=AA=20Parse=20a=20di?= =?UTF-8?q?ctionary.=20Just=20comments=20messing=20things=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeholder-plain/src/PdfDictionary.js | 229 ++++++++++++++++++ .../src/PdfDictionary.test.js | 47 ++++ 2 files changed, 276 insertions(+) create mode 100644 packages/placeholder-plain/src/PdfDictionary.js create mode 100644 packages/placeholder-plain/src/PdfDictionary.test.js diff --git a/packages/placeholder-plain/src/PdfDictionary.js b/packages/placeholder-plain/src/PdfDictionary.js new file mode 100644 index 00000000..16200ee2 --- /dev/null +++ b/packages/placeholder-plain/src/PdfDictionary.js @@ -0,0 +1,229 @@ +import {SignPdfError} from '@signpdf/utils'; + +const CHARS = { + NUL: 0, + HT: 9, + LF: 10, + FF: 12, + CR: 13, + SP: 32, + PERCENT: 37, + LPAREN: 40, + RPAREN: 41, + NUMSIGN: 43, + FSLASH: 47, + LT: 60, + GT: 62, + LSQUARE: 91, + RSQUARE: 93, + LBRACE: 123, + RBRACE: 125, +}; + +const spaces = [ + CHARS.NUL, + CHARS.HT, + CHARS.LF, + CHARS.FF, + CHARS.CR, + CHARS.SP, +]; + +const terminators = [ + CHARS.LPAREN, + CHARS.RPAREN, + CHARS.LT, + CHARS.GT, + CHARS.LSQUARE, + CHARS.RSQUARE, + CHARS.LBRACE, + CHARS.RBRACE, + CHARS.FSLASH, + CHARS.PERCENT, +]; + +const delimiter = [ + ...spaces, + ...terminators, +]; + +const pairs = [ + {open: CHARS.LPAREN, close: CHARS.RPAREN}, + {open: CHARS.LT, close: CHARS.GT}, + {open: CHARS.LSQUARE, close: CHARS.RSQUARE}, + {open: CHARS.LBRACE, close: CHARS.RBRACE}, +]; +const pairOpeners = pairs.map((pair) => pair.open); +const pairClosers = pairs.map((pair) => pair.close); + +export default class PdfDictionary { + /** + * @param {Buffer} buffer + */ + constructor(buffer) { + this.buffer = buffer; + + try { + this.map = this.parseBuffer(); + } catch (e) { + throw new SignPdfError( + `Failed to parse PDF dictionary: ${e.message}`, + SignPdfError.TYPE_PARSE, + ); + } + } + + toString() { + return this.buffer.toString(); + } + + get(key) { + return this.map.get(key); + } + + has(key) { + return this.map.has(key); + } + + #skipSpacesAt(start) { + let index = start; + while (index < this.buffer.length) { + const char = this.buffer[index]; + if (spaces.includes(char)) { + index += 1; + continue; + } + return index; + } + return index; + } + + /** + * @param {number} start + */ + #parseNameAt(start) { + let current = ''; + let index = this.#skipSpacesAt(start); + while (index < this.buffer.length) { + const char = this.buffer[index]; + if (current === '') { + if (char === CHARS.FSLASH) { + current += Buffer.from([char]).toString(); + index += 1; + continue; + } + + break; + } + + if (delimiter.includes(char)) { + return { + value: current, + lastIndex: index - 1, + }; + } + + current += Buffer.from([char]).toString(); + index += 1; + } + + return { + value: undefined, + lastIndex: index, + }; + } + + #parseValueAt(start) { + let current = ''; + let index = this.#skipSpacesAt(start); + + const parsedName = this.#parseNameAt(index); + if (parsedName.value !== undefined) { + return parsedName; + } + + const stack = []; + + while (index < this.buffer.length) { + const char = this.buffer[index]; + if (pairOpeners.includes(char)) { + stack.push(char); + } else if (stack.length > 0) { + if (pairClosers.includes(char)) { + const closerIndex = pairClosers.indexOf(char); + const lastOpener = stack.pop(); + const openerIndex = pairOpeners.indexOf(lastOpener); + if (openerIndex !== closerIndex) { + throw new SignPdfError( + `Unbalanced parentheses in PDF dictionary. Expected "${pairClosers[openerIndex]}" but got "${char}".`, + SignPdfError.TYPE_PARSE, + ); + } + } + } else if (char === CHARS.NUMSIGN) { + const lineEnd = this.buffer.indexOf(CHARS.LF, index); + current += this.buffer.subarray(index, lineEnd).toString(); + index = lineEnd + 1; + continue; + } else if (char === CHARS.FSLASH) { + return { + lastIndex: index - 1, + value: current.trim(), + }; + } + + current += Buffer.from([char]).toString(); + index += 1; + } + + if (stack.length > 0) { + const readable = stack.map((char) => Buffer.from([char]).toString()).join('", "'); + throw new SignPdfError( + `Unbalanced parentheses in PDF dictionary. Opened but did not close "${readable}".`, + SignPdfError.TYPE_PARSE, + ); + } + + return { + lastIndex: index, + value: current.trim(), + }; + } + + parseBuffer() { + const result = new Map(); + + let index = 0; + let key = ''; + let value = ''; + + while (index < this.buffer.length) { + const parsedKey = this.#parseNameAt(index); + console.debug({ + parsedKey, + rest: this.buffer.subarray(parsedKey.lastIndex + 1).toString(), + }); + if (parsedKey.value === undefined) { + console.debug({ + full: this.buffer.toString(), + read: this.buffer.subarray(0, parsedKey.lastIndex).toString(), + resultSoFar: result, + }); + throw new SignPdfError( + 'Failed to parse key in dictionary.', + SignPdfError.TYPE_PARSE, + ); + } + index = parsedKey.lastIndex + 1; + key = parsedKey.value; + + const parsedValue = this.#parseValueAt(index); + index = parsedValue.lastIndex + 1; + value = parsedValue.value; + + result.set(key, value); + } + + return result; + } +} diff --git a/packages/placeholder-plain/src/PdfDictionary.test.js b/packages/placeholder-plain/src/PdfDictionary.test.js new file mode 100644 index 00000000..f758520a --- /dev/null +++ b/packages/placeholder-plain/src/PdfDictionary.test.js @@ -0,0 +1,47 @@ +import {SignPdfError} from '@signpdf/utils'; +import PdfDictionary from './PdfDictionary'; + +describe(PdfDictionary, () => { + it('Throws an error when dictionary is not valid', () => { + const dictionary = Buffer.from('Not a dictionary.'); + try { + // eslint-disable-next-line no-new + new PdfDictionary(dictionary); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.type).toBe(SignPdfError.TYPE_PARSE); + expect(e.message).toMatchInlineSnapshot('"Failed to parse PDF dictionary: Failed to parse key in dictionary."'); + } + }); + it('Parses a dictionary', () => { + const dictionary = Buffer.from(' /Type /Catalog /Pages 2 0 R '); + const pdfDictionary = new PdfDictionary(dictionary); + + expect(pdfDictionary.get('/Type')).toBe('/Catalog'); + expect(pdfDictionary.get('/Pages')).toBe('2 0 R'); + }); + it('Parses a dictionary with spaces and different types of values', () => { + const dictionary = Buffer.from(` + /Key1 /Catalog + /Key2 2 0 R + /Key3 [3 0 R] + /Key4/Value + /Key5 (Value) + /Key6 <> # Subdictionary to mess things up + /Key7/Value7/Key8/Value8 # Multiple keys in a row and a comment + /Key9 + /Value9`); + const pdfDictionary = new PdfDictionary(dictionary); + + expect(pdfDictionary.get('/Key1')).toBe('/Catalog'); + expect(pdfDictionary.get('/Key2')).toBe('2 0 R'); + expect(pdfDictionary.get('/Key3')).toBe('[3 0 R]'); + expect(pdfDictionary.get('/Key4')).toBe('/Value'); + expect(pdfDictionary.get('/Key5')).toBe('(Value)'); + expect(pdfDictionary.get('/Key6')).toBe('<>'); + expect(pdfDictionary.get('/Key7')).toBe('/Value7'); + expect(pdfDictionary.get('/Key8')).toBe('/Value8'); + expect(pdfDictionary.get('/Key9')).toBe('/Value9'); + }); +}); From cbf7e17ad457f62065bade783fdfc3df8f77a5ec Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Fri, 1 Dec 2023 12:50:25 +0200 Subject: [PATCH 14/15] Parsing dictionary including comments --- .../placeholder-plain/src/PdfDictionary.js | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/packages/placeholder-plain/src/PdfDictionary.js b/packages/placeholder-plain/src/PdfDictionary.js index 16200ee2..39c5c67e 100644 --- a/packages/placeholder-plain/src/PdfDictionary.js +++ b/packages/placeholder-plain/src/PdfDictionary.js @@ -7,10 +7,10 @@ const CHARS = { FF: 12, CR: 13, SP: 32, + NUMSIGN: 35, PERCENT: 37, LPAREN: 40, RPAREN: 41, - NUMSIGN: 43, FSLASH: 47, LT: 60, GT: 62, @@ -29,6 +29,11 @@ const spaces = [ CHARS.SP, ]; +const newLines = [ + CHARS.LF, + CHARS.CR, +]; + const terminators = [ CHARS.LPAREN, CHARS.RPAREN, @@ -98,12 +103,36 @@ export default class PdfDictionary { return index; } + #skipCommentsAt(start) { + let index = start; + if (this.buffer[index] !== CHARS.NUMSIGN) { + return index; + } + + while (index < this.buffer.length) { + const char = this.buffer[index]; + index += 1; + if (newLines.includes(char)) { + return index; + } + } + + return index; + } + + #skipSpacesAndCommentsAt(start) { + let index = this.#skipSpacesAt(start); + index = this.#skipCommentsAt(index); + index = this.#skipSpacesAt(index); + return index; + } + /** * @param {number} start */ #parseNameAt(start) { let current = ''; - let index = this.#skipSpacesAt(start); + let index = start; while (index < this.buffer.length) { const char = this.buffer[index]; if (current === '') { @@ -127,6 +156,14 @@ export default class PdfDictionary { index += 1; } + if (index === this.buffer.length) { + // reached the end + return { + value: current, + lastIndex: index, + }; + } + return { value: undefined, lastIndex: index, @@ -135,7 +172,7 @@ export default class PdfDictionary { #parseValueAt(start) { let current = ''; - let index = this.#skipSpacesAt(start); + let index = start; const parsedName = this.#parseNameAt(index); if (parsedName.value !== undefined) { @@ -159,12 +196,14 @@ export default class PdfDictionary { SignPdfError.TYPE_PARSE, ); } + if (stack.length === 0) { + current += Buffer.from([char]).toString(); + return { + lastIndex: index, + value: current, + }; + } } - } else if (char === CHARS.NUMSIGN) { - const lineEnd = this.buffer.indexOf(CHARS.LF, index); - current += this.buffer.subarray(index, lineEnd).toString(); - index = lineEnd + 1; - continue; } else if (char === CHARS.FSLASH) { return { lastIndex: index - 1, @@ -198,17 +237,9 @@ export default class PdfDictionary { let value = ''; while (index < this.buffer.length) { + index = this.#skipSpacesAndCommentsAt(index); const parsedKey = this.#parseNameAt(index); - console.debug({ - parsedKey, - rest: this.buffer.subarray(parsedKey.lastIndex + 1).toString(), - }); if (parsedKey.value === undefined) { - console.debug({ - full: this.buffer.toString(), - read: this.buffer.subarray(0, parsedKey.lastIndex).toString(), - resultSoFar: result, - }); throw new SignPdfError( 'Failed to parse key in dictionary.', SignPdfError.TYPE_PARSE, @@ -217,6 +248,7 @@ export default class PdfDictionary { index = parsedKey.lastIndex + 1; key = parsedKey.value; + index = this.#skipSpacesAndCommentsAt(index); const parsedValue = this.#parseValueAt(index); index = parsedValue.lastIndex + 1; value = parsedValue.value; From 6c4bb13c7cbe041973737902e81b226f8796cc73 Mon Sep 17 00:00:00 2001 From: Valery Buchinsky Date: Fri, 1 Dec 2023 14:45:13 +0200 Subject: [PATCH 15/15] Use PdfDictionary to try and improve parsing --- .../__snapshots__/readRefTable.test.js.snap | 130 ++++++++---------- packages/placeholder-plain/src/findObject.js | 9 +- packages/placeholder-plain/src/getValue.js | 31 ----- .../placeholder-plain/src/getValue.test.js | 22 --- packages/placeholder-plain/src/readPdf.js | 39 +++--- .../placeholder-plain/src/readPdf.test.js | 36 +++-- .../placeholder-plain/src/readRefTable.js | 44 ++---- .../src/readRefTable.test.js | 42 +++--- 8 files changed, 140 insertions(+), 213 deletions(-) delete mode 100644 packages/placeholder-plain/src/getValue.js delete mode 100644 packages/placeholder-plain/src/getValue.test.js diff --git a/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap b/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap index 73d421c9..cfe9ebed 100644 --- a/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap +++ b/packages/placeholder-plain/src/__snapshots__/readRefTable.test.js.snap @@ -1,70 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getXref Throws an error when size has unexpected value 1`] = `"Failed to parse size of xref table."`; - -exports[`getXref Throws an error when size is not found 1`] = `"Size not found in xref table."`; - -exports[`getXref Throws an error when xref is not at its expected position 1`] = `"Cross-Reference Streams not yet implemented."`; - -exports[`getXref Throws an error when xref is not found at position 1`] = `"Cross-Reference Streams not yet implemented."`; - -exports[`readRefTable Expects to merge correctly the refTable of resources 1`] = ` -Object { - "maxIndex": 19, - "offsets": Map { - 1 => 19012, - 2 => 19, - 3 => 224, - 4 => 12330, - 5 => 244, - 6 => 11154, - 7 => 11176, - 8 => 11368, - 9 => 11709, - 10 => 11910, - 11 => 11943, - 12 => 12140, - 13 => 12196, - 14 => 18928, - 15 => 12494, - 17 => 13264, - 18 => 18742, - 19 => 18860, - }, - "startingIndex": 0, -} -`; - -exports[`readRefTable Expects to merge correctly the refTable of resources 2`] = ` -Object { - "maxIndex": 21, - "offsets": Map { - 1 => 25091, - 2 => 19, - 3 => 224, - 4 => 12330, - 5 => 244, - 6 => 11154, - 7 => 11176, - 8 => 11368, - 9 => 11709, - 10 => 11910, - 11 => 11943, - 12 => 12140, - 13 => 12196, - 14 => 18948, - 15 => 12494, - 17 => 13264, - 18 => 18742, - 19 => 25016, - 20 => 19431, - 21 => 24878, - }, - "startingIndex": 0, -} -`; - -exports[`readRefTable Expects to merge correctly the refTable of resources 3`] = ` +exports[`readRefTable Expects to merge correctly the refTable of contributing.pdf: contributing.pdf 1`] = ` Object { "maxIndex": 24, "offsets": Map { @@ -97,7 +33,7 @@ Object { } `; -exports[`readRefTable Expects to merge correctly the refTable of resources 4`] = ` +exports[`readRefTable Expects to merge correctly the refTable of formexample.pdf: formexample.pdf 1`] = ` Object { "maxIndex": 62, "offsets": Map { @@ -168,7 +104,7 @@ Object { } `; -exports[`readRefTable Expects to merge correctly the refTable of resources 5`] = ` +exports[`readRefTable Expects to merge correctly the refTable of incrementally_signed.pdf: incrementally_signed.pdf 1`] = ` Object { "maxIndex": 18, "offsets": Map { @@ -195,7 +131,7 @@ Object { } `; -exports[`readRefTable Expects to merge correctly the refTable of resources 6`] = ` +exports[`readRefTable Expects to merge correctly the refTable of signed.pdf: signed.pdf 1`] = ` Object { "maxIndex": 13, "offsets": Map { @@ -217,7 +153,63 @@ Object { } `; -exports[`readRefTable Expects to merge correctly the refTable of resources 7`] = ` +exports[`readRefTable Expects to merge correctly the refTable of signed-once.pdf: signed-once.pdf 1`] = ` +Object { + "maxIndex": 19, + "offsets": Map { + 1 => 19012, + 2 => 19, + 3 => 224, + 4 => 12330, + 5 => 244, + 6 => 11154, + 7 => 11176, + 8 => 11368, + 9 => 11709, + 10 => 11910, + 11 => 11943, + 12 => 12140, + 13 => 12196, + 14 => 18928, + 15 => 12494, + 17 => 13264, + 18 => 18742, + 19 => 18860, + }, + "startingIndex": 0, +} +`; + +exports[`readRefTable Expects to merge correctly the refTable of signed-twice.pdf: signed-twice.pdf 1`] = ` +Object { + "maxIndex": 21, + "offsets": Map { + 1 => 25091, + 2 => 19, + 3 => 224, + 4 => 12330, + 5 => 244, + 6 => 11154, + 7 => 11176, + 8 => 11368, + 9 => 11709, + 10 => 11910, + 11 => 11943, + 12 => 12140, + 13 => 12196, + 14 => 18948, + 15 => 12494, + 17 => 13264, + 18 => 18742, + 19 => 25016, + 20 => 19431, + 21 => 24878, + }, + "startingIndex": 0, +} +`; + +exports[`readRefTable Expects to merge correctly the refTable of w3dummy.pdf: w3dummy.pdf 1`] = ` Object { "maxIndex": 15, "offsets": Map { diff --git a/packages/placeholder-plain/src/findObject.js b/packages/placeholder-plain/src/findObject.js index 0d564b2b..372ae614 100644 --- a/packages/placeholder-plain/src/findObject.js +++ b/packages/placeholder-plain/src/findObject.js @@ -1,4 +1,5 @@ import getIndexFromRef from './getIndexFromRef'; +import PdfDictionary from './PdfDictionary'; /** * @typedef {object} FindObjectAtReturnType @@ -13,12 +14,12 @@ import getIndexFromRef from './getIndexFromRef'; */ export const findObjectAt = (pdf, position) => { let slice = pdf.subarray(position); - slice = slice.subarray(0, slice.indexOf('endobj', 'utf8') - 1); - // ^ Buffer from the start position until the first endobj (included). + slice = slice.subarray(slice.indexOf('obj') + 3, slice.indexOf('endobj', 'utf8') - 1); + // ^ Buffer from the start position until the first endobj. const dictionary = slice.subarray( slice.indexOf('<<', 'utf8') + 2, - slice.indexOf('>>', 'utf8') - 1, + slice.lastIndexOf('>>', 'utf8'), ); const stream = slice.subarray( slice.indexOf('stream', 'utf8') + 6, @@ -26,7 +27,7 @@ export const findObjectAt = (pdf, position) => { ); return { - dictionary, + dictionary: new PdfDictionary(dictionary), stream, }; }; diff --git a/packages/placeholder-plain/src/getValue.js b/packages/placeholder-plain/src/getValue.js deleted file mode 100644 index 2c637281..00000000 --- a/packages/placeholder-plain/src/getValue.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @param {Buffer} trailer - * @param {string} key - * @returns {string} - * - * FIXME: - * 0 00 000 Null (NUL) - * 9 09 011 Tab (HT) - * 10 0A 012 Line feed (LF) - * 12 0C 014 Form feed (FF) - * 13 0D 015 Carriage return (CR) - * 32 20 040 Space (SP) - * - * The delimiter characters (, ), <, >, [, ], {, }, /, and % - * - * ^ All are terminators - */ -export const getValue = (trailer, key) => { - let index = trailer.indexOf(key); - - if (index === -1) { - return undefined; - } - - const slice = trailer.subarray(index); - index = slice.indexOf('/', 1); - if (index === -1) { - index = slice.indexOf('>', 1); - } - return slice.subarray(key.length + 1, index).toString().trim(); // key + at least one space -}; diff --git a/packages/placeholder-plain/src/getValue.test.js b/packages/placeholder-plain/src/getValue.test.js deleted file mode 100644 index 3689a3c0..00000000 --- a/packages/placeholder-plain/src/getValue.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import {getValue} from './getValue'; - -describe(getValue, () => { - it('matches snapshots', () => { - const trailer = Buffer.from(`trailer - << - /Size 14 - /Root 2 0 R - /Info 10 0 R - /ID [<455364dbb1253da25540322def2a672b> <455364dbb1253da25540322def2a672b>] - /AtTheEnd 14 - >> - startxref - 4220 - %%EOF`); - - expect(getValue(trailer, '/Root')).toBe('2 0 R'); - expect(getValue(trailer, '/Info')).toBe('10 0 R'); - expect(getValue(trailer, '/Unknown')).toBe(undefined); - expect(getValue(trailer, '/AtTheEnd')).toBe('14'); - }); -}); diff --git a/packages/placeholder-plain/src/readPdf.js b/packages/placeholder-plain/src/readPdf.js index b76d974e..de534028 100644 --- a/packages/placeholder-plain/src/readPdf.js +++ b/packages/placeholder-plain/src/readPdf.js @@ -1,6 +1,6 @@ -import readRefTable from './readRefTable'; -import findObject from './findObject'; -import {getValue} from './getValue'; +import {SignPdfError} from '@signpdf/utils'; +import readRefTable, {getLastXrefPosition} from './readRefTable'; +import findObject, {findObjectAt} from './findObject'; /** * @typedef {object} ReadPdfReturnType @@ -23,26 +23,33 @@ import {getValue} from './getValue'; */ const readPdf = (pdfBuffer) => { // Extract the trailer dictionary. - const trailerStart = pdfBuffer.lastIndexOf('trailer'); - // The trailer is followed by xref. Then an EOF. EOF's length is 6 characters. - const trailer = pdfBuffer.slice(trailerStart, pdfBuffer.length - 6); - - let xRefPosition = trailer.slice(trailer.lastIndexOf('startxref') + 10).toString(); - - xRefPosition = parseInt(xRefPosition); - const refTable = readRefTable(pdfBuffer); - - const rootRef = getValue(trailer, '/Root'); + const xRefPosition = getLastXrefPosition(pdfBuffer); + + let refTable; + + const trailerObject = findObjectAt(pdfBuffer, xRefPosition); + if (trailerObject.stream.indexOf('trailer') !== -1) { + // assuming trailer + refTable = readRefTable(pdfBuffer, xRefPosition); + } else { + // assuming stream + if (!trailerObject.dictionary.has('/Filter')) { + throw new Error('Expected /Filter in trailer with streams.'); + } + throw new SignPdfError( + '/Filter is not implemented.', + SignPdfError.TYPE_PARSE, + ); + } + const rootRef = trailerObject.dictionary.get('/Root'); const root = findObject(pdfBuffer, refTable, rootRef).toString(); - - const infoRef = getValue(trailer, '/Info'); + const infoRef = trailerObject.dictionary.get('/Info'); return { xref: refTable, rootRef, root, infoRef, - trailerStart, xRefPosition, }; }; diff --git a/packages/placeholder-plain/src/readPdf.test.js b/packages/placeholder-plain/src/readPdf.test.js index 4811494f..cd48956a 100644 --- a/packages/placeholder-plain/src/readPdf.test.js +++ b/packages/placeholder-plain/src/readPdf.test.js @@ -2,20 +2,30 @@ import {readTestResource} from '@signpdf/internal-utils'; import readPdf from './readPdf'; describe(readPdf, () => { - it('reads contributing.pdf', () => { - const pdfBuffer = readTestResource('contributing.pdf'); + it.each([ + { + resource: 'signed-once.pdf', + xRefPosition: 19174, + root: 14, + rootByteOffset: 18928, + info: 15, + }, + { + resource: 'contributing.pdf', + xRefPosition: 72203, + root: 12, + rootByteOffset: 4394, + info: 1, + }, + ])('reads $resource', ({ + resource, root, info, xRefPosition, rootByteOffset, + }) => { + const pdfBuffer = readTestResource(resource); const result = readPdf(pdfBuffer); - expect(result.xRefPosition).toBe(72203); - expect(result.rootRef).toBe('12 0 R'); - expect(result.infoRef).toBe('1 0 R'); - }); - it('reads issue-79-test.pdf', () => { - const pdfBuffer = readTestResource('issue-79-test.pdf'); - const result = readPdf(pdfBuffer); - - expect(result.xRefPosition).toBe(1542); - expect(result.rootRef).toBe('2 0 R'); - expect(result.infoRef).toBe('3 0 R'); + expect(result.xRefPosition).toBe(xRefPosition); + expect(result.rootRef).toBe(`${root} 0 R`); + expect(result.infoRef).toBe(`${info} 0 R`); + expect(result.xref.offsets.get(root)).toBe(rootByteOffset); }); }); diff --git a/packages/placeholder-plain/src/readRefTable.js b/packages/placeholder-plain/src/readRefTable.js index 577a21bc..d3cc7c8d 100644 --- a/packages/placeholder-plain/src/readRefTable.js +++ b/packages/placeholder-plain/src/readRefTable.js @@ -1,7 +1,5 @@ import {SignPdfError} from '@signpdf/utils'; import xrefToRefMap from './xrefToRefMap'; -import {findObjectAt} from './findObject'; -// import {getValue} from './getValue'; /** * @param {Buffer} pdf @@ -86,31 +84,6 @@ const readXrefTableAt = (pdfSlice, position) => { }; }; -/** - * @param {Buffer} _pdfSlice - * @param {number} _position - * @returns {GetXRefReturnType | null} - */ -const readXrefStreamAt = (pdfSlice, position) => { - const {dictionary: _d, stream: _s} = findObjectAt(pdfSlice, position); - - // const parsed = { - // size: getValue(_d, '/Size'), - // root: getValue(_d, '/Root'), - // info: getValue(_d, '/Info'), - // filter: getValue(_d, '/Filter'), - // length: getValue(_d, '/Length'), - // }; - - // console.log(_d.toString(), parsed); - // console.log(zlib.deflateSync(stream)); - - throw new SignPdfError( - 'Cross-Reference Streams not yet implemented.', - SignPdfError.TYPE_PARSE, - ); -}; - /** * @typedef {object} GetXRefReturnType * // TODO @@ -123,8 +96,7 @@ const readXrefStreamAt = (pdfSlice, position) => { * @throws {SignPdfError} */ export const getXref = (pdf, position) => { - const table = readXrefTableAt(pdf, position) - || readXrefStreamAt(pdf, position); + const table = readXrefTableAt(pdf, position); if (!table) { throw new SignPdfError( `Could not find xref anywhere at or after startxref position ${position}.`, @@ -140,17 +112,16 @@ export const getXref = (pdf, position) => { /** * @param {Buffer} pdf + * @param {number} xRefPosition * @returns {GetFullXrefTableReturnType} */ -export const getFullXrefTable = (pdf) => { - const lastTrailerPosition = getLastXrefPosition(pdf); - const lastXrefTable = getXref(pdf, lastTrailerPosition); +export const getFullXrefTable = (pdf, xRefPosition) => { + const lastXrefTable = getXref(pdf, xRefPosition); if (lastXrefTable.prev === undefined) { return lastXrefTable.xRefContent; } - const pdfWithoutLastTrailer = pdf.slice(0, lastTrailerPosition); - const partOfXrefTable = getFullXrefTable(pdfWithoutLastTrailer); + const partOfXrefTable = getFullXrefTable(pdf, lastXrefTable.prev); const mergedXrefTable = new Map([ ...partOfXrefTable, @@ -169,10 +140,11 @@ export const getFullXrefTable = (pdf) => { /** * @param {Buffer} pdfBuffer + * @param {number} xRefPosition * @returns {ReadRefTableReturnType} */ -const readRefTable = (pdf) => { - const fullXrefTable = getFullXrefTable(pdf); +const readRefTable = (pdf, xRefPosition) => { + const fullXrefTable = getFullXrefTable(pdf, xRefPosition); const startingIndex = 0; const maxIndex = Math.max(...fullXrefTable.keys()); diff --git a/packages/placeholder-plain/src/readRefTable.test.js b/packages/placeholder-plain/src/readRefTable.test.js index a473f995..0c17081f 100644 --- a/packages/placeholder-plain/src/readRefTable.test.js +++ b/packages/placeholder-plain/src/readRefTable.test.js @@ -3,9 +3,6 @@ import {SignPdfError} from '@signpdf/utils'; import readRefTable, {getFullXrefTable, getXref} from './readRefTable'; describe(getFullXrefTable, () => { - it('works for #79', () => { - expect(getFullXrefTable(readTestResource('issue-79-test.pdf'))).toBeTruthy(); - }); it('skips unreferenced xref tables', () => { const pdf = Buffer.from(`xref 0 3 @@ -48,7 +45,7 @@ describe(getXref, () => { } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_PARSE); - expect(e.message).toMatchSnapshot(); + expect(e.message).toMatchInlineSnapshot('"Could not find xref anywhere at or after startxref position 0."'); } }); it('Throws an error when xref is not at its expected position', () => { @@ -60,7 +57,7 @@ describe(getXref, () => { } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_PARSE); - expect(e.message).toMatchSnapshot(); + expect(e.message).toMatchInlineSnapshot('"Could not find xref anywhere at or after startxref position 2."'); } }); it('Throws an error when size is not found', () => { @@ -72,7 +69,7 @@ describe(getXref, () => { } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_PARSE); - expect(e.message).toMatchSnapshot(); + expect(e.message).toMatchInlineSnapshot('"Size not found in xref table."'); } }); it('Throws an error when size has unexpected value', () => { @@ -84,25 +81,26 @@ describe(getXref, () => { } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_PARSE); - expect(e.message).toMatchSnapshot(); + expect(e.message).toMatchInlineSnapshot('"Failed to parse size of xref table."'); } }); }); describe(readRefTable, () => { - it('Expects to merge correctly the refTable of resources', () => { - [ - 'signed-once.pdf', - 'signed-twice.pdf', - 'contributing.pdf', - 'formexample.pdf', - 'incrementally_signed.pdf', - 'signed.pdf', - 'w3dummy.pdf', - ].forEach((fileName) => { - const pdf = readTestResource(fileName); - const r = readRefTable(pdf); - expect(r).toMatchSnapshot(); - }); - }); + it.each([ + {resource: 'signed-once.pdf', startxref: 19174}, + {resource: 'signed-twice.pdf', startxref: 25264}, + {resource: 'contributing.pdf', startxref: 72203}, + {resource: 'formexample.pdf', startxref: 64251}, + {resource: 'incrementally_signed.pdf', startxref: 17125}, + {resource: 'signed.pdf', startxref: 4220}, + {resource: 'w3dummy.pdf', startxref: 12787}, + ])( + 'Expects to merge correctly the refTable of $resource', + ({resource, startxref}) => { + const pdf = readTestResource(resource); + const r = readRefTable(pdf, startxref); + expect(r).toMatchSnapshot(resource); + }, + ); });