UNPKG

node-liblzma

Version:
662 lines 24.5 kB
/** * node-liblzma - Node.js bindings for liblzma * Copyright (C) Olivier Orabona <olivier.orabona@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import * as assert from 'node:assert'; import { createRequire } from 'node:module'; import * as os from 'node:os'; import * as path from 'node:path'; import { Transform } from 'node:stream'; import { fileURLToPath } from 'node:url'; import { createLZMAError, LZMABufferError, LZMADataError, LZMAError, LZMAFormatError, LZMAMemoryError, LZMAMemoryLimitError, LZMAOptionsError, LZMAProgrammingError, } from './errors.js'; // Re-export error classes for public API export { LZMAError, LZMAMemoryError, LZMAMemoryLimitError, LZMAFormatError, LZMAOptionsError, LZMADataError, LZMABufferError, LZMAProgrammingError, }; // Re-export pool for concurrency control export { LZMAPool } from './pool.js'; // Helper to safely access Node.js internal _writableState using official properties function getWritableState(stream) { return { /* v8 ignore next 2 - Node.js version compatibility fallback */ ending: stream._writableState?.ending ?? false, ended: stream._writableState?.ended ?? false, length: stream.writableLength, needDrain: stream.writableNeedDrain, }; } const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const require = createRequire(import.meta.url); const bindingPath = path.resolve(path.join(__dirname, '..')); const liblzma = require('node-gyp-build')(bindingPath); // Should not change over time... :) const maxThreads = os.cpus().length; // Export constants grouped by category for better organization export const check = { NONE: liblzma.LZMA_CHECK_NONE, CRC32: liblzma.LZMA_CHECK_CRC32, CRC64: liblzma.LZMA_CHECK_CRC64, SHA256: liblzma.LZMA_CHECK_SHA256, }; export const preset = { DEFAULT: liblzma.LZMA_PRESET_DEFAULT, EXTREME: liblzma.LZMA_PRESET_EXTREME, }; export const flag = { TELL_NO_CHECK: liblzma.LZMA_TELL_NO_CHECK, TELL_UNSUPPORTED_CHECK: liblzma.LZMA_TELL_UNSUPPORTED_CHECK, TELL_ANY_CHECK: liblzma.LZMA_TELL_ANY_CHECK, CONCATENATED: liblzma.LZMA_CONCATENATED, }; export const filter = { LZMA2: liblzma.LZMA_FILTER_LZMA2, X86: liblzma.LZMA_FILTER_X86, POWERPC: liblzma.LZMA_FILTER_POWERPC, IA64: liblzma.LZMA_FILTER_IA64, ARM: liblzma.LZMA_FILTER_ARM, ARMTHUMB: liblzma.LZMA_FILTER_ARMTHUMB, SPARC: liblzma.LZMA_FILTER_SPARC, }; /* v8 ignore next */ export const mode = { FAST: liblzma.LZMA_MODE_FAST, NORMAL: liblzma.LZMA_MODE_NORMAL, }; /* v8 ignore next 2 */ // LZMA action constants - grouped for better organization export const LZMAAction = { RUN: liblzma.LZMA_RUN, SYNC_FLUSH: liblzma.LZMA_SYNC_FLUSH, FULL_FLUSH: liblzma.LZMA_FULL_FLUSH, FINISH: liblzma.LZMA_FINISH, }; /* v8 ignore next 2 */ // LZMA status/return constants export const LZMAStatus = { OK: liblzma.LZMA_OK, STREAM_END: liblzma.LZMA_STREAM_END, NO_CHECK: liblzma.LZMA_NO_CHECK, UNSUPPORTED_CHECK: liblzma.LZMA_UNSUPPORTED_CHECK, GET_CHECK: liblzma.LZMA_GET_CHECK, MEM_ERROR: liblzma.LZMA_MEM_ERROR, MEMLIMIT_ERROR: liblzma.LZMA_MEMLIMIT_ERROR, FORMAT_ERROR: liblzma.LZMA_FORMAT_ERROR, OPTIONS_ERROR: liblzma.LZMA_OPTIONS_ERROR, DATA_ERROR: liblzma.LZMA_DATA_ERROR, BUF_ERROR: liblzma.LZMA_BUF_ERROR, PROG_ERROR: liblzma.LZMA_PROG_ERROR, }; // Additional filter constants export const LZMAFilter = { ...filter, X86_ALT: liblzma.LZMA_FILTER_X86, POWERPC_ALT: liblzma.LZMA_FILTER_POWERPC, IA64_ALT: liblzma.LZMA_FILTER_IA64, ARM_ALT: liblzma.LZMA_FILTER_ARM, ARMTHUMB_ALT: liblzma.LZMA_FILTER_ARMTHUMB, FILTERS_MAX: liblzma.LZMA_FILTERS_MAX, }; // Legacy individual exports for backward compatibility export const LZMA_RUN = LZMAAction.RUN; export const LZMA_SYNC_FLUSH = LZMAAction.SYNC_FLUSH; export const LZMA_FULL_FLUSH = LZMAAction.FULL_FLUSH; export const LZMA_FINISH = LZMAAction.FINISH; export const LZMA_OK = LZMAStatus.OK; export const LZMA_STREAM_END = LZMAStatus.STREAM_END; export const LZMA_NO_CHECK = LZMAStatus.NO_CHECK; export const LZMA_UNSUPPORTED_CHECK = LZMAStatus.UNSUPPORTED_CHECK; export const LZMA_GET_CHECK = LZMAStatus.GET_CHECK; export const LZMA_MEM_ERROR = LZMAStatus.MEM_ERROR; export const LZMA_MEMLIMIT_ERROR = LZMAStatus.MEMLIMIT_ERROR; export const LZMA_FORMAT_ERROR = LZMAStatus.FORMAT_ERROR; export const LZMA_OPTIONS_ERROR = LZMAStatus.OPTIONS_ERROR; export const LZMA_DATA_ERROR = LZMAStatus.DATA_ERROR; export const LZMA_BUF_ERROR = LZMAStatus.BUF_ERROR; export const LZMA_PROG_ERROR = LZMAStatus.PROG_ERROR; export const LZMA_FILTER_X86 = LZMAFilter.X86_ALT; export const LZMA_FILTER_POWERPC = LZMAFilter.POWERPC_ALT; export const LZMA_FILTER_IA64 = LZMAFilter.IA64_ALT; export const LZMA_FILTER_ARM = LZMAFilter.ARM_ALT; export const LZMA_FILTER_ARMTHUMB = LZMAFilter.ARMTHUMB_ALT; export const LZMA_FILTERS_MAX = LZMAFilter.FILTERS_MAX; export class XzStream extends Transform { // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Constructor needs complex validation for LZMA options constructor(streamMode, opts = {}, options) { super(options); let clonedFilters; if (opts.filters) { if (!Array.isArray(opts.filters)) { throw new Error('Filters need to be in an array!'); } try { clonedFilters = [...opts.filters]; /* v8 ignore next 3 */ } catch (_error) { throw new Error('Filters need to be in an array!'); } } else { clonedFilters = [filter.LZMA2]; } this._opts = { check: opts.check ?? check.NONE, preset: opts.preset ?? preset.DEFAULT, filters: clonedFilters, mode: opts.mode ?? mode.NORMAL, threads: opts.threads ?? 1, chunkSize: opts.chunkSize ?? liblzma.BUFSIZ, flushFlag: opts.flushFlag ?? liblzma.LZMA_RUN, }; this._chunkSize = this._opts.chunkSize; this._flushFlag = this._opts.flushFlag; assert.ok(Array.isArray(this._opts.filters), 'Filters need to be in an array!'); // Add default filter LZMA2 if none provided /* v8 ignore next 2 */ if (this._opts.filters.indexOf(filter.LZMA2) === -1) { this._opts.filters.push(filter.LZMA2); } // Ensure LZMA2 is always the last filter (requirement of liblzma) const lzma2Index = this._opts.filters.indexOf(filter.LZMA2); if (lzma2Index !== -1 && lzma2Index !== this._opts.filters.length - 1) { // Remove LZMA2 from its current position and add it to the end this._opts.filters.splice(lzma2Index, 1); this._opts.filters.push(filter.LZMA2); } // Multithreading is only available for encoding, so if we are encoding, check // opts threads value. if (streamMode === liblzma.STREAM_ENCODE) { /* If threads are requested but not supported, fallback to single thread */ /* c8 ignore start */ if (!liblzma.HAS_THREADS_SUPPORT) { this._opts.threads = 1; } /* c8 ignore stop */ // By default set to maximum available processors if (this._opts.threads === 0) { this._opts.threads = maxThreads; } } // Initialize engine this.lzma = new liblzma.LZMA(streamMode, this._opts); this._closed = false; this._hadError = false; this._offset = 0; this._buffer = Buffer.alloc(this._chunkSize); /* v8 ignore next */ this.on('onerror', (errno) => { this._hadError = true; const error = this._createLZMAError(errno); // Safely emit error - ensure there's at least one listener to prevent uncaught exception /* v8 ignore next 6 - Defensive error handling for streams without listeners */ if (this.listenerCount('error') === 0) { // If no error listeners, add a temporary one to prevent crash this.once('error', () => { // Error has been handled by emitting it }); } this.emit('error', error); }); /* v8 ignore next */ this.once('end', () => this.close()); } _createLZMAError(errno) { return createLZMAError(errno); } _reallocateBuffer() { this._offset = 0; this._buffer = Buffer.alloc(this._chunkSize); } flush(kindOrCallback, callback) { const ws = getWritableState(this); let kind; let cb; /* v8 ignore next */ if (typeof kindOrCallback === 'function' || (typeof kindOrCallback === 'undefined' && !callback)) { cb = kindOrCallback; kind = liblzma.LZMA_SYNC_FLUSH; } else { kind = kindOrCallback; cb = callback; } if (ws.ended) { if (cb) { process.nextTick(cb); } /* v8 ignore next 4 */ } else if (ws.ending) { if (cb) { this.once('end', cb); } /* v8 ignore next 5 - drain handling is difficult to test reliably */ } else if (ws.needDrain) { this.once('drain', () => { this.flush(cb); }); } else { this._flushFlag = kind; this.write(Buffer.alloc(0), 'utf8', cb); } } close(callback) { if (callback) { process.nextTick(callback); } // We will trigger this case with #xz and #unxz if (this._closed) { return; } /* v8 ignore next */ this.lzma.close(); this._closed = true; /* v8 ignore next */ process.nextTick(() => { this.emit('close'); }); } /* v8 ignore next */ _transform(chunk, _encoding, callback) { const ws = getWritableState(this); const ending = ws.ending || ws.ended; const last = ending && (!chunk || ws.length === chunk.length); if (chunk !== null && !(chunk instanceof Buffer)) { callback(new Error('invalid input')); return; } if (this._closed) { callback(new Error('lzma binding closed')); return; } /* v8 ignore next */ let flushFlag; if (last) { flushFlag = liblzma.LZMA_FINISH; } else { flushFlag = this._flushFlag; // once we've flushed the last of the queue, stop flushing and // go back to the normal behavior. if (chunk && chunk.length >= ws.length) { this._flushFlag = this._opts.flushFlag; } } /* v8 ignore next */ this._processChunk(chunk, flushFlag, callback); } _flush(callback) { /* v8 ignore next 4 - Defensive check for double close scenario */ if (this._closed) { process.nextTick(() => callback()); return; } this._transform(Buffer.alloc(0), 'utf8', callback); } _processChunk(chunk, flushFlag, cb) { const async = typeof cb === 'function'; // Sanity checks assert.ok(!this._closed, 'Stream closed!'); let availInBefore = chunk?.length; let availOutBefore = this._chunkSize - this._offset; let inOff = 0; /* v8 ignore next 3 */ if (!async) { // Doing it synchronously const buffers = []; let nread = 0; let error = null; /* v8 ignore next */ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex but necessary LZMA callback logic const callback = (errno, availInAfter, availOutAfter) => { /* v8 ignore start */ if (this._hadError) { return false; } // if LZMA engine returned something else, we are running into trouble! if (errno !== liblzma.LZMA_OK && errno !== liblzma.LZMA_STREAM_END) { this.emit('onerror', errno); return false; } /* v8 ignore stop */ const used = availOutBefore - availOutAfter; assert.ok(used >= 0, `More bytes after than before! Delta = ${used}`); if (used > 0) { const out = this._buffer.subarray(this._offset, this._offset + used); this._offset += used; buffers.push(out); nread += used; } /* v8 ignore start */ // exhausted the output buffer, or used all the input create a new one. if (availOutAfter === 0 || this._offset >= this._chunkSize) { availOutBefore = this._chunkSize; this._reallocateBuffer(); } if (availOutAfter === 0 || availInAfter > 0) { inOff += (availInBefore ?? 0) - availInAfter; availInBefore = availInAfter; return true; } /* v8 ignore stop */ return false; }; /* v8 ignore start */ this.on('error', (e) => { error = e; }); /* v8 ignore stop */ /* v8 ignore next - processing loop entry */ while (true) { const [status, availInAfter, availOutAfter] = this.lzma.codeSync(flushFlag, chunk, inOff, availInBefore, this._buffer, this._offset); /* v8 ignore start */ if (this._hadError || !callback(status, availInAfter, availOutAfter)) { break; } /* v8 ignore stop */ } /* v8 ignore start */ if (this._hadError) { throw error ?? new Error('Unknown LZMA error'); } /* v8 ignore stop */ /* v8 ignore next - normal cleanup path */ this.close(); const buf = Buffer.concat(buffers, nread); return buf; } // Async path const callback = (errno, availInAfter, availOutAfter) => { /* v8 ignore next 3 - error state handling is difficult to test */ if (this._hadError) { return false; } /* v8 ignore next 3 - async error path handling */ // if LZMA engine returned something else, we are running into trouble! if (errno !== liblzma.LZMA_OK && errno !== liblzma.LZMA_STREAM_END) { this.emit('onerror', errno); return false; } /* v8 ignore next */ const used = availOutBefore - availOutAfter; assert.ok(used >= 0, `More bytes after than before! Delta = ${used}`); if (used > 0) { const out = this._buffer.subarray(this._offset, this._offset + used); this._offset += used; this.push(out); } // exhausted the output buffer, or used all the input create a new one. if (availOutAfter === 0 || this._offset >= this._chunkSize) { availOutBefore = this._chunkSize; this._reallocateBuffer(); } if (availOutAfter === 0 || availInAfter > 0) { /* v8 ignore next 2 - complex async processing continuation */ inOff += (availInBefore ?? 0) - availInAfter; availInBefore = availInAfter; this.lzma.code(flushFlag, chunk, inOff, availInBefore, this._buffer, this._offset, callback); return false; } // Safely call callback to avoid uncaught exceptions if (cb && !this._closed) { try { cb(); } catch (error) { // If callback throws, emit error instead of crashing this.emit('onerror', liblzma.LZMA_PROG_ERROR); } } return false; }; this.lzma.code(flushFlag, chunk, inOff, availInBefore, this._buffer, this._offset, callback); return undefined; } } export class Xz extends XzStream { constructor(lzmaOptions, options) { super(liblzma.STREAM_ENCODE, lzmaOptions, options); } } export class Unxz extends XzStream { constructor(lzmaOptions, options) { super(liblzma.STREAM_DECODE, lzmaOptions, options); } } /* v8 ignore next */ // Factory functions - placed immediately after class definitions to avoid circular dependencies export function createXz(lzmaOptions, options) { return new Xz(lzmaOptions, options); } /* v8 ignore next */ export function createUnxz(lzmaOptions, options) { return new Unxz(lzmaOptions, options); } /* v8 ignore next */ export function hasThreads() { return liblzma.HAS_THREADS_SUPPORT; } /* v8 ignore next */ // Error messages enum for better type safety export var LZMAErrorMessage; (function (LZMAErrorMessage) { LZMAErrorMessage["SUCCESS"] = "Operation completed successfully"; LZMAErrorMessage["STREAM_END"] = "End of stream was reached"; LZMAErrorMessage["NO_CHECK"] = "Input stream has no integrity check"; LZMAErrorMessage["UNSUPPORTED_CHECK"] = "Cannot calculate the integrity check"; LZMAErrorMessage["GET_CHECK"] = "Integrity check type is not available"; LZMAErrorMessage["MEM_ERROR"] = "Cannot allocate memory"; LZMAErrorMessage["MEMLIMIT_ERROR"] = "Memory usage limit was reached"; LZMAErrorMessage["FORMAT_ERROR"] = "File format not recognized"; LZMAErrorMessage["OPTIONS_ERROR"] = "Invalid or unsupported options"; LZMAErrorMessage["DATA_ERROR"] = "Data is corrupt"; LZMAErrorMessage["BUF_ERROR"] = "No progress is possible"; LZMAErrorMessage["PROG_ERROR"] = "Programming error"; })(LZMAErrorMessage || (LZMAErrorMessage = {})); // Legacy array export for backward compatibility export const messages = [ LZMAErrorMessage.SUCCESS, LZMAErrorMessage.STREAM_END, LZMAErrorMessage.NO_CHECK, LZMAErrorMessage.UNSUPPORTED_CHECK, LZMAErrorMessage.GET_CHECK, LZMAErrorMessage.MEM_ERROR, LZMAErrorMessage.MEMLIMIT_ERROR, LZMAErrorMessage.FORMAT_ERROR, LZMAErrorMessage.OPTIONS_ERROR, LZMAErrorMessage.DATA_ERROR, LZMAErrorMessage.BUF_ERROR, LZMAErrorMessage.PROG_ERROR, ]; export function unxz(buffer, optsOrCallback, callback) { let opts; let cb; /* v8 ignore next - simple parameter parsing */ if (typeof optsOrCallback === 'function') { cb = optsOrCallback; opts = {}; } else { opts = optsOrCallback; cb = callback; } xzBuffer(new Unxz(opts), buffer, cb); } export function unxzSync(buffer, opts) { return xzBufferSync(new Unxz(opts), buffer); } export function xz(buffer, optsOrCallback, callback) { let opts; let cb; /* v8 ignore next - simple parameter parsing */ if (typeof optsOrCallback === 'function') { cb = optsOrCallback; opts = {}; } else { opts = optsOrCallback; cb = callback; } xzBuffer(new Xz(opts), buffer, cb); } export function xzSync(buffer, opts) { return xzBufferSync(new Xz(opts), buffer); } // Promise-based APIs for modern async/await usage export function xzAsync(buffer, opts) { return new Promise((resolve, reject) => { xz(buffer, opts || {}, (error, result) => { /* v8 ignore next 3 - error handling is tested in callback-based tests */ if (error) { reject(error); } else { resolve(result); } }); }); } export function unxzAsync(buffer, opts) { return new Promise((resolve, reject) => { unxz(buffer, opts || {}, (error, result) => { /* v8 ignore next 2 - error handling is tested in callback-based tests */ if (error) { reject(error); } else { resolve(result); } }); }); } function xzBuffer(engine, buffer, callback) { const buffers = []; let nread = 0; const flow = () => { let chunk; // biome-ignore lint/suspicious/noAssignInExpressions: necessary for while loop pattern while ((chunk = engine.read()) !== null) { buffers.push(chunk); nread += chunk.length; } engine.once('readable', flow); }; const onEnd = () => { const buf = Buffer.concat(buffers, nread); callback(null, buf); engine.close(); }; /* v8 ignore next 5 - error callback path */ const onError = (err) => { engine.removeListener('end', onEnd); engine.removeListener('readable', flow); callback(err); }; engine.on('error', onError); engine.on('end', onEnd); engine.end(buffer); flow(); } function xzBufferSync(engine, buffer) { let buf; if (typeof buffer === 'string') { buf = Buffer.from(buffer); } else if (buffer instanceof Buffer) { buf = buffer; /* v8 ignore next 3 - type validation error path */ } else { throw new TypeError('Not a string or buffer'); } return engine._processChunk(buf, liblzma.LZMA_FINISH); } // File-based compression/decompression helpers import { createReadStream, createWriteStream } from 'node:fs'; import { pipeline } from 'node:stream/promises'; /** * Compress a file using XZ compression * @param inputPath Path to input file * @param outputPath Path to output compressed file * @param opts LZMA compression options * @returns Promise that resolves when compression is complete */ export async function xzFile(inputPath, outputPath, opts) { const input = createReadStream(inputPath); const output = createWriteStream(outputPath); const compressor = createXz(opts); await pipeline(input, compressor, output); } /** * Decompress an XZ compressed file * @param inputPath Path to compressed input file * @param outputPath Path to output decompressed file * @param opts LZMA decompression options * @returns Promise that resolves when decompression is complete */ export async function unxzFile(inputPath, outputPath, opts) { const input = createReadStream(inputPath); const output = createWriteStream(outputPath); const decompressor = createUnxz(opts); await pipeline(input, decompressor, output); } // Export default object for CommonJS compatibility - use individual exports to avoid duplication export default { Xz, Unxz, XzStream, hasThreads, messages, check, preset, flag, filter, mode, createXz, createUnxz, unxz, unxzSync, xz, xzSync, xzAsync, unxzAsync, // Reference individual exports to avoid duplication LZMA_RUN, LZMA_SYNC_FLUSH, LZMA_FULL_FLUSH, LZMA_FINISH, LZMA_OK, LZMA_STREAM_END, LZMA_NO_CHECK, LZMA_UNSUPPORTED_CHECK, LZMA_GET_CHECK, LZMA_MEM_ERROR, LZMA_MEMLIMIT_ERROR, LZMA_FORMAT_ERROR, LZMA_OPTIONS_ERROR, LZMA_DATA_ERROR, LZMA_BUF_ERROR, LZMA_PROG_ERROR, LZMA_FILTER_X86, LZMA_FILTER_POWERPC, LZMA_FILTER_IA64, LZMA_FILTER_ARM, LZMA_FILTER_ARMTHUMB, LZMA_FILTERS_MAX, }; //# sourceMappingURL=lzma.js.map