@patrickdk77/pdf-defuse 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/parser.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Parser = exports.isWhite = exports.NeedMoreData = exports.ParseError = void 0;
3
+ exports.Parser = exports.isWhite = exports.NeedObject = exports.NeedMoreData = exports.EndOfData = exports.ParseError = void 0;
4
+ exports.endstreamIn = endstreamIn;
4
5
  exports.parseNumber = parseNumber;
5
6
  exports.parseObjectFrom = parseObjectFrom;
6
7
  const filters_1 = require("./filters");
@@ -8,10 +9,38 @@ const objects_1 = require("./objects");
8
9
  class ParseError extends Error {
9
10
  }
10
11
  exports.ParseError = ParseError;
11
- /** Thrown when the window ended before the object did. The caller retries with more bytes. */
12
+ /** The data ended inside an object. */
13
+ class EndOfData extends ParseError {
14
+ }
15
+ exports.EndOfData = EndOfData;
16
+ /**
17
+ * Thrown when the window ended before the object did. The caller retries with more bytes. `idle` says the window ended
18
+ * in a run of whitespace, and of comments when `comments` is set, that started at window position `from`, so the
19
+ * caller can skip the rest of the run in the source instead of holding it. `body` says it ended in the body of a stream
20
+ * written inside an object, and `line` before the end of the line after a "stream" keyword. The caller can find either
21
+ * end in the source.
22
+ */
12
23
  class NeedMoreData extends Error {
24
+ idle;
25
+ body;
26
+ line;
27
+ constructor(idle, body, line) {
28
+ super('More data is needed');
29
+ this.idle = idle;
30
+ this.body = body;
31
+ this.line = line;
32
+ }
13
33
  }
14
34
  exports.NeedMoreData = NeedMoreData;
35
+ /** Thrown when a stream inside an object has a /Length the caller has not read yet. The caller reads it and retries. */
36
+ class NeedObject extends Error {
37
+ ref;
38
+ constructor(ref) {
39
+ super(`Object ${ref.num} is needed`);
40
+ this.ref = ref;
41
+ }
42
+ }
43
+ exports.NeedObject = NeedObject;
15
44
  const WS = new Uint8Array(256);
16
45
  for (const c of [0x00, 0x09, 0x0a, 0x0c, 0x0d, 0x20])
17
46
  WS[c] = 1;
@@ -19,7 +48,9 @@ const DELIM = new Uint8Array(256);
19
48
  for (const c of '()<>[]{}/%')
20
49
  DELIM[c.charCodeAt(0)] = 1;
21
50
  /** Keywords that frame objects. Skipping one as junk could run an unterminated object into the next. */
22
- const KEYWORDS = new Set(['true', 'false', 'null', 'obj', 'endobj', 'stream', 'endstream', 'R', 'xref', 'trailer', 'startxref']);
51
+ const FRAMING = new Set(['obj', 'endobj', 'stream', 'endstream', 'xref', 'trailer', 'startxref']);
52
+ const KEYWORDS = new Set([...FRAMING, 'true', 'false', 'null', 'R']);
53
+ const ENDSTREAM = Buffer.from('endstream', 'latin1');
23
54
  /**
24
55
  * A number as ISO 32000 writes it. It must match only one way: a pattern that can split a digit run backtracks
25
56
  * quadratically on a long bad token.
@@ -33,6 +64,24 @@ const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
33
64
  const MAX_OVERHEAD = 16 * 1024 * 1024;
34
65
  const isWhite = (c) => WS[c] === 1;
35
66
  exports.isWhite = isWhite;
67
+ const END = Buffer.from('end', 'latin1');
68
+ /**
69
+ * Where pdf.js ends a stream body it searches for: at "endstream", or at "endsteam" or "endstrea" before a space, tab,
70
+ * CR or LF, which it takes too. Returns the first such place in `b` from `from` on and the keyword's length. A
71
+ * misspelling whose next byte `b` does not hold is left for a longer window.
72
+ */
73
+ function endstreamIn(b, from) {
74
+ const v = Buffer.from(b.buffer, b.byteOffset, b.byteLength);
75
+ const is = (at, word) => at + word.length <= v.length && v.toString('latin1', at, at + word.length) === word;
76
+ for (let k = v.indexOf(END, from); k >= 0; k = v.indexOf(END, k + 1)) {
77
+ if (is(k + 3, 'stream'))
78
+ return { at: k, length: 9 };
79
+ const c = v[k + 8];
80
+ if ((is(k + 3, 'steam') || is(k + 3, 'strea')) && (c === 0x20 || c === 0x09 || c === 0x0d || c === 0x0a))
81
+ return { at: k, length: 8 };
82
+ }
83
+ return undefined;
84
+ }
36
85
  const isRegular = (c) => !WS[c] && !DELIM[c];
37
86
  const isDigit = (c) => c >= 0x30 && c <= 0x39;
38
87
  /**
@@ -43,6 +92,7 @@ class Parser {
43
92
  buf;
44
93
  eof;
45
94
  hooks;
95
+ options;
46
96
  pos;
47
97
  depth = 0;
48
98
  /** Estimated heap bytes of what this parser has built, with the length of every name and string. */
