├── .gitignore ├── README.md ├── index.js ├── lib ├── decode.js └── encode.js ├── package.json └── test ├── decode.test.js └── encode.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | While Node.js has built-in support for Base64 data, it does not come with the ability to encode / decode data in a stream. 4 | 5 | This library contains a streaming Base64 encoder and a streaming Base64 decoder for use with Node.js. These classes are written using the Node.js [stream interfaces](http://nodejs.org/api/stream.html) and are well covered with unit tests. 6 | 7 | # Usage 8 | 9 | ## Installation 10 | 11 | To install base64-stream 12 | 13 | npm install base64-stream 14 | 15 | ## Examples 16 | This example encodes an image and pipes it to stdout. 17 | 18 | ```javascript 19 | var http = require('http'); 20 | var {Base64Encode} = require('base64-stream'); 21 | 22 | var img = 'http://farm3.staticflickr.com/2433/3973241798_86ddfa642b_o.jpg'; 23 | http.get(img, function(res) { 24 | if (res.statusCode === 200) 25 | res.pipe(new Base64Encode()).pipe(process.stdout); 26 | }); 27 | ``` 28 | 29 | This example takes in Base64 encoded data on stdin, decodes it, an pipes it to stdout. 30 | ```javascript 31 | var {Base64Decode} = require('base64-stream'); 32 | process.stdin.pipe(new Base64Decode()).pipe(process.stdout); 33 | ``` 34 | 35 | ## options: 36 | 37 | `Base64Encode` can take an optional object `{lineLength: number, prefix: string}` 38 | The prefix is useful for prepending for example `data:image/png;base64,` to make a base64 URL. 39 | This example proxies an image url, and send the base64 string in response. 40 | 41 | ``` 42 | app.get('/i/*', function(req, res){ // using express for example 43 | fetch(req.params[0]) // using node-fetch 44 | .then(r=>r.body.pipe(new Base64Encode({prefix:`data:${r.headers.get('content-type')};base64,`})).pipe(res)) 45 | .catch(console.error); 46 | }); 47 | ``` 48 | 49 | # Requirements 50 | 51 | This module currently requires Node 6.0.0 or higher. 52 | 53 | # Testing 54 | 55 | To run the unit tests 56 | 57 | npm test 58 | 59 | # License 60 | MIT 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Base64Encode = require('./lib/encode'); 2 | const Base64Decode = require('./lib/decode'); 3 | 4 | module.exports = { 5 | Base64Encode, 6 | Base64Decode 7 | }; 8 | -------------------------------------------------------------------------------- /lib/decode.js: -------------------------------------------------------------------------------- 1 | const { Transform } = require('stream'); 2 | 3 | /** 4 | * Decodes a Base64 data stream, coming in as a string or Buffer of UTF-8 text, into binary Buffers. 5 | * @extends Transform 6 | */ 7 | module.exports = class Base64Decode extends Transform { 8 | /** 9 | * Create a Base64Decode 10 | */ 11 | constructor() { 12 | super({ decodeStrings: false }); 13 | // Any extra chars from the last chunk 14 | this.extra = ''; 15 | } 16 | 17 | /** 18 | * Decodes a Base64 data stream, coming in as a string or Buffer of UTF-8 text, into binary Buffers. 19 | * @param {Buffer|string} chunk 20 | * @param encoding 21 | * @param cb 22 | * @private 23 | */ 24 | _transform(chunk, encoding, cb) { 25 | // Convert chunk to a string 26 | chunk = '' + chunk; 27 | 28 | // Add previous extra and remove any newline characters 29 | chunk = this.extra + chunk.replace(/(\r\n|\n|\r)/gm, ''); 30 | 31 | // 4 characters represent 3 bytes, so we can only decode in groups of 4 chars 32 | const remaining = chunk.length % 4; 33 | 34 | // Store the extra chars for later 35 | this.extra = chunk.slice(chunk.length - remaining); 36 | chunk = chunk.slice(0, chunk.length - remaining); 37 | 38 | // Create the new buffer and push 39 | const buf = Buffer.from(chunk, 'base64'); 40 | this.push(buf); 41 | cb(); 42 | } 43 | 44 | /** 45 | * Emits 1, 2, or 3 extra characters of base64 data. 46 | * @param cb 47 | * @private 48 | */ 49 | _flush(cb) { 50 | if (this.extra.length) { 51 | this.push(Buffer.from(this.extra, 'base64')); 52 | } 53 | 54 | cb(); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /lib/encode.js: -------------------------------------------------------------------------------- 1 | const { Transform } = require('stream'); 2 | 3 | /** 4 | * Transforms a Buffer stream of binary data to a stream of Base64 text. Note that this will 5 | * also work on a stream of pure strings, as the Writeable base class will automatically decode 6 | * text string chunks into Buffers. 7 | * You can pass optionally a line length or a prefix 8 | * @extends Transform 9 | */ 10 | module.exports = class Base64Encode extends Transform { 11 | /** 12 | * Creates a Base64Encode 13 | * @param {Object=} options - Options for stream creation. Passed to Transform constructor as-is. 14 | * @param {string=} options.inputEncoding - The input chunk format. Default is 'utf8'. No effect on Buffer input chunks. 15 | * @param {string=} options.outputEncoding - The output chunk format. Default is 'utf8'. Pass `null` for Buffer chunks. 16 | * @param {number=} options.lineLength - The max line-length of the output stream. 17 | * @param {string=} options.prefix - Prefix for output string. 18 | */ 19 | constructor(options) { 20 | super(options); 21 | 22 | // Any extra chars from the last chunk 23 | this.extra = null; 24 | this.lineLength = options && options.lineLength; 25 | this.currLineLength = 0; 26 | if (options && options.prefix) { 27 | this.push(options.prefix); 28 | } 29 | 30 | // Default string input to be treated as 'utf8' 31 | const encIn = options && options.inputEncoding; 32 | this.setDefaultEncoding(encIn || 'utf8'); 33 | 34 | // Default output to be strings 35 | const encOut = options && options.outputEncoding; 36 | if (encOut !== null) { 37 | this.setEncoding(encOut || 'utf8'); 38 | } 39 | } 40 | 41 | /** 42 | * Adds \r\n as needed to the data chunk to ensure that the output Base64 string meets 43 | * the maximum line length requirement. 44 | * @param {string} chunk 45 | * @returns {string} 46 | * @private 47 | */ 48 | _fixLineLength(chunk) { 49 | // If we care about line length, add line breaks 50 | if (!this.lineLength) { 51 | return chunk; 52 | } 53 | 54 | const size = chunk.length; 55 | const needed = this.lineLength - this.currLineLength; 56 | let start, end; 57 | 58 | let _chunk = ''; 59 | for (start = 0, end = needed; end < size; start = end, end += this.lineLength) { 60 | _chunk += chunk.slice(start, end); 61 | _chunk += '\r\n'; 62 | } 63 | 64 | const left = chunk.slice(start); 65 | this.currLineLength = left.length; 66 | 67 | _chunk += left; 68 | 69 | return _chunk; 70 | } 71 | 72 | /** 73 | * Transforms a Buffer chunk of data to a Base64 string chunk. 74 | * @param {Buffer} chunk 75 | * @param {string} encoding - unused since chunk is always a Buffer 76 | * @param cb 77 | * @private 78 | */ 79 | _transform(chunk, encoding, cb) { 80 | // Add any previous extra bytes to the chunk 81 | if (this.extra) { 82 | chunk = Buffer.concat([this.extra, chunk]); 83 | this.extra = null; 84 | } 85 | 86 | // 3 bytes are represented by 4 characters, so we can only encode in groups of 3 bytes 87 | const remaining = chunk.length % 3; 88 | 89 | if (remaining !== 0) { 90 | // Store the extra bytes for later 91 | this.extra = chunk.slice(chunk.length - remaining); 92 | chunk = chunk.slice(0, chunk.length - remaining); 93 | } 94 | 95 | // Convert chunk to a base 64 string 96 | chunk = chunk.toString('base64'); 97 | 98 | // Push the chunk 99 | this.push(Buffer.from(this._fixLineLength(chunk))); 100 | cb(); 101 | } 102 | 103 | /** 104 | * Emits 0 or 4 extra characters of Base64 data. 105 | * @param cb 106 | * @private 107 | */ 108 | _flush(cb) { 109 | if (this.extra) { 110 | this.push(Buffer.from(this._fixLineLength(this.extra.toString('base64')))); 111 | } 112 | 113 | cb(); 114 | } 115 | 116 | }; 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base64-stream", 3 | "description": "Contains new Node.js v0.10 style stream classes for encoding / decoding Base64 data", 4 | "keywords": [ 5 | "Base64", 6 | "stream", 7 | "streaming", 8 | "piping", 9 | "node", 10 | "node.js", 11 | "encode", 12 | "decode" 13 | ], 14 | "author": "Ross Johnson ", 15 | "version": "1.0.0", 16 | "repository": { 17 | "type": "git", 18 | "url": "http://github.com/mazira/base64-stream" 19 | }, 20 | "scripts": { 21 | "test": "mocha --reporter spec" 22 | }, 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "mocha": "*", 26 | "should": "*" 27 | }, 28 | "license": "MIT", 29 | "files": [ 30 | "lib", 31 | "index.js" 32 | ], 33 | "engine": "node >= 0.8.0" 34 | } 35 | -------------------------------------------------------------------------------- /test/decode.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | const should = require('should'); 3 | const { Base64Decode } = require('../'); 4 | 5 | describe('Base64Decode', function () { 6 | /** 7 | * This function emits an array of string chunks to the stream, and then compares the output to a given value. 8 | * @param stream 9 | * @param inputs 10 | * @param output 11 | */ 12 | function testStream(stream, inputs, output) { 13 | for (let i = 0; i < inputs.length; i++) { 14 | stream.write(inputs[i]); 15 | } 16 | 17 | stream.end(); 18 | 19 | const result = stream.read(); 20 | result.should.be.an.instanceOf(Buffer); 21 | result.toString('utf8').should.equal(output); 22 | } 23 | 24 | describe('input in a single chunk', function () { 25 | it('should properly decode a Buffer', function () { 26 | testStream(new Base64Decode(), [Buffer.from('YW55IGNhcm5hbCBwbGVhc3VyZS4=')], 'any carnal pleasure.'); 27 | }); 28 | 29 | it('should properly decode a string', function () { 30 | testStream(new Base64Decode(), ['YW55IGNhcm5hbCBwbGVhc3VyZS4='], 'any carnal pleasure.'); 31 | }); 32 | 33 | it('should properly decode string containing newlines', function () { 34 | testStream(new Base64Decode(), ['YW55I\nGNhcm\n5hbCB\nwbGVh\nc3VyZ\nS4='], 'any carnal pleasure.'); 35 | }); 36 | 37 | it('should properly decode a string without padding', function () { 38 | testStream(new Base64Decode(), ['YW55IGNhcm5hbCBwbGVhc3VyZS4'], 'any carnal pleasure.'); 39 | }); 40 | }); 41 | 42 | describe('input in multiple chunks, lengths divisible by 4', function () { 43 | it('should properly decode a string', function () { 44 | testStream(new Base64Decode(), ['YW55IGNhcm5h', 'bCBwbGVhc3Vy', 'ZS4='], 'any carnal pleasure.'); 45 | }); 46 | 47 | it('should properly decode string containing newlines', function () { 48 | testStream(new Base64Decode(), ['YW55IGNhcm5h\n', 'bCBwbGVhc3Vy\n', 'ZS4=\n'], 'any carnal pleasure.'); 49 | }); 50 | 51 | it('should properly decode a string without padding', function () { 52 | testStream(new Base64Decode(), ['YW55IGNhcm5h', 'bCBwbGVhc3Vy', 'ZS4'], 'any carnal pleasure.'); 53 | }); 54 | }); 55 | 56 | describe('input in multiple chunks, lengths not divisible by 4', function () { 57 | it('should properly decode a string', function () { 58 | testStream(new Base64Decode(), ['YW55I', 'GNhcm5h', 'bCBwbGVhc3VyZ', 'S4='], 'any carnal pleasure.'); 59 | testStream(new Base64Decode(), ['YW55I', 'GNhcm5h', 'bCBwbGVhc3VyZS4', '='], 'any carnal pleasure.'); 60 | }); 61 | 62 | it('should properly decode string containing newlines', function () { 63 | testStream(new Base64Decode(), ['YW55I\n', 'GNhcm5h\n', 'bCBwbGVhc3VyZ\n', 'S4=\n'], 'any carnal pleasure.'); 64 | }); 65 | 66 | it('should properly decode a string without padding', function () { 67 | testStream(new Base64Decode(), ['YW55I', 'GNhcm5h', 'bCBwbGVhc3VyZ', 'S4'], 'any carnal pleasure.'); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/encode.test.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | const should = require('should'); 3 | const { Base64Encode } = require('../'); 4 | 5 | describe('Base64Encode', function () { 6 | /** 7 | * This function emits an array of string chunks to the stream, and then compares the output to a given value. 8 | * @param stream 9 | * @param inputs 10 | * @param output 11 | */ 12 | function testStream(stream, inputs, output) { 13 | for (let i = 0; i < inputs.length; i++) { 14 | stream.write(inputs[i]); 15 | } 16 | 17 | stream.end(); 18 | 19 | const result = stream.read(); 20 | result.should.eql(output); 21 | } 22 | 23 | describe('input in a single chunk', function () { 24 | it('should properly encode a string', function () { 25 | testStream(new Base64Encode(), ['any carnal pleasur'], 'YW55IGNhcm5hbCBwbGVhc3Vy'); 26 | }); 27 | 28 | it('should properly encode a Buffer', function () { 29 | testStream(new Base64Encode(), [Buffer.from('any carnal pleasur')], 'YW55IGNhcm5hbCBwbGVhc3Vy'); 30 | }); 31 | 32 | it('should properly encode a Buffer and include padding', function () { 33 | testStream(new Base64Encode(), ['any carnal pleasure.'], 'YW55IGNhcm5hbCBwbGVhc3VyZS4='); 34 | }); 35 | }); 36 | 37 | describe('input in multiple chunks, lengths divisible by 3', function () { 38 | it('should properly encode a Buffer', function () { 39 | testStream(new Base64Encode(), [ 40 | Buffer.from('any ca'), Buffer.from('rnal p'), Buffer.from('leasur') 41 | ], 'YW55IGNhcm5hbCBwbGVhc3Vy'); 42 | }); 43 | 44 | it('should properly encode a Buffer and include padding', function () { 45 | testStream(new Base64Encode(), [ 46 | Buffer.from('any ca'), Buffer.from('rnal p'), Buffer.from('leasure.') 47 | ], 'YW55IGNhcm5hbCBwbGVhc3VyZS4='); 48 | }); 49 | }); 50 | 51 | describe('input in multiple chunks, lengths not divisible by 3', function () { 52 | it('should properly encode a Buffer', function () { 53 | testStream(new Base64Encode(), [ 54 | Buffer.from('any carn'), Buffer.from('al pl'), Buffer.from('easur') 55 | ], 'YW55IGNhcm5hbCBwbGVhc3Vy'); 56 | }); 57 | 58 | it('should properly encode a Buffer and include padding', function () { 59 | testStream(new Base64Encode(), [ 60 | Buffer.from('any carn'), Buffer.from('al pl'), Buffer.from('easure.') 61 | ], 'YW55IGNhcm5hbCBwbGVhc3VyZS4='); 62 | }); 63 | }); 64 | 65 | describe('with inputEncoding specified', function () { 66 | it('should properly encode strings with unusual input encoding', function () { 67 | const stream = new Base64Encode({ inputEncoding: 'hex' }); 68 | 69 | const input = Buffer.from('any carnal pleasur').toString('hex'); 70 | testStream(stream, [input], 'YW55IGNhcm5hbCBwbGVhc3Vy'); 71 | }); 72 | 73 | it('should not affect Buffer inputs', function () { 74 | const stream = new Base64Encode({ inputEncoding: 'base64' }); 75 | 76 | const input = Buffer.from('00010203', 'hex'); 77 | testStream(stream, [input], 'AAECAw=='); 78 | }); 79 | }); 80 | 81 | describe('with outputEncoding specified', function () { 82 | it('should properly output provided string encoding', function () { 83 | const stream = new Base64Encode({ outputEncoding: 'hex' }); 84 | 85 | const input = Buffer.from('any carnal pleasur'); 86 | const output = Buffer.from('YW55IGNhcm5hbCBwbGVhc3Vy').toString('hex'); 87 | testStream(stream, [input], output); 88 | }); 89 | 90 | it('should properly output Buffers when null given', function () { 91 | const stream = new Base64Encode({ outputEncoding: null }); 92 | 93 | const input = Buffer.from('any carnal pleasur'); 94 | const output = Buffer.from('YW55IGNhcm5hbCBwbGVhc3Vy'); 95 | testStream(stream, [input], output); 96 | }); 97 | }) 98 | 99 | describe('with line length specified', function () { 100 | it('should properly encode a Buffer', function () { 101 | testStream(new Base64Encode({ lineLength: 5 }), [ 102 | Buffer.from('any carn'), Buffer.from('al pl'), Buffer.from('easur') 103 | ], 'YW55I\r\nGNhcm\r\n5hbCB\r\nwbGVh\r\nc3Vy'); 104 | }); 105 | 106 | it('should properly encode a Buffer and include padding', function () { 107 | testStream(new Base64Encode({ lineLength: 5 }), [ 108 | Buffer.from('any carn'), Buffer.from('al pl'), Buffer.from('easure.') 109 | ], 'YW55I\r\nGNhcm\r\n5hbCB\r\nwbGVh\r\nc3VyZ\r\nS4='); 110 | }); 111 | }); 112 | 113 | describe('with prefix specified', function () { 114 | it('should properly encode a Buffer', function () { 115 | testStream(new Base64Encode({ prefix: 'base64: ' }), [ 116 | Buffer.from('any carn'), Buffer.from('al pl'), Buffer.from('easur') 117 | ], 'base64: YW55IGNhcm5hbCBwbGVhc3Vy'); 118 | }); 119 | 120 | it('should properly encode a Buffer and include padding', function () { 121 | testStream(new Base64Encode({ prefix: 'base64: ' }), [ 122 | Buffer.from('any carn'), Buffer.from('al pl'), Buffer.from('easure.') 123 | ], 'base64: YW55IGNhcm5hbCBwbGVhc3VyZS4='); 124 | }); 125 | }); 126 | }); 127 | --------------------------------------------------------------------------------