Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions apps/nest-node-probe/correctness/zlib-streams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Buffer } from 'node:buffer'
import { PassThrough, type Transform } from 'node:stream'
import {
brotliDecompressSync,
createBrotliDecompress,
createDeflate,
createDeflateRaw,
createGunzip,
createGzip,
createInflate,
createInflateRaw,
createUnzip,
gunzipSync
} from 'node:zlib'

const log = (line: string): void => {
console.log(line)
}

const TEXT = 'The quick brown fox jumps over the lazy dog. '.repeat(40)
const GZIP = '1f8b08000000000000130bc94855282ccd4cce56482aca2fcf5348cbaf50c82acd2d2856c82f4b2d5228c94855c849acaa5448c94fd7530819553caa7854f1a8e251c5a38a87976200e6c3956408070000'
const DEFLATE = '789c0bc94855282ccd4cce56482aca2fcf5348cbaf50c82acd2d2856c82f4b2d5228c94855c849acaa5448c94fd7530819553caa7854f1a8e251c5a38a87976200880a8637'
const DEFLATE_RAW = '0bc94855282ccd4cce56482aca2fcf5348cbaf50c82acd2d2856c82f4b2d5228c94855c849acaa5448c94fd7530819553caa7854f1a8e251c5a38a87976200'
const BROTLI = '1b0707882c0e78d3d0955d9710bb172ba9cad092cc8cad415ce6f236c8199e9e0a7b830d387048206f24bf41a715ce1c1e27aa2938c2a9749f0603'
const GZIP_MEMBERS = '1f8b08000000000000134bcb2c2a2e510000fc7af11c060000001f8b08000000000000132b4e4dcecf4b010069111fb606000000'

interface Case {
name: string
decoder: string
input: Buffer
expected: string
}

function decoder(kind: string): Transform {
switch (kind) {
case 'gunzip':
return createGunzip()
case 'inflate':
return createInflate()
case 'inflateRaw':
return createInflateRaw()
case 'unzip':
return createUnzip()
case 'brotli':
return createBrotliDecompress()
default:
throw new Error(`unknown decoder ${kind}`)
}
}

const cases: Case[] = [
{ name: 'gunzip', decoder: 'gunzip', input: Buffer.from(GZIP, 'hex'), expected: TEXT },
{ name: 'inflate', decoder: 'inflate', input: Buffer.from(DEFLATE, 'hex'), expected: TEXT },
{ name: 'inflateRaw', decoder: 'inflateRaw', input: Buffer.from(DEFLATE_RAW, 'hex'), expected: TEXT },
{ name: 'unzip gzip', decoder: 'unzip', input: Buffer.from(GZIP, 'hex'), expected: TEXT },
{ name: 'unzip zlib', decoder: 'unzip', input: Buffer.from(DEFLATE, 'hex'), expected: TEXT },
{ name: 'brotli', decoder: 'brotli', input: Buffer.from(BROTLI, 'hex'), expected: TEXT },
{ name: 'gunzip members', decoder: 'gunzip', input: Buffer.from(GZIP_MEMBERS, 'hex'), expected: 'first second' },
{ name: 'gunzip garbage', decoder: 'gunzip', input: Buffer.from('0102030405', 'hex'), expected: '' },
{ name: 'gunzip truncated', decoder: 'gunzip', input: Buffer.from(GZIP, 'hex').subarray(0, 40), expected: '' },
{ name: 'inflate garbage', decoder: 'inflate', input: Buffer.from('0102030405', 'hex'), expected: '' },
{ name: 'brotli truncated', decoder: 'brotli', input: Buffer.from(BROTLI, 'hex').subarray(0, 40), expected: '' },
{ name: 'brotli corrupt', decoder: 'brotli', input: Buffer.from('ff', 'hex'), expected: '' },
{ name: 'brotli empty', decoder: 'brotli', input: Buffer.from('3b', 'hex'), expected: '' }
]

let at = 0

