cmux/node_modules/yauzl-promise/lib/promisify.js
2026-01-29 17:36:26 -08:00

291 lines
7 KiB
JavaScript

/* --------------------
* yauzl-promise module
* Promisify yauzl
* ------------------*/
'use strict';
// Modules
const cloner = require('yauzl-clone');
// Constants
const STATE = Symbol(),
STORED_ERROR = Symbol();
// Exports
module.exports = (yauzl, Promise) => {
const {ZipFile, Entry} = yauzl;
// Promisify open + from... methods
promisifyMethod(yauzl, Promise, 'open');
promisifyMethod(yauzl, Promise, 'fromFd');
promisifyMethod(yauzl, Promise, 'fromBuffer');
promisifyMethod(yauzl, Promise, 'fromRandomAccessReader');
// Promisify `close` method
promisifyClose(ZipFile, Promise);
// Promisify ZipFile `readEntry` method
promisifyReadEntry(ZipFile, Promise);
// Add ZipFile `readEntries` + `walkEntries` methods
ZipFile.prototype.readEntries = readEntries;
addWalkEntriesMethod(ZipFile, Promise);
// Promisify ZipFile `openReadStream` method
promisifyOpenReadStream(ZipFile, Promise);
// Add Entry `openReadStream` method
Entry.prototype.openReadStream = entryOpenReadStream;
// Add reference to Entry to ZipFile (used by `readEntries`)
ZipFile.Entry = Entry;
};
/*
* Promisify open/from... method
*/
function promisifyMethod(yauzl, Promise, fnName) {
const fromBuffer = fnName == 'fromBuffer';
cloner.patch(yauzl, fnName, original => {
return function(path, totalSize, options) {
return new Promise((resolve, reject) => {
options = Object.assign({}, options, {lazyEntries: true, autoClose: false});
original(path, totalSize, options, (err, zipFile) => {
if (err) return reject(err);
opened(zipFile, resolve, fromBuffer, yauzl);
});
});
};
});
}
function opened(zipFile, resolve, fromBuffer, yauzl) {
// For `.fromBuffer()` calls, adapt `reader` to emit close event
if (fromBuffer) {
zipFile.reader.unref = yauzl.RandomAccessReader.prototype.unref;
zipFile.reader.close = cb => cb();
}
// Init
clearState(zipFile);
clearError(zipFile);
// Intercept events
zipFile.intercept('entry', emittedEntry);
zipFile.intercept('end', emittedEnd);
zipFile.intercept('close', emittedClose);
zipFile.intercept('error', emittedError);
// Resolve promise with zip object
resolve(zipFile);
}
/*
* Error event handler
*/
function emittedError(err) {
// jshint validthis:true
// If operation in progress, reject its promise
const state = getState(this);
if (state) {
clearState(this);
return state.reject(err);
}
// Store error to be returned on next call to
// `.readEntry()`, `.close()` or `.openReadStream()`.
if (!getError(this)) setError(this, err);
}
function rejectWithStoredError(zipFile, reject) {
const err = getError(zipFile);
clearError(zipFile);
reject(err);
}
/*
* Promisify ZipFile `close` method
*/
function promisifyClose(ZipFile, Promise) {
const close = ZipFile.prototype.close;
ZipFile.prototype.close = function() {
return new Promise((resolve, reject) => {
if (getError(this)) return rejectWithStoredError(this, reject);
if (!this.isOpen) return resolve();
if (getState(this)) return reject(new Error('Previous operation has not completed yet'));
setState(this, {action: 'close', resolve, reject});
close.call(this);
});
};
}
function emittedClose() {
// jshint validthis:true
// If not closing, emit error
const state = getState(this);
if (!state || state.action != 'close') return this.emit('error', new Error('Unexpected \'close\' event emitted'));
clearState(this);
// Resolve promise
state.resolve();
}
/*
* Promisify ZipFile `readEntry` method
*/
function promisifyReadEntry(ZipFile, Promise) {
const readEntry = ZipFile.prototype.readEntry;
ZipFile.prototype.readEntry = function() {
return new Promise((resolve, reject) => {
if (getError(this)) return rejectWithStoredError(this, reject);
if (!this.isOpen) return reject(new Error('ZipFile is not open'));
if (getState(this)) return reject(new Error('Previous operation has not completed yet'));
setState(this, {action: 'read', resolve, reject});
readEntry.call(this);
});
};
}
function emittedEntry(entry) {
// jshint validthis:true
// If not reading, emit error
const state = getState(this);
if (!state || state.action != 'read') return this.emit('error', new Error(`Unexpected '${entry ? 'entry' : 'end'}' event emitted`));
clearState(this);
// Set reference to zipFile on entry (used by `entry.openReadStream()`)
if (entry) entry.zipFile = this;
// Resolve promise with entry
state.resolve(entry);
}
function emittedEnd() {
// jshint validthis:true
emittedEntry.call(this, null);
}
/*
* Functions to access state
*/
function getState(zipFile) {
return zipFile[STATE];
}
function setState(zipFile, state) {
zipFile[STATE] = state;
}
function clearState(zipFile) {
zipFile[STATE] = undefined;
}
function getError(zipFile) {
return zipFile[STORED_ERROR];
}
function setError(zipFile, state) {
zipFile[STORED_ERROR] = state;
}
function clearError(zipFile) {
zipFile[STORED_ERROR] = undefined;
}
/*
* Read all ZipFile entries
* Reads all entries and returns a promise which resolves with an array of entries
* `options.max` limits number returned (default 100)
* `options.max` can be set to `0` for no limit
*/
function readEntries(numEntries) {
// jshint validthis:true
const entries = [];
return this.walkEntries(entry => {
entries.push(entry);
}, numEntries).then(() => {
return entries;
});
}
/*
* Walk all ZipFile entries
* Walks through each entry and calls `fn` with each.
* Returns a promise which resolves when all have been read.
*/
function addWalkEntriesMethod(ZipFile, Promise) {
ZipFile.prototype.walkEntries = function(callback, numEntries) {
callback = wrapFunctionToReturnPromise(callback, Promise);
return new Promise((resolve, reject) => {
walkNextEntry(this, callback, numEntries, 0, err => {
if (err) return reject(err);
resolve();
});
});
};
}
function walkNextEntry(zipFile, fn, numEntries, count, cb) {
if (numEntries && count == numEntries) return cb();
zipFile.readEntry().then(entry => {
if (!entry) return cb();
return fn(entry).then(() => {
walkNextEntry(zipFile, fn, numEntries, count + 1, cb);
});
}).catch(err => {
cb(err);
});
}
/*
* Promisify ZipFile `openReadStream` method
*/
function promisifyOpenReadStream(ZipFile, Promise) {
const openReadStream = ZipFile.prototype.openReadStream;
ZipFile.prototype.openReadStream = function(entry, options) {
return new Promise((resolve, reject) => {
if (getError(this)) return rejectWithStoredError(this, reject);
openReadStream.call(this, entry, options || {}, (err, stream) => {
if (err) return reject(err);
resolve(stream);
});
});
};
}
/*
* Entry `openReadStream` method
*/
function entryOpenReadStream(options) {
// jshint validthis:true
return this.zipFile.openReadStream(this, options);
}
/*
* Utility functions
*/
function wrapFunctionToReturnPromise(fn, Promise) {
return function() {
try {
const result = fn.apply(this, arguments);
if (result instanceof Promise) return result;
return Promise.resolve(result);
} catch (err) {
return new Promise((resolve, reject) => { // jshint ignore:line
reject(err);
});
}
};
}