Skip to content

Commit

Permalink
feat: add bigint input validation (#938)
Browse files Browse the repository at this point in the history
* feat: add bigint input validation

Input validation for too large int64 and uint64 values when using
the compiler option forceLong=bigint.

* fix: test if field isLong for array bigint input validation

* Format.

---------

Co-authored-by: Stephen Haberman <stephen.haberman@gmail.com>
  • Loading branch information
8de2fdb0 and stephenh authored Oct 5, 2023
1 parent 3eb068e commit 0f9b6b1
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 2 deletions.
6 changes: 6 additions & 0 deletions integration/map-bigint-optional/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,15 @@ function createBaseMapBigInt_MapEntry(): MapBigInt_MapEntry {
export const MapBigInt_MapEntry = {
encode(message: MapBigInt_MapEntry, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.key !== BigInt("0")) {
if (BigInt.asUintN(64, message.key) !== message.key) {
throw new Error("value provided for field message.key of type fixed64 too large");
}
writer.uint32(9).fixed64(message.key.toString());
}
if (message.value !== BigInt("0")) {
if (BigInt.asIntN(64, message.value) !== message.value) {
throw new Error("value provided for field message.value of type int64 too large");
}
writer.uint32(16).int64(message.value.toString());
}
if (message._unknownFields !== undefined) {
Expand Down
3 changes: 3 additions & 0 deletions integration/simple-long-bigint/google/protobuf/timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ function createBaseTimestamp(): Timestamp {
export const Timestamp = {
encode(message: Timestamp, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.seconds !== BigInt("0")) {
if (BigInt.asIntN(64, message.seconds) !== message.seconds) {
throw new Error("value provided for field message.seconds of type int64 too large");
}
writer.uint32(8).int64(message.seconds.toString());
}
if (message.nanos !== 0) {
Expand Down
6 changes: 6 additions & 0 deletions integration/simple-long-bigint/google/protobuf/wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ function createBaseInt64Value(): Int64Value {
export const Int64Value = {
encode(message: Int64Value, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.value !== BigInt("0")) {
if (BigInt.asIntN(64, message.value) !== message.value) {
throw new Error("value provided for field message.value of type int64 too large");
}
writer.uint32(8).int64(message.value.toString());
}
return writer;
Expand Down Expand Up @@ -272,6 +275,9 @@ function createBaseUInt64Value(): UInt64Value {
export const UInt64Value = {
encode(message: UInt64Value, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.value !== BigInt("0")) {
if (BigInt.asUintN(64, message.value) !== message.value) {
throw new Error("value provided for field message.value of type uint64 too large");
}
writer.uint32(8).uint64(message.value.toString());
}
return writer;
Expand Down
53 changes: 53 additions & 0 deletions integration/simple-long-bigint/numbers-long-string-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import PbNumbers = pbjs.Numbers;
import UInt64Value = google.protobuf.UInt64Value;
import PbTimestamp = google.protobuf.Timestamp;


// 18_446_744_073_709_551_615n
const MAX_UINT64 = BigInt("18446744073709551615")
// 9_223_372_036_854_775_807n
const MAX_INT64 = BigInt("9223372036854775807")

describe("number", () => {
it("generates types correctly", () => {
const simple: Numbers = {
Expand Down Expand Up @@ -168,4 +174,51 @@ describe("number", () => {
}
`);
});

it('throws error on too large bigints', () => {
const testCases = [
{
failValue: { int64: MAX_INT64 + BigInt("1") },
failMsg: "value provided for field message.int64 of type int64 too large",
passValue: { int64: MAX_INT64 },
},
{
failValue: { uint64: MAX_UINT64 + BigInt("1") },
failMsg: "value provided for field message.uint64 of type uint64 too large",
passValue: { uint64: MAX_UINT64 },
},
{
failValue: { sint64: MAX_INT64 + BigInt("1") },
failMsg: "value provided for field message.sint64 of type sint64 too large",
passValue: { sint64: MAX_INT64 },
},
{
failValue: { fixed64: MAX_UINT64 + BigInt("1") },
failMsg: "value provided for field message.fixed64 of type fixed64 too large",
passValue: { fixed64: MAX_UINT64 },
},
{
failValue: { sfixed64: MAX_INT64 + BigInt("1") },
failMsg: "value provided for field message.sfixed64 of type sfixed64 too large",
passValue: { sfixed64: MAX_INT64 },
},
{
failValue: { guint64: MAX_UINT64 + BigInt("1") },
failMsg: "value provided for field message.value of type uint64 too large",
passValue: { guint64: MAX_UINT64 },
},
{
failValue: { uint64s: [MAX_UINT64 + BigInt("1")] },
failMsg: "a value provided in array field uint64s of type uint64 is too large",
passValue: { uint64s: [MAX_UINT64] },
}
];

for (const testCase of testCases) {
const failValue = Numbers.fromPartial(testCase.failValue);
expect(() => { Numbers.encode(failValue) }).toThrow(testCase.failMsg);
const passValue = Numbers.fromPartial(testCase.passValue);
expect(() => Numbers.encode(passValue).finish()).not.toThrowError();
}
});
});
18 changes: 18 additions & 0 deletions integration/simple-long-bigint/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,30 +56,45 @@ export const Numbers = {
writer.uint32(24).int32(message.int32);
}
if (message.int64 !== BigInt("0")) {
if (BigInt.asIntN(64, message.int64) !== message.int64) {
throw new Error("value provided for field message.int64 of type int64 too large");
}
writer.uint32(32).int64(message.int64.toString());
}
if (message.uint32 !== 0) {
writer.uint32(40).uint32(message.uint32);
}
if (message.uint64 !== BigInt("0")) {
if (BigInt.asUintN(64, message.uint64) !== message.uint64) {
throw new Error("value provided for field message.uint64 of type uint64 too large");
}
writer.uint32(48).uint64(message.uint64.toString());
}
if (message.sint32 !== 0) {
writer.uint32(56).sint32(message.sint32);
}
if (message.sint64 !== BigInt("0")) {
if (BigInt.asIntN(64, message.sint64) !== message.sint64) {
throw new Error("value provided for field message.sint64 of type sint64 too large");
}
writer.uint32(64).sint64(message.sint64.toString());
}
if (message.fixed32 !== 0) {
writer.uint32(77).fixed32(message.fixed32);
}
if (message.fixed64 !== BigInt("0")) {
if (BigInt.asUintN(64, message.fixed64) !== message.fixed64) {
throw new Error("value provided for field message.fixed64 of type fixed64 too large");
}
writer.uint32(81).fixed64(message.fixed64.toString());
}
if (message.sfixed32 !== 0) {
writer.uint32(93).sfixed32(message.sfixed32);
}
if (message.sfixed64 !== BigInt("0")) {
if (BigInt.asIntN(64, message.sfixed64) !== message.sfixed64) {
throw new Error("value provided for field message.sfixed64 of type sfixed64 too large");
}
writer.uint32(97).sfixed64(message.sfixed64.toString());
}
if (message.guint64 !== undefined) {
Expand All @@ -90,6 +105,9 @@ export const Numbers = {
}
writer.uint32(122).fork();
for (const v of message.uint64s) {
if (BigInt.asUintN(64, v) !== v) {
throw new Error("a value provided in array field uint64s of type uint64 is too large");
}
writer.uint64(v.toString());
}
writer.ldelim();
Expand Down
56 changes: 54 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,24 @@ function getEncodeWriteSnippet(ctx: Context, field: FieldDescriptorProto): (plac
return (place) => code`writer.uint32(${tag}).${toReaderCall(field)}(${toNumber}(${place}))`;
} else if (isLong(field) && options.forceLong === LongOption.BIGINT) {
const tag = ((field.number << 3) | basicWireType(field.type)) >>> 0;
return (place) => code`writer.uint32(${tag}).${toReaderCall(field)}(${place}.toString())`;
const fieldType = toReaderCall(field);
switch (fieldType) {
case "int64":
case "sint64":
case "sfixed64":
return (place) => code`if (BigInt.asIntN(64, ${place}) !== ${place}) {
throw new Error('value provided for field ${place} of type ${fieldType} too large');
}
writer.uint32(${tag}).${toReaderCall(field)}(${place}.toString())`;
case "uint64":
case "fixed64":
return (place) => code`if (BigInt.asUintN(64, ${place}) !== ${place}) {
throw new Error('value provided for field ${place} of type ${fieldType} too large');
}
writer.uint32(${tag}).${toReaderCall(field)}(${place}.toString())`;
default:
throw new Error(`unexpected BigInt type: ${fieldType}`);
}
} else if (isScalar(field) || isEnum(field)) {
const tag = ((field.number << 3) | basicWireType(field.type)) >>> 0;
return (place) => code`writer.uint32(${tag}).${toReaderCall(field)}(${place})`;
Expand Down Expand Up @@ -1381,13 +1398,48 @@ function generateEncode(ctx: Context, fullName: string, messageDesc: DescriptorP
// Ideally we'd reuse `writeSnippet` but it has tagging embedded inside of it.
const tag = ((field.number << 3) | 2) >>> 0;
const rhs = (x: string) => (isLong(field) && options.forceLong === LongOption.BIGINT ? `${x}.toString()` : x);
const listWriteSnippet = code`
let listWriteSnippet = code`
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
writer.${toReaderCall(field)}(${rhs("v")});
}
writer.ldelim();
`;

if (isLong(field) && options.forceLong === LongOption.BIGINT) {
const fieldType = toReaderCall(field);
switch (fieldType) {
case "int64":
case "sint64":
case "sfixed64":
listWriteSnippet = code`
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
if (BigInt.asIntN(64, v) !== v) {
throw new Error('a value provided in array field ${fieldName} of type ${fieldType} is too large');
}
writer.${toReaderCall(field)}(${rhs("v")});
}
writer.ldelim();
`;
break;
case "uint64":
case "fixed64":
listWriteSnippet = code`
writer.uint32(${tag}).fork();
for (const v of message.${fieldName}) {
if (BigInt.asUintN(64, v) !== v) {
throw new Error('a value provided in array field ${fieldName} of type ${fieldType} is too large');
}
writer.${toReaderCall(field)}(${rhs("v")});
}
writer.ldelim();
`;
break;
default:
throw new Error(`unexpected BigInt type: ${fieldType}`);
}
}
if (isOptional) {
chunks.push(code`
if (message.${fieldName} !== undefined && message.${fieldName}.length !== 0) {
Expand Down

0 comments on commit 0f9b6b1

Please sign in to comment.