Skip to content

Commit afeccd7

Browse files
fix(parse): reject numeric strings with non-numbers (#2729)
* fix(parse): reject numeric strings with non-numbers This updates the number parsing utilities to reject strings like "1A", which `parseFloat` would ordinarily happily accept as the value `1`. * chore: use more compact regex
1 parent 05a7701 commit afeccd7

File tree

2 files changed

+120
-30
lines changed

2 files changed

+120
-30
lines changed

packages/smithy-client/src/parse-utils.spec.ts

+98-10
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,44 @@ describe("strictParseDouble", () => {
320320
expect(strictParseDouble("NaN")).toEqual(NaN);
321321
});
322322

323-
it("rejects implicit NaN", () => {
324-
expect(() => strictParseDouble("foo")).toThrowError();
323+
describe("rejects implicit NaN", () => {
324+
it.each([
325+
"foo",
326+
"123ABC",
327+
"ABC123",
328+
"12AB3C",
329+
"1.A",
330+
"1.1A",
331+
"1.1A1",
332+
"0xFF",
333+
"0XFF",
334+
"0b1111",
335+
"0B1111",
336+
"0777",
337+
"0o777",
338+
"0O777",
339+
"1n",
340+
"1N",
341+
"1_000",
342+
"e",
343+
"e1",
344+
".1",
345+
])("rejects %s", (value) => {
346+
expect(() => strictParseDouble(value)).toThrowError();
347+
});
325348
});
326349

327350
it("accepts numeric strings", () => {
328351
expect(strictParseDouble("1")).toEqual(1);
352+
expect(strictParseDouble("-1")).toEqual(-1);
329353
expect(strictParseDouble("1.1")).toEqual(1.1);
354+
expect(strictParseDouble("1e1")).toEqual(10);
355+
expect(strictParseDouble("-1e1")).toEqual(-10);
356+
expect(strictParseDouble("1e+1")).toEqual(10);
357+
expect(strictParseDouble("1e-1")).toEqual(0.1);
358+
expect(strictParseDouble("1E1")).toEqual(10);
359+
expect(strictParseDouble("1E+1")).toEqual(10);
360+
expect(strictParseDouble("1E-1")).toEqual(0.1);
330361
});
331362

332363
describe("accepts numbers", () => {
@@ -347,8 +378,31 @@ describe("strictParseFloat32", () => {
347378
expect(strictParseFloat32("NaN")).toEqual(NaN);
348379
});
349380

350-
it("rejects implicit NaN", () => {
351-
expect(() => strictParseFloat32("foo")).toThrowError();
381+
describe("rejects implicit NaN", () => {
382+
it.each([
383+
"foo",
384+
"123ABC",
385+
"ABC123",
386+
"12AB3C",
387+
"1.A",
388+
"1.1A",
389+
"1.1A1",
390+
"0xFF",
391+
"0XFF",
392+
"0b1111",
393+
"0B1111",
394+
"0777",
395+
"0o777",
396+
"0O777",
397+
"1n",
398+
"1N",
399+
"1_000",
400+
"e",
401+
"e1",
402+
".1",
403+
])("rejects %s", (value) => {
404+
expect(() => strictParseFloat32(value)).toThrowError();
405+
});
352406
});
353407

354408
describe("rejects doubles", () => {
@@ -359,7 +413,15 @@ describe("strictParseFloat32", () => {
359413

360414
it("accepts numeric strings", () => {
361415
expect(strictParseFloat32("1")).toEqual(1);
416+
expect(strictParseFloat32("-1")).toEqual(-1);
362417
expect(strictParseFloat32("1.1")).toEqual(1.1);
418+
expect(strictParseFloat32("1e1")).toEqual(10);
419+
expect(strictParseFloat32("-1e1")).toEqual(-10);
420+
expect(strictParseFloat32("1e+1")).toEqual(10);
421+
expect(strictParseFloat32("1e-1")).toEqual(0.1);
422+
expect(strictParseFloat32("1E1")).toEqual(10);
423+
expect(strictParseFloat32("1E+1")).toEqual(10);
424+
expect(strictParseFloat32("1E-1")).toEqual(0.1);
363425
});
364426

365427
describe("accepts numbers", () => {
@@ -489,12 +551,26 @@ describe("strictParseLong", () => {
489551
});
490552

491553
describe("rejects non-integers", () => {
492-
it.each([1.1, "1.1", "NaN", "Infinity", "-Infinity", NaN, Infinity, -Infinity, true, false, [], {}])(
493-
"rejects %s",
494-
(value) => {
495-
expect(() => strictParseLong(value as any)).toThrowError();
496-
}
497-
);
554+
it.each([
555+
1.1,
556+
"1.1",
557+
"NaN",
558+
"Infinity",
559+
"-Infinity",
560+
NaN,
561+
Infinity,
562+
-Infinity,
563+
true,
564+
false,
565+
[],
566+
{},
567+
"foo",
568+
"123ABC",
569+
"ABC123",
570+
"12AB3C",
571+
])("rejects %s", (value) => {
572+
expect(() => strictParseLong(value as any)).toThrowError();
573+
});
498574
});
499575
});
500576

@@ -530,6 +606,10 @@ describe("strictParseInt32", () => {
530606
-(2 ** 63 + 1),
531607
2 ** 31,
532608
-(2 ** 31 + 1),
609+
"foo",
610+
"123ABC",
611+
"ABC123",
612+
"12AB3C",
533613
])("rejects %s", (value) => {
534614
expect(() => strictParseInt32(value as any)).toThrowError();
535615
});
@@ -570,6 +650,10 @@ describe("strictParseShort", () => {
570650
-(2 ** 31 + 1),
571651
2 ** 15,
572652
-(2 ** 15 + 1),
653+
"foo",
654+
"123ABC",
655+
"ABC123",
656+
"12AB3C",
573657
])("rejects %s", (value) => {
574658
expect(() => strictParseShort(value as any)).toThrowError();
575659
});
@@ -612,6 +696,10 @@ describe("strictParseByte", () => {
612696
-(2 ** 15 + 1),
613697
128,
614698
-129,
699+
"foo",
700+
"123ABC",
701+
"ABC123",
702+
"12AB3C",
615703
])("rejects %s", (value) => {
616704
expect(() => strictParseByte(value as any)).toThrowError();
617705
});

packages/smithy-client/src/parse-utils.ts

+22-20
Original file line numberDiff line numberDiff line change
@@ -233,15 +233,8 @@ export const expectString = (value: any): string | undefined => {
233233
* @returns The value as a number, or undefined if it's null/undefined.
234234
*/
235235
export const strictParseDouble = (value: string | number): number | undefined => {
236-
if (value === "NaN") {
237-
return NaN;
238-
}
239236
if (typeof value == "string") {
240-
const parsed: number = parseFloat(value);
241-
if (Number.isNaN(parsed)) {
242-
throw new TypeError(`Expected real number, got implicit NaN`);
243-
}
244-
return expectNumber(parsed);
237+
return expectNumber(parseNumber(value));
245238
}
246239
return expectNumber(value);
247240
};
@@ -262,19 +255,28 @@ export const strictParseFloat = strictParseDouble;
262255
* @returns The value as a number, or undefined if it's null/undefined.
263256
*/
264257
export const strictParseFloat32 = (value: string | number): number | undefined => {
265-
if (value === "NaN") {
266-
return NaN;
267-
}
268258
if (typeof value == "string") {
269-
const parsed: number = parseFloat(value);
270-
if (Number.isNaN(parsed)) {
271-
throw new TypeError(`Expected real number, got implicit NaN`);
272-
}
273-
return expectFloat32(parsed);
259+
return expectFloat32(parseNumber(value));
274260
}
275261
return expectFloat32(value);
276262
};
277263

264+
// This regex matches JSON-style numbers. In short:
265+
// * The integral may start with a negative sign, but not a positive one
266+
// * No leading 0 on the integral unless it's immediately followed by a '.'
267+
// * Exponent indicated by a case-insensitive 'E' optionally followed by a
268+
// positive/negative sign and some number of digits.
269+
// It also matches both positive and negative infinity as well and explicit NaN.
270+
const NUMBER_REGEX = /(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)|(-?Infinity)|(NaN)/g;
271+
272+
const parseNumber = (value: string): number => {
273+
const matches = value.match(NUMBER_REGEX);
274+
if (matches === null || matches[0].length !== value.length) {
275+
throw new TypeError(`Expected real number, got implicit NaN`);
276+
}
277+
return parseFloat(value);
278+
};
279+
278280
/**
279281
* Asserts a value is a number and returns it. If the value is a string
280282
* representation of a non-numeric number type (NaN, Infinity, -Infinity),
@@ -346,7 +348,7 @@ export const strictParseLong = (value: string | number): number | undefined => {
346348
if (typeof value === "string") {
347349
// parseInt can't be used here, because it will silently discard any
348350
// existing decimals. We want to instead throw an error if there are any.
349-
return expectLong(parseFloat(value));
351+
return expectLong(parseNumber(value));
350352
}
351353
return expectLong(value);
352354
};
@@ -370,7 +372,7 @@ export const strictParseInt32 = (value: string | number): number | undefined =>
370372
if (typeof value === "string") {
371373
// parseInt can't be used here, because it will silently discard any
372374
// existing decimals. We want to instead throw an error if there are any.
373-
return expectInt32(parseFloat(value));
375+
return expectInt32(parseNumber(value));
374376
}
375377
return expectInt32(value);
376378
};
@@ -389,7 +391,7 @@ export const strictParseShort = (value: string | number): number | undefined =>
389391
if (typeof value === "string") {
390392
// parseInt can't be used here, because it will silently discard any
391393
// existing decimals. We want to instead throw an error if there are any.
392-
return expectShort(parseFloat(value));
394+
return expectShort(parseNumber(value));
393395
}
394396
return expectShort(value);
395397
};
@@ -408,7 +410,7 @@ export const strictParseByte = (value: string | number): number | undefined => {
408410
if (typeof value === "string") {
409411
// parseInt can't be used here, because it will silently discard any
410412
// existing decimals. We want to instead throw an error if there are any.
411-
return expectByte(parseFloat(value));
413+
return expectByte(parseNumber(value));
412414
}
413415
return expectByte(value);
414416
};

0 commit comments

Comments
 (0)