'use strict'; const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const isStream = require('is-stream'); const {Worker} = (() => { try { return require('worker_threads'); } catch (_) { return {}; } })(); let worker; // Lazy let taskIdCounter = 0; const tasks = new Map(); const recreateWorkerError = sourceError => { const error = new Error(sourceError.message); for (const [key, value] of Object.entries(sourceError)) { if (key !== 'message') { error[key] = value; } } return error; }; const createWorker = () => { worker = new Worker(path.join(__dirname, 'thread.js')); worker.on('message', message => { const task = tasks.get(message.id); tasks.delete(message.id); if (tasks.size === 0) { worker.unref(); } if (message.error === undefined) { task.resolve(message.value); } else { task.reject(recreateWorkerError(message.error)); } }); worker.on('error', error => { // Any error here is effectively an equivalent of segfault, and have no scope, so we just throw it on callback level throw error; }); }; const taskWorker = (method, args, transferList) => new Promise((resolve, reject) => { const id = taskIdCounter++; tasks.set(id, {resolve, reject}); if (worker === undefined) { createWorker(); } worker.ref(); worker.postMessage({id, method, args}, transferList); }); const hasha = (input, options = {}) => { let outputEncoding = options.encoding || 'hex'; if (outputEncoding === 'buffer') { outputEncoding = undefined; } const hash = crypto.createHash(options.algorithm || 'sha512'); const update = buffer => { const inputEncoding = typeof buffer === 'string' ? 'utf8' : undefined; hash.update(buffer, inputEncoding); }; if (Array.isArray(input)) { input.forEach(update); } else { update(input); } return hash.digest(outputEncoding); }; hasha.stream = (options = {}) => { let outputEncoding = options.encoding || 'hex'; if (outputEncoding === 'buffer') { outputEncoding = undefined; } const stream = crypto.createHash(options.algorithm || 'sha512'); stream.setEncoding(outputEncoding); return stream; }; hasha.fromStream = async (stream, options = {}) => { if (!isStream(stream)) { throw new TypeError('Expected a stream'); } return new Promise((resolve, reject) => { // TODO: Use `stream.pipeline` and `stream.finished` when targeting Node.js 10 stream .on('error', reject) .pipe(hasha.stream(options)) .on('error', reject) .on('finish', function () { resolve(this.read()); }); }); }; if (Worker === undefined) { hasha.fromFile = async (filePath, options) => hasha.fromStream(fs.createReadStream(filePath), options); hasha.async = async (input, options) => hasha(input, options); } else { hasha.fromFile = async (filePath, {algorithm = 'sha512', encoding = 'hex'} = {}) => { const hash = await taskWorker('hashFile', [algorithm, filePath]); if (encoding === 'buffer') { return Buffer.from(hash); } return Buffer.from(hash).toString(encoding); }; hasha.async = async (input, {algorithm = 'sha512', encoding = 'hex'} = {}) => { if (encoding === 'buffer') { encoding = undefined; } const hash = await taskWorker('hash', [algorithm, input]); if (encoding === undefined) { return Buffer.from(hash); } return Buffer.from(hash).toString(encoding); }; } hasha.fromFileSync = (filePath, options) => hasha(fs.readFileSync(filePath), options); module.exports = hasha;