Skip to content

Commit ab6f968

Browse files
committed
array validation
1 parent 2cdf4e5 commit ab6f968

File tree

4 files changed

+217
-42
lines changed

4 files changed

+217
-42
lines changed

src/buffer/base.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
DEFAULT_MAX_BUFFER_SIZE,
1111
} from "./index";
1212
import {
13+
ArrayPrimitive,
1314
isInteger,
14-
getDimensions,
1515
timestampToMicros,
1616
timestampToNanos,
1717
TimestampUnit,
@@ -390,35 +390,29 @@ abstract class SenderBufferBase implements SenderBuffer {
390390
this.position = this.buffer.writeDoubleLE(data, this.position);
391391
}
392392

393-
protected writeArray(arr: unknown[]) {
394-
const dimensions = getDimensions(arr);
393+
protected writeArray(
394+
arr: unknown[],
395+
dimensions: number[],
396+
type: ArrayPrimitive,
397+
) {
395398
this.checkCapacity([], 1 + dimensions.length * 4);
396399
this.writeByte(dimensions.length);
397-
let numOfElements = 1;
398400
for (let i = 0; i < dimensions.length; i++) {
399-
numOfElements *= dimensions[i];
400401
this.writeInt(dimensions[i]);
401402
}
402403

403-
this.checkCapacity([], numOfElements * 8);
404+
this.checkCapacity([], SenderBufferBase.arraySize(dimensions, type));
404405
this.writeArrayValues(arr, dimensions);
405406
}
406407

407408
private writeArrayValues(arr: unknown[], dimensions: number[]) {
408409
if (Array.isArray(arr[0])) {
409-
const length = arr[0].length;
410410
for (let i = 0; i < arr.length; i++) {
411-
const subArray = arr[i] as unknown[];
412-
if (subArray.length !== length) {
413-
throw new Error(
414-
`length does not match array dimensions [dimensions=[${dimensions}], length=${subArray.length}]`,
415-
);
416-
}
417-
this.writeArrayValues(subArray, dimensions);
411+
this.writeArrayValues(arr[i] as unknown[], dimensions);
418412
}
419413
} else {
420-
const dataType = typeof arr[0];
421-
switch (dataType) {
414+
const type = typeof arr[0];
415+
switch (type) {
422416
case "number":
423417
for (let i = 0; i < arr.length; i++) {
424418
this.position = this.buffer.writeDoubleLE(
@@ -428,7 +422,7 @@ abstract class SenderBufferBase implements SenderBuffer {
428422
}
429423
break;
430424
default:
431-
throw new Error(`unsupported array type [type=${dataType}]`);
425+
throw new Error(`Unsupported array type [type=${type}]`);
432426
}
433427
}
434428
}
@@ -470,6 +464,25 @@ abstract class SenderBufferBase implements SenderBuffer {
470464
}
471465
}
472466

467+
private static arraySize(dimensions: number[], type: ArrayPrimitive): number {
468+
let numOfElements = 1;
469+
for (let i = 0; i < dimensions.length; i++) {
470+
numOfElements *= dimensions[i];
471+
}
472+
473+
switch (type) {
474+
case "number":
475+
return numOfElements * 8;
476+
case "boolean":
477+
return numOfElements;
478+
case "string":
479+
// in case of string[] capacity check is done separately for each array element
480+
return 0;
481+
default:
482+
throw new Error(`Unsupported array type [type=${type}]`);
483+
}
484+
}
485+
473486
private assertBufferOverflow() {
474487
if (this.position > this.bufferSize) {
475488
// should never happen, if checkCapacity() is correctly used

src/buffer/bufferv2.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { SenderOptions } from "../options";
33
import { SenderBuffer } from "./index";
44
import { SenderBufferBase } from "./base";
5+
import { getDimensions, validateArray } from "../utils";
56

67
const COLUMN_TYPE_DOUBLE: number = 10;
78
const COLUMN_TYPE_NULL: number = 33;
@@ -32,9 +33,13 @@ class SenderBufferV2 extends SenderBufferBase {
3233
}
3334

3435
arrayColumn(name: string, value: unknown[]): SenderBuffer {
35-
if (value !== null && value !== undefined && !Array.isArray(value)) {
36-
throw new Error(`The value must be an array [value=${JSON.stringify(value)}, type=${typeof value}]`);
36+
const dimensions = getDimensions(value);
37+
const type = validateArray(value, dimensions);
38+
// only number arrays and NULL supported for now
39+
if (type !== "number" && type !== null) {
40+
throw new Error(`Unsupported array type [type=${type}]`);
3741
}
42+
3843
this.writeColumn(name, value, () => {
3944
this.checkCapacity([], 3);
4045
this.writeByte(EQUALS_SIGN);
@@ -44,7 +49,7 @@ class SenderBufferV2 extends SenderBufferBase {
4449
this.writeByte(COLUMN_TYPE_NULL);
4550
} else {
4651
this.writeByte(COLUMN_TYPE_DOUBLE);
47-
this.writeArray(value);
52+
this.writeArray(value, dimensions, type);
4853
}
4954
});
5055
return this;

src/utils.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
type ArrayPrimitive = "number" | "boolean" | "string" | null;
2+
13
type TimestampUnit = "ns" | "us" | "ms";
24

35
function isBoolean(value: unknown): value is boolean {
@@ -36,18 +38,73 @@ function timestampToNanos(timestamp: bigint, unit: TimestampUnit) {
3638
}
3739
}
3840

39-
function getDimensions(arr: unknown) {
41+
function getDimensions(data: unknown) {
4042
const dimensions: number[] = [];
41-
while (Array.isArray(arr)) {
42-
if (arr.length === 0) {
43-
throw new Error("zero length array not supported");
43+
while (Array.isArray(data)) {
44+
if (data.length === 0) {
45+
throw new Error("Zero length array not supported");
4446
}
45-
dimensions.push(arr.length);
46-
arr = arr[0];
47+
dimensions.push(data.length);
48+
data = data[0];
4749
}
4850
return dimensions;
4951
}
5052

53+
function validateArray(data: unknown[], dimensions: number[]): ArrayPrimitive {
54+
if (data === null || data === undefined) {
55+
return null;
56+
}
57+
if (!Array.isArray(data)) {
58+
throw new Error(
59+
`The value must be an array [value=${JSON.stringify(data)}, type=${typeof data}]`,
60+
);
61+
}
62+
63+
let expectedType: ArrayPrimitive = null;
64+
65+
function checkArray(
66+
array: unknown[],
67+
depth: number = 0,
68+
path: string = "",
69+
): void {
70+
const expectedLength = dimensions[depth];
71+
if (array.length !== expectedLength) {
72+
throw new Error(
73+
`Length of arrays do not match [expected=${expectedLength}, actual=${array.length}, dimensions=[${dimensions}], path=${path}]`,
74+
);
75+
}
76+
77+
if (depth < dimensions.length - 1) {
78+
// intermediate level, expecting arrays
79+
for (let i = 0; i < array.length; i++) {
80+
if (!Array.isArray(array[i])) {
81+
throw new Error(
82+
`Mixed types found [expected=array, current=${typeof array[i]}, path=${path}[${i}]]`,
83+
);
84+
}
85+
checkArray(array[i] as unknown[], depth + 1, `${path}[${i}]`);
86+
}
87+
} else {
88+
// leaf level, expecting primitives
89+
if (expectedType === null) {
90+
expectedType = typeof array[0] as ArrayPrimitive;
91+
}
92+
93+
for (let i = 0; i < array.length; i++) {
94+
const currentType = typeof array[i] as ArrayPrimitive;
95+
if (currentType !== expectedType) {
96+
throw new Error(
97+
`Mixed types found [expected=${expectedType}, current=${currentType}, path=${path}[${i}]]`,
98+
);
99+
}
100+
}
101+
}
102+
}
103+
104+
checkArray(data);
105+
return expectedType;
106+
}
107+
51108
async function fetchJson<T>(url: string): Promise<T> {
52109
let response: globalThis.Response;
53110
try {
@@ -72,4 +129,6 @@ export {
72129
TimestampUnit,
73130
fetchJson,
74131
getDimensions,
132+
validateArray,
133+
ArrayPrimitive,
75134
};

test/sender.buffer.test.ts

Lines changed: 114 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -227,25 +227,101 @@ describe("Sender message builder test suite (anything not covered in client inte
227227
});
228228
sender.table("tableName");
229229
expect(() => sender.arrayColumn("arrayCol", [])).toThrow(
230-
"zero length array not supported",
230+
"Zero length array not supported",
231231
);
232232
expect(() => sender.arrayColumn("arrayCol", [[], []])).toThrow(
233-
"zero length array not supported",
233+
"Zero length array not supported",
234234
);
235235
await sender.close();
236236
});
237237

238-
it("does not accept irregular array", async function () {
238+
it("does not accept irregularly sized array", async function () {
239239
const sender = new Sender({
240240
protocol: "tcp",
241241
protocol_version: "2",
242242
host: "host",
243243
init_buf_size: 1024,
244244
});
245245
expect(() =>
246-
sender.table("tableName").arrayColumn("arrayCol", [[1.1, 2.2], [3.3]]),
246+
sender.table("tableName").arrayColumn("arrayCol", [
247+
[
248+
[1.1, 2.2],
249+
[3.3, 4.4],
250+
[5.5, 6.6],
251+
],
252+
[
253+
[1.1, 2.2],
254+
[3.3, 4.4],
255+
[5.5, 6.6],
256+
],
257+
[
258+
[1.1, 2.2],
259+
[3.3, 4.4],
260+
[5.5, 6.6],
261+
],
262+
[[1.1, 2.2], [3.3], [5.5, 6.6]],
263+
]),
247264
).toThrow(
248-
"length does not match array dimensions [dimensions=[2,2], length=1]",
265+
"Length of arrays do not match [expected=2, actual=1, dimensions=[4,3,2], path=[3][1]]",
266+
);
267+
await sender.close();
268+
});
269+
270+
it("does not accept non-homogenous array", async function () {
271+
const sender = new Sender({
272+
protocol: "tcp",
273+
protocol_version: "2",
274+
host: "host",
275+
init_buf_size: 1024,
276+
});
277+
sender.table("tableName");
278+
expect(() =>
279+
sender.arrayColumn("arrayCol", [
280+
[
281+
[1.1, 2.2],
282+
[3.3, 4.4],
283+
[5.5, 6.6],
284+
],
285+
[
286+
[1.1, 2.2],
287+
[3.3, 4.4],
288+
[5.5, 6.6],
289+
],
290+
[
291+
[1.1, 2.2],
292+
[3.3, 4.4],
293+
[5.5, 6.6],
294+
],
295+
[
296+
[1.1, 2.2],
297+
[3.3, "4.4"],
298+
[5.5, 6.6],
299+
],
300+
]),
301+
).toThrow(
302+
"Mixed types found [expected=number, current=string, path=[3][1][1]]",
303+
);
304+
expect(() =>
305+
sender.arrayColumn("arrayCol", [
306+
[
307+
[1.1, 2.2],
308+
[3.3, 4.4],
309+
[5.5, 6.6],
310+
],
311+
[
312+
[1.1, 2.2],
313+
[3.3, 4.4],
314+
[5.5, 6.6],
315+
],
316+
[
317+
[1.1, 2.2],
318+
[3.3, 4.4],
319+
[5.5, 6.6],
320+
],
321+
[[1.1, 2.2], 3.3, [5.5, 6.6]],
322+
]),
323+
).toThrow(
324+
"Mixed types found [expected=array, current=number, path=[3][1]]",
249325
);
250326
await sender.close();
251327
});
@@ -258,11 +334,21 @@ describe("Sender message builder test suite (anything not covered in client inte
258334
init_buf_size: 1024,
259335
});
260336
sender.table("tableName");
261-
expect(() => sender.arrayColumn("col", ['str'])).toThrow("unsupported array type [type=string]");
262-
expect(() => sender.arrayColumn("col", [true])).toThrow("unsupported array type [type=boolean]");
263-
expect(() => sender.arrayColumn("col", [{}])).toThrow("unsupported array type [type=object]");
264-
expect(() => sender.arrayColumn("col", [null])).toThrow("unsupported array type [type=object]");
265-
expect(() => sender.arrayColumn("col", [undefined])).toThrow("unsupported array type [type=undefined]");
337+
expect(() => sender.arrayColumn("col", ["str"])).toThrow(
338+
"Unsupported array type [type=string]",
339+
);
340+
expect(() => sender.arrayColumn("col", [true])).toThrow(
341+
"Unsupported array type [type=boolean]",
342+
);
343+
expect(() => sender.arrayColumn("col", [{}])).toThrow(
344+
"Unsupported array type [type=object]",
345+
);
346+
expect(() => sender.arrayColumn("col", [null])).toThrow(
347+
"Unsupported array type [type=object]",
348+
);
349+
expect(() => sender.arrayColumn("col", [undefined])).toThrow(
350+
"Unsupported array type [type=undefined]",
351+
);
266352
await sender.close();
267353
});
268354

@@ -275,17 +361,29 @@ describe("Sender message builder test suite (anything not covered in client inte
275361
});
276362
sender.table("tableName");
277363
// @ts-expect-error - Testing invalid input
278-
expect(() => sender.arrayColumn("col", 12.345)).toThrow("The value must be an array [value=12.345, type=number]");
364+
expect(() => sender.arrayColumn("col", 12.345)).toThrow(
365+
"The value must be an array [value=12.345, type=number]",
366+
);
279367
// @ts-expect-error - Testing invalid input
280-
expect(() => sender.arrayColumn("col", 42)).toThrow("The value must be an array [value=42, type=number]");
368+
expect(() => sender.arrayColumn("col", 42)).toThrow(
369+
"The value must be an array [value=42, type=number]",
370+
);
281371
// @ts-expect-error - Testing invalid input
282-
expect(() => sender.arrayColumn("col", "str")).toThrow("The value must be an array [value=\"str\", type=string]");
372+
expect(() => sender.arrayColumn("col", "str")).toThrow(
373+
'The value must be an array [value="str", type=string]',
374+
);
283375
// @ts-expect-error - Testing invalid input
284-
expect(() => sender.arrayColumn("col", "")).toThrow("The value must be an array [value=\"\", type=string]");
376+
expect(() => sender.arrayColumn("col", "")).toThrow(
377+
'The value must be an array [value="", type=string]',
378+
);
285379
// @ts-expect-error - Testing invalid input
286-
expect(() => sender.arrayColumn("col", true)).toThrow("The value must be an array [value=true, type=boolean]");
380+
expect(() => sender.arrayColumn("col", true)).toThrow(
381+
"The value must be an array [value=true, type=boolean]",
382+
);
287383
// @ts-expect-error - Testing invalid input
288-
expect(() => sender.arrayColumn("col", {})).toThrow("The value must be an array [value={}, type=object]");
384+
expect(() => sender.arrayColumn("col", {})).toThrow(
385+
"The value must be an array [value={}, type=object]",
386+
);
289387
await sender.close();
290388
});
291389

0 commit comments

Comments
 (0)