encode.js 3.62 KB
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();
    }

};