function runDecoder(item: Case, done: () => void): void {
const source = new PassThrough()
const stream = decoder(item.decoder)
const chunks: Buffer[] = []
stream.on('data', (chunk: Buffer) => {
chunks.push(chunk)
})
stream.on('end', () => {
const output = Buffer.concat(chunks)
log(`${item.name}: end ${output.length} ${output.toString() === item.expected ? 'match' : 'MISMATCH'}`)
done()
})
stream.on('error', (error: Error) => {
log(`${item.name}: error ${error.message}`)
done()
})
source.pipe(stream)
const third = Math.max(1, Math.floor(item.input.length / 3))
source.write(item.input.subarray(0, third))
source.write(item.input.subarray(third, 2 * third))
source.end(item.input.subarray(2 * third))
}

function roundTrip(name: string, encoder: Transform, decode: Transform, done: () => void): void {
const chunks: Buffer[] = []
encoder.pipe(decode)
decode.on('data', (chunk: Buffer) => {
chunks.push(chunk)
})
decode.on('end', () => {
const output = Buffer.concat(chunks)
log(`${name}: end ${output.length} ${output.toString() === TEXT ? 'match' : 'MISMATCH'}`)
done()
})
encoder.write(Buffer.from(TEXT.slice(0, 600)))
encoder.write(Buffer.from(TEXT.slice(600, 1200)))
encoder.end(Buffer.from(TEXT.slice(1200)))
}

function next(): void {
if (at < cases.length) {
const item = cases[at]
at++
runDecoder(item, next)
return
}
if (at === cases.length) {
at++
roundTrip('gzip round trip', createGzip(), createGunzip(), next)
return
}
if (at === cases.length + 1) {
at++
roundTrip('deflate round trip', createDeflate({ level: 9 }), createInflate(), next)
return
}
if (at === cases.length + 2) {
at++
roundTrip('deflateRaw round trip', createDeflateRaw(), createInflateRaw(), next)
return
}
log(`brotliDecompressSync: ${brotliDecompressSync(Buffer.from(BROTLI, 'hex')).toString() === TEXT ? 'match' : 'MISMATCH'}`)
log(`gunzipSync: ${gunzipSync(Buffer.from(GZIP, 'hex')).toString() === TEXT ? 'match' : 'MISMATCH'}`)
log('done')
}

log('start')
next()
7 changes: 5 additions & 2 deletions plugin/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ const nodeOnlyHostFunctions = new Map([
['__gea_node_crypto_timing_safe_equal', 'gea::node::crypto::timingSafeEqual'],
['__gea_node_crypto_get_fips', 'gea::node::crypto::getFips'],
['__gea_node_zlib_inflate', 'gea::node::zlib::inflate'],
['__gea_node_zlib_deflate', 'gea::node::zlib::deflate']
['__gea_node_zlib_deflate', 'gea::node::zlib::deflate'],
['__gea_node_brotli_decompress', 'gea::node::brotli::decompress']
])

