Skip to content
File

Blob: src/node/internal/internal_zlib_base.ts

typescript896 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4// Copyright Joyent and Node contributors. All rights reserved. MIT license.
5 
6import {
7 default as zlibUtil,
8 type ZlibOptions,
9 type BrotliOptions,
10 type ZstdOptions,
11} from 'node-internal:zlib';
12import { Buffer, kMaxLength } from 'node-internal:internal_buffer';
13import {
14 checkRangesOrGetDefault,
15 checkFiniteNumber,
16} from 'node-internal:validators';
17import {
18 ERR_OUT_OF_RANGE,
19 ERR_BUFFER_TOO_LARGE,
20 ERR_INVALID_ARG_TYPE,
21 ERR_BROTLI_INVALID_PARAM,
22 ERR_ZSTD_INVALID_PARAM,
23 ERR_ZLIB_INITIALIZATION_FAILED,
24 NodeError,
25} from 'node-internal:internal_errors';
26import { Transform, type DuplexOptions } from 'node-internal:streams_transform';
27import { eos as finished } from 'node-internal:streams_end_of_stream';
28import {
29 isArrayBufferView,
30 isAnyArrayBuffer,
31} from 'node-internal:internal_types';
32import { constants } from 'node-internal:internal_zlib_constants';
33 
34// Explicitly import `ok()` to avoid typescript error requiring every name in the call target to
35// be annotated with an explicit type annotation.
36import assert, { ok } from 'node-internal:internal_assert';
37 
38const {
39 CONST_INFLATE,
40 CONST_GUNZIP,
41 CONST_GZIP,
42 CONST_UNZIP,
43 CONST_Z_DEFAULT_CHUNK,
44 CONST_Z_DEFAULT_STRATEGY,
45 CONST_Z_DEFAULT_MEMLEVEL,
46 CONST_Z_DEFAULT_WINDOWBITS,
47 CONST_Z_DEFAULT_COMPRESSION,
48 CONST_Z_FIXED,
49 CONST_Z_MAX_LEVEL,
50 CONST_Z_MAX_MEMLEVEL,
51 CONST_Z_MAX_WINDOWBITS,
52 CONST_Z_MIN_LEVEL,
53 CONST_Z_MIN_MEMLEVEL,
54 CONST_Z_SYNC_FLUSH,
55 CONST_Z_NO_FLUSH,
56 CONST_Z_BLOCK,
57 CONST_Z_MIN_CHUNK,
58 CONST_Z_PARTIAL_FLUSH,
59 CONST_Z_FULL_FLUSH,
60 CONST_Z_FINISH,
61 CONST_BROTLI_ENCODE,
62 CONST_BROTLI_DECODE,
63 CONST_BROTLI_OPERATION_PROCESS,
64 CONST_BROTLI_OPERATION_EMIT_METADATA,
65 CONST_BROTLI_OPERATION_FINISH,
66 CONST_BROTLI_OPERATION_FLUSH,
67 CONST_ZSTD_ENCODE,
68 CONST_ZSTD_DECODE,
69 CONST_ZSTD_e_continue,
70 CONST_ZSTD_e_end,
71 CONST_ZSTD_e_flush,
72} = zlibUtil;
73 
74// This type contains all possible handler types.
75type ZlibHandleType =
76 | zlibUtil.ZlibStream
77 | zlibUtil.BrotliEncoder
78 | zlibUtil.BrotliDecoder
79 | zlibUtil.ZstdEncoder
80 | zlibUtil.ZstdDecoder;
81export const owner_symbol = Symbol('owner');
82 
83const FLUSH_BOUND_IDX_NORMAL: number = 0;
84const FLUSH_BOUND_IDX_BROTLI: number = 1;
85const FLUSH_BOUND_IDX_ZSTD: number = 2;
86const FLUSH_BOUND: [[number, number], [number, number], [number, number]] = [
87 [CONST_Z_NO_FLUSH, CONST_Z_BLOCK],
88 [CONST_BROTLI_OPERATION_PROCESS, CONST_BROTLI_OPERATION_EMIT_METADATA],
89 [CONST_ZSTD_e_continue, CONST_ZSTD_e_end],
90];
91 
92const kFlushFlag = Symbol('kFlushFlag');
93const kError = Symbol('kError');
94 
95function processCallback(this: ZlibHandleType): void {
96 // This callback's context (`this`) is the `_handle` (ZCtx) object. It is
97 // important to null out the values once they are no longer needed since
98 // `_handle` can stay in memory long after the buffer is needed.
99 // eslint-disable-next-line @typescript-eslint/no-this-alias
100 const handle = this;
101 const self = this[owner_symbol];
102 ok(self, 'Owner symbol should exist');
103 const state = self._writeState;
104 
105 if (self.destroyed) {
106 this.buffer = null;
107 this.cb();
108 return;
109 }
110 
111 const availOutAfter = state[0] as number;
112 const availInAfter = state[1] as number;
113 
114 const inDelta = handle.availInBefore - availInAfter;
115 self.bytesWritten += inDelta;
116 
117 const have = handle.availOutBefore - availOutAfter;
118 let streamBufferIsFull = false;
119 if (have > 0) {
120 const out = self._outBuffer.slice(self._outOffset, self._outOffset + have);
121 self._outOffset += have;
122 streamBufferIsFull = !self.push(out);
123 } else {
124 assert.strictEqual(have, 0, 'have should not go down');
125 }
126 
127 /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
128 if (self.destroyed) {
129 this.cb();
130 return;
131 }
132 
133 // Exhausted the output buffer, or used all the input create a new one.
134 if (availOutAfter === 0 || self._outOffset >= self._chunkSize) {
135 handle.availOutBefore = self._chunkSize;
136 self._outOffset = 0;
137 self._outBuffer = Buffer.allocUnsafe(self._chunkSize);
138 }
139 
140 if (availOutAfter === 0) {
141 // Not actually done. Need to reprocess.
142 // Also, update the availInBefore to the availInAfter value,
143 // so that if we have to hit it a third (fourth, etc.) time,
144 // it'll have the correct byte counts.
145 handle.inOff += inDelta;
146 handle.availInBefore = availInAfter;
147 
148 if (!streamBufferIsFull) {
149 ok(this.buffer, 'Buffer should not have been null');
150 this.write(
151 handle.flushFlag,
152 this.buffer, // in
153 handle.inOff, // in_off
154 handle.availInBefore, // in_len
155 self._outBuffer, // out
156 self._outOffset, // out_off
157 self._chunkSize
158 ); // out_len
159 } else {
160 // eslint-disable-next-line @typescript-eslint/unbound-method
161 const oldRead = self._read;
162 self._read = (n): void => {
163 ok(this.buffer, 'Buffer should not have been null');
164 self._read = oldRead;
165 this.write(
166 handle.flushFlag,
167 this.buffer, // in
168 handle.inOff, // in_off
169 handle.availInBefore, // in_len
170 self._outBuffer, // out
171 self._outOffset, // out_off
172 self._chunkSize // out_len
173 );
174 self._read(n);
175 };
176 }
177 return;
178 }
179 
180 if (availInAfter > 0) {
181 // If we have more input that should be written, but we also have output
182 // space available, that means that the compression library was not
183 // interested in receiving more data, and in particular that the input
184 // stream has ended early.
185 // This applies to streams where we don't check data past the end of
186 // what was consumed; that is, everything except Gunzip/Unzip.
187 self.push(null);
188 }
189 
190 // Finished with the chunk.
191 this.buffer = null;
192 this.cb();
193}
194 
195// If a flush is scheduled while another flush is still pending, a way to figure
196// out which one is the "stronger" flush is needed.
197// This is currently only used to figure out which flush flag to use for the
198// last chunk.
199// Roughly, the following holds:
200// Z_NO_FLUSH (< Z_TREES) < Z_BLOCK < Z_PARTIAL_FLUSH <
201// Z_SYNC_FLUSH < Z_FULL_FLUSH < Z_FINISH
202const flushiness: number[] = [];
203const kFlushFlagList: number[] = [
204 CONST_Z_NO_FLUSH,
205 CONST_Z_BLOCK,
206 CONST_Z_PARTIAL_FLUSH,
207 CONST_Z_SYNC_FLUSH,
208 CONST_Z_FULL_FLUSH,
209 CONST_Z_FINISH,
210];
211for (let i = 0; i < kFlushFlagList.length; i++) {
212 flushiness[kFlushFlagList[i] as number] = i;
213}
214 
215function maxFlush(a: number, b: number): number {
216 return (flushiness[a] as number) > (flushiness[b] as number) ? a : b;
217}
218 
219// Set up a list of 'special' buffers that can be written using .write()
220// from the .flush() code as a way of introducing flushing operations into the
221// write sequence.
222const kFlushBuffers: (Buffer & { [kFlushFlag]: number })[] = [];
223{
224 const dummyArrayBuffer = new ArrayBuffer(0);
225 for (const flushFlag of kFlushFlagList) {
226 const buf = Buffer.from(dummyArrayBuffer) as Buffer & {
227 [kFlushFlag]: number;
228 };
229 buf[kFlushFlag] = flushFlag;
230 kFlushBuffers[flushFlag] = buf;
231 }
232}
233 
234function zlibOnError(
235 this: ZlibHandleType,
236 errno: number,
237 code: string,
238 message: string
239): void {
240 const self = this[owner_symbol];
241 ok(self, 'Owner symbol should exist');
242 const error = new NodeError(code, message);
243 // @ts-expect-error Err number is expected.
244 error.errno = errno;
245 self.destroy(error);
246 self[kError] = error;
247}
248 
249function processChunkSync(
250 self: Zlib,
251 chunk: Buffer,
252 flushFlag: number
253): Buffer | Uint8Array {
254 let availInBefore = chunk.byteLength;
255 let availOutBefore = self._chunkSize - self._outOffset;
256 let inOff = 0;
257 let availOutAfter;
258 let availInAfter;
259 
260 const buffers: (Buffer | Uint8Array)[] = [];
261 let nread = 0;
262 let inputRead = 0;
263 const state = self._writeState;
264 const handle = self._handle;
265 let buffer = self._outBuffer;
266 let offset = self._outOffset;
267 const chunkSize = self._chunkSize;
268 
269 let error: Error | undefined;
270 self.on('error', function onError(er) {
271 error = er;
272 });
273 
274 /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
275 while (true) {
276 ok(handle, 'Handle should have been defined');
277 handle.writeSync(
278 flushFlag,
279 chunk, // in
280 inOff, // in_off
281 availInBefore, // in_len
282 buffer, // out
283 offset, // out_off
284 availOutBefore // out_len
285 );
286 if (error) throw error;
287 else if (self[kError]) throw self[kError];
288 
289 [availOutAfter, availInAfter] = state as unknown as [number, number];
290 
291 const inDelta = availInBefore - availInAfter;
292 inputRead += inDelta;
293 
294 const have = availOutBefore - availOutAfter;
295 if (have > 0) {
296 const out = buffer.slice(offset, offset + have);
297 offset += have;
298 buffers.push(out);
299 nread += out.byteLength;
300 
301 if (nread > self._maxOutputLength) {
302 _close(self);
303 throw new ERR_BUFFER_TOO_LARGE(self._maxOutputLength);
304 }
305 } else {
306 assert.strictEqual(have, 0, 'have should not go down');
307 }
308 
309 // Exhausted the output buffer, or used all the input create a new one.
310 if (availOutAfter === 0 || offset >= chunkSize) {
311 availOutBefore = chunkSize;
312 offset = 0;
313 buffer = Buffer.allocUnsafe(chunkSize);
314 }
315 
316 if (availOutAfter === 0) {
317 // Not actually done. Need to reprocess.
318 // Also, update the availInBefore to the availInAfter value,
319 // so that if we have to hit it a third (fourth, etc.) time,
320 // it'll have the correct byte counts.
321 inOff += inDelta;
322 availInBefore = availInAfter;
323 } else {
324 break;
325 }
326 }
327 
328 self.bytesWritten = inputRead;
329 _close(self);
330 
331 if (nread === 0) return Buffer.alloc(0);
332 
333 return buffers.length === 1
334 ? (buffers[0] as Buffer)
335 : Buffer.concat(buffers, nread);
336}
337 
338function _close(engine: ZlibBase): void {
339 engine._handle?.close();
340 engine._handle = null;
341}
342 
343type ZlibDefaultOptions = {
344 flush: number;
345 finishFlush: number;
346 fullFlush: number;
347};
348 
349const zlibDefaultOptions = {
350 flush: CONST_Z_NO_FLUSH,
351 finishFlush: CONST_Z_FINISH,
352 fullFlush: CONST_Z_FULL_FLUSH,
353};
354 
355export class ZlibBase extends Transform {
356 bytesWritten: number = 0;
357 
358 _maxOutputLength: number;
359 _outBuffer: Buffer;
360 _outOffset: number = 0;
361 _chunkSize: number;
362 _defaultFlushFlag: number;
363 _finishFlushFlag: number;
364 _defaultFullFlushFlag: number;
365 _info: boolean;
366 _handle: ZlibHandleType | null = null;
367 _writeState = new Uint32Array(2);
368 _writesInProgress: number = 0;
369 _hadWrites: boolean = false;
370 
371 [kError]: NodeError | undefined;
372 
373 constructor(
374 opts: ZlibOptions & DuplexOptions,
375 mode: number,
376 handle: ZlibHandleType,
377 { flush, finishFlush, fullFlush }: ZlibDefaultOptions = zlibDefaultOptions
378 ) {
379 let chunkSize = CONST_Z_DEFAULT_CHUNK;
380 let maxOutputLength = kMaxLength;
381 
382 let flushBoundIdx;
383 if (mode === CONST_BROTLI_ENCODE || mode === CONST_BROTLI_DECODE) {
384 flushBoundIdx = FLUSH_BOUND_IDX_BROTLI;
385 } else if (mode === CONST_ZSTD_ENCODE || mode === CONST_ZSTD_DECODE) {
386 flushBoundIdx = FLUSH_BOUND_IDX_ZSTD;
387 } else {
388 flushBoundIdx = FLUSH_BOUND_IDX_NORMAL;
389 }
390 
391 /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
392 if (opts) {
393 if (opts.chunkSize != null) {
394 chunkSize = opts.chunkSize;
395 }
396 if (!checkFiniteNumber(chunkSize, 'options.chunkSize')) {
397 chunkSize = CONST_Z_DEFAULT_CHUNK;
398 } else if (chunkSize < CONST_Z_MIN_CHUNK) {
399 throw new ERR_OUT_OF_RANGE(
400 'options.chunkSize',
401 `>= ${CONST_Z_MIN_CHUNK}`,
402 chunkSize
403 );
404 }
405 
406 flush = checkRangesOrGetDefault(
407 opts.flush,
408 'options.flush',
409 FLUSH_BOUND[flushBoundIdx]?.[0] as number,
410 FLUSH_BOUND[flushBoundIdx]?.[1] as number,
411 flush
412 );
413 
414 finishFlush = checkRangesOrGetDefault(
415 opts.finishFlush,
416 'options.finishFlush',
417 FLUSH_BOUND[flushBoundIdx]?.[0] as number,
418 FLUSH_BOUND[flushBoundIdx]?.[1] as number,
419 finishFlush
420 );
421 
422 maxOutputLength = checkRangesOrGetDefault(
423 opts.maxOutputLength,
424 'options.maxOutputLength',
425 1,
426 kMaxLength,
427 kMaxLength
428 );
429 
430 if (opts.encoding || opts.objectMode || opts.writableObjectMode) {
431 opts = { ...opts };
432 opts.encoding = undefined;
433 opts.objectMode = false;
434 opts.writableObjectMode = false;
435 }
436 }
437 
438 // @ts-expect-error TODO: Find a way to avoid having "unknown"
439 super({ autoDestroy: true, ...opts } as unknown);
440 
441 // Error handler by processCallback() and zlibOnError()
442 handle.setErrorHandler(zlibOnError.bind(handle));
443 handle[owner_symbol] = this as never;
444 this._handle = handle;
445 this._outBuffer = Buffer.allocUnsafe(chunkSize);
446 this._outOffset = 0;
447 this._chunkSize = chunkSize;
448 this._defaultFlushFlag = flush;
449 this._finishFlushFlag = finishFlush;
450 this._defaultFullFlushFlag = fullFlush;
451 this._info = Boolean(opts.info);
452 this._maxOutputLength = maxOutputLength;
453 }
454 
455 // Note: This is intentionally a getter that shadows the property from Transform
456 // @ts-expect-error TS2611 Property/accessor mismatch with Transform._closed
457 get _closed(): boolean {
458 return this._handle == null;
459 }
460 
461 // @deprecated Use `bytesWritten` instead.
462 get bytesRead(): number {
463 return this.bytesWritten;
464 }
465 
466 // @deprecated Use `bytesWritten` instead.
467 set bytesRead(value: number) {
468 this.bytesWritten = value;
469 }
470 
471 reset(): void {
472 ok(this._handle, 'zlib binding closed');
473 this._handle.reset();
474 }
475 
476 // This is the _flush function called by the transform class,
477 // internally, when the last chunk has been written.
478 override _flush(callback: () => void): void {
479 // If there are writes in progress, wait for them to complete
480 if (this._writesInProgress > 0) {
481 // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
482 queueMicrotask(() => this._flush(callback));
483 return;
484 }
485 
486 // If there were writes, add extra microtask to ensure data from processCallback has been pushed
487 if (this._hadWrites) {
488 queueMicrotask(() => {
489 const chunk = Buffer.alloc(0) as Buffer & { [kFlushFlag]?: number };
490 chunk[kFlushFlag] = this._finishFlushFlag;
491 this._transform(chunk, 'utf8', callback);
492 });
493 } else {
494 // No writes occurred, flush immediately
495 const chunk = Buffer.alloc(0) as Buffer & { [kFlushFlag]?: number };
496 chunk[kFlushFlag] = this._finishFlushFlag;
497 this._transform(chunk, 'utf8', callback);
498 }
499 }
500 
501 // Force Transform compat behavior.
502 override _final(callback: () => void): void {
503 callback();
504 }
505 
506 flush(kind: number, callback: () => void): void;
507 flush(callback?: () => void): void;
508 flush(kind?: number | (() => void), callback?: () => void): void {
509 if (typeof kind === 'function' || (kind === undefined && !callback)) {
510 callback = kind as (() => void) | undefined;
511 kind = this._defaultFullFlushFlag;
512 }
513 
514 if (this.writableFinished) {
515 if (callback) {
516 queueMicrotask(callback);
517 }
518 } else if (this.writableEnded) {
519 if (callback) {
520 this.once('end', callback);
521 }
522 } else {
523 this.write(kFlushBuffers[kind as number], 'utf8', callback);
524 }
525 }
526 
527 close(callback?: () => void): void {
528 if (callback) {
529 finished(this, callback);
530 }
531 this.destroy();
532 }
533 
534 override _destroy<T extends Error>(
535 err: T,
536 callback: (err: T) => never
537 ): void {
538 _close(this);
539 callback(err);
540 }
541 
542 override _transform(
543 chunk: Buffer & { [kFlushFlag]?: number },
544 _encoding: BufferEncoding,
545 cb: () => void
546 ): void {
547 let flushFlag = this._defaultFlushFlag;
548 // We use a 'fake' zero-length chunk to carry information about flushes from
549 // the public API to the actual stream implementation.
550 if (typeof chunk[kFlushFlag] === 'number') {
551 flushFlag = chunk[kFlushFlag];
552 }
553 
554 // For the last chunk, also apply `_finishFlushFlag`.
555 if (this.writableEnded && this.writableLength === chunk.byteLength) {
556 flushFlag = maxFlush(flushFlag, this._finishFlushFlag);
557 }
558 this.#processChunk(chunk, flushFlag, cb);
559 }
560 
561 // This function is left for backwards compatibility.
562 _processChunk(chunk: Buffer, flushFlag: number, cb?: undefined): Buffer;
563 _processChunk(chunk: Buffer, flushFlag: number, cb: () => void): undefined;
564 _processChunk(
565 chunk: Buffer,
566 flushFlag: number,
567 cb?: () => void
568 ): Buffer | Uint8Array | undefined {
569 if (cb != null && typeof cb === 'function') {
570 this.#processChunk(chunk, flushFlag, cb);
571 return;
572 }
573 return processChunkSync(this as never, chunk, flushFlag);
574 }
575 
576 #processChunk(chunk: Buffer, flushFlag: number, cb: () => void): void {
577 if (!this._handle) {
578 queueMicrotask(cb);
579 return;
580 }
581 
582 // Track that we have a write in progress
583 if (chunk.byteLength > 0) {
584 this._hadWrites = true;
585 }
586 this._writesInProgress++;
587 const originalCb = cb;
588 const wrappedCb = (): void => {
589 this._writesInProgress--;
590 originalCb();
591 };
592 
593 this._handle.buffer = chunk;
594 this._handle.cb = wrappedCb;
595 this._handle.availOutBefore = this._chunkSize - this._outOffset;
596 this._handle.availInBefore = chunk.byteLength;
597 this._handle.inOff = 0;
598 this._handle.flushFlag = flushFlag;
599 
600 this._handle.write(
601 flushFlag,
602 chunk, // in
603 0, // in_off
604 this._handle.availInBefore, // in_len
605 this._outBuffer, // out
606 this._outOffset, // out_off
607 this._handle.availOutBefore // out_len
608 );
609 }
610}
611 
612export class Zlib extends ZlibBase {
613 _level = CONST_Z_DEFAULT_COMPRESSION;
614 _strategy = CONST_Z_DEFAULT_STRATEGY;
615 
616 constructor(options: ZlibOptions | null | undefined, mode: number) {
617 let windowBits = CONST_Z_DEFAULT_WINDOWBITS;
618 let level = CONST_Z_DEFAULT_COMPRESSION;
619 let memLevel = CONST_Z_DEFAULT_MEMLEVEL;
620 let strategy = CONST_Z_DEFAULT_STRATEGY;
621 let dictionary: ZlibOptions['dictionary'];
622 
623 if (options != null) {
624 // Special case:
625 // - Compression: 0 is an invalid case.
626 // - Decompression: 0 indicates zlib to use the window size in the header of the compressed stream.
627 if (
628 (options.windowBits == null || options.windowBits === 0) &&
629 (mode === CONST_INFLATE ||
630 mode === CONST_GUNZIP ||
631 mode === CONST_UNZIP)
632 ) {
633 windowBits = 0;
634 } else {
635 // `{ windowBits: 8 }` is valid for DEFLATE but not for GZIP.
636 const min =
637 zlibUtil.CONST_Z_MIN_WINDOWBITS + (mode === CONST_GZIP ? 1 : 0);
638 windowBits = checkRangesOrGetDefault(
639 options.windowBits,
640 'options.windowBits',
641 min,
642 CONST_Z_MAX_WINDOWBITS,
643 CONST_Z_DEFAULT_WINDOWBITS
644 );
645 }
646 
647 level = checkRangesOrGetDefault(
648 options.level,
649 'options.level',
650 CONST_Z_MIN_LEVEL,
651 CONST_Z_MAX_LEVEL,
652 CONST_Z_DEFAULT_COMPRESSION
653 );
654 memLevel = checkRangesOrGetDefault(
655 options.memLevel,
656 'options.memLevel',
657 CONST_Z_MIN_MEMLEVEL,
658 CONST_Z_MAX_MEMLEVEL,
659 CONST_Z_DEFAULT_MEMLEVEL
660 );
661 strategy = checkRangesOrGetDefault(
662 options.strategy,
663 'options.strategy',
664 CONST_Z_DEFAULT_STRATEGY,
665 CONST_Z_FIXED,
666 CONST_Z_DEFAULT_STRATEGY
667 );
668 dictionary = options.dictionary;
669 
670 if (dictionary !== undefined && !isArrayBufferView(dictionary)) {
671 if (isAnyArrayBuffer(dictionary)) {
672 dictionary = Buffer.from(dictionary);
673 } else {
674 throw new ERR_INVALID_ARG_TYPE(
675 'options.dictionary',
676 ['Buffer', 'TypedArray', 'DataView', 'ArrayBuffer'],
677 dictionary
678 );
679 }
680 }
681 }
682 
683 const writeState = new Uint32Array(2);
684 const handle = new zlibUtil.ZlibStream(mode);
685 
686 handle.initialize(
687 windowBits,
688 level,
689 memLevel,
690 strategy,
691 writeState,
692 
693 () => {
694 queueMicrotask(processCallback.bind(handle));
695 },
696 dictionary
697 );
698 super(options ?? {}, mode, handle);
699 this._level = level;
700 this._strategy = strategy;
701 this._handle = handle;
702 this._writeState = writeState;
703 }
704 
705 params(level: number, strategy: number, callback: () => void): void {
706 checkRangesOrGetDefault(
707 level,
708 'level',
709 CONST_Z_MIN_LEVEL,
710 CONST_Z_MAX_LEVEL
711 );
712 checkRangesOrGetDefault(
713 strategy,
714 'strategy',
715 CONST_Z_DEFAULT_STRATEGY,
716 CONST_Z_FIXED
717 );
718 
719 if (this._level !== level || this._strategy !== strategy) {
720 this.flush(
721 CONST_Z_SYNC_FLUSH,
722 this.#paramsAfterFlushCallback.bind(this, level, strategy, callback)
723 );
724 } else {
725 queueMicrotask(callback);
726 }
727 }
728 
729 // This callback is used by `.params()` to wait until a full flush happened
730 // before adjusting the parameters. In particular, the call to the native
731 // `params()` function should not happen while a write is currently in progress
732 // on the threadpool.
733 #paramsAfterFlushCallback(
734 level: number,
735 strategy: number,
736 callback?: () => void
737 ): void {
738 ok(this._handle, 'zlib binding closed');
739 this._handle.params(level, strategy);
740 if (!this.destroyed) {
741 this._level = level;
742 this._strategy = strategy;
743 callback?.();
744 }
745 }
746}
747 
748const kMaxBrotliParam = Math.max(
749 ...Object.entries(constants).map(([key, value]) =>
750 key.startsWith('BROTLI_PARAM_') ? value : 0
751 )
752);
753const brotliInitParamsArray = new Uint32Array(kMaxBrotliParam + 1);
754const brotliDefaultOptions: ZlibDefaultOptions = {
755 flush: CONST_BROTLI_OPERATION_PROCESS,
756 finishFlush: CONST_BROTLI_OPERATION_FINISH,
757 fullFlush: CONST_BROTLI_OPERATION_FLUSH,
758};
759 
760export class Brotli extends ZlibBase {
761 constructor(options: BrotliOptions | undefined | null, mode: number) {
762 ok(mode === CONST_BROTLI_DECODE || mode === CONST_BROTLI_ENCODE);
763 brotliInitParamsArray.fill(-1);
764 
765 if (options?.params) {
766 for (const [origKey, value] of Object.entries(options.params)) {
767 const key = +origKey;
768 if (
769 Number.isNaN(key) ||
770 key < 0 ||
771 key > kMaxBrotliParam ||
772 ((brotliInitParamsArray[key] as number) | 0) !== -1
773 ) {
774 throw new ERR_BROTLI_INVALID_PARAM(origKey);
775 }
776 
777 if (typeof value !== 'number' && typeof value !== 'boolean') {
778 throw new ERR_INVALID_ARG_TYPE(
779 'options.params[key]',
780 'number',
781 value
782 );
783 }
784 // as number is required to avoid force type coercion on runtime.
785 // boolean has number representation, but typescript doesn't understand it.
786 brotliInitParamsArray[key] = value as number;
787 }
788 }
789 
790 const handle =
791 mode === CONST_BROTLI_DECODE
792 ? new zlibUtil.BrotliDecoder(mode)
793 : new zlibUtil.BrotliEncoder(mode);
794 
795 const _writeState = new Uint32Array(2);
796 
797 // TODO(addaleax): Sometimes we generate better error codes in C++ land,
798 // e.g. ERR_BROTLI_PARAM_SET_FAILED -- it's hard to access them with
799 // the current bindings setup, though.
800 if (
801 !handle.initialize(
802 brotliInitParamsArray,
803 _writeState,
804 processCallback.bind(handle)
805 )
806 ) {
807 throw new ERR_ZLIB_INITIALIZATION_FAILED();
808 }
809 
810 super(options ?? {}, mode, handle, brotliDefaultOptions);
811 this._writeState = _writeState;
812 }
813}
814 
815export const kMaxZstdCParam = Math.max(
816 ...Object.entries(constants).map(([key, value]) =>
817 key.startsWith('ZSTD_c_') ? value : 0
818 )
819);
820export const zstdInitCParamsArray = new Int32Array(kMaxZstdCParam + 1);
821 
822export const kMaxZstdDParam = Math.max(
823 ...Object.entries(constants).map(([key, value]) =>
824 key.startsWith('ZSTD_d_') ? value : 0
825 )
826);
827export const zstdInitDParamsArray = new Int32Array(kMaxZstdDParam + 1);
828 
829const zstdDefaultOptions: ZlibDefaultOptions = {
830 flush: CONST_ZSTD_e_continue,
831 finishFlush: CONST_ZSTD_e_end,
832 fullFlush: CONST_ZSTD_e_flush,
833};
834 
835export class Zstd extends ZlibBase {
836 constructor(
837 options: ZstdOptions | undefined | null,
838 mode: number,
839 initParamsArray: Int32Array,
840 maxParam: number
841 ) {
842 ok(mode === CONST_ZSTD_DECODE || mode === CONST_ZSTD_ENCODE);
843 initParamsArray.fill(-1);
844 
845 if (options?.params) {
846 for (const [origKey, value] of Object.entries(options.params)) {
847 const key = +origKey;
848 if (
849 Number.isNaN(key) ||
850 key < 0 ||
851 key > maxParam ||
852 ((initParamsArray[key] as number) | 0) !== -1
853 ) {
854 throw new ERR_ZSTD_INVALID_PARAM(origKey);
855 }
856 
857 if (typeof value !== 'number' && typeof value !== 'boolean') {
858 throw new ERR_INVALID_ARG_TYPE(
859 'options.params[key]',
860 'number',
861 value
862 );
863 }
864 // as number is required to avoid force type coercion on runtime.
865 // boolean has number representation, but typescript doesn't understand it.
866 initParamsArray[key] = value as number;
867 }
868 }
869 
870 const handle =
871 mode === CONST_ZSTD_DECODE
872 ? new zlibUtil.ZstdDecoder(mode)
873 : new zlibUtil.ZstdEncoder(mode);
874 
875 const _writeState = new Uint32Array(2);
876 
877 const pledgedSrcSize = options?.pledgedSrcSize;
878 
879 if (
880 !handle.initialize(
881 initParamsArray,
882 _writeState,
883 () => {
884 queueMicrotask(processCallback.bind(handle));
885 },
886 pledgedSrcSize
887 )
888 ) {
889 throw new ERR_ZLIB_INITIALIZATION_FAILED();
890 }
891 
892 super(options ?? {}, mode, handle, zstdDefaultOptions);
893 this._writeState = _writeState;
894 }
895}