348 lines
9.2 KiB
JavaScript
348 lines
9.2 KiB
JavaScript
'use strict'
|
|
|
|
const assert = require('assert')
|
|
const Buffer = require('buffer').Buffer
|
|
const realZlib = require('zlib')
|
|
|
|
const constants = exports.constants = require('./constants.js')
|
|
const Minipass = require('minipass')
|
|
|
|
const OriginalBufferConcat = Buffer.concat
|
|
|
|
const _superWrite = Symbol('_superWrite')
|
|
class ZlibError extends Error {
|
|
constructor (err) {
|
|
super('zlib: ' + err.message)
|
|
this.code = err.code
|
|
this.errno = err.errno
|
|
/* istanbul ignore if */
|
|
if (!this.code)
|
|
this.code = 'ZLIB_ERROR'
|
|
|
|
this.message = 'zlib: ' + err.message
|
|
Error.captureStackTrace(this, this.constructor)
|
|
}
|
|
|
|
get name () {
|
|
return 'ZlibError'
|
|
}
|
|
}
|
|
|
|
// the Zlib class they all inherit from
|
|
// This thing manages the queue of requests, and returns
|
|
// true or false if there is anything in the queue when
|
|
// you call the .write() method.
|
|
const _opts = Symbol('opts')
|
|
const _flushFlag = Symbol('flushFlag')
|
|
const _finishFlushFlag = Symbol('finishFlushFlag')
|
|
const _fullFlushFlag = Symbol('fullFlushFlag')
|
|
const _handle = Symbol('handle')
|
|
const _onError = Symbol('onError')
|
|
const _sawError = Symbol('sawError')
|
|
const _level = Symbol('level')
|
|
const _strategy = Symbol('strategy')
|
|
const _ended = Symbol('ended')
|
|
const _defaultFullFlush = Symbol('_defaultFullFlush')
|
|
|
|
class ZlibBase extends Minipass {
|
|
constructor (opts, mode) {
|
|
if (!opts || typeof opts !== 'object')
|
|
throw new TypeError('invalid options for ZlibBase constructor')
|
|
|
|
super(opts)
|
|
this[_sawError] = false
|
|
this[_ended] = false
|
|
this[_opts] = opts
|
|
|
|
this[_flushFlag] = opts.flush
|
|
this[_finishFlushFlag] = opts.finishFlush
|
|
// this will throw if any options are invalid for the class selected
|
|
try {
|
|
this[_handle] = new realZlib[mode](opts)
|
|
} catch (er) {
|
|
// make sure that all errors get decorated properly
|
|
throw new ZlibError(er)
|
|
}
|
|
|
|
this[_onError] = (err) => {
|
|
// no sense raising multiple errors, since we abort on the first one.
|
|
if (this[_sawError])
|
|
return
|
|
|
|
this[_sawError] = true
|
|
|
|
// there is no way to cleanly recover.
|
|
// continuing only obscures problems.
|
|
this.close()
|
|
this.emit('error', err)
|
|
}
|
|
|
|
this[_handle].on('error', er => this[_onError](new ZlibError(er)))
|
|
this.once('end', () => this.close)
|
|
}
|
|
|
|
close () {
|
|
if (this[_handle]) {
|
|
this[_handle].close()
|
|
this[_handle] = null
|
|
this.emit('close')
|
|
}
|
|
}
|
|
|
|
reset () {
|
|
if (!this[_sawError]) {
|
|
assert(this[_handle], 'zlib binding closed')
|
|
return this[_handle].reset()
|
|
}
|
|
}
|
|
|
|
flush (flushFlag) {
|
|
if (this.ended)
|
|
return
|
|
|
|
if (typeof flushFlag !== 'number')
|
|
flushFlag = this[_fullFlushFlag]
|
|
this.write(Object.assign(Buffer.alloc(0), { [_flushFlag]: flushFlag }))
|
|
}
|
|
|
|
end (chunk, encoding, cb) {
|
|
if (chunk)
|
|
this.write(chunk, encoding)
|
|
this.flush(this[_finishFlushFlag])
|
|
this[_ended] = true
|
|
return super.end(null, null, cb)
|
|
}
|
|
|
|
get ended () {
|
|
return this[_ended]
|
|
}
|
|
|
|
write (chunk, encoding, cb) {
|
|
// process the chunk using the sync process
|
|
// then super.write() all the outputted chunks
|
|
if (typeof encoding === 'function')
|
|
cb = encoding, encoding = 'utf8'
|
|
|
|
if (typeof chunk === 'string')
|
|
chunk = Buffer.from(chunk, encoding)
|
|
|
|
if (this[_sawError])
|
|
return
|
|
assert(this[_handle], 'zlib binding closed')
|
|
|
|
// _processChunk tries to .close() the native handle after it's done, so we
|
|
// intercept that by temporarily making it a no-op.
|
|
const nativeHandle = this[_handle]._handle
|
|
const originalNativeClose = nativeHandle.close
|
|
nativeHandle.close = () => {}
|
|
const originalClose = this[_handle].close
|
|
this[_handle].close = () => {}
|
|
// It also calls `Buffer.concat()` at the end, which may be convenient
|
|
// for some, but which we are not interested in as it slows us down.
|
|
Buffer.concat = (args) => args
|
|
let result
|
|
try {
|
|
const flushFlag = typeof chunk[_flushFlag] === 'number'
|
|
? chunk[_flushFlag] : this[_flushFlag]
|
|
result = this[_handle]._processChunk(chunk, flushFlag)
|
|
// if we don't throw, reset it back how it was
|
|
Buffer.concat = OriginalBufferConcat
|
|
} catch (err) {
|
|
// or if we do, put Buffer.concat() back before we emit error
|
|
// Error events call into user code, which may call Buffer.concat()
|
|
Buffer.concat = OriginalBufferConcat
|
|
this[_onError](new ZlibError(err))
|
|
} finally {
|
|
if (this[_handle]) {
|
|
// Core zlib resets `_handle` to null after attempting to close the
|
|
// native handle. Our no-op handler prevented actual closure, but we
|
|
// need to restore the `._handle` property.
|
|
this[_handle]._handle = nativeHandle
|
|
nativeHandle.close = originalNativeClose
|
|
this[_handle].close = originalClose
|
|
// `_processChunk()` adds an 'error' listener. If we don't remove it
|
|
// after each call, these handlers start piling up.
|
|
this[_handle].removeAllListeners('error')
|
|
// make sure OUR error listener is still attached tho
|
|
}
|
|
}
|
|
|
|
if (this[_handle])
|
|
this[_handle].on('error', er => this[_onError](new ZlibError(er)))
|
|
|
|
let writeReturn
|
|
if (result) {
|
|
if (Array.isArray(result) && result.length > 0) {
|
|
// The first buffer is always `handle._outBuffer`, which would be
|
|
// re-used for later invocations; so, we always have to copy that one.
|
|
writeReturn = this[_superWrite](Buffer.from(result[0]))
|
|
for (let i = 1; i < result.length; i++) {
|
|
writeReturn = this[_superWrite](result[i])
|
|
}
|
|
} else {
|
|
writeReturn = this[_superWrite](Buffer.from(result))
|
|
}
|
|
}
|
|
|
|
if (cb)
|
|
cb()
|
|
return writeReturn
|
|
}
|
|
|
|
[_superWrite] (data) {
|
|
return super.write(data)
|
|
}
|
|
}
|
|
|
|
class Zlib extends ZlibBase {
|
|
constructor (opts, mode) {
|
|
opts = opts || {}
|
|
|
|
opts.flush = opts.flush || constants.Z_NO_FLUSH
|
|
opts.finishFlush = opts.finishFlush || constants.Z_FINISH
|
|
super(opts, mode)
|
|
|
|
this[_fullFlushFlag] = constants.Z_FULL_FLUSH
|
|
this[_level] = opts.level
|
|
this[_strategy] = opts.strategy
|
|
}
|
|
|
|
params (level, strategy) {
|
|
if (this[_sawError])
|
|
return
|
|
|
|
if (!this[_handle])
|
|
throw new Error('cannot switch params when binding is closed')
|
|
|
|
// no way to test this without also not supporting params at all
|
|
/* istanbul ignore if */
|
|
if (!this[_handle].params)
|
|
throw new Error('not supported in this implementation')
|
|
|
|
if (this[_level] !== level || this[_strategy] !== strategy) {
|
|
this.flush(constants.Z_SYNC_FLUSH)
|
|
assert(this[_handle], 'zlib binding closed')
|
|
// .params() calls .flush(), but the latter is always async in the
|
|
// core zlib. We override .flush() temporarily to intercept that and
|
|
// flush synchronously.
|
|
const origFlush = this[_handle].flush
|
|
this[_handle].flush = (flushFlag, cb) => {
|
|
this.flush(flushFlag)
|
|
cb()
|
|
}
|
|
try {
|
|
this[_handle].params(level, strategy)
|
|
} finally {
|
|
this[_handle].flush = origFlush
|
|
}
|
|
/* istanbul ignore else */
|
|
if (this[_handle]) {
|
|
this[_level] = level
|
|
this[_strategy] = strategy
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// minimal 2-byte header
|
|
class Deflate extends Zlib {
|
|
constructor (opts) {
|
|
super(opts, 'Deflate')
|
|
}
|
|
}
|
|
|
|
class Inflate extends Zlib {
|
|
constructor (opts) {
|
|
super(opts, 'Inflate')
|
|
}
|
|
}
|
|
|
|
// gzip - bigger header, same deflate compression
|
|
const _portable = Symbol('_portable')
|
|
class Gzip extends Zlib {
|
|
constructor (opts) {
|
|
super(opts, 'Gzip')
|
|
this[_portable] = opts && !!opts.portable
|
|
}
|
|
|
|
[_superWrite] (data) {
|
|
if (!this[_portable])
|
|
return super[_superWrite](data)
|
|
|
|
// we'll always get the header emitted in one first chunk
|
|
// overwrite the OS indicator byte with 0xFF
|
|
this[_portable] = false
|
|
data[9] = 255
|
|
return super[_superWrite](data)
|
|
}
|
|
}
|
|
|
|
class Gunzip extends Zlib {
|
|
constructor (opts) {
|
|
super(opts, 'Gunzip')
|
|
}
|
|
}
|
|
|
|
// raw - no header
|
|
class DeflateRaw extends Zlib {
|
|
constructor (opts) {
|
|
super(opts, 'DeflateRaw')
|
|
}
|
|
}
|
|
|
|
class InflateRaw extends Zlib {
|
|
constructor (opts) {
|
|
super(opts, 'InflateRaw')
|
|
}
|
|
}
|
|
|
|
// auto-detect header.
|
|
class Unzip extends Zlib {
|
|
constructor (opts) {
|
|
super(opts, 'Unzip')
|
|
}
|
|
}
|
|
|
|
class Brotli extends ZlibBase {
|
|
constructor (opts, mode) {
|
|
opts = opts || {}
|
|
|
|
opts.flush = opts.flush || constants.BROTLI_OPERATION_PROCESS
|
|
opts.finishFlush = opts.finishFlush || constants.BROTLI_OPERATION_FINISH
|
|
|
|
super(opts, mode)
|
|
|
|
this[_fullFlushFlag] = constants.BROTLI_OPERATION_FLUSH
|
|
}
|
|
}
|
|
|
|
class BrotliCompress extends Brotli {
|
|
constructor (opts) {
|
|
super(opts, 'BrotliCompress')
|
|
}
|
|
}
|
|
|
|
class BrotliDecompress extends Brotli {
|
|
constructor (opts) {
|
|
super(opts, 'BrotliDecompress')
|
|
}
|
|
}
|
|
|
|
exports.Deflate = Deflate
|
|
exports.Inflate = Inflate
|
|
exports.Gzip = Gzip
|
|
exports.Gunzip = Gunzip
|
|
exports.DeflateRaw = DeflateRaw
|
|
exports.InflateRaw = InflateRaw
|
|
exports.Unzip = Unzip
|
|
/* istanbul ignore else */
|
|
if (typeof realZlib.BrotliCompress === 'function') {
|
|
exports.BrotliCompress = BrotliCompress
|
|
exports.BrotliDecompress = BrotliDecompress
|
|
} else {
|
|
exports.BrotliCompress = exports.BrotliDecompress = class {
|
|
constructor () {
|
|
throw new Error('Brotli is not supported in this version of Node.js')
|
|
}
|
|
}
|
|
}
|