diff --git a/.changeset/young-dodos-invent.md b/.changeset/young-dodos-invent.md new file mode 100644 index 000000000..b42a01329 --- /dev/null +++ b/.changeset/young-dodos-invent.md @@ -0,0 +1,5 @@ +--- +"@react-pdf/pdfkit": minor +--- + +Add encryption support and access privileges diff --git a/packages/pdfkit/jest.config.js b/packages/pdfkit/jest.config.js new file mode 100644 index 000000000..fb81d1ccc --- /dev/null +++ b/packages/pdfkit/jest.config.js @@ -0,0 +1,3 @@ +export default { + testRegex: 'tests/unit/.*?(spec)\\.js$', +}; diff --git a/packages/pdfkit/src/document.js b/packages/pdfkit/src/document.js index 2333d0f67..6e5cb9ebe 100644 --- a/packages/pdfkit/src/document.js +++ b/packages/pdfkit/src/document.js @@ -119,7 +119,7 @@ class PDFDocument extends stream.Readable { this._id = PDFSecurity.generateFileID(this.info); // Initialize security settings - // this._security = PDFSecurity.create(this, options); + this._security = PDFSecurity.create(this, options); // Write the header PDF version this._write(`%PDF-${this.version}`); @@ -276,9 +276,9 @@ class PDFDocument extends stream.Readable { this._root.data.ViewerPreferences.end(); } - // if (this._security) { - // this._security.end(); - // } + if (this._security) { + this._security.end(); + } if (this._waiting === 0) { return this._finalize(); @@ -307,9 +307,9 @@ class PDFDocument extends stream.Readable { ID: [this._id, this._id] }; - // if (this._security) { - // trailer.Encrypt = this._security.dictionary; - // } + if (this._security) { + trailer.Encrypt = this._security.dictionary; + } this._write('trailer'); this._write(PDFObject.convert(trailer)); diff --git a/packages/pdfkit/src/font/embedded.js b/packages/pdfkit/src/font/embedded.js index 1e34ec200..f9e887fdf 100644 --- a/packages/pdfkit/src/font/embedded.js +++ b/packages/pdfkit/src/font/embedded.js @@ -149,7 +149,7 @@ const createEmbeddedFont = (PDFFont) => fontFile.data.Subtype = 'CIDFontType0C'; } - fontFile.end(this.subset.encode()); + fontFile.end(Buffer.from(this.subset.encode())); const familyClass = ((this.font['OS/2'] != null diff --git a/packages/pdfkit/src/object.js b/packages/pdfkit/src/object.js index b142c3218..41fb03c61 100644 --- a/packages/pdfkit/src/object.js +++ b/packages/pdfkit/src/object.js @@ -32,7 +32,7 @@ const swapBytes = function (buff) { }; class PDFObject { - static convert(object) { + static convert(object, encryptFn = null) { // String literals are converted to the PDF name type if (typeof object === 'string') { return `/${object}`; @@ -51,10 +51,18 @@ class PDFObject { } // If so, encode it as big endian UTF-16 + let stringBuffer; if (isUnicode) { - string = swapBytes(Buffer.from(`\ufeff${string}`, 'utf16le')).toString( - 'binary' - ); + stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, 'utf16le')); + } else { + stringBuffer = Buffer.from(string.valueOf(), 'ascii'); + } + + // Encrypt the string when necessary + if (encryptFn) { + string = encryptFn(stringBuffer).toString('binary'); + } else { + string = stringBuffer.toString('binary'); } // Escape characters as required by the spec @@ -62,9 +70,9 @@ class PDFObject { return `(${string})`; - // Buffers are converted to PDF hex strings } + // Buffers are converted to PDF hex strings if (Buffer.isBuffer(object)) { return `<${object.toString('hex')}>`; } @@ -74,20 +82,29 @@ class PDFObject { } if (object instanceof Date) { - return ( - `(D:${pad(object.getUTCFullYear(), 4)}` + + let string = + `D:${pad(object.getUTCFullYear(), 4)}` + pad(object.getUTCMonth() + 1, 2) + pad(object.getUTCDate(), 2) + pad(object.getUTCHours(), 2) + pad(object.getUTCMinutes(), 2) + pad(object.getUTCSeconds(), 2) + - 'Z)' - ); + 'Z'; + + // Encrypt the string when necessary + if (encryptFn) { + string = encryptFn(Buffer.from(string, 'ascii')).toString('binary'); + + // Escape characters as required by the spec + string = string.replace(escapableRe, c => escapable[c]); + } + + return `(${string})`; } if (Array.isArray(object)) { const items = Array.from(object) - .map((e) => PDFObject.convert(e)) + .map(e => PDFObject.convert(e, encryptFn)) .join(' '); return `[${items}]`; } @@ -96,7 +113,7 @@ class PDFObject { const out = ['<<']; for (let key in object) { const val = object[key]; - out.push(`/${key} ${PDFObject.convert(val)}`); + out.push(`/${key} ${PDFObject.convert(val, encryptFn)}`); } out.push('>>'); diff --git a/packages/pdfkit/src/reference.js b/packages/pdfkit/src/reference.js index b5b4533d4..0780e1744 100644 --- a/packages/pdfkit/src/reference.js +++ b/packages/pdfkit/src/reference.js @@ -1,40 +1,19 @@ import zlib from 'zlib'; -import stream from 'stream'; import PDFObject from './object'; -class PDFReference extends stream.Writable { - constructor(document, id, data) { - super({ decodeStrings: false }); - - this.finalize = this.finalize.bind(this); +class PDFReference { + constructor(document, id, data = {}) { this.document = document; this.id = id; - if (data == null) { - data = {}; - } this.data = data; - this.gen = 0; - this.deflate = null; this.compress = this.document.compress && !this.data.Filter; this.uncompressedLength = 0; - this.chunks = []; - } - - initDeflate() { - this.data.Filter = 'FlateDecode'; - - this.deflate = zlib.createDeflate(); - this.deflate.on('data', (chunk) => { - this.chunks.push(chunk); - return (this.data.Length += chunk.length); - }); - - return this.deflate.on('end', this.finalize); + this.buffer = []; } - _write(chunk, encoding, callback) { - if (!(chunk instanceof Uint8Array)) { + write(chunk) { + if (!Buffer.isBuffer(chunk)) { chunk = Buffer.from(chunk + '\n', 'binary'); } @@ -42,48 +21,53 @@ class PDFReference extends stream.Writable { if (this.data.Length == null) { this.data.Length = 0; } - + this.buffer.push(chunk); + this.data.Length += chunk.length; if (this.compress) { - if (!this.deflate) { - this.initDeflate(); - } - this.deflate.write(chunk); - } else { - this.chunks.push(chunk); - this.data.Length += chunk.length; + return (this.data.Filter = 'FlateDecode'); } - - return callback(); } - end() { - super.end(...arguments); - - if (this.deflate) { - return this.deflate.end(); + end(chunk) { + if (chunk) { + this.write(chunk); } - return this.finalize(); } finalize() { this.offset = this.document._offset; + const encryptFn = this.document._security + ? this.document._security.getEncryptFn(this.id, this.gen) + : null; + + if (this.buffer.length) { + this.buffer = Buffer.concat(this.buffer); + if (this.compress) { + this.buffer = zlib.deflateSync(this.buffer); + } + + if (encryptFn) { + this.buffer = encryptFn(this.buffer); + } + + this.data.Length = this.buffer.length; + } + this.document._write(`${this.id} ${this.gen} obj`); - this.document._write(PDFObject.convert(this.data)); + this.document._write(PDFObject.convert(this.data, encryptFn)); - if (this.chunks.length) { + if (this.buffer.length) { this.document._write('stream'); - for (let chunk of Array.from(this.chunks)) { - this.document._write(chunk); - } + this.document._write(this.buffer); - this.chunks.length = 0; // free up memory + this.buffer = []; // free up memory this.document._write('\nendstream'); } this.document._write('endobj'); - return this.document._refEnd(this); + this.document._refEnd(this); } toString() { diff --git a/packages/pdfkit/src/saslprep/index.js b/packages/pdfkit/src/saslprep/index.js new file mode 100644 index 000000000..4e1f5a7a4 --- /dev/null +++ b/packages/pdfkit/src/saslprep/index.js @@ -0,0 +1,147 @@ +import { + isUnassignedCodePoint, + isCommonlyMappedToNothing, + isNonASCIISpaceCharacter, + isProhibitedCharacter, + isBidirectionalRAL, + isBidirectionalL +} from './lib/code-points'; + +// 2.1. Mapping + +/** + * non-ASCII space characters [StringPrep, C.1.2] that can be + * mapped to SPACE (U+0020) + */ +const mapping2space = isNonASCIISpaceCharacter; + +/** + * the "commonly mapped to nothing" characters [StringPrep, B.1] + * that can be mapped to nothing. + */ +const mapping2nothing = isCommonlyMappedToNothing; + +// utils +const getCodePoint = character => character.codePointAt(0); +const first = x => x[0]; +const last = x => x[x.length - 1]; + +/** + * Convert provided string into an array of Unicode Code Points. + * Based on https://stackoverflow.com/a/21409165/1556249 + * and https://www.npmjs.com/package/code-point-at. + * @param {string} input + * @returns {number[]} + */ +function toCodePoints(input) { + const codepoints = []; + const size = input.length; + + for (let i = 0; i < size; i += 1) { + const before = input.charCodeAt(i); + + if (before >= 0xd800 && before <= 0xdbff && size > i + 1) { + const next = input.charCodeAt(i + 1); + + if (next >= 0xdc00 && next <= 0xdfff) { + codepoints.push((before - 0xd800) * 0x400 + next - 0xdc00 + 0x10000); + i += 1; + continue; + } + } + + codepoints.push(before); + } + + return codepoints; +} + +/** + * SASLprep. + * @param {string} input + * @param {Object} opts + * @param {boolean} opts.allowUnassigned + * @returns {string} + */ +function saslprep(input, opts = {}) { + if (typeof input !== 'string') { + throw new TypeError('Expected string.'); + } + + if (input.length === 0) { + return ''; + } + + // 1. Map + const mapped_input = toCodePoints(input) + // 1.1 mapping to space + .map(character => (mapping2space(character) ? 0x20 : character)) + // 1.2 mapping to nothing + .filter(character => !mapping2nothing(character)); + + // 2. Normalize + const normalized_input = String.fromCodePoint + .apply(null, mapped_input) + .normalize('NFKC'); + + const normalized_map = toCodePoints(normalized_input); + + // 3. Prohibit + const hasProhibited = normalized_map.some(isProhibitedCharacter); + + if (hasProhibited) { + throw new Error( + 'Prohibited character, see https://tools.ietf.org/html/rfc4013#section-2.3' + ); + } + + // Unassigned Code Points + if (opts.allowUnassigned !== true) { + const hasUnassigned = normalized_map.some(isUnassignedCodePoint); + + if (hasUnassigned) { + throw new Error( + 'Unassigned code point, see https://tools.ietf.org/html/rfc4013#section-2.5' + ); + } + } + + // 4. check bidi + + const hasBidiRAL = normalized_map.some(isBidirectionalRAL); + + const hasBidiL = normalized_map.some(isBidirectionalL); + + // 4.1 If a string contains any RandALCat character, the string MUST NOT + // contain any LCat character. + if (hasBidiRAL && hasBidiL) { + throw new Error( + 'String must not contain RandALCat and LCat at the same time,' + + ' see https://tools.ietf.org/html/rfc3454#section-6' + ); + } + + /** + * 4.2 If a string contains any RandALCat character, a RandALCat + * character MUST be the first character of the string, and a + * RandALCat character MUST be the last character of the string. + */ + + const isFirstBidiRAL = isBidirectionalRAL( + getCodePoint(first(normalized_input)) + ); + const isLastBidiRAL = isBidirectionalRAL( + getCodePoint(last(normalized_input)) + ); + + if (hasBidiRAL && !(isFirstBidiRAL && isLastBidiRAL)) { + throw new Error( + 'Bidirectional RandALCat character must be the first and the last' + + ' character of the string, see https://tools.ietf.org/html/rfc3454#section-6' + ); + } + + return normalized_input; +} + +export default saslprep; diff --git a/packages/pdfkit/src/saslprep/lib/code-points.js b/packages/pdfkit/src/saslprep/lib/code-points.js new file mode 100644 index 000000000..fbe07e573 --- /dev/null +++ b/packages/pdfkit/src/saslprep/lib/code-points.js @@ -0,0 +1,1928 @@ +import { inRange } from './util'; + +// prettier-ignore-start +/** + * A.1 Unassigned code points in Unicode 3.2 + * @link https://tools.ietf.org/html/rfc3454#appendix-A.1 + */ +const unassigned_code_points = [ + 0x0221, + 0x0221, + 0x0234, + 0x024f, + 0x02ae, + 0x02af, + 0x02ef, + 0x02ff, + 0x0350, + 0x035f, + 0x0370, + 0x0373, + 0x0376, + 0x0379, + 0x037b, + 0x037d, + 0x037f, + 0x0383, + 0x038b, + 0x038b, + 0x038d, + 0x038d, + 0x03a2, + 0x03a2, + 0x03cf, + 0x03cf, + 0x03f7, + 0x03ff, + 0x0487, + 0x0487, + 0x04cf, + 0x04cf, + 0x04f6, + 0x04f7, + 0x04fa, + 0x04ff, + 0x0510, + 0x0530, + 0x0557, + 0x0558, + 0x0560, + 0x0560, + 0x0588, + 0x0588, + 0x058b, + 0x0590, + 0x05a2, + 0x05a2, + 0x05ba, + 0x05ba, + 0x05c5, + 0x05cf, + 0x05eb, + 0x05ef, + 0x05f5, + 0x060b, + 0x060d, + 0x061a, + 0x061c, + 0x061e, + 0x0620, + 0x0620, + 0x063b, + 0x063f, + 0x0656, + 0x065f, + 0x06ee, + 0x06ef, + 0x06ff, + 0x06ff, + 0x070e, + 0x070e, + 0x072d, + 0x072f, + 0x074b, + 0x077f, + 0x07b2, + 0x0900, + 0x0904, + 0x0904, + 0x093a, + 0x093b, + 0x094e, + 0x094f, + 0x0955, + 0x0957, + 0x0971, + 0x0980, + 0x0984, + 0x0984, + 0x098d, + 0x098e, + 0x0991, + 0x0992, + 0x09a9, + 0x09a9, + 0x09b1, + 0x09b1, + 0x09b3, + 0x09b5, + 0x09ba, + 0x09bb, + 0x09bd, + 0x09bd, + 0x09c5, + 0x09c6, + 0x09c9, + 0x09ca, + 0x09ce, + 0x09d6, + 0x09d8, + 0x09db, + 0x09de, + 0x09de, + 0x09e4, + 0x09e5, + 0x09fb, + 0x0a01, + 0x0a03, + 0x0a04, + 0x0a0b, + 0x0a0e, + 0x0a11, + 0x0a12, + 0x0a29, + 0x0a29, + 0x0a31, + 0x0a31, + 0x0a34, + 0x0a34, + 0x0a37, + 0x0a37, + 0x0a3a, + 0x0a3b, + 0x0a3d, + 0x0a3d, + 0x0a43, + 0x0a46, + 0x0a49, + 0x0a4a, + 0x0a4e, + 0x0a58, + 0x0a5d, + 0x0a5d, + 0x0a5f, + 0x0a65, + 0x0a75, + 0x0a80, + 0x0a84, + 0x0a84, + 0x0a8c, + 0x0a8c, + 0x0a8e, + 0x0a8e, + 0x0a92, + 0x0a92, + 0x0aa9, + 0x0aa9, + 0x0ab1, + 0x0ab1, + 0x0ab4, + 0x0ab4, + 0x0aba, + 0x0abb, + 0x0ac6, + 0x0ac6, + 0x0aca, + 0x0aca, + 0x0ace, + 0x0acf, + 0x0ad1, + 0x0adf, + 0x0ae1, + 0x0ae5, + 0x0af0, + 0x0b00, + 0x0b04, + 0x0b04, + 0x0b0d, + 0x0b0e, + 0x0b11, + 0x0b12, + 0x0b29, + 0x0b29, + 0x0b31, + 0x0b31, + 0x0b34, + 0x0b35, + 0x0b3a, + 0x0b3b, + 0x0b44, + 0x0b46, + 0x0b49, + 0x0b4a, + 0x0b4e, + 0x0b55, + 0x0b58, + 0x0b5b, + 0x0b5e, + 0x0b5e, + 0x0b62, + 0x0b65, + 0x0b71, + 0x0b81, + 0x0b84, + 0x0b84, + 0x0b8b, + 0x0b8d, + 0x0b91, + 0x0b91, + 0x0b96, + 0x0b98, + 0x0b9b, + 0x0b9b, + 0x0b9d, + 0x0b9d, + 0x0ba0, + 0x0ba2, + 0x0ba5, + 0x0ba7, + 0x0bab, + 0x0bad, + 0x0bb6, + 0x0bb6, + 0x0bba, + 0x0bbd, + 0x0bc3, + 0x0bc5, + 0x0bc9, + 0x0bc9, + 0x0bce, + 0x0bd6, + 0x0bd8, + 0x0be6, + 0x0bf3, + 0x0c00, + 0x0c04, + 0x0c04, + 0x0c0d, + 0x0c0d, + 0x0c11, + 0x0c11, + 0x0c29, + 0x0c29, + 0x0c34, + 0x0c34, + 0x0c3a, + 0x0c3d, + 0x0c45, + 0x0c45, + 0x0c49, + 0x0c49, + 0x0c4e, + 0x0c54, + 0x0c57, + 0x0c5f, + 0x0c62, + 0x0c65, + 0x0c70, + 0x0c81, + 0x0c84, + 0x0c84, + 0x0c8d, + 0x0c8d, + 0x0c91, + 0x0c91, + 0x0ca9, + 0x0ca9, + 0x0cb4, + 0x0cb4, + 0x0cba, + 0x0cbd, + 0x0cc5, + 0x0cc5, + 0x0cc9, + 0x0cc9, + 0x0cce, + 0x0cd4, + 0x0cd7, + 0x0cdd, + 0x0cdf, + 0x0cdf, + 0x0ce2, + 0x0ce5, + 0x0cf0, + 0x0d01, + 0x0d04, + 0x0d04, + 0x0d0d, + 0x0d0d, + 0x0d11, + 0x0d11, + 0x0d29, + 0x0d29, + 0x0d3a, + 0x0d3d, + 0x0d44, + 0x0d45, + 0x0d49, + 0x0d49, + 0x0d4e, + 0x0d56, + 0x0d58, + 0x0d5f, + 0x0d62, + 0x0d65, + 0x0d70, + 0x0d81, + 0x0d84, + 0x0d84, + 0x0d97, + 0x0d99, + 0x0db2, + 0x0db2, + 0x0dbc, + 0x0dbc, + 0x0dbe, + 0x0dbf, + 0x0dc7, + 0x0dc9, + 0x0dcb, + 0x0dce, + 0x0dd5, + 0x0dd5, + 0x0dd7, + 0x0dd7, + 0x0de0, + 0x0df1, + 0x0df5, + 0x0e00, + 0x0e3b, + 0x0e3e, + 0x0e5c, + 0x0e80, + 0x0e83, + 0x0e83, + 0x0e85, + 0x0e86, + 0x0e89, + 0x0e89, + 0x0e8b, + 0x0e8c, + 0x0e8e, + 0x0e93, + 0x0e98, + 0x0e98, + 0x0ea0, + 0x0ea0, + 0x0ea4, + 0x0ea4, + 0x0ea6, + 0x0ea6, + 0x0ea8, + 0x0ea9, + 0x0eac, + 0x0eac, + 0x0eba, + 0x0eba, + 0x0ebe, + 0x0ebf, + 0x0ec5, + 0x0ec5, + 0x0ec7, + 0x0ec7, + 0x0ece, + 0x0ecf, + 0x0eda, + 0x0edb, + 0x0ede, + 0x0eff, + 0x0f48, + 0x0f48, + 0x0f6b, + 0x0f70, + 0x0f8c, + 0x0f8f, + 0x0f98, + 0x0f98, + 0x0fbd, + 0x0fbd, + 0x0fcd, + 0x0fce, + 0x0fd0, + 0x0fff, + 0x1022, + 0x1022, + 0x1028, + 0x1028, + 0x102b, + 0x102b, + 0x1033, + 0x1035, + 0x103a, + 0x103f, + 0x105a, + 0x109f, + 0x10c6, + 0x10cf, + 0x10f9, + 0x10fa, + 0x10fc, + 0x10ff, + 0x115a, + 0x115e, + 0x11a3, + 0x11a7, + 0x11fa, + 0x11ff, + 0x1207, + 0x1207, + 0x1247, + 0x1247, + 0x1249, + 0x1249, + 0x124e, + 0x124f, + 0x1257, + 0x1257, + 0x1259, + 0x1259, + 0x125e, + 0x125f, + 0x1287, + 0x1287, + 0x1289, + 0x1289, + 0x128e, + 0x128f, + 0x12af, + 0x12af, + 0x12b1, + 0x12b1, + 0x12b6, + 0x12b7, + 0x12bf, + 0x12bf, + 0x12c1, + 0x12c1, + 0x12c6, + 0x12c7, + 0x12cf, + 0x12cf, + 0x12d7, + 0x12d7, + 0x12ef, + 0x12ef, + 0x130f, + 0x130f, + 0x1311, + 0x1311, + 0x1316, + 0x1317, + 0x131f, + 0x131f, + 0x1347, + 0x1347, + 0x135b, + 0x1360, + 0x137d, + 0x139f, + 0x13f5, + 0x1400, + 0x1677, + 0x167f, + 0x169d, + 0x169f, + 0x16f1, + 0x16ff, + 0x170d, + 0x170d, + 0x1715, + 0x171f, + 0x1737, + 0x173f, + 0x1754, + 0x175f, + 0x176d, + 0x176d, + 0x1771, + 0x1771, + 0x1774, + 0x177f, + 0x17dd, + 0x17df, + 0x17ea, + 0x17ff, + 0x180f, + 0x180f, + 0x181a, + 0x181f, + 0x1878, + 0x187f, + 0x18aa, + 0x1dff, + 0x1e9c, + 0x1e9f, + 0x1efa, + 0x1eff, + 0x1f16, + 0x1f17, + 0x1f1e, + 0x1f1f, + 0x1f46, + 0x1f47, + 0x1f4e, + 0x1f4f, + 0x1f58, + 0x1f58, + 0x1f5a, + 0x1f5a, + 0x1f5c, + 0x1f5c, + 0x1f5e, + 0x1f5e, + 0x1f7e, + 0x1f7f, + 0x1fb5, + 0x1fb5, + 0x1fc5, + 0x1fc5, + 0x1fd4, + 0x1fd5, + 0x1fdc, + 0x1fdc, + 0x1ff0, + 0x1ff1, + 0x1ff5, + 0x1ff5, + 0x1fff, + 0x1fff, + 0x2053, + 0x2056, + 0x2058, + 0x205e, + 0x2064, + 0x2069, + 0x2072, + 0x2073, + 0x208f, + 0x209f, + 0x20b2, + 0x20cf, + 0x20eb, + 0x20ff, + 0x213b, + 0x213c, + 0x214c, + 0x2152, + 0x2184, + 0x218f, + 0x23cf, + 0x23ff, + 0x2427, + 0x243f, + 0x244b, + 0x245f, + 0x24ff, + 0x24ff, + 0x2614, + 0x2615, + 0x2618, + 0x2618, + 0x267e, + 0x267f, + 0x268a, + 0x2700, + 0x2705, + 0x2705, + 0x270a, + 0x270b, + 0x2728, + 0x2728, + 0x274c, + 0x274c, + 0x274e, + 0x274e, + 0x2753, + 0x2755, + 0x2757, + 0x2757, + 0x275f, + 0x2760, + 0x2795, + 0x2797, + 0x27b0, + 0x27b0, + 0x27bf, + 0x27cf, + 0x27ec, + 0x27ef, + 0x2b00, + 0x2e7f, + 0x2e9a, + 0x2e9a, + 0x2ef4, + 0x2eff, + 0x2fd6, + 0x2fef, + 0x2ffc, + 0x2fff, + 0x3040, + 0x3040, + 0x3097, + 0x3098, + 0x3100, + 0x3104, + 0x312d, + 0x3130, + 0x318f, + 0x318f, + 0x31b8, + 0x31ef, + 0x321d, + 0x321f, + 0x3244, + 0x3250, + 0x327c, + 0x327e, + 0x32cc, + 0x32cf, + 0x32ff, + 0x32ff, + 0x3377, + 0x337a, + 0x33de, + 0x33df, + 0x33ff, + 0x33ff, + 0x4db6, + 0x4dff, + 0x9fa6, + 0x9fff, + 0xa48d, + 0xa48f, + 0xa4c7, + 0xabff, + 0xd7a4, + 0xd7ff, + 0xfa2e, + 0xfa2f, + 0xfa6b, + 0xfaff, + 0xfb07, + 0xfb12, + 0xfb18, + 0xfb1c, + 0xfb37, + 0xfb37, + 0xfb3d, + 0xfb3d, + 0xfb3f, + 0xfb3f, + 0xfb42, + 0xfb42, + 0xfb45, + 0xfb45, + 0xfbb2, + 0xfbd2, + 0xfd40, + 0xfd4f, + 0xfd90, + 0xfd91, + 0xfdc8, + 0xfdcf, + 0xfdfd, + 0xfdff, + 0xfe10, + 0xfe1f, + 0xfe24, + 0xfe2f, + 0xfe47, + 0xfe48, + 0xfe53, + 0xfe53, + 0xfe67, + 0xfe67, + 0xfe6c, + 0xfe6f, + 0xfe75, + 0xfe75, + 0xfefd, + 0xfefe, + 0xff00, + 0xff00, + 0xffbf, + 0xffc1, + 0xffc8, + 0xffc9, + 0xffd0, + 0xffd1, + 0xffd8, + 0xffd9, + 0xffdd, + 0xffdf, + 0xffe7, + 0xffe7, + 0xffef, + 0xfff8, + 0x10000, + 0x102ff, + 0x1031f, + 0x1031f, + 0x10324, + 0x1032f, + 0x1034b, + 0x103ff, + 0x10426, + 0x10427, + 0x1044e, + 0x1cfff, + 0x1d0f6, + 0x1d0ff, + 0x1d127, + 0x1d129, + 0x1d1de, + 0x1d3ff, + 0x1d455, + 0x1d455, + 0x1d49d, + 0x1d49d, + 0x1d4a0, + 0x1d4a1, + 0x1d4a3, + 0x1d4a4, + 0x1d4a7, + 0x1d4a8, + 0x1d4ad, + 0x1d4ad, + 0x1d4ba, + 0x1d4ba, + 0x1d4bc, + 0x1d4bc, + 0x1d4c1, + 0x1d4c1, + 0x1d4c4, + 0x1d4c4, + 0x1d506, + 0x1d506, + 0x1d50b, + 0x1d50c, + 0x1d515, + 0x1d515, + 0x1d51d, + 0x1d51d, + 0x1d53a, + 0x1d53a, + 0x1d53f, + 0x1d53f, + 0x1d545, + 0x1d545, + 0x1d547, + 0x1d549, + 0x1d551, + 0x1d551, + 0x1d6a4, + 0x1d6a7, + 0x1d7ca, + 0x1d7cd, + 0x1d800, + 0x1fffd, + 0x2a6d7, + 0x2f7ff, + 0x2fa1e, + 0x2fffd, + 0x30000, + 0x3fffd, + 0x40000, + 0x4fffd, + 0x50000, + 0x5fffd, + 0x60000, + 0x6fffd, + 0x70000, + 0x7fffd, + 0x80000, + 0x8fffd, + 0x90000, + 0x9fffd, + 0xa0000, + 0xafffd, + 0xb0000, + 0xbfffd, + 0xc0000, + 0xcfffd, + 0xd0000, + 0xdfffd, + 0xe0000, + 0xe0000, + 0xe0002, + 0xe001f, + 0xe0080, + 0xefffd +]; +// prettier-ignore-end + +const isUnassignedCodePoint = character => + inRange(character, unassigned_code_points); + +// prettier-ignore-start +/** + * B.1 Commonly mapped to nothing + * @link https://tools.ietf.org/html/rfc3454#appendix-B.1 + */ +const commonly_mapped_to_nothing = [ + 0x00ad, + 0x00ad, + 0x034f, + 0x034f, + 0x1806, + 0x1806, + 0x180b, + 0x180b, + 0x180c, + 0x180c, + 0x180d, + 0x180d, + 0x200b, + 0x200b, + 0x200c, + 0x200c, + 0x200d, + 0x200d, + 0x2060, + 0x2060, + 0xfe00, + 0xfe00, + 0xfe01, + 0xfe01, + 0xfe02, + 0xfe02, + 0xfe03, + 0xfe03, + 0xfe04, + 0xfe04, + 0xfe05, + 0xfe05, + 0xfe06, + 0xfe06, + 0xfe07, + 0xfe07, + 0xfe08, + 0xfe08, + 0xfe09, + 0xfe09, + 0xfe0a, + 0xfe0a, + 0xfe0b, + 0xfe0b, + 0xfe0c, + 0xfe0c, + 0xfe0d, + 0xfe0d, + 0xfe0e, + 0xfe0e, + 0xfe0f, + 0xfe0f, + 0xfeff, + 0xfeff +]; +// prettier-ignore-end + +const isCommonlyMappedToNothing = character => + inRange(character, commonly_mapped_to_nothing); + +// prettier-ignore-start +/** + * C.1.2 Non-ASCII space characters + * @link https://tools.ietf.org/html/rfc3454#appendix-C.1.2 + */ +const non_ASCII_space_characters = [ + 0x00a0, + 0x00a0 /* NO-BREAK SPACE */, + 0x1680, + 0x1680 /* OGHAM SPACE MARK */, + 0x2000, + 0x2000 /* EN QUAD */, + 0x2001, + 0x2001 /* EM QUAD */, + 0x2002, + 0x2002 /* EN SPACE */, + 0x2003, + 0x2003 /* EM SPACE */, + 0x2004, + 0x2004 /* THREE-PER-EM SPACE */, + 0x2005, + 0x2005 /* FOUR-PER-EM SPACE */, + 0x2006, + 0x2006 /* SIX-PER-EM SPACE */, + 0x2007, + 0x2007 /* FIGURE SPACE */, + 0x2008, + 0x2008 /* PUNCTUATION SPACE */, + 0x2009, + 0x2009 /* THIN SPACE */, + 0x200a, + 0x200a /* HAIR SPACE */, + 0x200b, + 0x200b /* ZERO WIDTH SPACE */, + 0x202f, + 0x202f /* NARROW NO-BREAK SPACE */, + 0x205f, + 0x205f /* MEDIUM MATHEMATICAL SPACE */, + 0x3000, + 0x3000 /* IDEOGRAPHIC SPACE */ +]; +// prettier-ignore-end + +const isNonASCIISpaceCharacter = character => + inRange(character, non_ASCII_space_characters); + +// prettier-ignore-start +const non_ASCII_controls_characters = [ + /** + * C.2.2 Non-ASCII control characters + * @link https://tools.ietf.org/html/rfc3454#appendix-C.2.2 + */ + 0x0080, + 0x009f /* [CONTROL CHARACTERS] */, + 0x06dd, + 0x06dd /* ARABIC END OF AYAH */, + 0x070f, + 0x070f /* SYRIAC ABBREVIATION MARK */, + 0x180e, + 0x180e /* MONGOLIAN VOWEL SEPARATOR */, + 0x200c, + 0x200c /* ZERO WIDTH NON-JOINER */, + 0x200d, + 0x200d /* ZERO WIDTH JOINER */, + 0x2028, + 0x2028 /* LINE SEPARATOR */, + 0x2029, + 0x2029 /* PARAGRAPH SEPARATOR */, + 0x2060, + 0x2060 /* WORD JOINER */, + 0x2061, + 0x2061 /* FUNCTION APPLICATION */, + 0x2062, + 0x2062 /* INVISIBLE TIMES */, + 0x2063, + 0x2063 /* INVISIBLE SEPARATOR */, + 0x206a, + 0x206f /* [CONTROL CHARACTERS] */, + 0xfeff, + 0xfeff /* ZERO WIDTH NO-BREAK SPACE */, + 0xfff9, + 0xfffc /* [CONTROL CHARACTERS] */, + 0x1d173, + 0x1d17a /* [MUSICAL CONTROL CHARACTERS] */ +]; + +const non_character_codepoints = [ + /** + * C.4 Non-character code points + * @link https://tools.ietf.org/html/rfc3454#appendix-C.4 + */ + 0xfdd0, + 0xfdef /* [NONCHARACTER CODE POINTS] */, + 0xfffe, + 0xffff /* [NONCHARACTER CODE POINTS] */, + 0x1fffe, + 0x1ffff /* [NONCHARACTER CODE POINTS] */, + 0x2fffe, + 0x2ffff /* [NONCHARACTER CODE POINTS] */, + 0x3fffe, + 0x3ffff /* [NONCHARACTER CODE POINTS] */, + 0x4fffe, + 0x4ffff /* [NONCHARACTER CODE POINTS] */, + 0x5fffe, + 0x5ffff /* [NONCHARACTER CODE POINTS] */, + 0x6fffe, + 0x6ffff /* [NONCHARACTER CODE POINTS] */, + 0x7fffe, + 0x7ffff /* [NONCHARACTER CODE POINTS] */, + 0x8fffe, + 0x8ffff /* [NONCHARACTER CODE POINTS] */, + 0x9fffe, + 0x9ffff /* [NONCHARACTER CODE POINTS] */, + 0xafffe, + 0xaffff /* [NONCHARACTER CODE POINTS] */, + 0xbfffe, + 0xbffff /* [NONCHARACTER CODE POINTS] */, + 0xcfffe, + 0xcffff /* [NONCHARACTER CODE POINTS] */, + 0xdfffe, + 0xdffff /* [NONCHARACTER CODE POINTS] */, + 0xefffe, + 0xeffff /* [NONCHARACTER CODE POINTS] */, + 0x10fffe, + 0x10ffff /* [NONCHARACTER CODE POINTS] */ +]; + +/** + * 2.3. Prohibited Output + */ +const prohibited_characters = [ + /** + * C.2.1 ASCII control characters + * @link https://tools.ietf.org/html/rfc3454#appendix-C.2.1 + */ + 0, + 0x001f /* [CONTROL CHARACTERS] */, + 0x007f, + 0x007f /* DELETE */, + + /** + * C.8 Change display properties or are deprecated + * @link https://tools.ietf.org/html/rfc3454#appendix-C.8 + */ + 0x0340, + 0x0340 /* COMBINING GRAVE TONE MARK */, + 0x0341, + 0x0341 /* COMBINING ACUTE TONE MARK */, + 0x200e, + 0x200e /* LEFT-TO-RIGHT MARK */, + 0x200f, + 0x200f /* RIGHT-TO-LEFT MARK */, + 0x202a, + 0x202a /* LEFT-TO-RIGHT EMBEDDING */, + 0x202b, + 0x202b /* RIGHT-TO-LEFT EMBEDDING */, + 0x202c, + 0x202c /* POP DIRECTIONAL FORMATTING */, + 0x202d, + 0x202d /* LEFT-TO-RIGHT OVERRIDE */, + 0x202e, + 0x202e /* RIGHT-TO-LEFT OVERRIDE */, + 0x206a, + 0x206a /* INHIBIT SYMMETRIC SWAPPING */, + 0x206b, + 0x206b /* ACTIVATE SYMMETRIC SWAPPING */, + 0x206c, + 0x206c /* INHIBIT ARABIC FORM SHAPING */, + 0x206d, + 0x206d /* ACTIVATE ARABIC FORM SHAPING */, + 0x206e, + 0x206e /* NATIONAL DIGIT SHAPES */, + 0x206f, + 0x206f /* NOMINAL DIGIT SHAPES */, + + /** + * C.7 Inappropriate for canonical representation + * @link https://tools.ietf.org/html/rfc3454#appendix-C.7 + */ + 0x2ff0, + 0x2ffb /* [IDEOGRAPHIC DESCRIPTION CHARACTERS] */, + + /** + * C.5 Surrogate codes + * @link https://tools.ietf.org/html/rfc3454#appendix-C.5 + */ + 0xd800, + 0xdfff, + + /** + * C.3 Private use + * @link https://tools.ietf.org/html/rfc3454#appendix-C.3 + */ + 0xe000, + 0xf8ff /* [PRIVATE USE, PLANE 0] */, + + /** + * C.6 Inappropriate for plain text + * @link https://tools.ietf.org/html/rfc3454#appendix-C.6 + */ + 0xfff9, + 0xfff9 /* INTERLINEAR ANNOTATION ANCHOR */, + 0xfffa, + 0xfffa /* INTERLINEAR ANNOTATION SEPARATOR */, + 0xfffb, + 0xfffb /* INTERLINEAR ANNOTATION TERMINATOR */, + 0xfffc, + 0xfffc /* OBJECT REPLACEMENT CHARACTER */, + 0xfffd, + 0xfffd /* REPLACEMENT CHARACTER */, + + /** + * C.9 Tagging characters + * @link https://tools.ietf.org/html/rfc3454#appendix-C.9 + */ + 0xe0001, + 0xe0001 /* LANGUAGE TAG */, + 0xe0020, + 0xe007f /* [TAGGING CHARACTERS] */, + + /** + * C.3 Private use + * @link https://tools.ietf.org/html/rfc3454#appendix-C.3 + */ + + 0xf0000, + 0xffffd /* [PRIVATE USE, PLANE 15] */, + 0x100000, + 0x10fffd /* [PRIVATE USE, PLANE 16] */ +]; +// prettier-ignore-end + +const isProhibitedCharacter = character => + inRange(character, non_ASCII_space_characters) || + inRange(character, prohibited_characters) || + inRange(character, non_ASCII_controls_characters) || + inRange(character, non_character_codepoints); + +// prettier-ignore-start +/** + * D.1 Characters with bidirectional property "R" or "AL" + * @link https://tools.ietf.org/html/rfc3454#appendix-D.1 + */ +const bidirectional_r_al = [ + 0x05be, + 0x05be, + 0x05c0, + 0x05c0, + 0x05c3, + 0x05c3, + 0x05d0, + 0x05ea, + 0x05f0, + 0x05f4, + 0x061b, + 0x061b, + 0x061f, + 0x061f, + 0x0621, + 0x063a, + 0x0640, + 0x064a, + 0x066d, + 0x066f, + 0x0671, + 0x06d5, + 0x06dd, + 0x06dd, + 0x06e5, + 0x06e6, + 0x06fa, + 0x06fe, + 0x0700, + 0x070d, + 0x0710, + 0x0710, + 0x0712, + 0x072c, + 0x0780, + 0x07a5, + 0x07b1, + 0x07b1, + 0x200f, + 0x200f, + 0xfb1d, + 0xfb1d, + 0xfb1f, + 0xfb28, + 0xfb2a, + 0xfb36, + 0xfb38, + 0xfb3c, + 0xfb3e, + 0xfb3e, + 0xfb40, + 0xfb41, + 0xfb43, + 0xfb44, + 0xfb46, + 0xfbb1, + 0xfbd3, + 0xfd3d, + 0xfd50, + 0xfd8f, + 0xfd92, + 0xfdc7, + 0xfdf0, + 0xfdfc, + 0xfe70, + 0xfe74, + 0xfe76, + 0xfefc +]; +// prettier-ignore-end + +const isBidirectionalRAL = character => inRange(character, bidirectional_r_al); + +// prettier-ignore-start +/** + * D.2 Characters with bidirectional property "L" + * @link https://tools.ietf.org/html/rfc3454#appendix-D.2 + */ +const bidirectional_l = [ + 0x0041, + 0x005a, + 0x0061, + 0x007a, + 0x00aa, + 0x00aa, + 0x00b5, + 0x00b5, + 0x00ba, + 0x00ba, + 0x00c0, + 0x00d6, + 0x00d8, + 0x00f6, + 0x00f8, + 0x0220, + 0x0222, + 0x0233, + 0x0250, + 0x02ad, + 0x02b0, + 0x02b8, + 0x02bb, + 0x02c1, + 0x02d0, + 0x02d1, + 0x02e0, + 0x02e4, + 0x02ee, + 0x02ee, + 0x037a, + 0x037a, + 0x0386, + 0x0386, + 0x0388, + 0x038a, + 0x038c, + 0x038c, + 0x038e, + 0x03a1, + 0x03a3, + 0x03ce, + 0x03d0, + 0x03f5, + 0x0400, + 0x0482, + 0x048a, + 0x04ce, + 0x04d0, + 0x04f5, + 0x04f8, + 0x04f9, + 0x0500, + 0x050f, + 0x0531, + 0x0556, + 0x0559, + 0x055f, + 0x0561, + 0x0587, + 0x0589, + 0x0589, + 0x0903, + 0x0903, + 0x0905, + 0x0939, + 0x093d, + 0x0940, + 0x0949, + 0x094c, + 0x0950, + 0x0950, + 0x0958, + 0x0961, + 0x0964, + 0x0970, + 0x0982, + 0x0983, + 0x0985, + 0x098c, + 0x098f, + 0x0990, + 0x0993, + 0x09a8, + 0x09aa, + 0x09b0, + 0x09b2, + 0x09b2, + 0x09b6, + 0x09b9, + 0x09be, + 0x09c0, + 0x09c7, + 0x09c8, + 0x09cb, + 0x09cc, + 0x09d7, + 0x09d7, + 0x09dc, + 0x09dd, + 0x09df, + 0x09e1, + 0x09e6, + 0x09f1, + 0x09f4, + 0x09fa, + 0x0a05, + 0x0a0a, + 0x0a0f, + 0x0a10, + 0x0a13, + 0x0a28, + 0x0a2a, + 0x0a30, + 0x0a32, + 0x0a33, + 0x0a35, + 0x0a36, + 0x0a38, + 0x0a39, + 0x0a3e, + 0x0a40, + 0x0a59, + 0x0a5c, + 0x0a5e, + 0x0a5e, + 0x0a66, + 0x0a6f, + 0x0a72, + 0x0a74, + 0x0a83, + 0x0a83, + 0x0a85, + 0x0a8b, + 0x0a8d, + 0x0a8d, + 0x0a8f, + 0x0a91, + 0x0a93, + 0x0aa8, + 0x0aaa, + 0x0ab0, + 0x0ab2, + 0x0ab3, + 0x0ab5, + 0x0ab9, + 0x0abd, + 0x0ac0, + 0x0ac9, + 0x0ac9, + 0x0acb, + 0x0acc, + 0x0ad0, + 0x0ad0, + 0x0ae0, + 0x0ae0, + 0x0ae6, + 0x0aef, + 0x0b02, + 0x0b03, + 0x0b05, + 0x0b0c, + 0x0b0f, + 0x0b10, + 0x0b13, + 0x0b28, + 0x0b2a, + 0x0b30, + 0x0b32, + 0x0b33, + 0x0b36, + 0x0b39, + 0x0b3d, + 0x0b3e, + 0x0b40, + 0x0b40, + 0x0b47, + 0x0b48, + 0x0b4b, + 0x0b4c, + 0x0b57, + 0x0b57, + 0x0b5c, + 0x0b5d, + 0x0b5f, + 0x0b61, + 0x0b66, + 0x0b70, + 0x0b83, + 0x0b83, + 0x0b85, + 0x0b8a, + 0x0b8e, + 0x0b90, + 0x0b92, + 0x0b95, + 0x0b99, + 0x0b9a, + 0x0b9c, + 0x0b9c, + 0x0b9e, + 0x0b9f, + 0x0ba3, + 0x0ba4, + 0x0ba8, + 0x0baa, + 0x0bae, + 0x0bb5, + 0x0bb7, + 0x0bb9, + 0x0bbe, + 0x0bbf, + 0x0bc1, + 0x0bc2, + 0x0bc6, + 0x0bc8, + 0x0bca, + 0x0bcc, + 0x0bd7, + 0x0bd7, + 0x0be7, + 0x0bf2, + 0x0c01, + 0x0c03, + 0x0c05, + 0x0c0c, + 0x0c0e, + 0x0c10, + 0x0c12, + 0x0c28, + 0x0c2a, + 0x0c33, + 0x0c35, + 0x0c39, + 0x0c41, + 0x0c44, + 0x0c60, + 0x0c61, + 0x0c66, + 0x0c6f, + 0x0c82, + 0x0c83, + 0x0c85, + 0x0c8c, + 0x0c8e, + 0x0c90, + 0x0c92, + 0x0ca8, + 0x0caa, + 0x0cb3, + 0x0cb5, + 0x0cb9, + 0x0cbe, + 0x0cbe, + 0x0cc0, + 0x0cc4, + 0x0cc7, + 0x0cc8, + 0x0cca, + 0x0ccb, + 0x0cd5, + 0x0cd6, + 0x0cde, + 0x0cde, + 0x0ce0, + 0x0ce1, + 0x0ce6, + 0x0cef, + 0x0d02, + 0x0d03, + 0x0d05, + 0x0d0c, + 0x0d0e, + 0x0d10, + 0x0d12, + 0x0d28, + 0x0d2a, + 0x0d39, + 0x0d3e, + 0x0d40, + 0x0d46, + 0x0d48, + 0x0d4a, + 0x0d4c, + 0x0d57, + 0x0d57, + 0x0d60, + 0x0d61, + 0x0d66, + 0x0d6f, + 0x0d82, + 0x0d83, + 0x0d85, + 0x0d96, + 0x0d9a, + 0x0db1, + 0x0db3, + 0x0dbb, + 0x0dbd, + 0x0dbd, + 0x0dc0, + 0x0dc6, + 0x0dcf, + 0x0dd1, + 0x0dd8, + 0x0ddf, + 0x0df2, + 0x0df4, + 0x0e01, + 0x0e30, + 0x0e32, + 0x0e33, + 0x0e40, + 0x0e46, + 0x0e4f, + 0x0e5b, + 0x0e81, + 0x0e82, + 0x0e84, + 0x0e84, + 0x0e87, + 0x0e88, + 0x0e8a, + 0x0e8a, + 0x0e8d, + 0x0e8d, + 0x0e94, + 0x0e97, + 0x0e99, + 0x0e9f, + 0x0ea1, + 0x0ea3, + 0x0ea5, + 0x0ea5, + 0x0ea7, + 0x0ea7, + 0x0eaa, + 0x0eab, + 0x0ead, + 0x0eb0, + 0x0eb2, + 0x0eb3, + 0x0ebd, + 0x0ebd, + 0x0ec0, + 0x0ec4, + 0x0ec6, + 0x0ec6, + 0x0ed0, + 0x0ed9, + 0x0edc, + 0x0edd, + 0x0f00, + 0x0f17, + 0x0f1a, + 0x0f34, + 0x0f36, + 0x0f36, + 0x0f38, + 0x0f38, + 0x0f3e, + 0x0f47, + 0x0f49, + 0x0f6a, + 0x0f7f, + 0x0f7f, + 0x0f85, + 0x0f85, + 0x0f88, + 0x0f8b, + 0x0fbe, + 0x0fc5, + 0x0fc7, + 0x0fcc, + 0x0fcf, + 0x0fcf, + 0x1000, + 0x1021, + 0x1023, + 0x1027, + 0x1029, + 0x102a, + 0x102c, + 0x102c, + 0x1031, + 0x1031, + 0x1038, + 0x1038, + 0x1040, + 0x1057, + 0x10a0, + 0x10c5, + 0x10d0, + 0x10f8, + 0x10fb, + 0x10fb, + 0x1100, + 0x1159, + 0x115f, + 0x11a2, + 0x11a8, + 0x11f9, + 0x1200, + 0x1206, + 0x1208, + 0x1246, + 0x1248, + 0x1248, + 0x124a, + 0x124d, + 0x1250, + 0x1256, + 0x1258, + 0x1258, + 0x125a, + 0x125d, + 0x1260, + 0x1286, + 0x1288, + 0x1288, + 0x128a, + 0x128d, + 0x1290, + 0x12ae, + 0x12b0, + 0x12b0, + 0x12b2, + 0x12b5, + 0x12b8, + 0x12be, + 0x12c0, + 0x12c0, + 0x12c2, + 0x12c5, + 0x12c8, + 0x12ce, + 0x12d0, + 0x12d6, + 0x12d8, + 0x12ee, + 0x12f0, + 0x130e, + 0x1310, + 0x1310, + 0x1312, + 0x1315, + 0x1318, + 0x131e, + 0x1320, + 0x1346, + 0x1348, + 0x135a, + 0x1361, + 0x137c, + 0x13a0, + 0x13f4, + 0x1401, + 0x1676, + 0x1681, + 0x169a, + 0x16a0, + 0x16f0, + 0x1700, + 0x170c, + 0x170e, + 0x1711, + 0x1720, + 0x1731, + 0x1735, + 0x1736, + 0x1740, + 0x1751, + 0x1760, + 0x176c, + 0x176e, + 0x1770, + 0x1780, + 0x17b6, + 0x17be, + 0x17c5, + 0x17c7, + 0x17c8, + 0x17d4, + 0x17da, + 0x17dc, + 0x17dc, + 0x17e0, + 0x17e9, + 0x1810, + 0x1819, + 0x1820, + 0x1877, + 0x1880, + 0x18a8, + 0x1e00, + 0x1e9b, + 0x1ea0, + 0x1ef9, + 0x1f00, + 0x1f15, + 0x1f18, + 0x1f1d, + 0x1f20, + 0x1f45, + 0x1f48, + 0x1f4d, + 0x1f50, + 0x1f57, + 0x1f59, + 0x1f59, + 0x1f5b, + 0x1f5b, + 0x1f5d, + 0x1f5d, + 0x1f5f, + 0x1f7d, + 0x1f80, + 0x1fb4, + 0x1fb6, + 0x1fbc, + 0x1fbe, + 0x1fbe, + 0x1fc2, + 0x1fc4, + 0x1fc6, + 0x1fcc, + 0x1fd0, + 0x1fd3, + 0x1fd6, + 0x1fdb, + 0x1fe0, + 0x1fec, + 0x1ff2, + 0x1ff4, + 0x1ff6, + 0x1ffc, + 0x200e, + 0x200e, + 0x2071, + 0x2071, + 0x207f, + 0x207f, + 0x2102, + 0x2102, + 0x2107, + 0x2107, + 0x210a, + 0x2113, + 0x2115, + 0x2115, + 0x2119, + 0x211d, + 0x2124, + 0x2124, + 0x2126, + 0x2126, + 0x2128, + 0x2128, + 0x212a, + 0x212d, + 0x212f, + 0x2131, + 0x2133, + 0x2139, + 0x213d, + 0x213f, + 0x2145, + 0x2149, + 0x2160, + 0x2183, + 0x2336, + 0x237a, + 0x2395, + 0x2395, + 0x249c, + 0x24e9, + 0x3005, + 0x3007, + 0x3021, + 0x3029, + 0x3031, + 0x3035, + 0x3038, + 0x303c, + 0x3041, + 0x3096, + 0x309d, + 0x309f, + 0x30a1, + 0x30fa, + 0x30fc, + 0x30ff, + 0x3105, + 0x312c, + 0x3131, + 0x318e, + 0x3190, + 0x31b7, + 0x31f0, + 0x321c, + 0x3220, + 0x3243, + 0x3260, + 0x327b, + 0x327f, + 0x32b0, + 0x32c0, + 0x32cb, + 0x32d0, + 0x32fe, + 0x3300, + 0x3376, + 0x337b, + 0x33dd, + 0x33e0, + 0x33fe, + 0x3400, + 0x4db5, + 0x4e00, + 0x9fa5, + 0xa000, + 0xa48c, + 0xac00, + 0xd7a3, + 0xd800, + 0xfa2d, + 0xfa30, + 0xfa6a, + 0xfb00, + 0xfb06, + 0xfb13, + 0xfb17, + 0xff21, + 0xff3a, + 0xff41, + 0xff5a, + 0xff66, + 0xffbe, + 0xffc2, + 0xffc7, + 0xffca, + 0xffcf, + 0xffd2, + 0xffd7, + 0xffda, + 0xffdc, + 0x10300, + 0x1031e, + 0x10320, + 0x10323, + 0x10330, + 0x1034a, + 0x10400, + 0x10425, + 0x10428, + 0x1044d, + 0x1d000, + 0x1d0f5, + 0x1d100, + 0x1d126, + 0x1d12a, + 0x1d166, + 0x1d16a, + 0x1d172, + 0x1d183, + 0x1d184, + 0x1d18c, + 0x1d1a9, + 0x1d1ae, + 0x1d1dd, + 0x1d400, + 0x1d454, + 0x1d456, + 0x1d49c, + 0x1d49e, + 0x1d49f, + 0x1d4a2, + 0x1d4a2, + 0x1d4a5, + 0x1d4a6, + 0x1d4a9, + 0x1d4ac, + 0x1d4ae, + 0x1d4b9, + 0x1d4bb, + 0x1d4bb, + 0x1d4bd, + 0x1d4c0, + 0x1d4c2, + 0x1d4c3, + 0x1d4c5, + 0x1d505, + 0x1d507, + 0x1d50a, + 0x1d50d, + 0x1d514, + 0x1d516, + 0x1d51c, + 0x1d51e, + 0x1d539, + 0x1d53b, + 0x1d53e, + 0x1d540, + 0x1d544, + 0x1d546, + 0x1d546, + 0x1d54a, + 0x1d550, + 0x1d552, + 0x1d6a3, + 0x1d6a8, + 0x1d7c9, + 0x20000, + 0x2a6d6, + 0x2f800, + 0x2fa1d, + 0xf0000, + 0xffffd, + 0x100000, + 0x10fffd +]; +// prettier-ignore-end + +const isBidirectionalL = character => inRange(character, bidirectional_l); + +export { + isUnassignedCodePoint, + isCommonlyMappedToNothing, + isNonASCIISpaceCharacter, + isProhibitedCharacter, + isBidirectionalRAL, + isBidirectionalL +}; diff --git a/packages/pdfkit/src/saslprep/lib/util.js b/packages/pdfkit/src/saslprep/lib/util.js new file mode 100644 index 000000000..0c96880ba --- /dev/null +++ b/packages/pdfkit/src/saslprep/lib/util.js @@ -0,0 +1,36 @@ +/** + * Check if value is in a range group. + * @param {number} value + * @param {number[]} rangeGroup + * @returns {boolean} + */ +function inRange(value, rangeGroup) { + if (value < rangeGroup[0]) return false; + let startRange = 0; + let endRange = rangeGroup.length / 2; + while (startRange <= endRange) { + const middleRange = Math.floor((startRange + endRange) / 2); + + // actual array index + const arrayIndex = middleRange * 2; + + // Check if value is in range pointed by actual index + if ( + value >= rangeGroup[arrayIndex] && + value <= rangeGroup[arrayIndex + 1] + ) { + return true; + } + + if (value > rangeGroup[arrayIndex + 1]) { + // Search Right Side Of Array + startRange = middleRange + 1; + } else { + // Search Left Side Of Array + endRange = middleRange - 1; + } + } + return false; +} + +export { inRange }; diff --git a/packages/pdfkit/src/security.js b/packages/pdfkit/src/security.js index 957520f4c..963ca9647 100644 --- a/packages/pdfkit/src/security.js +++ b/packages/pdfkit/src/security.js @@ -5,33 +5,557 @@ By Yang Liu */ -// This file is ran directly with Node - needs to have .js extension -// eslint-disable-next-line import/extensions -import MD5 from 'crypto-js/md5.js'; - -const wordArrayToBuffer = (wordArray) => { - const byteArray = []; - - for (let i = 0; i < wordArray.sigBytes; i++) { - byteArray.push( - (wordArray.words[Math.floor(i / 4)] >> (8 * (3 - (i % 4)))) & 0xff - ); - } - - return Buffer.from(byteArray); -}; +import CryptoJS from 'crypto-js'; +import saslprep from './saslprep/index'; class PDFSecurity { static generateFileID(info = {}) { let infoStr = `${info.CreationDate.getTime()}\n`; for (let key in info) { - if (!info.hasOwnProperty(key)) continue; + if (!info.hasOwnProperty(key)) { + continue; + } infoStr += `${key}: ${info[key].valueOf()}\n`; } - return wordArrayToBuffer(MD5(infoStr)); + return wordArrayToBuffer(CryptoJS.MD5(infoStr)); + } + + static generateRandomWordArray(bytes) { + return CryptoJS.lib.WordArray.random(bytes); + } + + static create(document, options = {}) { + if (!options.ownerPassword && !options.userPassword) { + return null; + } + return new PDFSecurity(document, options); + } + + constructor(document, options = {}) { + if (!options.ownerPassword && !options.userPassword) { + throw new Error('None of owner password and user password is defined.'); + } + + this.document = document; + this._setupEncryption(options); + } + + _setupEncryption(options) { + switch (options.pdfVersion) { + case '1.4': + case '1.5': + this.version = 2; + break; + case '1.6': + case '1.7': + this.version = 4; + break; + case '1.7ext3': + this.version = 5; + break; + default: + this.version = 1; + break; + } + + const encDict = { + Filter: 'Standard' + }; + + switch (this.version) { + case 1: + case 2: + case 4: + this._setupEncryptionV1V2V4(this.version, encDict, options); + break; + case 5: + this._setupEncryptionV5(encDict, options); + break; + } + + this.dictionary = this.document.ref(encDict); + } + + _setupEncryptionV1V2V4(v, encDict, options) { + let r; let permissions; + switch (v) { + case 1: + r = 2; + this.keyBits = 40; + permissions = getPermissionsR2(options.permissions); + break; + case 2: + r = 3; + this.keyBits = 128; + permissions = getPermissionsR3(options.permissions); + break; + case 4: + r = 4; + this.keyBits = 128; + permissions = getPermissionsR3(options.permissions); + break; + } + + const paddedUserPassword = processPasswordR2R3R4(options.userPassword); + const paddedOwnerPassword = options.ownerPassword + ? processPasswordR2R3R4(options.ownerPassword) + : paddedUserPassword; + + const ownerPasswordEntry = getOwnerPasswordR2R3R4( + r, + this.keyBits, + paddedUserPassword, + paddedOwnerPassword + ); + this.encryptionKey = getEncryptionKeyR2R3R4( + r, + this.keyBits, + this.document._id, + paddedUserPassword, + ownerPasswordEntry, + permissions + ); + let userPasswordEntry; + if (r === 2) { + userPasswordEntry = getUserPasswordR2(this.encryptionKey); + } else { + userPasswordEntry = getUserPasswordR3R4( + this.document._id, + this.encryptionKey + ); + } + + encDict.V = v; + if (v >= 2) { + encDict.Length = this.keyBits; + } + if (v === 4) { + encDict.CF = { + StdCF: { + AuthEvent: 'DocOpen', + CFM: 'AESV2', + Length: this.keyBits / 8 + } + }; + encDict.StmF = 'StdCF'; + encDict.StrF = 'StdCF'; + } + encDict.R = r; + encDict.O = wordArrayToBuffer(ownerPasswordEntry); + encDict.U = wordArrayToBuffer(userPasswordEntry); + encDict.P = permissions; + } + + _setupEncryptionV5(encDict, options) { + this.keyBits = 256; + const permissions = getPermissionsR3(options.permissions); + + const processedUserPassword = processPasswordR5(options.userPassword); + const processedOwnerPassword = options.ownerPassword + ? processPasswordR5(options.ownerPassword) + : processedUserPassword; + + this.encryptionKey = getEncryptionKeyR5( + PDFSecurity.generateRandomWordArray + ); + const userPasswordEntry = getUserPasswordR5( + processedUserPassword, + PDFSecurity.generateRandomWordArray + ); + const userKeySalt = CryptoJS.lib.WordArray.create( + userPasswordEntry.words.slice(10, 12), + 8 + ); + const userEncryptionKeyEntry = getUserEncryptionKeyR5( + processedUserPassword, + userKeySalt, + this.encryptionKey + ); + const ownerPasswordEntry = getOwnerPasswordR5( + processedOwnerPassword, + userPasswordEntry, + PDFSecurity.generateRandomWordArray + ); + const ownerKeySalt = CryptoJS.lib.WordArray.create( + ownerPasswordEntry.words.slice(10, 12), + 8 + ); + const ownerEncryptionKeyEntry = getOwnerEncryptionKeyR5( + processedOwnerPassword, + ownerKeySalt, + userPasswordEntry, + this.encryptionKey + ); + const permsEntry = getEncryptedPermissionsR5( + permissions, + this.encryptionKey, + PDFSecurity.generateRandomWordArray + ); + + encDict.V = 5; + encDict.Length = this.keyBits; + encDict.CF = { + StdCF: { + AuthEvent: 'DocOpen', + CFM: 'AESV3', + Length: this.keyBits / 8 + } + }; + encDict.StmF = 'StdCF'; + encDict.StrF = 'StdCF'; + encDict.R = 5; + encDict.O = wordArrayToBuffer(ownerPasswordEntry); + encDict.OE = wordArrayToBuffer(ownerEncryptionKeyEntry); + encDict.U = wordArrayToBuffer(userPasswordEntry); + encDict.UE = wordArrayToBuffer(userEncryptionKeyEntry); + encDict.P = permissions; + encDict.Perms = wordArrayToBuffer(permsEntry); + } + + getEncryptFn(obj, gen) { + let digest; + if (this.version < 5) { + digest = this.encryptionKey + .clone() + .concat( + CryptoJS.lib.WordArray.create( + [ + ((obj & 0xff) << 24) | + ((obj & 0xff00) << 8) | + ((obj >> 8) & 0xff00) | + (gen & 0xff), + (gen & 0xff00) << 16 + ], + 5 + ) + ); + } + + if (this.version === 1 || this.version === 2) { + let key = CryptoJS.MD5(digest); + key.sigBytes = Math.min(16, this.keyBits / 8 + 5); + return buffer => + wordArrayToBuffer( + CryptoJS.RC4.encrypt(CryptoJS.lib.WordArray.create(buffer), key) + .ciphertext + ); + } + + let key; + if (this.version === 4) { + key = CryptoJS.MD5( + digest.concat(CryptoJS.lib.WordArray.create([0x73416c54], 4)) + ); + } else { + key = this.encryptionKey; + } + + const iv = PDFSecurity.generateRandomWordArray(16); + const options = { + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + iv + }; + + return buffer => + wordArrayToBuffer( + iv + .clone() + .concat( + CryptoJS.AES.encrypt( + CryptoJS.lib.WordArray.create(buffer), + key, + options + ).ciphertext + ) + ); + } + + end() { + this.dictionary.end(); + } +} + +function getPermissionsR2(permissionObject = {}) { + let permissions = 0xffffffc0 >> 0; + if (permissionObject.printing) { + permissions |= 0b000000000100; + } + if (permissionObject.modifying) { + permissions |= 0b000000001000; + } + if (permissionObject.copying) { + permissions |= 0b000000010000; + } + if (permissionObject.annotating) { + permissions |= 0b000000100000; + } + return permissions; +} + +function getPermissionsR3(permissionObject = {}) { + let permissions = 0xfffff0c0 >> 0; + if (permissionObject.printing === 'lowResolution') { + permissions |= 0b000000000100; + } + if (permissionObject.printing === 'highResolution') { + permissions |= 0b100000000100; + } + if (permissionObject.modifying) { + permissions |= 0b000000001000; + } + if (permissionObject.copying) { + permissions |= 0b000000010000; + } + if (permissionObject.annotating) { + permissions |= 0b000000100000; + } + if (permissionObject.fillingForms) { + permissions |= 0b000100000000; + } + if (permissionObject.contentAccessibility) { + permissions |= 0b001000000000; + } + if (permissionObject.documentAssembly) { + permissions |= 0b010000000000; + } + return permissions; +} + +function getUserPasswordR2(encryptionKey) { + return CryptoJS.RC4.encrypt(processPasswordR2R3R4(), encryptionKey) + .ciphertext; +} + +function getUserPasswordR3R4(documentId, encryptionKey) { + const key = encryptionKey.clone(); + let cipher = CryptoJS.MD5( + processPasswordR2R3R4().concat(CryptoJS.lib.WordArray.create(documentId)) + ); + for (let i = 0; i < 20; i++) { + const xorRound = Math.ceil(key.sigBytes / 4); + for (let j = 0; j < xorRound; j++) { + key.words[j] = + encryptionKey.words[j] ^ (i | (i << 8) | (i << 16) | (i << 24)); + } + cipher = CryptoJS.RC4.encrypt(cipher, key).ciphertext; + } + return cipher.concat(CryptoJS.lib.WordArray.create(null, 16)); +} + +function getOwnerPasswordR2R3R4( + r, + keyBits, + paddedUserPassword, + paddedOwnerPassword +) { + let digest = paddedOwnerPassword; + let round = r >= 3 ? 51 : 1; + for (let i = 0; i < round; i++) { + digest = CryptoJS.MD5(digest); + } + + const key = digest.clone(); + key.sigBytes = keyBits / 8; + let cipher = paddedUserPassword; + round = r >= 3 ? 20 : 1; + for (let i = 0; i < round; i++) { + const xorRound = Math.ceil(key.sigBytes / 4); + for (let j = 0; j < xorRound; j++) { + key.words[j] = digest.words[j] ^ (i | (i << 8) | (i << 16) | (i << 24)); + } + cipher = CryptoJS.RC4.encrypt(cipher, key).ciphertext; + } + return cipher; +} + +function getEncryptionKeyR2R3R4( + r, + keyBits, + documentId, + paddedUserPassword, + ownerPasswordEntry, + permissions +) { + let key = paddedUserPassword + .clone() + .concat(ownerPasswordEntry) + .concat(CryptoJS.lib.WordArray.create([lsbFirstWord(permissions)], 4)) + .concat(CryptoJS.lib.WordArray.create(documentId)); + const round = r >= 3 ? 51 : 1; + for (let i = 0; i < round; i++) { + key = CryptoJS.MD5(key); + key.sigBytes = keyBits / 8; } + return key; +} + +function getUserPasswordR5(processedUserPassword, generateRandomWordArray) { + const validationSalt = generateRandomWordArray(8); + const keySalt = generateRandomWordArray(8); + return CryptoJS.SHA256(processedUserPassword.clone().concat(validationSalt)) + .concat(validationSalt) + .concat(keySalt); } +function getUserEncryptionKeyR5( + processedUserPassword, + userKeySalt, + encryptionKey +) { + const key = CryptoJS.SHA256( + processedUserPassword.clone().concat(userKeySalt) + ); + const options = { + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.NoPadding, + iv: CryptoJS.lib.WordArray.create(null, 16) + }; + return CryptoJS.AES.encrypt(encryptionKey, key, options).ciphertext; +} + +function getOwnerPasswordR5( + processedOwnerPassword, + userPasswordEntry, + generateRandomWordArray +) { + const validationSalt = generateRandomWordArray(8); + const keySalt = generateRandomWordArray(8); + return CryptoJS.SHA256( + processedOwnerPassword + .clone() + .concat(validationSalt) + .concat(userPasswordEntry) + ) + .concat(validationSalt) + .concat(keySalt); +} + +function getOwnerEncryptionKeyR5( + processedOwnerPassword, + ownerKeySalt, + userPasswordEntry, + encryptionKey +) { + const key = CryptoJS.SHA256( + processedOwnerPassword + .clone() + .concat(ownerKeySalt) + .concat(userPasswordEntry) + ); + const options = { + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.NoPadding, + iv: CryptoJS.lib.WordArray.create(null, 16) + }; + return CryptoJS.AES.encrypt(encryptionKey, key, options).ciphertext; +} + +function getEncryptionKeyR5(generateRandomWordArray) { + return generateRandomWordArray(32); +} + +function getEncryptedPermissionsR5( + permissions, + encryptionKey, + generateRandomWordArray +) { + const cipher = CryptoJS.lib.WordArray.create( + [lsbFirstWord(permissions), 0xffffffff, 0x54616462], + 12 + ).concat(generateRandomWordArray(4)); + const options = { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.NoPadding + }; + return CryptoJS.AES.encrypt(cipher, encryptionKey, options).ciphertext; +} + +function processPasswordR2R3R4(password = '') { + const out = Buffer.alloc(32); + const length = password.length; + let index = 0; + while (index < length && index < 32) { + const code = password.charCodeAt(index); + if (code > 0xff) { + throw new Error('Password contains one or more invalid characters.'); + } + out[index] = code; + index++; + } + while (index < 32) { + out[index] = PASSWORD_PADDING[index - length]; + index++; + } + return CryptoJS.lib.WordArray.create(out); +} + +function processPasswordR5(password = '') { + password = unescape(encodeURIComponent(saslprep(password))); + const length = Math.min(127, password.length); + const out = Buffer.alloc(length); + + for (let i = 0; i < length; i++) { + out[i] = password.charCodeAt(i); + } + + return CryptoJS.lib.WordArray.create(out); +} + +function lsbFirstWord(data) { + return ( + ((data & 0xff) << 24) | + ((data & 0xff00) << 8) | + ((data >> 8) & 0xff00) | + ((data >> 24) & 0xff) + ); +} + +function wordArrayToBuffer(wordArray) { + const byteArray = []; + for (let i = 0; i < wordArray.sigBytes; i++) { + byteArray.push( + (wordArray.words[Math.floor(i / 4)] >> (8 * (3 - (i % 4)))) & 0xff + ); + } + return Buffer.from(byteArray); +} + +const PASSWORD_PADDING = [ + 0x28, + 0xbf, + 0x4e, + 0x5e, + 0x4e, + 0x75, + 0x8a, + 0x41, + 0x64, + 0x00, + 0x4e, + 0x56, + 0xff, + 0xfa, + 0x01, + 0x08, + 0x2e, + 0x2e, + 0x00, + 0xb6, + 0xd0, + 0x68, + 0x3e, + 0x80, + 0x2f, + 0x0c, + 0xa9, + 0xfe, + 0x64, + 0x53, + 0x69, + 0x7a +]; + export default PDFSecurity; diff --git a/packages/pdfkit/tests/fonts/Roboto-Italic.ttf b/packages/pdfkit/tests/fonts/Roboto-Italic.ttf new file mode 100644 index 000000000..ff6046d5b Binary files /dev/null and b/packages/pdfkit/tests/fonts/Roboto-Italic.ttf differ diff --git a/packages/pdfkit/tests/fonts/Roboto-Medium.ttf b/packages/pdfkit/tests/fonts/Roboto-Medium.ttf new file mode 100644 index 000000000..39c63d746 Binary files /dev/null and b/packages/pdfkit/tests/fonts/Roboto-Medium.ttf differ diff --git a/packages/pdfkit/tests/fonts/Roboto-MediumItalic.ttf b/packages/pdfkit/tests/fonts/Roboto-MediumItalic.ttf new file mode 100644 index 000000000..dc743f0a6 Binary files /dev/null and b/packages/pdfkit/tests/fonts/Roboto-MediumItalic.ttf differ diff --git a/packages/pdfkit/tests/fonts/Roboto-Regular.ttf b/packages/pdfkit/tests/fonts/Roboto-Regular.ttf new file mode 100644 index 000000000..8c082c8de Binary files /dev/null and b/packages/pdfkit/tests/fonts/Roboto-Regular.ttf differ diff --git a/packages/pdfkit/tests/unit/gradient.spec.js b/packages/pdfkit/tests/unit/gradient.spec.js new file mode 100644 index 000000000..0ac62f311 --- /dev/null +++ b/packages/pdfkit/tests/unit/gradient.spec.js @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import PDFDocument from '../../src/document'; +import { logData } from './helpers'; +import matcher from './toContainChunk'; + +expect.extend(matcher); + +describe('Gradient', () => { + let document; + + beforeEach(() => { + document = new PDFDocument({ + info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) } + }); + }); + + test('Multiple stops', () => { + const docData = logData(document); + const gradient = document.linearGradient(0, 0, 300, 0); + gradient + .stop(0, 'green') + .stop(0.5, 'red') + .stop(1, 'green'); + document.rect(0, 0, 300, 300).fill(gradient); + document.end(); + + expect(docData).toContainChunk([ + '8 0 obj', + `<< +/FunctionType 2 +/Domain [0 1] +/C0 [0 0.501961 0] +/C1 [1 0 0] +/N 1 +>>` + ]); + expect(docData).toContainChunk([ + '9 0 obj', + `<< +/FunctionType 2 +/Domain [0 1] +/C0 [1 0 0] +/C1 [0 0.501961 0] +/N 1 +>>` + ]); + + expect(docData).toContainChunk([ + '10 0 obj', + `<< +/FunctionType 3 +/Domain [0 1] +/Functions [8 0 R 9 0 R] +/Bounds [0.5] +/Encode [0 1 0 1] +>>` + ]); + + expect(docData).toContainChunk([ + '11 0 obj', + `<< +/ShadingType 2 +/ColorSpace /DeviceRGB +/Coords [0 0 300 0] +/Function 10 0 R +/Extend [true true] +>>` + ]); + + expect(docData).toContainChunk([ + '12 0 obj', + `<< +/Type /Pattern +/PatternType 2 +/Shading 11 0 R +/Matrix [1 0 0 -1 0 792] +>>` + ]); + }); +}); diff --git a/packages/pdfkit/tests/unit/helpers.js b/packages/pdfkit/tests/unit/helpers.js new file mode 100644 index 000000000..213ebb9bc --- /dev/null +++ b/packages/pdfkit/tests/unit/helpers.js @@ -0,0 +1,23 @@ +function logData(doc) { + const loggedData = []; + const originalMethod = doc._write; + doc._write = function(data) { + loggedData.push(data); + originalMethod.call(this, data); + }; + return loggedData; +} + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +function joinTokens(...args) { + let a = args.map(i => escapeRegExp(i)); + let r = new RegExp('^' + a.join('\\s*') + '$'); + return r; +} + +export { logData, joinTokens } + + diff --git a/packages/pdfkit/tests/unit/metadata.spec.js b/packages/pdfkit/tests/unit/metadata.spec.js new file mode 100644 index 000000000..466256cc9 --- /dev/null +++ b/packages/pdfkit/tests/unit/metadata.spec.js @@ -0,0 +1,35 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import PDFMetadata from '../../src/metadata'; + +describe('PDFMetadata', () => { + let metadata; + beforeEach(() => { + metadata = new PDFMetadata(); + }); + + test('initialising metadata', () => { + expect(metadata._metadata).toBeDefined(); + expect(metadata.getLength()).toBeGreaterThan(0); + expect(typeof metadata._metadata).toBe('string') + }); + + test('contains appended XML', () => { + let xml = ` + + + Test + + + ` + metadata.append(xml); + expect(metadata.getXML()).toContain(xml); + }); + + test('closing tags', () => { + let length = metadata.getLength(); + metadata.end(); + expect(metadata.getLength()).toBeGreaterThan(length); + }); + +}); diff --git a/packages/pdfkit/tests/unit/object.spec.js b/packages/pdfkit/tests/unit/object.spec.js new file mode 100644 index 000000000..b9711ccdd --- /dev/null +++ b/packages/pdfkit/tests/unit/object.spec.js @@ -0,0 +1,25 @@ +import { describe, expect, test } from 'vitest'; + +import PDFObject from '../../src/object'; + +describe('PDFObject', () => { + describe('convert', () => { + test('string literal', () => { + expect(PDFObject.convert('test')).toEqual('/test'); + }); + + test('string literal with unicode', () => { + expect(PDFObject.convert('αβγδ')).toEqual('/αβγδ'); + }); + + test('String object', () => { + expect(PDFObject.convert(new String('test'))).toEqual('(test)'); + }); + + test('String object with unicode', () => { + const result = PDFObject.convert(new String('αβγδ')); + expect(result.length).toEqual(12); + expect(result).toMatchInlineSnapshot(`"(þÿ±²³´)"`); + }); + }); +}); diff --git a/packages/pdfkit/tests/unit/pattern.spec.js b/packages/pdfkit/tests/unit/pattern.spec.js new file mode 100644 index 000000000..a2d50a4f7 --- /dev/null +++ b/packages/pdfkit/tests/unit/pattern.spec.js @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import PDFDocument from '../../src/document'; +import { logData } from './helpers'; +import matcher from './toContainChunk'; + +expect.extend(matcher); + +describe('Pattern', () => { + let document; + + beforeEach(() => { + document = new PDFDocument({ + info: { CreationDate: new Date(Date.UTC(2018, 1, 1)) }, + compress: false + }); + }); + + test('Uncolored tiling pattern', () => { + const docData = logData(document); + const patternStream = '1 w 0 1 m 4 5 l s 2 0 m 5 3 l s'; + const binaryStream = Buffer.from(`${patternStream}\n`, 'binary'); + const pattern = document.pattern([1, 1, 4, 4], 3, 3, patternStream); + document + .rect(0, 0, 100, 100) + .fill([pattern, 'blue']) + .end(); + + // empty resources + expect(docData).toContainChunk(['10 0 obj', `<<\n>>`]); + + // pattern dictionary + expect(docData).toContainChunk([ + '11 0 obj', + `<< +/Type /Pattern +/PatternType 1 +/PaintType 2 +/TilingType 2 +/BBox [1 1 4 4] +/XStep 3 +/YStep 3 +/Matrix [1 0 0 -1 0 792] +/Resources 10 0 R +/Length 32 +>>`, + 'stream', + binaryStream, + '\nendstream' + ]); + + // page resource dictionary with color space and pattern + expect(docData).toContainChunk([ + '6 0 obj', + `<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ColorSpace << +/CsPDeviceCMYK 8 0 R +/CsPDeviceRGB 9 0 R +>> +/Pattern << +/P1 11 0 R +>> +>>` + ]); + // map to the underlying color space + expect(docData).toContainChunk(['8 0 obj', `[/Pattern /DeviceCMYK]`]); + expect(docData).toContainChunk(['9 0 obj', `[/Pattern /DeviceRGB]`]); + // graphics + const graphicsStream = Buffer.from( + `1 0 0 -1 0 792 cm +0 0 100 100 re +/CsPDeviceRGB cs +0 0 1 /P1 scn +f\n`, + 'binary' + ); + expect(docData).toContainChunk([ + '5 0 obj', + `<< +/Length 66 +>>`, + 'stream', + graphicsStream, + '\nendstream' + ]); + }); + + test('Pattern naming', () => { + const docData = logData(document); + const pattern1 = document.pattern( + [1, 1, 4, 4], + 3, + 3, + '1 w 0 1 m 4 5 l s 2 0 m 5 3 l s' + ); + const pattern2 = document.pattern( + [1, 1, 7, 7], + 6, + 6, + '1 w 0 1 m 7 8 l s 5 0 m 8 3 l s' + ); + document.rect(0, 0, 100, 100).fill([pattern1, 'blue']); + document + .rect(0, 0, 100, 100) + .fill([pattern2, 'red']) + .end(); + + // patterns P1 and P2 + expect(docData).toContainChunk([ + '6 0 obj', + `<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/ColorSpace << +/CsPDeviceCMYK 8 0 R +/CsPDeviceRGB 9 0 R +>> +/Pattern << +/P1 11 0 R +/P2 13 0 R +>> +>>` + ]); + }); +}); diff --git a/packages/pdfkit/tests/unit/reference.spec.js b/packages/pdfkit/tests/unit/reference.spec.js new file mode 100644 index 000000000..6329311b9 --- /dev/null +++ b/packages/pdfkit/tests/unit/reference.spec.js @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, test } from 'vitest'; +import zlib from 'zlib'; + +import PDFReference from '../../src/reference'; +import PDFDocument from '../../src/document'; +import { logData } from './helpers'; +import matcher from './toContainChunk'; + +expect.extend(matcher); + +describe('PDFReference', () => { + let document; + beforeEach(() => { + document = new PDFDocument(); + }); + + test('instantiated without data', () => { + const ref = new PDFReference(document, 1); + + expect(ref.id).toBeDefined(); + expect(ref.data).toBeDefined(); + expect(ref.data).toBeInstanceOf(Object); + }); + + test('instantiated with data', () => { + const refData = { Pages: 0 }; + const ref = new PDFReference(document, 1, refData); + + expect(ref.id).toBe(1); + expect(ref.data).toBe(refData); + }); + + test('written data of empty reference', () => { + const ref = new PDFReference(document, 1); + + const docData = logData(document); + ref.finalize(); + + expect(docData).toContainChunk(['1 0 obj', '<<\n>>', 'endobj']); + }); + + test('written data of reference with uncompressed data', () => { + const docData = logData(document); + const chunk = Buffer.from('test'); + const ref = new PDFReference(document, 1); + ref.compress = false; + ref.write(chunk); + ref.finalize(); + expect(docData).toContainChunk([ + '1 0 obj', + `<< +/Length ${chunk.length} +>>`, + 'stream', + chunk, + '\nendstream', + 'endobj' + ]); + }); + + test('written data of reference with compressed data', () => { + const docData = logData(document); + const chunk = Buffer.from('test'); + const compressed = zlib.deflateSync(chunk); + const ref = new PDFReference(document, 1); + ref.write(chunk); + + ref.finalize(); + expect(docData).toContainChunk([ + '1 0 obj', + `<< +/Length ${compressed.length} +/Filter /FlateDecode +>>`, + 'stream', + compressed, + '\nendstream', + 'endobj' + ]); + }); +}); diff --git a/packages/pdfkit/tests/unit/saslprep.spec.js b/packages/pdfkit/tests/unit/saslprep.spec.js new file mode 100644 index 000000000..863f1d32e --- /dev/null +++ b/packages/pdfkit/tests/unit/saslprep.spec.js @@ -0,0 +1,76 @@ +import { expect, test } from 'vitest'; + +import saslprep from '../../src/saslprep'; + +const chr = String.fromCodePoint; + +test('should work with liatin letters', () => { + const str = 'user'; + expect(saslprep(str)).toEqual(str); +}); + +test('should work be case preserved', () => { + const str = 'USER'; + expect(saslprep(str)).toEqual(str); +}); + +test('should work with high code points (> U+FFFF)', () => { + const str = '\uD83D\uDE00'; + expect(saslprep(str, { allowUnassigned: true })).toEqual(str); +}); + +test('should remove `mapped to nothing` characters', () => { + expect(saslprep('I\u00ADX')).toEqual('IX'); +}); + +test('should replace `Non-ASCII space characters` with space', () => { + expect(saslprep('a\u00A0b')).toEqual('a\u0020b'); +}); + +test('should normalize as NFKC', () => { + expect(saslprep('\u00AA')).toEqual('a'); + expect(saslprep('\u2168')).toEqual('IX'); +}); + +test('should throws when prohibited characters', () => { + // C.2.1 ASCII control characters + expect(() => saslprep('a\u007Fb')).toThrow(); + + // C.2.2 Non-ASCII control characters + expect(() => saslprep('a\u06DDb')).toThrow(); + + // C.3 Private use + expect(() => saslprep('a\uE000b')).toThrow(); + + // C.4 Non-character code points + expect(() => saslprep(`a${chr(0x1fffe)}b`)).toThrow(); + + // C.5 Surrogate codes + expect(() => saslprep('a\uD800b')).toThrow(); + + // C.6 Inappropriate for plain text + expect(() => saslprep('a\uFFF9b')).toThrow(); + + // C.7 Inappropriate for canonical representation + expect(() => saslprep('a\u2FF0b')).toThrow(); + + // C.8 Change display properties or are deprecated + expect(() => saslprep('a\u200Eb')).toThrow(); + + // C.9 Tagging characters + expect(() => saslprep(`a${chr(0xe0001)}b`)).toThrow(); +}); + +test('should not containt RandALCat and LCat bidi', () => { + expect(() => saslprep('a\u06DD\u00AAb')).toThrow(); +}); + +test('RandALCat should be first and last', () => { + expect(() => saslprep('\u0627\u0031\u0628')).not.toThrow(); + expect(() => saslprep('\u0627\u0031')).toThrow(); +}); + +test('should handle unassigned code points', () => { + expect(() => saslprep('a\u0487')).toThrow(); + expect(() => saslprep('a\u0487', { allowUnassigned: true })).not.toThrow(); +}); diff --git a/packages/pdfkit/tests/unit/toContainChunk/index.js b/packages/pdfkit/tests/unit/toContainChunk/index.js new file mode 100644 index 000000000..bf1fc0b80 --- /dev/null +++ b/packages/pdfkit/tests/unit/toContainChunk/index.js @@ -0,0 +1,58 @@ +import { diff } from 'jest-diff'; + +const buildMessage = (utils, data, chunk, headIndex) => { + let message; + if (headIndex !== -1) { + const received = data.slice(headIndex, headIndex + chunk.length); + const difference = diff(chunk, received); + message = `Difference:\n\n${difference}`; + } else { + message = + 'Expected data to contain chunk:\n' + ` ${utils.printExpected(chunk)}\n`; + } + return message; +}; + +const passMessage = (utils, data, chunk, headIndex) => () => { + return ( + utils.matcherHint('.not.toContainChunk', 'data', 'chunk') + + '\n\n' + + buildMessage(utils, data, chunk, headIndex) + ); +}; + +const failMessage = (utils, data, chunk, headIndex) => () => { + return ( + utils.matcherHint('.toContainChunk', 'data', 'chunk') + + '\n\n' + + buildMessage(utils, data, chunk, headIndex) + ); +}; + +export default { + toContainChunk(data, chunk) { + const headIndex = data.indexOf(chunk[0]); + let pass = headIndex !== -1; + if (pass) { + for (let i = 1; i < chunk.length; ++i) { + if (chunk[i] instanceof RegExp) { + pass = pass && chunk[i].test(data[headIndex + i]); + } else { + pass = pass && this.equals(data[headIndex + i], chunk[i]); + } + } + } + + if (pass) { + return { + pass: true, + message: passMessage(this.utils, data, chunk, headIndex) + }; + } + + return { + pass: false, + message: failMessage(this.utils, data, chunk, headIndex) + }; + } +}; diff --git a/vitest.workspace.js b/vitest.workspace.js index 36e23f545..e75e051be 100644 --- a/vitest.workspace.js +++ b/vitest.workspace.js @@ -2,6 +2,7 @@ export default [ 'packages/fns', 'packages/font', 'packages/image', + 'packages/pdfkit', 'packages/render', 'packages/layout', 'packages/svgkit',