Skip to content

Commit

Permalink
add zlib.Gzip class
Browse files Browse the repository at this point in the history
  • Loading branch information
anonrig committed Aug 13, 2024
1 parent 57dd452 commit 18a70ba
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 21 deletions.
125 changes: 107 additions & 18 deletions src/node/internal/internal_zlib.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { default as zlibUtil } from 'node-internal:zlib';
import { Options, ZlibMode, default as zlibUtil } from 'node-internal:zlib';
import { Buffer } from 'node-internal:internal_buffer';
import { validateUint32 } from 'node-internal:validators';
import { isArrayBufferView } from 'node-internal:internal_types';
import { validateUint32, checkRangesOrGetDefault } from 'node-internal:validators';
import { ERR_INVALID_ARG_TYPE } from 'node-internal:internal_errors';
import {
isArrayBufferView,
isAnyArrayBuffer
} from 'node-internal:internal_types';
import type { ZlibOptions } from "node:zlib";
import { ZlibBase } from 'node-internal:internal_zlib_base';

function crc32(data: ArrayBufferView | string, value: number = 0): number {
if (typeof data === 'string') {
data = Buffer.from(data);
} else if (!isArrayBufferView(data)) {
throw new ERR_INVALID_ARG_TYPE('data', 'ArrayBufferView', typeof data);
}
validateUint32(value, 'value');
return zlibUtil.crc32(data, value);
}
const {
CONST_INFLATE,
CONST_GUNZIP,
CONST_GZIP,
CONST_UNZIP,
CONST_Z_DEFAULT_STRATEGY,
CONST_Z_DEFAULT_MEMLEVEL,
CONST_Z_DEFAULT_WINDOWBITS,
CONST_Z_DEFAULT_COMPRESSION,
CONST_Z_FIXED,
CONST_Z_MAX_LEVEL,
CONST_Z_MAX_MEMLEVEL,
CONST_Z_MAX_WINDOWBITS,
CONST_Z_MIN_LEVEL,
CONST_Z_MIN_MEMLEVEL,
} = zlibUtil;


const constPrefix = 'CONST_';
const constants = {};
const constPrefix = 'CONST_';
export const constants: Record<string, number> = {};

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
Object.defineProperties(constants, Object.fromEntries(Object.entries(Object.getPrototypeOf(zlibUtil))
Expand All @@ -29,7 +40,85 @@ Object.defineProperties(constants, Object.fromEntries(Object.entries(Object.getP
}])
));

