Skip to content
File

Blob: src/workerd/api/node/tests/zlib-zstd-nodejs-test.js

javascript449 lines
1// Copyright (c) 2026 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
4import assert from 'node:assert';
5import { Buffer } from 'node:buffer';
6import zlib from 'node:zlib';
7 
8// Basic sync compress/decompress test
9export const zstdBasicSyncTest = {
10 test() {
11 const input = Buffer.from('Hello, Zstd compression!');
12 const compressed = zlib.zstdCompressSync(input);
13 assert(Buffer.isBuffer(compressed), 'Compressed output should be a buffer');
14 assert(compressed.length > 0, 'Compressed output should not be empty');
15 
16 const decompressed = zlib.zstdDecompressSync(compressed);
17 assert(
18 Buffer.isBuffer(decompressed),
19 'Decompressed output should be a buffer'
20 );
21 assert.strictEqual(
22 decompressed.toString(),
23 input.toString(),
24 'Round-trip should match'
25 );
26 },
27};
28 
29// Basic async compress/decompress test
30export const zstdBasicAsyncTest = {
31 async test() {
32 const input = Buffer.from('Hello, async Zstd compression!');
33 
34 const {
35 promise: compressPromise,
36 resolve: compressResolve,
37 reject: compressReject,
38 } = Promise.withResolvers();
39 zlib.zstdCompress(input, (err, res) => {
40 if (err) compressReject(err);
41 else compressResolve(res);
42 });
43 const compressed = await compressPromise;
44 
45 assert(Buffer.isBuffer(compressed), 'Compressed output should be a buffer');
46 assert(compressed.length > 0, 'Compressed output should not be empty');
47 
48 const {
49 promise: decompressPromise,
50 resolve: decompressResolve,
51 reject: decompressReject,
52 } = Promise.withResolvers();
53 zlib.zstdDecompress(compressed, (err, res) => {
54 if (err) decompressReject(err);
55 else decompressResolve(res);
56 });
57 const decompressed = await decompressPromise;
58 
59 assert(
60 Buffer.isBuffer(decompressed),
61 'Decompressed output should be a buffer'
62 );
63 assert.strictEqual(
64 decompressed.toString(),
65 input.toString(),
66 'Round-trip should match'
67 );
68 },
69};
70 
71// Test with string input
72export const zstdStringInputTest = {
73 test() {
74 const input = 'This is a string input for Zstd compression';
75 const compressed = zlib.zstdCompressSync(input);
76 const decompressed = zlib.zstdDecompressSync(compressed);
77 assert.strictEqual(
78 decompressed.toString(),
79 input,
80 'String input round-trip should match'
81 );
82 },
83};
84 
85// Test with larger data
86export const zstdLargeDataTest = {
87 test() {
88 // Create a 100KB buffer with repetitive content (compresses well)
89 const input = Buffer.alloc(100 * 1024);
90 for (let i = 0; i < input.length; i++) {
91 input[i] = i % 256;
92 }
93 
94 const compressed = zlib.zstdCompressSync(input);
95 assert(
96 compressed.length < input.length,
97 'Compressed should be smaller than input'
98 );
99 
100 const decompressed = zlib.zstdDecompressSync(compressed);
101 assert(input.equals(decompressed), 'Large data round-trip should match');
102 },
103};
104 
105// Test with different compression levels
106export const zstdCompressionLevelsTest = {
107 test() {
108 const input = Buffer.from(
109 'Test data for compression level testing'.repeat(100)
110 );
111 
112 // Test compression level 1 (fastest)
113 const compressedFast = zlib.zstdCompressSync(input, {
114 params: { [zlib.constants.ZSTD_c_compressionLevel]: 1 },
115 });
116 
117 // Test compression level 19 (best compression)
118 const compressedBest = zlib.zstdCompressSync(input, {
119 params: { [zlib.constants.ZSTD_c_compressionLevel]: 19 },
120 });
121 
122 // Both should decompress correctly
123 const decompressedFast = zlib.zstdDecompressSync(compressedFast);
124 const decompressedBest = zlib.zstdDecompressSync(compressedBest);
125 
126 assert(
127 input.equals(decompressedFast),
128 'Fast compression should decompress correctly'
129 );
130 assert(
131 input.equals(decompressedBest),
132 'Best compression should decompress correctly'
133 );
134 
135 // Higher compression level should typically produce smaller output
136 assert(
137 compressedBest.length <= compressedFast.length,
138 'Higher compression level should produce smaller or equal output'
139 );
140 },
141};
142 
143// Test with default compression level
144export const zstdDefaultCompressionTest = {
145 test() {
146 const input = Buffer.from('Testing default compression level');
147 const compressed = zlib.zstdCompressSync(input);
148 const decompressed = zlib.zstdDecompressSync(compressed);
149 assert.strictEqual(
150 decompressed.toString(),
151 input.toString(),
152 'Default compression should work'
153 );
154 },
155};
156 
157// Test stream API
158export const zstdStreamTest = {
159 async test() {
160 const input = Buffer.from('Stream compression test data'.repeat(50));
161 
162 // Create compress stream
163 const compress = zlib.createZstdCompress();
164 const decompress = zlib.createZstdDecompress();
165 
166 const chunks = [];
167 
168 // Pipe through compress
169 compress.on('data', (chunk) => chunks.push(chunk));
170 
171 await new Promise((resolve, reject) => {
172 compress.on('end', resolve);
173 compress.on('error', reject);
174 compress.end(input);
175 });
176 
177 const compressed = Buffer.concat(chunks);
178 assert(compressed.length > 0, 'Stream should produce output');
179 
180 // Decompress
181 const decompressedChunks = [];
182 decompress.on('data', (chunk) => decompressedChunks.push(chunk));
183 
184 await new Promise((resolve, reject) => {
185 decompress.on('end', resolve);
186 decompress.on('error', reject);
187 decompress.end(compressed);
188 });
189 
190 const decompressed = Buffer.concat(decompressedChunks);
191 assert(input.equals(decompressed), 'Stream round-trip should match');
192 },
193};
194 
195// Test empty input
196export const zstdEmptyInputTest = {
197 test() {
198 const input = Buffer.alloc(0);
199 const compressed = zlib.zstdCompressSync(input);
200 const decompressed = zlib.zstdDecompressSync(compressed);
201 assert.strictEqual(
202 decompressed.length,
203 0,
204 'Empty input should produce empty output'
205 );
206 },
207};
208 
209// Test invalid compressed data
210export const zstdInvalidDataTest = {
211 test() {
212 const invalidData = Buffer.from('This is not valid zstd compressed data');
213 assert.throws(
214 () => zlib.zstdDecompressSync(invalidData),
215 /Zstd decompression failed/,
216 'Should throw on invalid compressed data'
217 );
218 },
219};
220 
221// Test chunkSize option
222export const zstdChunkSizeTest = {
223 test() {
224 const input = Buffer.from('Testing chunk size option'.repeat(100));
225 const compressed = zlib.zstdCompressSync(input, { chunkSize: 1024 });
226 const decompressed = zlib.zstdDecompressSync(compressed, {
227 chunkSize: 1024,
228 });
229 assert(
230 input.equals(decompressed),
231 'Custom chunkSize should work correctly'
232 );
233 },
234};
235 
236// Test maxOutputLength option
237export const zstdMaxOutputLengthTest = {
238 test() {
239 const input = Buffer.from('A'.repeat(1000));
240 const compressed = zlib.zstdCompressSync(input);
241 
242 // Try to decompress with a maxOutputLength that's too small
243 assert.throws(
244 () => zlib.zstdDecompressSync(compressed, { maxOutputLength: 10 }),
245 /Memory limit exceeded/,
246 'Should throw when maxOutputLength is exceeded'
247 );
248 },
249};
250 
251// Test callback error handling
252export const zstdCallbackErrorTest = {
253 async test() {
254 const invalidData = Buffer.from('invalid zstd data');
255 
256 const { promise, resolve, reject } = Promise.withResolvers();
257 zlib.zstdDecompress(invalidData, (err, res) => {
258 if (err) reject(err);
259 else resolve(res);
260 });
261 
262 try {
263 await promise;
264 assert.fail('Should have thrown');
265 } catch (err) {
266 assert(err instanceof Error, 'Should receive an error');
267 }
268 },
269};
270 
271// Test with params for strategy
272export const zstdStrategyTest = {
273 test() {
274 const input = Buffer.from('Testing compression strategy'.repeat(50));
275 
276 // Test with ZSTD_fast strategy
277 const compressedFast = zlib.zstdCompressSync(input, {
278 params: {
279 [zlib.constants.ZSTD_c_strategy]: zlib.constants.ZSTD_fast,
280 },
281 });
282 
283 const decompressed = zlib.zstdDecompressSync(compressedFast);
284 assert(input.equals(decompressed), 'Strategy option should work correctly');
285 },
286};
287 
288// Test with info option
289export const zstdInfoOptionTest = {
290 test() {
291 const input = Buffer.from('Testing info option');
292 const result = zlib.zstdCompressSync(input, { info: true });
293 
294 // When info is true, result should be an object with buffer and engine properties
295 assert(
296 typeof result === 'object',
297 'Result should be an object when info is true'
298 );
299 assert(result.buffer, 'Result should have a buffer property');
300 assert(result.engine, 'Result should have an engine property');
301 },
302};
303 
304// Test classes are exported
305export const zstdClassesExportedTest = {
306 test() {
307 assert.strictEqual(
308 typeof zlib.ZstdCompress,
309 'function',
310 'ZstdCompress should be exported'
311 );
312 assert.strictEqual(
313 typeof zlib.ZstdDecompress,
314 'function',
315 'ZstdDecompress should be exported'
316 );
317 assert.strictEqual(
318 typeof zlib.createZstdCompress,
319 'function',
320 'createZstdCompress should be exported'
321 );
322 assert.strictEqual(
323 typeof zlib.createZstdDecompress,
324 'function',
325 'createZstdDecompress should be exported'
326 );
327 },
328};
329 
330// Test sync functions are exported
331export const zstdSyncFunctionsExportedTest = {
332 test() {
333 assert.strictEqual(
334 typeof zlib.zstdCompressSync,
335 'function',
336 'zstdCompressSync should be exported'
337 );
338 assert.strictEqual(
339 typeof zlib.zstdDecompressSync,
340 'function',
341 'zstdDecompressSync should be exported'
342 );
343 assert.strictEqual(
344 typeof zlib.zstdCompress,
345 'function',
346 'zstdCompress should be exported'
347 );
348 assert.strictEqual(
349 typeof zlib.zstdDecompress,
350 'function',
351 'zstdDecompress should be exported'
352 );
353 },
354};
355 
356// Test sync decompression of truncated compressed data (EOF mid-frame)
357export const zstdTruncatedSyncTest = {
358 test() {
359 const input = Buffer.from('Hello, Zstd truncation test!'.repeat(100));
360 const compressed = zlib.zstdCompressSync(input);
361 
362 // Cut the compressed data in half
363 const truncated = compressed.subarray(0, Math.floor(compressed.length / 2));
364 assert.throws(
365 () => zlib.zstdDecompressSync(truncated),
366 (err) => err instanceof Error,
367 'Should throw on truncated compressed data'
368 );
369 },
370};
371 
372// Test async decompression of truncated compressed data
373export const zstdTruncatedAsyncTest = {
374 async test() {
375 const input = Buffer.from('Hello, Zstd truncation test!'.repeat(100));
376 const compressed = zlib.zstdCompressSync(input);
377 const truncated = compressed.subarray(0, Math.floor(compressed.length / 2));
378 
379 const { promise, resolve, reject } = Promise.withResolvers();
380 zlib.zstdDecompress(truncated, (err, res) => {
381 if (err) reject(err);
382 else resolve(res);
383 });
384 
385 try {
386 await promise;
387 assert.fail('Should have thrown on truncated data');
388 } catch (err) {
389 assert(
390 err instanceof Error,
391 'Should receive an error for truncated data'
392 );
393 }
394 },
395};
396 
397// Test stream decompression of truncated compressed data
398export const zstdTruncatedStreamTest = {
399 async test() {
400 const input = Buffer.from('Stream truncation test data'.repeat(100));
401 const compressed = zlib.zstdCompressSync(input);
402 const truncated = compressed.subarray(0, Math.floor(compressed.length / 2));
403 
404 const decompress = zlib.createZstdDecompress();
405 
406 let errored = false;
407 decompress.on('error', () => {
408 errored = true;
409 });
410 
411 decompress.end(truncated);
412 try {
413 for await (const _chunk of decompress) {
414 // consume chunks
415 }
416 } catch {
417 errored = true;
418 }
419 
420 assert(errored, 'Stream should error on truncated compressed data');
421 },
422};
423 
424// Test stream decompression with large output and small chunkSize to exercise
425// the processCallback recursion path; verify we don't get a stack overflow.
426export const zstdStreamLargeDecompressTest = {
427 async test() {
428 // 1MB of compressible data — compresses small, decompresses large.
429 // With chunkSize=64, this requires ~16,000 output chunks, which triggers
430 // deep recursion in processCallback if the callback is synchronous.
431 const input = Buffer.alloc(1024 * 1024, 'A');
432 const compressed = zlib.zstdCompressSync(input);
433 
434 const decompress = zlib.createZstdDecompress({ chunkSize: 64 });
435 let totalBytes = 0;
436 
437 decompress.end(compressed);
438 for await (const chunk of decompress) {
439 totalBytes += chunk.length;
440 }
441 
442 assert.strictEqual(
443 totalBytes,
444 input.length,
445 `Decompressed size should be ${input.length}, got ${totalBytes}`
446 );
447 },
448};