/**
Expand Down Expand Up @@ -442,7 +443,9 @@ const hostPreambles = () =>
? [runtimeHeader, '#include "gea_node_crypto.hpp"']
: spelling.startsWith('gea::node::zlib::')
? [runtimeHeader, '#include "gea_node_zlib.hpp"']
: [runtimeHeader]
: spelling.startsWith('gea::node::brotli::')
? [runtimeHeader, '#include "gea_node_brotli.hpp"']
: [runtimeHeader]
])
)

Expand Down
44 changes: 44 additions & 0 deletions runtime/node/gea_node_brotli.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include "gea_runtime.h"

#include <brotli/decode.h>

#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

namespace gea::node::brotli {
using Bytes = gea::TypedArray<std::uint8_t>;

inline std::vector<std::uint8_t> decompress(const Bytes& bytes) {
BrotliDecoderState* state = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr);
if (state == nullptr) gea::host::throwRuntimeError("Error", "Cannot allocate memory");
static const std::uint8_t empty = 0;
const std::uint8_t* nextIn = bytes.size() == 0 ? &empty : reinterpret_cast<const std::uint8_t*>(bytes.data());
std::size_t availableIn = bytes.size();
std::vector<std::uint8_t> output;
constexpr std::size_t chunk = 64 * 1024;
BrotliDecoderResult result = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT;
while (result == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) {
const std::size_t used = output.size();
output.resize(used + chunk);
std::uint8_t* nextOut = output.data() + used;
std::size_t availableOut = chunk;
result = BrotliDecoderDecompressStream(state, &availableIn, &nextIn, &availableOut, &nextOut, nullptr);
output.resize(used + (chunk - availableOut));
}
if (result == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) {
BrotliDecoderDestroyInstance(state);
gea::host::throwRuntimeError("Error", "unexpected end of file");
}
if (result != BROTLI_DECODER_RESULT_SUCCESS) {
BrotliDecoderDestroyInstance(state);
gea::host::throwRuntimeError("Error", "Decompression failed");
}
BrotliDecoderDestroyInstance(state);
return output;
}

}
108 changes: 1 addition & 107 deletions runtime/node/generated/facades/zlib.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Generated from the pinned Node 24 declaration inventory. Do not edit.
// Canonical builtin: node:zlib
import { nodeNotImplemented } from "../../not-implemented"
export { constants, deflate, deflateRawSync, deflateSync, gunzip, gunzipSync, gzip, gzipSync, inflate, inflateRawSync, inflateSync, unzipSync } from "../../zlib"
export { brotliDecompress, BrotliDecompress, brotliDecompressSync, constants, createBrotliDecompress, createDeflate, createDeflateRaw, createGunzip, createGzip, createInflate, createInflateRaw, createUnzip, deflate, Deflate, DeflateRaw, deflateRawSync, deflateSync, gunzip, Gunzip, gunzipSync, gzip, Gzip, gzipSync, inflate, Inflate, InflateRaw, inflateRawSync, inflateSync, Unzip, unzipSync } from "../../zlib"

export function brotliCompress(...args: unknown[]): never {
void args
Expand All @@ -20,23 +20,6 @@ export function brotliCompressSync(...args: unknown[]): never {
return nodeNotImplemented("node:zlib", "brotliCompressSync")
}

export function brotliDecompress(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "brotliDecompress")
}

export class BrotliDecompress {
constructor(...args: unknown[]) {
void args
nodeNotImplemented("node:zlib", "BrotliDecompress")
}
}

export function brotliDecompressSync(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "brotliDecompressSync")
}

export function crc32(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "crc32")
Expand All @@ -47,46 +30,6 @@ export function createBrotliCompress(...args: unknown[]): never {
return nodeNotImplemented("node:zlib", "createBrotliCompress")
}

export function createBrotliDecompress(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createBrotliDecompress")
}

export function createDeflate(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createDeflate")
}

export function createDeflateRaw(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createDeflateRaw")
}

export function createGunzip(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createGunzip")
}

export function createGzip(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createGzip")
}

export function createInflate(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createInflate")
}

export function createInflateRaw(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createInflateRaw")
}

export function createUnzip(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createUnzip")
}

export function createZstdCompress(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "createZstdCompress")
Expand All @@ -97,70 +40,21 @@ export function createZstdDecompress(...args: unknown[]): never {
return nodeNotImplemented("node:zlib", "createZstdDecompress")
}

export class Deflate {
constructor(...args: unknown[]) {
void args
nodeNotImplemented("node:zlib", "Deflate")
}
}

export function deflateRaw(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "deflateRaw")
}

export class DeflateRaw {
constructor(...args: unknown[]) {
void args
nodeNotImplemented("node:zlib", "DeflateRaw")
}
}

export class Gunzip {
constructor(...args: unknown[]) {
void args
nodeNotImplemented("node:zlib", "Gunzip")
}
}

export class Gzip {
constructor(...args: unknown[]) {
void args
nodeNotImplemented("node:zlib", "Gzip")
}
}

export class Inflate {
constructor(...args: unknown[]) {
void args
nodeNotImplemented("node:zlib", "Inflate")
}
}

export function inflateRaw(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "inflateRaw")
}

export class InflateRaw {
constructor(...args: unknown[]) {
void args
nodeNotImplemented("node:zlib", "InflateRaw")
}
}

export function unzip(...args: unknown[]): never {
void args
return nodeNotImplemented("node:zlib", "unzip")
}

export class Unzip {
constructor(...args: unknown[]) {
void args
nodeNotImplemented("node:zlib", "Unzip")
}
}

export const Z_ASCII: unknown = undefined

export const Z_BEST_COMPRESSION: unknown = undefined
Expand Down
Loading
Loading