export {
crc32,
constants,
export function crc32(data: ArrayBufferView | string, value: number = 0): number {
if (typeof data === 'string') {
data = Buffer.from(data);
} else if (!isArrayBufferView(data)) {
throw new ERR_INVALID_ARG_TYPE('data', 'ArrayBufferView', typeof data);
}
validateUint32(value, 'value');
return zlibUtil.crc32(data, value);
}

class Zlib {
public _writeState = new Uint32Array(2);
public _level = CONST_Z_DEFAULT_COMPRESSION;
public _strategy = CONST_Z_DEFAULT_STRATEGY;
#handle: zlibUtil.ZlibHandle;

public constructor(options: ZlibOptions | null | undefined, mode: ZlibMode) {
let windowBits = CONST_Z_DEFAULT_WINDOWBITS;
let level = CONST_Z_DEFAULT_COMPRESSION;
let memLevel = CONST_Z_DEFAULT_MEMLEVEL;
let strategy = CONST_Z_DEFAULT_STRATEGY;
let dictionary: Options['dictionary'] | undefined = undefined;

if (options != null) {
// Special case:
// - Compression: 0 is an invalid case.
// - Decompression: 0 indicates zlib to use the window size in the header of the compressed stream.
if ((options.windowBits == null || options.windowBits === 0) && (mode === CONST_INFLATE || mode === CONST_GUNZIP || mode === CONST_UNZIP)) {
windowBits = 0;
} else {
// `{ windowBits: 8 }` is valid for DEFLATE but not for GZIP.
const min = zlibUtil.CONST_Z_MIN_WINDOWBITS + (mode === CONST_GZIP ? 1 : 0);
windowBits = checkRangesOrGetDefault(options.windowBits, 'options.windowBits', min, CONST_Z_MAX_WINDOWBITS, CONST_Z_DEFAULT_WINDOWBITS);
}

level = checkRangesOrGetDefault(options.level, 'options.level', CONST_Z_MIN_LEVEL, CONST_Z_MAX_LEVEL, CONST_Z_DEFAULT_MEMLEVEL);
memLevel = checkRangesOrGetDefault(options.memLevel, 'options.memLevel', CONST_Z_MIN_MEMLEVEL, CONST_Z_MAX_MEMLEVEL, CONST_Z_DEFAULT_MEMLEVEL);
strategy = checkRangesOrGetDefault(options.strategy, 'options.strategy', CONST_Z_DEFAULT_STRATEGY, CONST_Z_FIXED, CONST_Z_DEFAULT_STRATEGY);
dictionary = options.dictionary;

if (options.dictionary != null && !isArrayBufferView(options.dictionary)) {
if (isAnyArrayBuffer(options.dictionary)) {
dictionary = Buffer.from(options.dictionary);
} else {
throw new ERR_INVALID_ARG_TYPE('options.dictionary', ['Buffer', 'TypedArray', 'DataView', 'ArrayBuffer'], options.dictionary);
}
}
}

this.#handle = new zlibUtil.ZlibHandle(mode, {
windowBits,
level,
memLevel,
strategy,
dictionary,
}, this._writeState);
this._level = level;
this._strategy = strategy;

Reflect.apply(ZlibBase, this, [options, mode, this.#handle])
}

public params(level: number, strategy: number, callback: () => never): void {
checkRangesOrGetDefault(level, 'level', CONST_Z_MIN_LEVEL, CONST_Z_MAX_LEVEL);
checkRangesOrGetDefault(strategy, 'strategy', CONST_Z_DEFAULT_STRATEGY, CONST_Z_FIXED);

if (this._level !== level || this._strategy !== strategy) {
// TODO: Implement this
} else {
queueMicrotask(callback);
}
}
}

Object.setPrototypeOf(Zlib.prototype, ZlibBase.prototype);
Object.setPrototypeOf(Zlib, ZlibBase);

export class Gzip extends Zlib {
public constructor(options: Options) {
super(options, CONST_GZIP);
}
}
212 changes: 212 additions & 0 deletions src/node/internal/internal_zlib_base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { ZlibMode, default as zlibUtil } from 'node-internal:zlib';
import { Buffer } from 'node-internal:internal_buffer';
import { validateUint32, checkRangesOrGetDefault, checkFiniteNumber } from 'node-internal:validators';
import { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } from 'node-internal:internal_errors';
import {
isArrayBufferView,
} from 'node-internal:internal_types';
import { kMaxLength } from 'node-internal:internal_buffer';
import { Transform } from 'node-internal:streams_transform'
import type { ZlibOptions } from "node:zlib";
import type { DuplexOptions } from 'node:stream';

const {
CONST_BROTLI_DECODE,
CONST_BROTLI_ENCODE,
CONST_BROTLI_OPERATION_EMIT_METADATA,
CONST_BROTLI_OPERATION_PROCESS,
CONST_Z_BLOCK,
CONST_Z_DEFAULT_CHUNK,
CONST_Z_MIN_CHUNK,
CONST_Z_NO_FLUSH,
CONST_Z_FINISH,
CONST_Z_FULL_FLUSH,
CONST_Z_SYNC_FLUSH,
CONST_Z_PARTIAL_FLUSH,
} = zlibUtil;

const FLUSH_BOUND = [
[ CONST_Z_NO_FLUSH, CONST_Z_BLOCK ],
[ CONST_BROTLI_OPERATION_PROCESS, CONST_BROTLI_OPERATION_EMIT_METADATA ],
] as const;
const FLUSH_BOUND_IDX_NORMAL: number = 0;
const FLUSH_BOUND_IDX_BROTLI: number = 1;

const kFlushFlag = Symbol('kFlushFlag');
const kError = Symbol('kError');

// If a flush is scheduled while another flush is still pending, a way to figure
// out which one is the "stronger" flush is needed.
// This is currently only used to figure out which flush flag to use for the
// last chunk.
// Roughly, the following holds:
// Z_NO_FLUSH (< Z_TREES) < Z_BLOCK < Z_PARTIAL_FLUSH <
// Z_SYNC_FLUSH < Z_FULL_FLUSH < Z_FINISH
const flushiness: number[] = [];
const kFlushFlagList = [CONST_Z_NO_FLUSH, CONST_Z_BLOCK, CONST_Z_PARTIAL_FLUSH,
CONST_Z_SYNC_FLUSH, CONST_Z_FULL_FLUSH, CONST_Z_FINISH];
for (let i = 0; i < kFlushFlagList.length; i++) {
flushiness[kFlushFlagList[i] as number] = i;
}

type BufferWithFlushFlag = Buffer & { [kFlushFlag]: number};

// Set up a list of 'special' buffers that can be written using .write()
// from the .flush() code as a way of introducing flushing operations into the
// write sequence.
const kFlushBuffers: BufferWithFlushFlag[] = [];
{
const dummyArrayBuffer = new ArrayBuffer(0);
for (const flushFlag of kFlushFlagList) {
const buf = Buffer.from(dummyArrayBuffer) as BufferWithFlushFlag;
buf[kFlushFlag] = flushFlag;
kFlushBuffers[flushFlag] = buf;
}
}

type ZlibBaseDefaultOptions = {
flush?: number | undefined;
finishFlush?: number | undefined;
fullFlush?: number | undefined
}

const zlibDefaultOptions = {
flush: CONST_Z_NO_FLUSH,
finishFlush: CONST_Z_FINISH,
fullFlush: CONST_Z_FULL_FLUSH,
};

export class ZlibBase extends Transform {
public bytesWritten: number = 0;

public _maxOutputLength: number;
public _outBuffer: Buffer;
public _outOffset: number = 0;
public _chunkSize: number;
public _defaultFlushFlag: number | undefined;
public _finishFlushFlag: number | undefined;
public _defaultFullFlushFlag: number | undefined;
public _info: unknown;
public _handle: zlibUtil.ZlibHandle;

public [kError]: null;

public constructor(opts: ZlibOptions & DuplexOptions, mode: ZlibMode, handle: zlibUtil.ZlibHandle, { flush, finishFlush, fullFlush }: ZlibBaseDefaultOptions = zlibDefaultOptions) {
let chunkSize = CONST_Z_DEFAULT_CHUNK;
let maxOutputLength = kMaxLength;

let flushBoundIdx;
if (mode !== CONST_BROTLI_ENCODE && mode !== CONST_BROTLI_DECODE) {
flushBoundIdx = FLUSH_BOUND_IDX_NORMAL;
} else {
flushBoundIdx = FLUSH_BOUND_IDX_BROTLI;
}

if (opts) {
if (opts.chunkSize != null) {
chunkSize = opts.chunkSize;
}
if (!checkFiniteNumber(chunkSize, 'options.chunkSize')) {
chunkSize = CONST_Z_DEFAULT_CHUNK;
} else if (chunkSize < CONST_Z_MIN_CHUNK) {
throw new ERR_OUT_OF_RANGE('options.chunkSize',
`>= ${CONST_Z_MIN_CHUNK}`, chunkSize);
}

flush = checkRangesOrGetDefault(
opts.flush, 'options.flush',
FLUSH_BOUND[flushBoundIdx]![0], FLUSH_BOUND[flushBoundIdx]![1], flush as number);

finishFlush = checkRangesOrGetDefault(
opts.finishFlush, 'options.finishFlush',
FLUSH_BOUND[flushBoundIdx]![0], FLUSH_BOUND[flushBoundIdx]![1],
finishFlush as number);

maxOutputLength = checkRangesOrGetDefault(
opts.maxOutputLength, 'options.maxOutputLength',
1, kMaxLength, kMaxLength);

if (opts.encoding || opts.objectMode || opts.writableObjectMode) {
opts = { ...opts };
opts.encoding = undefined;
opts.objectMode = false;
opts.writableObjectMode = false;
}
}

// TODO: Find a way to avoid having "any"
super({ autoDestroy: true, ...opts } as any);
this[kError] = null;
this._handle = handle;
// handle[owner_symbol] = this;
// Used by processCallback() and zlibOnError()
// handle.onerror = zlibOnError;
this._outBuffer = Buffer.allocUnsafe(chunkSize);
this._outOffset = 0;

this._chunkSize = chunkSize;
this._defaultFlushFlag = flush;
this._finishFlushFlag = finishFlush;
this._defaultFullFlushFlag = fullFlush;
this._info = opts && opts.info;
this._maxOutputLength = maxOutputLength;
}

public flush(kind: number | (() => void), callback: (() => void) | undefined = undefined): void {
if (typeof kind === 'function' || (kind == null && !callback)) {
callback = kind;
kind = this._defaultFlushFlag as number;
}

if (this.writableFinished) {
if (callback) {
queueMicrotask(callback);
}
} else if (this.writableEnded) {
if (callback) {
queueMicrotask(callback);
}
} else {
// encoding is not used. utf8 is passed to conform to typescript.
this.write(kFlushBuffers[kind], 'utf8', callback);
}
}

public close(callback?: (() => never)): void {
if (callback) {
finished(this, callback);
}
this.destroy();
}

public override _destroy<T extends Error>(err: T, callback: (err: T) => never): void {
_close(this);
callback(err);
}

public override _transform(chunk: Buffer & { [kFlushFlag]?: number }, _encoding: BufferEncoding, cb: (args: unknown) => never): void {
let flushFlag = this._defaultFlushFlag;
// We use a 'fake' zero-length chunk to carry information about flushes from
// the public API to the actual stream implementation.
if (typeof chunk[kFlushFlag] === 'number') {
flushFlag = chunk[kFlushFlag];
}

// For the last chunk, also apply `_finishFlushFlag`.
if (this.writableEnded && this.writableLength === chunk.byteLength) {
flushFlag = maxFlush(flushFlag, this._finishFlushFlag);
}
processChunk(this, chunk, flushFlag, cb);
}

public _processChunk(chunk: Buffer, flushFlag: number, cb?: () => never): number | undefined {
if (typeof cb === 'function') {
processChunk(this, chunk, flushFlag, cb);
} else {
return processChunkSync(this, chunk, flushFlag);
}
}
}

Object.setPrototypeOf(ZlibBase.prototype, Transform.prototype);
Object.setPrototypeOf(ZlibBase, Transform);
Loading

0 comments on commit 18a70ba

Please sign in to comment.