diff --git a/index.js b/index.js index 44d1151..cbb197f 100644 --- a/index.js +++ b/index.js @@ -311,8 +311,21 @@ function tinf_inflate_uncompressed_block(d) { return TINF_OK; } +/* read an integer from a byte array in little-endian order */ +function tinf_readle(source, start, len) { + var res = 0; + for (var i = 0; i < len; ++i) { + var dat = source[start + i]; + /* verify in bounds */ + if (typeof dat === 'undefined') + throw new Error('out of bounds'); + res += dat << (8 * i); + } + return res; +} + /* inflate stream from source to dest */ -function tinf_uncompress(source, dest) { +function tinf_inflate_base(source, dest) { var d = new Data(source, dest); var bfinal, btype, res; @@ -357,6 +370,100 @@ function tinf_uncompress(source, dest) { return d.dest; } +/** + * Decompresses deflate data. Similar to `pako.inflateRaw()` + * @param {Uint8Array} source The deflate data + * @param {Uint8Array} [dest] Where to copy the uncompressed data to. If none + * is provided, a new Uint8Array will be returned. + * If the decompressed size is known, passing in an + * empty Uint8Array of that size reduces memory + * usage. + * @returns {Uint8Array} The original data + */ +function tinf_inflate(source, dest) { + if (dest) + return tinf_inflate_base(source, dest); + return new Uint8Array(tinf_inflate_base(source, [])); +} + +/** + * Decompresses gzip data. Similar to `pako.ungzip()` + * @param {Uint8Array} source The gzip data + * @param {Uint8Array} [dest] Where to copy the uncompressed data to. If none + * is provided, a new Uint8Array will be returned. + * If the decompressed size is known, passing in an + * empty Uint8Array of that size reduces memory + * usage. + * @returns {Uint8Array} The original data + */ +function tinf_gunzip(source, dest) { + var len = source.length; + if (len < 18 || source[0] !== 31 || source[1] !== 139 || source[2] !== 8) + throw new Error('invalid gzip data'); + var flg = source[3]; + var start = 10; + if (flg & 4) { + try { start += tinf_readle(source, start, 2) + 2; } + catch(e) { throw new Error('invalid gzip data'); } + } + /* skip FNAME, FCOMMENT (0 terminated) */ + for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= (source[start++] === 0)) {} + /* skip 2 bytes if FHCRC */ + start += flg & 2; + if (!dest) { + /* use header-provided size */ + dest = new Uint8Array(tinf_readle(source, len - 4, 4)); + } + return tinf_inflate_base(source.subarray(start, len - 8), dest); +} + +/** + * Decompresses zlib data. Similar to `pako.inflate()` + * @param {Uint8Array} source The zlib data + * @param {Uint8Array} [dest] Where to copy the uncompressed data to. If none + * is provided, a new Uint8Array will be returned. + * If the decompressed size is known, passing in an + * empty Uint8Array of that size reduces memory + * usage. + * @returns {Uint8Array} The original data + */ +function tinf_decompress(source, dest) { + var len = source.length; + if (len < 6 || source[0] & 15 !== 8 || source[0] >> 4 > 7) + throw new Error('invalid zlib data'); + if (source[1] & 32) + throw new Error('invalid zlib data: dictionaries not supported'); + return tinf_inflate(source.subarray(2, -4), dest); +} + +/** + * Decompresses deflate/gzip/zlib data. If format autodetection fails, try + * `inflate.inflate()`, `inflate.gunzip()`, and `inflate.decompress()`. + * @param {Uint8Array} source The deflate/gzip/zlib data + * @param {Uint8Array} [dest] Where to copy the uncompressed data to. If none + * is provided, a new Uint8Array will be returned. + * If the decompressed size is known, passing in an + * empty Uint8Array of that size reduces memory + * usage. + * @returns {Uint8Array} The original data + */ +function tinf_uncompress(source, dest) { + if (source[0] === 31 && source[1] === 139) { + /* data is gzipped */ + return tinf_gunzip(source, dest); + } + if (source[0] & 15 !== 8 || source[0] >> 4 > 7) { + /* data cannot be zlib, assume deflate */ + return tinf_inflate(source, dest); + } + /* data should be zlib - in rare cases can still be deflate */ + return tinf_decompress(source, dest); +} + +tinf_uncompress.inflate = tinf_inflate; +tinf_uncompress.gunzip = tinf_gunzip; +tinf_uncompress.decompress = tinf_decompress; + /* -------------------- * * -- initialization -- * * -------------------- */ diff --git a/readme.md b/readme.md index dd8c408..3f30d7e 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # tiny-inflate This is a port of Joergen Ibsen's [tiny inflate](https://bitbucket.org/jibsen/tinf) to JavaScript. -Minified it is about 3KB, or 1.3KB gzipped. While being very small, it is also reasonably fast +Minified it is about 3.7KB, or 1.5KB gzipped. While being very small, it is also reasonably fast (about 30% - 50% slower than [pako](https://github.com/nodeca/pako) on average), and should be good enough for many applications. If you need the absolute best performance, however, you'll need to use a larger library such as pako that contains additional optimizations. @@ -12,18 +12,47 @@ need to use a larger library such as pako that contains additional optimizations ## Example -To use tiny-inflate, you need two things: a buffer of data compressed with deflate, -and the decompressed size (often stored in a file header) to allocate your output buffer. -Input and output buffers can be either node `Buffer`s, or `Uint8Array`s. +To use tiny-inflate, you only need a buffer of data compressed with `deflate`, `zlib`, or `gzip`. +If you have the decompressed size, you can allocate your output buffer ahead of time to save memory. +(Note that this is unecessary for GZIP data because `tiny-inflate` automatically detects the output +size from the header.) + +Input and output buffers must be `Uint8Array`s. Since `Buffer`s are instances of `Uint8Array`s, you can +also pass those in directly. + +`tiny-inflate` will try to automatically detect what compression method the data is in, but in rare cases +it fails. If that happens, you can use `inflate.gunzip()` for GZIP data (like `pako.ungzip()`), +`inflate.inflate()` for deflated data (like `pako.inflateRaw()`), and `inflate.decompress()` for Zlib data +(like `pako.inflate()`). + + +Example: decoding a Base64, GZIP string to the source string +```javascript +var inflate = require('tiny-inflate'); + +var compressedBuffer = Buffer.from('H4sIAAAAAAAAA/NIzcnJVyjPL8pJUQQAlRmFGwwAAAA=', 'base64'); + +var outputUint8Array = inflate(compressedBuffer); /* Can also use inflate.gunzip(compressedBuffer) */ + +var outputBuffer = Buffer.from(outputUint8Array); + +console.log(outputBuffer.toString('utf8')); /* Hello world! */ +``` +Example: efficiently decoding a Base64, Zlib string with known uncompressed length ```javascript var inflate = require('tiny-inflate'); -var compressedBuffer = new Bufer([ ... ]); -var decompressedSize = ...; -var outputBuffer = new Buffer(decompressedSize); +var compressedBuffer = Buffer.from('eJzzSM3JyVcozy/KSVEEAB0JBF4=', 'base64'); + +var outputSize = 12; /* Assuming you know this previously... */ + +var outputArray = new Uint8Array(outputSize); /* Then you can create an efficient output space */ + +/* Output array that is passed in is mutated - no need to extract return value */ +inflate(compressedBuffer, outputArray); /* Can also use inflate.inflate(compressedBuffer, outputArray) */ -inflate(compressedBuffer, outputBuffer); +console.log(Buffer.from(outputArray).toString('utf8')); /* Hello world! */ ``` ## License diff --git a/test/index.js b/test/index.js index f4e8c88..29ead89 100644 --- a/test/index.js +++ b/test/index.js @@ -5,7 +5,7 @@ var assert = require('assert'); var uncompressed = fs.readFileSync(__dirname + '/lorem.txt'); describe('tiny-inflate', function() { - var compressed, noCompression, fixed; + var compressed, noCompression, fixed, zlibCompressed, gzipped; function deflate(buf, options, fn) { var chunks = []; @@ -40,6 +40,20 @@ describe('tiny-inflate', function() { done(); }); }); + + before(function(done) { + zlib.deflate(uncompressed, function(err, data) { + zlibCompressed = data; + done(); + }); + }); + + before(function(done) { + zlib.gzip(uncompressed, function(err, data) { + gzipped = data; + done(); + }); + }); it('should inflate some data', function() { var out = Buffer.alloc(uncompressed.length); @@ -72,4 +86,30 @@ describe('tiny-inflate', function() { inflate(input, out); assert.deepEqual(out, new Uint8Array(uncompressed)); }); + + it('should handle no output array', function() { + var out = inflate(compressed); + assert.deepEqual(out, new Uint8Array(uncompressed)); + }) + + it('should handle gzip', function() { + var out = Buffer.alloc(uncompressed.length); + inflate(gzipped, out); + assert.deepEqual(out, uncompressed); + }); + + it('should handle zlib', function() { + var out = Buffer.alloc(uncompressed.length); + inflate(zlibCompressed, out); + assert.deepEqual(out, uncompressed); + }) + + it('should autodetect format', function() { + var outGzip = inflate(gzipped); + assert.deepEqual(outGzip, uncompressed); + var outZlib = inflate(zlibCompressed); + assert.deepEqual(outZlib, uncompressed); + var outDeflate = inflate(compressed); + assert.deepEqual(outDeflate, uncompressed); + }) });