const { Transform } = require('stream'); /** * Transforms a Buffer stream of binary data to a stream of Base64 text. Note that this will * also work on a stream of pure strings, as the Writeable base class will automatically decode * text string chunks into Buffers. * You can pass optionally a line length or a prefix * @extends Transform */ module.exports = class Base64Encode extends Transform { /** * Creates a Base64Encode * @param {Object=} options - Options for stream creation. Passed to Transform constructor as-is. * @param {string=} options.inputEncoding - The input chunk format. Default is 'utf8'. No effect on Buffer input chunks. * @param {string=} options.outputEncoding - The output chunk format. Default is 'utf8'. Pass `null` for Buffer chunks. * @param {number=} options.lineLength - The max line-length of the output stream. * @param {string=} options.prefix - Prefix for output string. */ constructor(options) { super(options); // Any extra chars from the last chunk this.extra = null; this.lineLength = options && options.lineLength; this.currLineLength = 0; if (options && options.prefix) { this.push(options.prefix); } // Default string input to be treated as 'utf8' const encIn = options && options.inputEncoding; this.setDefaultEncoding(encIn || 'utf8'); // Default output to be strings const encOut = options && options.outputEncoding; if (encOut !== null) { this.setEncoding(encOut || 'utf8'); } } /** * Adds \r\n as needed to the data chunk to ensure that the output Base64 string meets * the maximum line length requirement. * @param {string} chunk * @returns {string} * @private */ _fixLineLength(chunk) { // If we care about line length, add line breaks if (!this.lineLength) { return chunk; } const size = chunk.length; const needed = this.lineLength - this.currLineLength; let start, end; let _chunk = ''; for (start = 0, end = needed; end < size; start = end, end += this.lineLength) { _chunk += chunk.slice(start, end); _chunk += '\r\n'; } const left = chunk.slice(start); this.currLineLength = left.length; _chunk += left; return _chunk; } /** * Transforms a Buffer chunk of data to a Base64 string chunk. * @param {Buffer} chunk * @param {string} encoding - unused since chunk is always a Buffer * @param cb * @private */ _transform(chunk, encoding, cb) { // Add any previous extra bytes to the chunk if (this.extra) { chunk = Buffer.concat([this.extra, chunk]); this.extra = null; } // 3 bytes are represented by 4 characters, so we can only encode in groups of 3 bytes const remaining = chunk.length % 3; if (remaining !== 0) { // Store the extra bytes for later this.extra = chunk.slice(chunk.length - remaining); chunk = chunk.slice(0, chunk.length - remaining); } // Convert chunk to a base 64 string chunk = chunk.toString('base64'); // Push the chunk this.push(Buffer.from(this._fixLineLength(chunk))); cb(); } /** * Emits 0 or 4 extra characters of Base64 data. * @param cb * @private */ _flush(cb) { if (this.extra) { this.push(Buffer.from(this._fixLineLength(this.extra.toString('base64')))); } cb(); } };