@vlasky/quoted-printable
Version:
Fast, robust RFC 2045 (Quoted-Printable) and RFC 2047 (Q-Encoding) encoder/decoder for Buffers in pure Javascript with an optional C++ binding. A fork of @ronomon/quoted-printable that supports Node.js 8+.
454 lines (423 loc) • 13.8 kB
JavaScript
'use strict';
var QuotedPrintable = {};
QuotedPrintable.assertBinding = function(binding) {
var self = this;
if (!binding) throw new Error('binding must be defined');
if (!binding.decode) throw new Error('binding.decode must be defined');
if (!binding.encode) throw new Error('binding.encode must be defined');
if (typeof binding.decode != 'function') {
throw new Error('binding.decode must be a function');
}
if (typeof binding.encode != 'function') {
throw new Error('binding.encode must be a function');
}
};
QuotedPrintable.assertQEncoding = function(qEncoding) {
var self = this;
if (qEncoding !== true && qEncoding !== false) {
throw new Error('qEncoding must be a boolean');
}
};
QuotedPrintable.binding = {};
QuotedPrintable.binding.javascript = {};
QuotedPrintable.binding.javascript.decode = function(
source, target, qEncoding, tableDecoding, tableLegal
) {
if (!Buffer.isBuffer(source)) {
throw new Error('source must be a buffer');
}
if (!Buffer.isBuffer(target)) {
throw new Error('target must be a buffer');
}
if (
typeof qEncoding !== 'number' ||
Math.floor(qEncoding) !== qEncoding ||
qEncoding < 0
) {
throw new Error('qEncoding must be a positive integer');
}
if (!Buffer.isBuffer(tableDecoding)) {
throw new Error('tableDecoding must be a buffer');
}
if (!Buffer.isBuffer(tableLegal)) {
throw new Error('tableLegal must be a buffer');
}
if (target.length < source.length) {
throw new Error('target too small');
}
if (qEncoding !== 0 && qEncoding !== 1) {
throw new Error('qEncoding must be 0 or 1');
}
if (tableDecoding.length !== 256) {
throw new Error('tableDecoding must be 256 bytes');
}
if (tableLegal.length !== 256) {
throw new Error('tableLegal must be 256 bytes');
}
function removeTransportPaddingThenTestSoftLineBreak() {
// We assume here that if we see a TAB or SPACE in the source, then we can
// trim the target. This requires that TAB or SPACE will not form part of
// any encoded symbol. This should always hold, since if the previous code
// in the source is part of a symbol, i.e. not a literal TAB or SPACE, then
// none of its [equals/hexadecimal] codes would match 9 or 32.
var rewindIndex = sourceIndex;
while (
(targetIndex > 0) &&
(rewindIndex > 0) &&
(source[rewindIndex - 1] === 9 || source[rewindIndex - 1] === 32)
) {
targetIndex--;
rewindIndex--;
}
return (
targetIndex > 0 &&
rewindIndex > 0 &&
source[rewindIndex - 1] === 61
);
}
function sizeCRLF() {
// If the current position contains a CRLF, return the number of characters.
// Assume at least one code is available to read.
// i.e. We are within the for loop.
if (
source[sourceIndex] === 13 &&
sourceIndex + 1 < sourceLength &&
source[sourceIndex + 1] === 10
) {
return 2;
} else if (
source[sourceIndex] === 13 ||
source[sourceIndex] === 10
) {
return 1;
} else {
return 0;
}
}
var sourceIndex = 0;
var sourceLength = source.length;
var targetIndex = 0;
var targetLength = target.length;
while (sourceIndex < sourceLength) {
if (
source[sourceIndex] === 61 &&
sourceIndex + 2 < sourceLength &&
tableDecoding[source[sourceIndex + 1]] &&
tableDecoding[source[sourceIndex + 2]]
) {
// Symbol:
target[targetIndex++] = (
((tableDecoding[source[sourceIndex + 1]] - 1) << 4) +
((tableDecoding[source[sourceIndex + 2]] - 1))
);
sourceIndex += 3;
} else if (source[sourceIndex] === 13 || source[sourceIndex] === 10) {
if (removeTransportPaddingThenTestSoftLineBreak()) {
// Soft Line Break:
targetIndex--; // Remove "="
sourceIndex += sizeCRLF(); // Pass over CRLF.
} else if (sizeCRLF() === 2) {
// CRLF:
target[targetIndex++] = source[sourceIndex++];
target[targetIndex++] = source[sourceIndex++];
} else {
// CR/LF:
target[targetIndex++] = source[sourceIndex++];
}
} else if (qEncoding && source[sourceIndex] === 95) {
// Replace "_" with " " (independent of charset):
target[targetIndex++] = 32;
sourceIndex++;
} else if (tableLegal[source[sourceIndex]]) {
// Literal:
target[targetIndex++] = source[sourceIndex++];
} else {
// Illegal:
sourceIndex++;
// throw new Error('illegal character');
}
}
removeTransportPaddingThenTestSoftLineBreak();
if (sourceIndex > sourceLength) {
throw new Error('source overflow');
}
if (targetIndex > targetLength) {
throw new Error('target overflow');
}
return targetIndex;
};
QuotedPrintable.binding.javascript.encode = function(
source, target, qEncoding, tableEncoding, tableLiterals
) {
if (arguments.length !== 5) {
throw new Error('bad number of arguments');
}
if (!Buffer.isBuffer(source)) {
throw new Error('source must be a buffer');
}
if (!Buffer.isBuffer(target)) {
throw new Error('target must be a buffer');
}
if (
typeof qEncoding !== 'number' ||
Math.floor(qEncoding) !== qEncoding ||
qEncoding < 0
) {
throw new Error('qEncoding must be a positive integer');
}
if (qEncoding !== 0 && qEncoding !== 1) {
throw new Error('qEncoding must be 0 or 1');
}
if (!Buffer.isBuffer(tableEncoding)) {
throw new Error('tableEncoding must be a buffer');
}
if (!Buffer.isBuffer(tableLiterals)) {
throw new Error('tableLiterals must be a buffer');
}
if (target.length < QuotedPrintable.encodeTargetLength(source.length)) {
throw new Error('target too small');
}
if (tableEncoding.length !== 512) {
throw new Error('tableEncoding must be 512 bytes');
}
if (tableLiterals.length !== 256) {
throw new Error('tableLiterals must be 256 bytes');
}
// Regarding qEncoding:
// We do not use "_" to represent " " since decoders may not support this.
// Instead, we represent " " using "=20".
// This is compatible with the spec and also simplifies the implementation.
// Regarding non-qEncoding:
// We do not use "_" as a literal since some decoders may decode this as " ".
// Instead, we represent "_" using "=5F".
function encodeSoftLineBreak(size) {
// (Soft Line Breaks) The Quoted-Printable encoding
// REQUIRES that encoded lines be no more than 76
// characters long.
if (line + size >= 76 && !qEncoding) {
target[targetIndex++] = 61; // =
target[targetIndex++] = 13; // \r
target[targetIndex++] = 10; // \n
line = 0;
}
line += size;
}
function encodeSymbol(code) {
encodeSoftLineBreak(3);
target[targetIndex++] = 61; // =
var tableIndex = code << 1;
target[targetIndex++] = tableEncoding[tableIndex];
target[targetIndex++] = tableEncoding[tableIndex + 1];
}
function encodeTrailingSpace() {
if (
(targetIndex > 0) &&
(target[targetIndex - 1] === 9 || target[targetIndex - 1] === 32)
) {
encodeSymbol(target[--targetIndex]);
}
}
var line = 0;
var sourceIndex = 0;
var sourceLength = source.length;
var targetIndex = 0;
var targetLength = target.length;
while (sourceIndex < sourceLength) {
if (tableLiterals[source[sourceIndex]]) {
encodeSoftLineBreak(1);
target[targetIndex++] = source[sourceIndex++];
} else if (
(source[sourceIndex] === 13) &&
(sourceIndex + 1 < sourceLength) &&
(source[sourceIndex + 1] === 10) &&
!qEncoding
) {
encodeTrailingSpace();
target[targetIndex++] = 13;
target[targetIndex++] = 10;
sourceIndex += 2;
line = 0;
} else {
encodeSymbol(source[sourceIndex++]);
}
}
encodeTrailingSpace();
if (sourceIndex > sourceLength) {
throw new Error('source overflow');
}
if (targetIndex > targetLength) {
throw new Error('target overflow');
}
return targetIndex;
};
try {
QuotedPrintable.binding.native = require('./binding.node');
QuotedPrintable.binding.active = QuotedPrintable.binding.native;
} catch (exception) {
// We use the Javascript binding if the native binding has not been compiled.
QuotedPrintable.binding.active = QuotedPrintable.binding.javascript;
}
QuotedPrintable.decode = function(source, options) {
var self = this;
var binding = self.binding.active;
var qEncoding = 0;
if (options) {
if (options.hasOwnProperty('binding')) {
self.assertBinding(options.binding);
binding = options.binding;
}
if (options.hasOwnProperty('qEncoding')) {
self.assertQEncoding(options.qEncoding);
qEncoding = options.qEncoding ? 1 : 0;
}
}
var target = Buffer.alloc(source.length);
var targetSize = binding.decode(
source,
target,
qEncoding,
self.tableDecoding,
self.tableLegal
);
if (targetSize > target.length) {
throw new Error('target overflow');
}
return target.slice(0, targetSize);
};
QuotedPrintable.encode = function(source, options) {
var self = this;
var binding = self.binding.active;
var qEncoding = 0;
if (options) {
if (options.hasOwnProperty('binding')) {
self.assertBinding(options.binding);
binding = options.binding;
}
if (options.hasOwnProperty('qEncoding')) {
self.assertQEncoding(options.qEncoding);
qEncoding = options.qEncoding ? 1 : 0;
}
}
var target = Buffer.alloc(self.encodeTargetLength(source.length));
var targetSize = binding.encode(
source,
target,
qEncoding,
self.tableEncoding,
qEncoding ? self.tableLiteralsRestricted : self.tableLiterals
);
if (targetSize > target.length) {
throw new Error('target overflow');
}
return target.slice(0, targetSize);
};
QuotedPrintable.encodeTargetLength = function(sourceLength) {
var self = this;
if (
typeof sourceLength !== 'number' ||
Math.round(sourceLength) !== sourceLength ||
sourceLength < 0
) {
throw new Error('sourceLength must be a positive integer');
}
// Assume every byte must be represented by a symbol.
// Assume there are no line-breaks and that soft line-breaks must be added.
var lines = Math.ceil(sourceLength * 3 / (76 - 1));
var softLineBreaks = lines > 0 ? lines - 1 : 0;
return (sourceLength * 3) + (softLineBreaks * 3);
};
QuotedPrintable.generateTableDecoding = function() {
var self = this;
// This table does the following faster:
// parseInt(String.fromCharCode(code), 16)
var alphabet = '0123456789ABCDEFabcdef';
var table = Buffer.alloc(256);
for (var index = 0, length = alphabet.length; index < length; index++) {
var char = alphabet[index];
// Add 1 to all values so that we can detect hex digits with the same table.
// Subtract 1 when needed to get to the integer value of the hex digit.
table[char.charCodeAt(0)] = parseInt(char, 16) + 1;
}
return table;
};
QuotedPrintable.generateTableEncoding = function() {
var self = this;
// This table does the following faster:
// var hex = code.toString(16).toUpperCase();
// if (hex.length === 1) hex = '0' + hex;
// hex.charCodeAt(0);
// hex.charCodeAt(1);
var table = Buffer.alloc(256 * 2);
var tableIndex = 0;
for (var code = 0; code < 256; code++) {
var hex = code.toString(16).toUpperCase();
if (hex.length === 1) hex = '0' + hex;
table[tableIndex++] = hex.charCodeAt(0);
table[tableIndex++] = hex.charCodeAt(1);
}
return table;
};
QuotedPrintable.generateTableLegal = function() {
var self = this;
var table = self.generateTableLiterals();
table[61] = 1; // "="
table[95] = 1; // "_"
return table;
};
QuotedPrintable.generateTableLiterals = function() {
var self = this;
// This table does the following faster:
// TAB (9) || SPACE (32)
// 33 through 60 inclusive, and 62 through 126.
// Except "_" (95) (some parsers may decode this as " ").
// (code === 9) ||
// (code >= 32 && code <= 60) ||
// (code >= 62 && code <= 126 && code !== 95)
var table = Buffer.alloc(256);
for (var code = 0; code < 256; code++) {
if (
(code === 9) ||
(code >= 32 && code <= 60) ||
(code >= 62 && code <= 126 && code !== 95)
) {
table[code] = 1;
}
}
return table;
};
QuotedPrintable.generateTableLiteralsRestricted = function() {
var self = this;
// This table does the following faster:
// 0-9 || A-Z || a-z
// We use this for determining literals for qEncoding.
// RFC 2047 Section 5 (3):
// In this case [`phrase`] the set of characters that may be used in a
// "Q"-encoded 'encoded-word' is restricted to: <upper and lower case ASCII
// letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_"
// (underscore, ASCII 95.)>.
// We support alphanumeric literals only:
// "_" cannot be used literally.
// "=" cannot be used literally.
// We further give up "!", "*", "+", "-", "/" to avoid issues with decoders
// and structured field parsers.
var table = Buffer.alloc(256);
for (var code = 0; code < 256; code++) {
if (
(code >= 48 && code <= 57) || // 0-9
(code >= 65 && code <= 90) || // A-Z
(code >= 97 && code <= 122) // a-z
) {
table[code] = 1;
}
}
return table;
};
QuotedPrintable.tableDecoding = QuotedPrintable.generateTableDecoding();
QuotedPrintable.tableEncoding = QuotedPrintable.generateTableEncoding();
QuotedPrintable.tableLegal = QuotedPrintable.generateTableLegal();
QuotedPrintable.tableLiterals = QuotedPrintable.generateTableLiterals();
QuotedPrintable.tableLiteralsRestricted = (
QuotedPrintable.generateTableLiteralsRestricted()
);
module.exports = QuotedPrintable;
// S.D.G.