@@ -50,12 +100,25 @@ class Parser {
50
100
  /** The part of `cost` that does not grow with the length of a name or string. */
51
101
  overhead = 0;
52
102
  values = 0;
53
- constructor(buf, pos = 0, eof = true, hooks = {}) {
103
+ /** The part of `cost` that the dictionaries of streams written inside the object take. */
104
+ directCost = 0;
105
+ constructor(buf, pos = 0, eof = true, hooks = {}, options = {}) {
54
106
  this.buf = buf;
55
107
  this.eof = eof;
56
108
  this.hooks = hooks;
109
+ this.options = options;
57
110
  this.pos = pos;
58
111
  }
112
+ /** In recovery, whether nothing but whitespace and comments is left before the end of the data. */
113
+ cutOff() {
114
+ if (!this.options.recover || !this.eof)
115
+ return false;
116
+ const save = this.pos;
117
+ if (!this.skipToToken())
118
+ return true;
119
+ this.pos = save;
120
+ return false;
121
+ }
59
122
  /** Rough heap costs: a slot for a number or keyword, an object for a name or reference, a buffer for a string. */
60
123
  charge(overhead, length = 0) {
61
124
  this.cost += overhead + length;
@@ -67,7 +130,7 @@ class Parser {
67
130
  }
68
131
  more() {
69
132
  if (this.eof)
70
- throw new ParseError(`Unexpected end of data at ${this.pos}`);
133
+ throw new EndOfData(`Unexpected end of data at ${this.pos}`);
71
134
  throw new NeedMoreData();
72
135
  }
73
136
  peek() {
@@ -76,7 +139,16 @@ class Parser {
76
139
  return this.buf[this.pos];
77
140
  }
78
141
  skipWhitespace() {
142
+ if (!this.skipToToken())
143
+ this.more();
144
+ }
145
+ /**
146
+ * Skips whitespace and comments, and says whether anything follows them. A window that ends first asks for more,
147
+ * unless it ends the data.
148
+ */
149
+ skipToToken() {
79
150
  const b = this.buf;
151
+ const from = this.pos;
80
152
  for (;;) {
81
153
  while (this.pos < b.length && WS[b[this.pos]])
82
154
  this.pos++;
@@ -87,8 +159,11 @@ class Parser {
87
159
  }
88
160
  break;
89
161
  }
90
- if (this.pos >= b.length)
91
- this.more();
162
+ if (this.pos < b.length)
163
+ return true;
164
+ if (this.eof)
165
+ return false;
166
+ throw new NeedMoreData({ from, comments: true });
92
167
  }
93
168
  /** Reads a regular-character token without consuming delimiters. */
94
169
  readToken() {
@@ -108,15 +183,9 @@ class Parser {
108
183
  }
109
184
  matchKeyword(word) {
110
185
  const save = this.pos;
111
- try {
112
- this.skipWhitespace();
113
- }
114
- catch (e) {
115
- if (e instanceof ParseError) {
116
- this.pos = save;
117
- return false;
118
- }
119
- throw e;
186
+ if (!this.skipToToken()) {
187
+ this.pos = save;
188
+ return false;
120
189
  }
121
190
  for (let i = 0; i < word.length; i++) {
122
191
  if (this.pos + i >= this.buf.length) {
@@ -160,9 +229,19 @@ class Parser {
160
229
  if (c === 0x3c) {
161
230
  if (this.pos + 1 >= this.buf.length)
162
231
  this.more();
163
- if (this.buf[this.pos + 1] === 0x3c)
164
- return this.parseDict();
165
- return this.parseHexString();
232
+ if (this.buf[this.pos + 1] !== 0x3c)
233
+ return this.parseHexString();
234
+ const before = this.cost;
235
+ const counted = this.directCost;
236
+ const d = this.parseDict();
237
+ // An object's own stream is the caller's to read, unless it sits in an object stream. pdf.js reads a stream after
238
+ // any dictionary inside an object too.
239
+ const s = (this.depth > 0 || this.options.topStream) && this.options.streams ? this.directStream(d, this.options.streams) : undefined;
240
+ if (!s)
241
+ return d;
242
+ // The whole dictionary, which already holds what the streams inside it cost.
243
+ this.directCost = counted + this.cost - before;
244
+ return s;
166
245
  }
167
246
  if (c === 0x5b)
168
247
  return this.parseArray();
@@ -191,6 +270,8 @@ class Parser {
191
270
  let q = p;
192
271
  while (q < b.length && WS[b[q]])
193
272
  q++;
273
+ if (q >= b.length && !this.eof)
274
+ throw new NeedMoreData({ from: p, comments: false });
194
275
  const gs = q;
195
276
  let g = 0;
196
277
  while (q < b.length && isDigit(b[q]) && q - gs < 6)
@@ -200,6 +281,8 @@ class Parser {
200
281
  if (q > gs && q < b.length && (WS[b[q]] || b[q] === 0x52)) {
201
282
  while (q < b.length && WS[b[q]])
202
283
  q++;
284
+ if (q >= b.length && !this.eof)
285
+ throw new NeedMoreData({ from: ge, comments: false });
203
286
  if (b[q] === 0x52) {
204
287
  if (q + 1 >= b.length && !this.eof)
205
288
  throw new NeedMoreData();
@@ -382,6 +465,8 @@ class Parser {
382
465
  this.charge(48);
383
466
  const arr = [];
384
467
  for (;;) {
468
+ if (this.cutOff())
469
+ break;
385
470
  this.skipWhitespace();
386
471
  if (this.peek() === 0x5d) {
387
472
  this.pos++;
@@ -399,6 +484,8 @@ class Parser {
399
484
  this.charge(256);
400
485
  const d = new objects_1.PdfDict();
401
486
  for (;;) {
487
+ if (this.cutOff())
488
+ break;
402
489
  this.skipWhitespace();
403
490
  const c = this.peek();
404
491
  if (c === 0x3e) {
@@ -419,13 +506,16 @@ class Parser {
419
506
  this.hooks.onBadToken?.(open === 1 ? '[' : '<<');
420
507
  this.pos += open;
421
508
  }
422
- else if (!this.skipBadToken())
509
+ else if (!this.skipBadToken(true))
423
510
  this.parseObject();
424
511
  continue;
425
512
  }
426
513
  const k = this.parseName();
427
514
  if (d.has(k.name))
428
515
  this.hooks.onDuplicateKey?.(k.name);
516
+ // pdf.js drops a key that the end of the data cuts off from its value.
517
+ if (this.cutOff())
518
+ break;
429
519
  this.skipWhitespace();
430
520
  if (this.peek() === 0x3e) {
431
521
  if (this.pos + 1 >= this.buf.length)
@@ -445,9 +535,10 @@ class Parser {
445
535
  /**
446
536
  * Skips junk inside a container instead of losing the whole object: an unknown bare keyword, or a delimiter that
447
537
  * cannot start an object, such as "]" in a dictionary or ">>" in an array. qpdf reads each of these as null. The
448
- * container's own closing delimiter was checked before this is called.
538
+ * container's own closing delimiter was checked before this is called. Where a key belongs, pdf.js also skips a
539
+ * number, "R", "true", "false" or "null" one token at a time, and so does this.
449
540
  */
450
- skipBadToken() {
541
+ skipBadToken(key = false) {
451
542
  const c = this.peek();
452
543
  if (c === 0x5d || c === 0x29 || c === 0x7b || c === 0x7d || c === 0x3e) {
453
544
  if (c === 0x3e && this.pos + 1 >= this.buf.length)
@@ -457,59 +548,105 @@ class Parser {
457
548
  this.pos += n;
458
549
  return true;
459
550
  }
460
- if (!isRegular(c) || c === 0x2b || c === 0x2d || c === 0x2e || isDigit(c))
551
+ if (!isRegular(c) || (!key && (c === 0x2b || c === 0x2d || c === 0x2e || isDigit(c))))
461
552
  return false;
462
553
  const save = this.pos;
463
554
  const tok = this.readToken();
464
- if (KEYWORDS.has(tok)) {
555
+ if (FRAMING.has(tok) || (!key && KEYWORDS.has(tok))) {
465
556
  this.pos = save;
466
557
  return false;
467
558
  }
468
559
  this.hooks.onBadToken?.(tok);
469
560
  return true;
470
561
  }
562
+ /**
563
+ * After a dictionary inside an object, reads the stream that follows it, if any, as pdf.js does: the body ends at
564
+ * /Length when "endstream" follows there, and at the first "endstream" otherwise. Returns undefined when no stream
565
+ * follows. The body stays in the source; the stream records where it is.
566
+ */
567
+ directStream(dict, streams) {
568
+ const start = this.streamStart();
569
+ if (start < 0)
570
+ return undefined;
571
+ const b = this.buf;
572
+ const at = streams.at(start);
573
+ const known = streams.known(at);
574
+ if (known === null)
575
+ throw new ParseError(`Missing endstream at ${start}`);
576
+ if (known) {
577
+ this.pos = start + 1;
578
+ return new objects_1.PdfStream(dict, at, known.length);
579
+ }
580
+ const L = dict.get('Length');
581
+ const declared = typeof L === 'number' && Number.isInteger(L) ? L : L instanceof objects_1.PdfRef ? streams.length(L) : undefined;
582
+ if (declared !== undefined && declared >= 0) {
583
+ let p = start + declared;
584
+ while (p < b.length && WS[b[p]])
585
+ p++;
586
+ if (p + ENDSTREAM.length > b.length && !this.eof)
587
+ throw new NeedMoreData(undefined, { start, declared });
588
+ if (p + ENDSTREAM.length <= b.length && ENDSTREAM.every((x, i) => b[p + i] === x)) {
589
+ this.pos = p + ENDSTREAM.length;
590
+ return new objects_1.PdfStream(dict, at, declared);
591
+ }
592
+ }
593
+ const found = endstreamIn(b, start);
594
+ if (!found) {
595
+ if (!this.eof)
596
+ throw new NeedMoreData(undefined, { start, declared });
597
+ throw new ParseError(`Missing endstream at ${start}`);
598
+ }
599
+ const k = found.at;
600
+ // The EOL before "endstream" belongs to the keyword, as for any other stream.
601
+ let end = k;
602
+ if (end - 2 >= start && b[end - 2] === 0x0d && b[end - 1] === 0x0a)
603
+ end -= 2;
604
+ else if (end - 1 >= start && (b[end - 1] === 0x0a || b[end - 1] === 0x0d))
605
+ end -= 1;
606
+ this.pos = k + found.length;
607
+ return new objects_1.PdfStream(dict, at, end - start);
608
+ }
471
609
  /**
472
610
  * After a dictionary, checks for the "stream" keyword. Returns the position of the first body byte,
473
611
  * or -1 when the object is not a stream.
474
612
  */
475
613
  streamStart() {
476
614
  const save = this.pos;
477
- try {
478
- this.skipWhitespace();
479
- }
480
- catch (e) {
481
- if (e instanceof ParseError)
482
- return -1;
483
- throw e;
615
+ if (!this.skipToToken()) {
616
+ this.pos = save;
617
+ return -1;
484
618
  }
485
619
  if (!this.matchKeyword('stream')) {
486
620
  this.pos = save;
487
621
  return -1;
488
622
  }
489
623
  const b = this.buf;
490
- // Spaces and tabs before the EOL are skipped. Anything else leaves the body starting right after the keyword,
491
- // while pdf.js skips everything up to the EOL, so that text is reported.
624
+ // pdf.js starts the body after the first EOL that follows the keyword, whatever comes before it. Other readers take
625
+ // text there as part of the body, so it is reported.
626
+ const streams = this.options.streams;
627
+ const line = streams?.line(streams.at(this.pos - 1) + 1);
628
+ if (line) {
629
+ if (line.text)
630
+ this.hooks.onBadToken?.('stream');
631
+ return this.pos;
632
+ }
492
633
  let p = this.pos;
493
- while (p < b.length && (b[p] === 0x20 || b[p] === 0x09 || b[p] === 0x0c || b[p] === 0x00))
494
- p++;
495
- if (p >= b.length && !this.eof)
634
+ let text = false;
635
+ for (; p < b.length && b[p] !== 0x0d && b[p] !== 0x0a; p++)
636
+ if (b[p] !== 0x20 && b[p] !== 0x09 && b[p] !== 0x0c && b[p] !== 0x00)
637
+ text = true;
638
+ // A line the window cuts off, or a CR that may have its LF after the window, is found in the source.
639
+ if ((p >= b.length || (b[p] === 0x0d && p + 1 >= b.length)) && !this.eof) {
640
+ if (streams)
641
+ throw new NeedMoreData(undefined, undefined, { from: this.pos });
496
642
  throw new NeedMoreData();
497
- if (b[p] === 0x0d || b[p] === 0x0a)
498
- this.pos = p;
499
- else
500
- this.hooks.onBadToken?.('stream');
501
- if (this.pos >= b.length)
502
- this.more();
503
- if (b[this.pos] === 0x0d) {
504
- this.pos++;
505
- if (this.pos >= b.length && !this.eof)
506
- throw new NeedMoreData();
507
- if (b[this.pos] === 0x0a)
508
- this.pos++;
509
643
  }
510
- else if (b[this.pos] === 0x0a)
511
- this.pos++;
512
- return this.pos;
644
+ if (text)
645
+ this.hooks.onBadToken?.('stream');
646
+ if (p < b.length)
647
+ p += b[p] === 0x0d && b[p + 1] === 0x0a ? 2 : 1;
648
+ this.pos = p;
649
+ return p;
513
650
  }
514
651
  }
515
652
  exports.Parser = Parser;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * SASLprep (RFC 4013), the profile of stringprep (RFC 3454) that ISO 32000-2 7.6.4.3.3 applies to a revision 6
3
+ * password before it is encoded as UTF-8. The tables are RFC 3454's, which describe Unicode 3.2.
4
+ */
5
+ /**
6
+ * Prepares a password as pdf.js does: the same two mappings as SASLprep, then NFKC with the Unicode tables of the
7
+ * running engine. It refuses nothing.
8
+ */
9
+ export declare function pdfjsPrep(input: string): string;
10
+ /** Prepares a password with SASLprep, or returns undefined when the profile refuses it. */
11
+ export declare function saslprep(input: string): string | undefined;
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ /**
3
+ * SASLprep (RFC 4013), the profile of stringprep (RFC 3454) that ISO 32000-2 7.6.4.3.3 applies to a revision 6
4
+ * password before it is encoded as UTF-8. The tables are RFC 3454's, which describe Unicode 3.2.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.pdfjsPrep = pdfjsPrep;
8
+ exports.saslprep = saslprep;
9
+ /** Table A.1: code points Unicode 3.2 leaves unassigned. */
10
+ const UNASSIGNED = `
11
+ 0221 0234-024F 02AE-02AF 02EF-02FF 0350-035F 0370-0373 0376-0379 037B-037D 037F-0383 038B 038D 03A2 03CF 03F7-03FF 0487 04CF 04F6-04F7 04FA-04FF
12
+ 0510-0530 0557-0558 0560 0588 058B-0590 05A2 05BA 05C5-05CF 05EB-05EF 05F5-060B 060D-061A 061C-061E 0620 063B-063F 0656-065F 06EE-06EF 06FF 070E
13
+ 072D-072F 074B-077F 07B2-0900 0904 093A-093B 094E-094F 0955-0957 0971-0980 0984 098D-098E 0991-0992 09A9 09B1 09B3-09B5 09BA-09BB 09BD 09C5-09C6
14
+ 09C9-09CA 09CE-09D6 09D8-09DB 09DE 09E4-09E5 09FB-0A01 0A03-0A04 0A0B-0A0E 0A11-0A12 0A29 0A31 0A34 0A37 0A3A-0A3B 0A3D 0A43-0A46 0A49-0A4A 0A4E-0A58
15
+ 0A5D 0A5F-0A65 0A75-0A80 0A84 0A8C 0A8E 0A92 0AA9 0AB1 0AB4 0ABA-0ABB 0AC6 0ACA 0ACE-0ACF 0AD1-0ADF 0AE1-0AE5 0AF0-0B00 0B04 0B0D-0B0E 0B11-0B12 0B29
16
+ 0B31 0B34-0B35 0B3A-0B3B 0B44-0B46 0B49-0B4A 0B4E-0B55 0B58-0B5B 0B5E 0B62-0B65 0B71-0B81 0B84 0B8B-0B8D 0B91 0B96-0B98 0B9B 0B9D 0BA0-0BA2 0BA5-0BA7
17
+ 0BAB-0BAD 0BB6 0BBA-0BBD 0BC3-0BC5 0BC9 0BCE-0BD6 0BD8-0BE6 0BF3-0C00 0C04 0C0D 0C11 0C29 0C34 0C3A-0C3D 0C45 0C49 0C4E-0C54 0C57-0C5F 0C62-0C65
18
+ 0C70-0C81 0C84 0C8D 0C91 0CA9 0CB4 0CBA-0CBD 0CC5 0CC9 0CCE-0CD4 0CD7-0CDD 0CDF 0CE2-0CE5 0CF0-0D01 0D04 0D0D 0D11 0D29 0D3A-0D3D 0D44-0D45 0D49
19
+ 0D4E-0D56 0D58-0D5F 0D62-0D65 0D70-0D81 0D84 0D97-0D99 0DB2 0DBC 0DBE-0DBF 0DC7-0DC9 0DCB-0DCE 0DD5 0DD7 0DE0-0DF1 0DF5-0E00 0E3B-0E3E 0E5C-0E80 0E83
20
+ 0E85-0E86 0E89 0E8B-0E8C 0E8E-0E93 0E98 0EA0 0EA4 0EA6 0EA8-0EA9 0EAC 0EBA 0EBE-0EBF 0EC5 0EC7 0ECE-0ECF 0EDA-0EDB 0EDE-0EFF 0F48 0F6B-0F70 0F8C-0F8F
21
+ 0F98 0FBD 0FCD-0FCE 0FD0-0FFF 1022 1028 102B 1033-1035 103A-103F 105A-109F 10C6-10CF 10F9-10FA 10FC-10FF 115A-115E 11A3-11A7 11FA-11FF 1207 1247 1249
22
+ 124E-124F 1257 1259 125E-125F 1287 1289 128E-128F 12AF 12B1 12B6-12B7 12BF 12C1 12C6-12C7 12CF 12D7 12EF 130F 1311 1316-1317 131F 1347 135B-1360
23
+ 137D-139F 13F5-1400 1677-167F 169D-169F 16F1-16FF 170D 1715-171F 1737-173F 1754-175F 176D 1771 1774-177F 17DD-17DF 17EA-17FF 180F 181A-181F 1878-187F
24
+ 18AA-1DFF 1E9C-1E9F 1EFA-1EFF 1F16-1F17 1F1E-1F1F 1F46-1F47 1F4E-1F4F 1F58 1F5A 1F5C 1F5E 1F7E-1F7F 1FB5 1FC5 1FD4-1FD5 1FDC 1FF0-1FF1 1FF5 1FFF
25
+ 2053-2056 2058-205E 2064-2069 2072-2073 208F-209F 20B2-20CF 20EB-20FF 213B-213C 214C-2152 2184-218F 23CF-23FF 2427-243F 244B-245F 24FF 2614-2615 2618
26
+ 267E-267F 268A-2700 2705 270A-270B 2728 274C 274E 2753-2755 2757 275F-2760 2795-2797 27B0 27BF-27CF 27EC-27EF 2B00-2E7F 2E9A 2EF4-2EFF 2FD6-2FEF
27
+ 2FFC-2FFF 3040 3097-3098 3100-3104 312D-3130 318F 31B8-31EF 321D-321F 3244-3250 327C-327E 32CC-32CF 32FF 3377-337A 33DE-33DF 33FF 4DB6-4DFF 9FA6-9FFF
28
+ A48D-A48F A4C7-ABFF D7A4-D7FF FA2E-FA2F FA6B-FAFF FB07-FB12 FB18-FB1C FB37 FB3D FB3F FB42 FB45 FBB2-FBD2 FD40-FD4F FD90-FD91 FDC8-FDCF FDFD-FDFF
29
+ FE10-FE1F FE24-FE2F FE47-FE48 FE53 FE67 FE6C-FE6F FE75 FEFD-FEFE FF00 FFBF-FFC1 FFC8-FFC9 FFD0-FFD1 FFD8-FFD9 FFDD-FFDF FFE7 FFEF-FFF8 10000-102FF
30
+ 1031F 10324-1032F 1034B-103FF 10426-10427 1044E-1CFFF 1D0F6-1D0FF 1D127-1D129 1D1DE-1D3FF 1D455 1D49D 1D4A0-1D4A1 1D4A3-1D4A4 1D4A7-1D4A8 1D4AD 1D4BA
31
+ 1D4BC 1D4C1 1D4C4 1D506 1D50B-1D50C 1D515 1D51D 1D53A 1D53F 1D545 1D547-1D549 1D551 1D6A4-1D6A7 1D7CA-1D7CD 1D800-1FFFD 2A6D7-2F7FF 2FA1E-2FFFD
32
+ 30000-3FFFD 40000-4FFFD 50000-5FFFD 60000-6FFFD 70000-7FFFD 80000-8FFFD 90000-9FFFD A0000-AFFFD B0000-BFFFD C0000-CFFFD D0000-DFFFD E0000 E0002-E001F
33
+ E0080-EFFFD
34
+ `;
35
+ /** Table B.1: characters mapped to nothing. */
36
+ const TO_NOTHING = `
37
+ 00AD 034F 1806 180B-180D 200B-200D 2060 FE00-FE0F FEFF
38
+ `;
39
+ /** Table C.1.2: non-ASCII spaces, mapped to U+0020. */
40
+ const TO_SPACE = `
41
+ 00A0 1680 2000-200B 202F 205F 3000
42
+ `;
43
+ /** Tables C.1.2, C.2.1, C.2.2 and C.3 to C.9 merged: what SASLprep may not output. */
44
+ const PROHIBITED = `
45
+ 0000-001F 007F-00A0 0340-0341 06DD 070F 1680 180E 2000-200F 2028-202F 205F-2063 206A-206F 2FF0-2FFB 3000 D800-F8FF FDD0-FDEF FEFF FFF9-FFFF
46
+ 1D173-1D17A 1FFFE-1FFFF 2FFFE-2FFFF 3FFFE-3FFFF 4FFFE-4FFFF 5FFFE-5FFFF 6FFFE-6FFFF 7FFFE-7FFFF 8FFFE-8FFFF 9FFFE-9FFFF AFFFE-AFFFF BFFFE-BFFFF
47
+ CFFFE-CFFFF DFFFE-DFFFF E0001 E0020-E007F EFFFE-10FFFF
48
+ `;
49
+ /** RFC 3454's RandALCat characters, of bidirectional category R or AL. */
50
+ const RIGHT_TO_LEFT = `
51
+ 05BE 05C0 05C3 05D0-05EA 05F0-05F4 061B 061F 0621-063A 0640-064A 066D-066F 0671-06D5 06DD 06E5-06E6 06FA-06FE 0700-070D 0710 0712-072C 0780-07A5 07B1
52
+ 200F FB1D FB1F-FB28 FB2A-FB36 FB38-FB3C FB3E FB40-FB41 FB43-FB44 FB46-FBB1 FBD3-FD3D FD50-FD8F FD92-FDC7 FDF0-FDFC FE70-FE74 FE76-FEFC
53
+ `;
54
+ /** RFC 3454's LCat characters, of bidirectional category L. */
55
+ const LEFT_TO_RIGHT = `
56
+ 0041-005A 0061-007A 00AA 00B5 00BA 00C0-00D6 00D8-00F6 00F8-0220 0222-0233 0250-02AD 02B0-02B8 02BB-02C1 02D0-02D1 02E0-02E4 02EE 037A 0386 0388-038A
57
+ 038C 038E-03A1 03A3-03CE 03D0-03F5 0400-0482 048A-04CE 04D0-04F5 04F8-04F9 0500-050F 0531-0556 0559-055F 0561-0587 0589 0903 0905-0939 093D-0940
58
+ 0949-094C 0950 0958-0961 0964-0970 0982-0983 0985-098C 098F-0990 0993-09A8 09AA-09B0 09B2 09B6-09B9 09BE-09C0 09C7-09C8 09CB-09CC 09D7 09DC-09DD
59
+ 09DF-09E1 09E6-09F1 09F4-09FA 0A05-0A0A 0A0F-0A10 0A13-0A28 0A2A-0A30 0A32-0A33 0A35-0A36 0A38-0A39 0A3E-0A40 0A59-0A5C 0A5E 0A66-0A6F 0A72-0A74 0A83
60
+ 0A85-0A8B 0A8D 0A8F-0A91 0A93-0AA8 0AAA-0AB0 0AB2-0AB3 0AB5-0AB9 0ABD-0AC0 0AC9 0ACB-0ACC 0AD0 0AE0 0AE6-0AEF 0B02-0B03 0B05-0B0C 0B0F-0B10 0B13-0B28
61
+ 0B2A-0B30 0B32-0B33 0B36-0B39 0B3D-0B3E 0B40 0B47-0B48 0B4B-0B4C 0B57 0B5C-0B5D 0B5F-0B61 0B66-0B70 0B83 0B85-0B8A 0B8E-0B90 0B92-0B95 0B99-0B9A 0B9C
62
+ 0B9E-0B9F 0BA3-0BA4 0BA8-0BAA 0BAE-0BB5 0BB7-0BB9 0BBE-0BBF 0BC1-0BC2 0BC6-0BC8 0BCA-0BCC 0BD7 0BE7-0BF2 0C01-0C03 0C05-0C0C 0C0E-0C10 0C12-0C28
63
+ 0C2A-0C33 0C35-0C39 0C41-0C44 0C60-0C61 0C66-0C6F 0C82-0C83 0C85-0C8C 0C8E-0C90 0C92-0CA8 0CAA-0CB3 0CB5-0CB9 0CBE 0CC0-0CC4 0CC7-0CC8 0CCA-0CCB
64
+ 0CD5-0CD6 0CDE 0CE0-0CE1 0CE6-0CEF 0D02-0D03 0D05-0D0C 0D0E-0D10 0D12-0D28 0D2A-0D39 0D3E-0D40 0D46-0D48 0D4A-0D4C 0D57 0D60-0D61 0D66-0D6F 0D82-0D83
65
+ 0D85-0D96 0D9A-0DB1 0DB3-0DBB 0DBD 0DC0-0DC6 0DCF-0DD1 0DD8-0DDF 0DF2-0DF4 0E01-0E30 0E32-0E33 0E40-0E46 0E4F-0E5B 0E81-0E82 0E84 0E87-0E88 0E8A 0E8D
66
+ 0E94-0E97 0E99-0E9F 0EA1-0EA3 0EA5 0EA7 0EAA-0EAB 0EAD-0EB0 0EB2-0EB3 0EBD 0EC0-0EC4 0EC6 0ED0-0ED9 0EDC-0EDD 0F00-0F17 0F1A-0F34 0F36 0F38 0F3E-0F47
67
+ 0F49-0F6A 0F7F 0F85 0F88-0F8B 0FBE-0FC5 0FC7-0FCC 0FCF 1000-1021 1023-1027 1029-102A 102C 1031 1038 1040-1057 10A0-10C5 10D0-10F8 10FB 1100-1159
68
+ 115F-11A2 11A8-11F9 1200-1206 1208-1246 1248 124A-124D 1250-1256 1258 125A-125D 1260-1286 1288 128A-128D 1290-12AE 12B0 12B2-12B5 12B8-12BE 12C0
69
+ 12C2-12C5 12C8-12CE 12D0-12D6 12D8-12EE 12F0-130E 1310 1312-1315 1318-131E 1320-1346 1348-135A 1361-137C 13A0-13F4 1401-1676 1681-169A 16A0-16F0
70
+ 1700-170C 170E-1711 1720-1731 1735-1736 1740-1751 1760-176C 176E-1770 1780-17B6 17BE-17C5 17C7-17C8 17D4-17DA 17DC 17E0-17E9 1810-1819 1820-1877
71
+ 1880-18A8 1E00-1E9B 1EA0-1EF9 1F00-1F15 1F18-1F1D 1F20-1F45 1F48-1F4D 1F50-1F57 1F59 1F5B 1F5D 1F5F-1F7D 1F80-1FB4 1FB6-1FBC 1FBE 1FC2-1FC4 1FC6-1FCC
72
+ 1FD0-1FD3 1FD6-1FDB 1FE0-1FEC 1FF2-1FF4 1FF6-1FFC 200E 2071 207F 2102 2107 210A-2113 2115 2119-211D 2124 2126 2128 212A-212D 212F-2131 2133-2139
73
+ 213D-213F 2145-2149 2160-2183 2336-237A 2395 249C-24E9 3005-3007 3021-3029 3031-3035 3038-303C 3041-3096 309D-309F 30A1-30FA 30FC-30FF 3105-312C
74
+ 3131-318E 3190-31B7 31F0-321C 3220-3243 3260-327B 327F-32B0 32C0-32CB 32D0-32FE 3300-3376 337B-33DD 33E0-33FE 3400-4DB5 4E00-9FA5 A000-A48C AC00-D7A3
75
+ D800-FA2D FA30-FA6A FB00-FB06 FB13-FB17 FF21-FF3A FF41-FF5A FF66-FFBE FFC2-FFC7 FFCA-FFCF FFD2-FFD7 FFDA-FFDC 10300-1031E 10320-10323 10330-1034A
76
+ 10400-10425 10428-1044D 1D000-1D0F5 1D100-1D126 1D12A-1D166 1D16A-1D172 1D183-1D184 1D18C-1D1A9 1D1AE-1D1DD 1D400-1D454 1D456-1D49C 1D49E-1D49F 1D4A2
77
+ 1D4A5-1D4A6 1D4A9-1D4AC 1D4AE-1D4B9 1D4BB 1D4BD-1D4C0 1D4C2-1D4C3 1D4C5-1D505 1D507-1D50A 1D50D-1D514 1D516-1D51C 1D51E-1D539 1D53B-1D53E 1D540-1D544
78
+ 1D546 1D54A-1D550 1D552-1D6A3 1D6A8-1D7C9 20000-2A6D6 2F800-2FA1D F0000-FFFFD 100000-10FFFD
79
+ `;
80
+ /**
81
+ * Unicode Corrigendum #4 changed how five CJK compatibility ideographs normalize, after Unicode 3.2. Mapping them to
82
+ * their Unicode 3.2 forms first makes String.prototype.normalize give the Unicode 3.2 result for every code point
83
+ * Unicode 3.2 assigns.
84
+ */
85
+ const UNICODE_32 = new Map([
86
+ [0x2f868, 0x2136a],
87
+ [0x2f874, 0x5f33],
88
+ [0x2f91f, 0x43ab],
89
+ [0x2f95f, 0x7aae],
90
+ [0x2f9bf, 0x4d57],
91
+ ]);
92
+ /** Inclusive [start, end] pairs from a table above, in order. */
93
+ function parse(text) {
94
+ const items = text.trim().split(/\s+/);
95
+ const out = new Uint32Array(items.length * 2);
96
+ for (let i = 0; i < items.length; i++) {
97
+ const [a, b = a] = items[i].split('-');
98
+ out[2 * i] = Number.parseInt(a, 16);
99
+ out[2 * i + 1] = Number.parseInt(b, 16);
100
+ }
101
+ return out;
102
+ }
103
+ /** Parsed on the first call. */
104
+ let tables;
105
+ function inTable(table, cp) {
106
+ let lo = 0;
107
+ let hi = table.length / 2;
108
+ while (lo < hi) {
109
+ const mid = (lo + hi) >> 1;
110
+ if (table[2 * mid + 1] < cp)
111
+ lo = mid + 1;
112
+ else
113
+ hi = mid;
114
+ }
115
+ return lo < table.length / 2 && table[2 * lo] <= cp;
116
+ }
117
+ /** The code points of a string. A lone surrogate counts as one. */
118
+ function codePoints(s) {
119
+ return Array.from(s, ch => ch.codePointAt(0) ?? 0);
120
+ }
121
+ /**
122
+ * Prepares a password as pdf.js does: the same two mappings as SASLprep, then NFKC with the Unicode tables of the
123
+ * running engine. It refuses nothing.
124
+ */
125
+ function pdfjsPrep(input) {
126
+ tables ??= parseTables();
127
+ const t = tables;
128
+ let mapped = '';
129
+ for (const cp of codePoints(input)) {
130
+ if (inTable(t.toSpace, cp))
131
+ mapped += ' ';
132
+ else if (!inTable(t.toNothing, cp))
133
+ mapped += String.fromCodePoint(cp);
134
+ }
135
+ return mapped.normalize('NFKC');
136
+ }
137
+ function parseTables() {
138
+ return {
139
+ unassigned: parse(UNASSIGNED),
140
+ toNothing: parse(TO_NOTHING),
141
+ toSpace: parse(TO_SPACE),
142
+ prohibited: parse(PROHIBITED),
143
+ rtl: parse(RIGHT_TO_LEFT),
144
+ ltr: parse(LEFT_TO_RIGHT),
145
+ };
146
+ }
147
+ /** Prepares a password with SASLprep, or returns undefined when the profile refuses it. */
148
+ function saslprep(input) {
149
+ tables ??= parseTables();
150
+ const t = tables;
151
+ let mapped = '';
152
+ for (const cp of codePoints(input)) {
153
+ // A password checked against a stored hash is a stored string, which may not hold unassigned code points. The
154
+ // check runs before normalizing, which uses a later Unicode version and could map them to assigned ones.
155
+ if (inTable(t.unassigned, cp))
156
+ return undefined;
157
+ if (inTable(t.toSpace, cp))
158
+ mapped += ' ';
159
+ else if (!inTable(t.toNothing, cp))
160
+ mapped += String.fromCodePoint(UNICODE_32.get(cp) ?? cp);
161
+ }
162
+ const out = mapped.normalize('NFKC');
163
+ const cps = codePoints(out);
164
+ let rtl = false;
165
+ let ltr = false;
166
+ for (const cp of cps) {
167
+ if (inTable(t.prohibited, cp))
168
+ return undefined;
169
+ if (inTable(t.rtl, cp))
170
+ rtl = true;
171
+ else if (inTable(t.ltr, cp))
172
+ ltr = true;
173
+ }
174
+ // RFC 3454 section 6: right-to-left text holds no left-to-right characters, and starts and ends right to left.
175
+ if (rtl && (ltr || !inTable(t.rtl, cps[0]) || !inTable(t.rtl, cps[cps.length - 1])))
176
+ return undefined;
177
+ return out;
178
+ }
package/dist/types.d.ts CHANGED
@@ -22,6 +22,8 @@ export declare enum PdfDetail {
22
22
  UserPassword = "USER_PASSWORD",
23
23
  OwnerPassword = "OWNER_PASSWORD",
24
24
  PasswordRequired = "PASSWORD_REQUIRED",
25
+ AttachmentsOnly = "ATTACHMENTS_ONLY",
26
+ NoKey = "NO_KEY",
25
27
  CertificateHandler = "CERTIFICATE_HANDLER",
26
28
  UnknownHandler = "UNKNOWN_HANDLER",
27
29
  UnknownCryptFilter = "UNKNOWN_CRYPT_FILTER",
@@ -38,6 +40,7 @@ export declare enum PdfDetail {
38
40
  StreamLengthWrong = "STREAM_LENGTH_WRONG",
39
41
  LeadingBytes = "LEADING_BYTES",
40
42
  TrailingBytes = "TRAILING_BYTES",
43
+ MissingHeader = "MISSING_HEADER",
41
44
  Document = "DOCUMENT",
42
45
  OpenAction = "OPEN_ACTION",
43
46
  Page = "PAGE",
package/dist/types.js CHANGED
@@ -28,6 +28,8 @@ var PdfDetail;
28
28
  PdfDetail["UserPassword"] = "USER_PASSWORD";
29
29
  PdfDetail["OwnerPassword"] = "OWNER_PASSWORD";
30
30
  PdfDetail["PasswordRequired"] = "PASSWORD_REQUIRED";
31
+ PdfDetail["AttachmentsOnly"] = "ATTACHMENTS_ONLY";
32
+ PdfDetail["NoKey"] = "NO_KEY";
31
33
  PdfDetail["CertificateHandler"] = "CERTIFICATE_HANDLER";
32
34
  PdfDetail["UnknownHandler"] = "UNKNOWN_HANDLER";
33
35
  PdfDetail["UnknownCryptFilter"] = "UNKNOWN_CRYPT_FILTER";
@@ -45,6 +47,7 @@ var PdfDetail;
45
47
  PdfDetail["StreamLengthWrong"] = "STREAM_LENGTH_WRONG";
46
48
  PdfDetail["LeadingBytes"] = "LEADING_BYTES";
47
49
  PdfDetail["TrailingBytes"] = "TRAILING_BYTES";
50
+ PdfDetail["MissingHeader"] = "MISSING_HEADER";
48
51
  // JAVASCRIPT
49
52
  PdfDetail["Document"] = "DOCUMENT";
50
53
  PdfDetail["OpenAction"] = "OPEN_ACTION";
package/dist/walker.d.ts CHANGED
@@ -75,6 +75,8 @@ export declare class Walker {
75
75
  private readonly inlinePages;
76
76
  /** Objects first reached as file specifications, which stay file specifications whatever their content says. */
77
77
  private readonly fileRole;
78
+ /** Page content streams, which pdf.js draws whatever their /Type says. */
79
+ private readonly contentRole;
78
80
  /** Objects listed in /Annots and kept there though they are not annotations, which get the annotation rules. */
79
81
  private readonly annotRole;
80
82
  /** Objects that only removed content refers to, which are not unreferenced. */