This repository was archived by the owner on Aug 7, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathcompat.test.ts
More file actions
169 lines (151 loc) · 7.16 KB
/
Copy pathcompat.test.ts
File metadata and controls
169 lines (151 loc) · 7.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import { describe, it, expect } from "vitest";
import { Blosc, GZip, Zlib, LZ4, Zstd } from "../src/index.js";
import { range } from "./helpers.js";
describe("numcodecs compatibility", () => {
it("Blosc has all numcodecs static members", () => {
expect(Blosc.codecId).toBe("blosc");
expect(typeof Blosc.NOSHUFFLE).toBe("number");
expect(typeof Blosc.SHUFFLE).toBe("number");
expect(typeof Blosc.BITSHUFFLE).toBe("number");
expect(typeof Blosc.AUTOSHUFFLE).toBe("number");
expect(Array.isArray(Blosc.COMPRESSORS)).toBe(true);
});
it("Blosc constructor signature matches numcodecs", () => {
const c = new Blosc(5, "lz4", Blosc.SHUFFLE, 0);
expect(c.clevel).toBe(5);
expect(c.cname).toBe("lz4");
expect(c.shuffle).toBe(Blosc.SHUFFLE);
expect(c.blocksize).toBe(0);
});
it("All codecs have fromConfig accepting { id: string, ...options }", () => {
const b = Blosc.fromConfig({ id: "blosc", clevel: 3, cname: "zstd", shuffle: 1 });
expect(b.clevel).toBe(3);
const g = GZip.fromConfig({ id: "gzip", level: 6 });
expect(g.level).toBe(6);
const z = Zlib.fromConfig({ id: "zlib", level: 4 });
expect(z.level).toBe(4);
});
it("LZ4 has numcodecs static members", () => {
expect(LZ4.codecId).toBe("lz4");
expect(LZ4.DEFAULT_ACCELERATION).toBe(1);
expect(typeof LZ4.max_buffer_size).toBe("number");
});
it("Zstd has numcodecs static members", () => {
expect(Zstd.codecId).toBe("zstd");
expect(Zstd.DEFAULT_CLEVEL).toBe(1);
expect(Zstd.MAX_CLEVEL).toBe(22);
});
it("LZ4 and Zstd fromConfig round-trip their options", () => {
const l = LZ4.fromConfig({ id: "lz4", acceleration: 10 });
expect((l as any).acceleration).toBe(10);
const z = Zstd.fromConfig({ id: "zstd", level: 9 });
expect((z as any).level).toBe(9);
});
describe("decode(data, out)", () => {
const arr = range(1000, "<u4");
const bytes = new Uint8Array((arr as any).buffer);
// Zarr passes a pre-allocated chunk buffer here and reads it back rather than
// the return value, so `out` must be written into and returned as-is.
const wasmBacked = [
["Blosc", Blosc.fromConfig({ id: "blosc", cname: "lz4", clevel: 5, shuffle: 1 })],
["LZ4", LZ4.fromConfig({ id: "lz4" })],
["Zstd", Zstd.fromConfig({ id: "zstd" })],
] as const;
for (const [name, codec] of wasmBacked) {
it(`${name} decodes into the out buffer`, async () => {
const encoded = await codec.encode(bytes);
const out = new Uint8Array(bytes.length);
const decoded = await codec.decode(encoded, out);
expect(decoded).toBe(out);
expect(Array.from(out)).toEqual(Array.from(bytes));
});
}
// fflate hands back its own buffer; numcodecs.js does the same, so consumers
// that pass `out` to GZip/Zlib must keep using the return value.
for (const [name, codec] of [
["GZip", GZip.fromConfig({ id: "gzip" })],
["Zlib", Zlib.fromConfig({ id: "zlib" })],
] as const) {
it(`${name} still returns the decoded bytes when given an out buffer`, async () => {
const encoded = await codec.encode(bytes);
const out = new Uint8Array(bytes.length);
const decoded = await codec.decode(encoded, out);
// Asserting the bytes alone would pass under either behavior. Pinning
// the identity is what documents which one callers actually get: the
// divergence from Blosc/LZ4/Zstd above is deliberate parity with
// numcodecs.js, so it should fail here if it ever changes silently.
expect(decoded).not.toBe(out);
expect(Array.from(decoded)).toEqual(Array.from(bytes));
expect(Array.from(out)).toEqual(Array.from(new Uint8Array(bytes.length)));
});
}
});
// A decoder that reports failure by returning nothing is indistinguishable
// from one that succeeded on an empty payload — and with `out` supplied, the
// caller gets their own untouched buffer back and reads it as data. Corrupt
// chunks have to throw, in both call shapes.
describe("corrupt input", () => {
const corrupt = (encoded: Uint8Array) => {
const bad = new Uint8Array(encoded);
// Damage the payload, not the header: a header the guards reject is the
// easy case. This is the one that reaches the decompressor.
for (let i = Math.floor(bad.length / 2); i < bad.length; i++) {
bad[i] ^= 0xff;
}
return bad;
};
const arr = range(1000, "<u4");
const bytes = new Uint8Array((arr as any).buffer);
const codecs = [
["Blosc", Blosc.fromConfig({ id: "blosc", cname: "lz4", clevel: 5, shuffle: 1 })],
["LZ4", LZ4.fromConfig({ id: "lz4" })],
["Zstd", Zstd.fromConfig({ id: "zstd" })],
] as const;
// Truncation is detectable in every format here, because each one states
// up front how many bytes it should produce.
for (const [name, codec] of codecs) {
it(`${name} throws on truncated input`, async () => {
const encoded = await codec.encode(bytes);
const truncated = encoded.slice(0, Math.floor(encoded.length / 2));
await expect(codec.decode(truncated)).rejects.toThrow();
});
}
// Corruption *within* the payload is a different matter, and only LZ4 and
// Zstd are asserted here. A Blosc1 frame carries no checksum over its
// compressed body, so flipped bytes that still parse can decode to the
// declared length and be returned as valid data — see the note below. That
// is a property of the format, not something this layer can detect.
for (const [name, codec] of codecs.filter(([n]) => n !== "Blosc")) {
it(`${name} throws on a corrupt chunk`, async () => {
const bad = corrupt(await codec.encode(bytes));
await expect(codec.decode(bad)).rejects.toThrow();
});
it(`${name} throws on a corrupt chunk rather than returning an untouched out`, async () => {
const bad = corrupt(await codec.encode(bytes));
const out = new Uint8Array(bytes.length);
await expect(codec.decode(bad, out)).rejects.toThrow();
});
}
// Pinned so the limitation above is a recorded fact rather than an
// assumption: if blusc ever does start rejecting this, that is a behavior
// change worth noticing here rather than discovering in the field.
it("Blosc cannot detect payload corruption its format does not checksum", async () => {
const codec = Blosc.fromConfig({ id: "blosc", cname: "lz4", clevel: 5, shuffle: 1 });
const bad = corrupt(await codec.encode(bytes));
const decoded = await codec.decode(bad).catch(() => null);
if (decoded !== null) {
expect(decoded.length).toBe(bytes.length);
expect(Array.from(decoded)).not.toEqual(Array.from(bytes));
}
});
// The flip side: an empty payload is a legitimate thing to encode, and must
// not be mistaken for the empty buffer a failed decode returns.
for (const [name, codec] of codecs) {
it(`${name} round-trips an empty buffer without reporting failure`, async () => {
const encoded = await codec.encode(new Uint8Array(0));
const decoded = await codec.decode(encoded);
expect(decoded.length).toBe(0);
});
}
});
});