Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added gunzip, zlib decompression support and revamped API #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 -- *
* -------------------- */
Expand Down
45 changes: 37 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down
42 changes: 41 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
})
});