Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport binary encoding from "wire" subpath #885

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions packages/protobuf-test/src/wire/binary-encoding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// Copyright 2021-2024 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { describe, expect, it } from "@jest/globals";
import { BinaryReader, BinaryWriter, WireType } from "@bufbuild/protobuf/wire";
import { User } from "../gen/ts/extra/example_pb.js";

it("wire/BinaryReader and wire/BinaryWriter interop with message classes", () => {
const user = new User({
firstName: "Homer",
active: true,
});
const bytes = user.toBinary({
writerFactory() {
return new BinaryWriter();
},
});
const user2 = User.fromBinary(bytes, {
readerFactory(bytes) {
return new BinaryReader(bytes);
},
});
expect(user2.firstName).toBe("Homer");
expect(user2.active).toBe(true);
});

describe("BinaryWriter", () => {
it("example should work as expected", () => {
const bytes = new BinaryWriter()
// string first_name = 1
.tag(1, WireType.LengthDelimited)
.string("Homer")
// bool active = 3
.tag(3, WireType.Varint)
.bool(true)
.finish();
const user = User.fromBinary(bytes);
expect(user.firstName).toBe("Homer");
expect(user.active).toBe(true);
});
describe("float32()", () => {
it.each([
1024,
3.142,
-3.142,
-1024,
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY,
Number.NaN,
])("should encode %s", (val) => {
const bytes = new BinaryWriter().float(val).finish();
expect(bytes.length).toBeGreaterThan(0);
// @ts-expect-error test string
const bytesStr = new BinaryWriter().float(val.toString()).finish();
expect(bytesStr.length).toBeGreaterThan(0);
expect(bytesStr).toStrictEqual(bytes);
});
it.each([
{ val: null, err: "invalid float32: object" },
{ val: new Date(), err: "invalid float32: object" },
{ val: undefined, err: "invalid float32: undefined" },
{ val: true, err: "invalid float32: bool" },
])("should error for wrong type $val", ({ val, err }) => {
// @ts-expect-error test wrong type
expect(() => new BinaryWriter().float(val)).toThrow(err);
});
it.each([Number.MAX_VALUE, -Number.MAX_VALUE])(
"should error for value out of range %s",
(val) => {
expect(() => new BinaryWriter().float(val)).toThrow(
/^invalid float32: .*/,
);
// @ts-expect-error test string
expect(() => new BinaryWriter().float(val.toString())).toThrow(
/^invalid float32: .*/,
);
},
);
});
// sfixed32, sint32, and int32 are signed 32-bit integers, just with different encoding
describe.each(["sfixed32", "sint32", "int32"] as const)("%s()", (type) => {
it.each([-0x80000000, 1024, 0x7fffffff])("should encode %s", (val) => {
const bytes = new BinaryWriter()[type](val).finish();
expect(bytes.length).toBeGreaterThan(0);
// @ts-expect-error test string
const bytesStr = new BinaryWriter()[type](val.toString()).finish();
expect(bytesStr.length).toBeGreaterThan(0);
expect(bytesStr).toStrictEqual(bytes);
});
it.each([
{ val: null, err: "invalid int32: object" },
{ val: new Date(), err: "invalid int32: object" },
{ val: undefined, err: "invalid int32: undefined" },
{ val: true, err: "invalid int32: bool" },
])("should error for wrong type $val", ({ val, err }) => {
// @ts-expect-error TS2345
expect(() => new BinaryWriter()[type](val)).toThrow(err);
});
it.each([0x7fffffff + 1, -0x80000000 - 1, 3.142])(
"should error for value out of range %s",
(val) => {
expect(() => new BinaryWriter()[type](val)).toThrow(
/^invalid int32: .*/,
);
// @ts-expect-error test string
expect(() => new BinaryWriter()[type](val.toString())).toThrow(
/^invalid int32: .*/,
);
},
);
});
// fixed32 and uint32 are unsigned 32-bit integers, just with different encoding
describe.each(["fixed32", "uint32"] as const)("%s()", (type) => {
it.each([0, 1024, 0xffffffff])("should encode %s", (val) => {
const bytes = new BinaryWriter()[type](val).finish();
expect(bytes.length).toBeGreaterThan(0);
// @ts-expect-error test string
const bytesStr = new BinaryWriter()[type](val.toString()).finish();
expect(bytesStr.length).toBeGreaterThan(0);
expect(bytesStr).toStrictEqual(bytes);
});
it.each([
{ val: null, err: `invalid uint32: object` },
{ val: new Date(), err: `invalid uint32: object` },
{ val: undefined, err: `invalid uint32: undefined` },
{ val: true, err: `invalid uint32: bool` },
])("should error for wrong type $val", ({ val, err }) => {
// @ts-expect-error TS2345
expect(() => new BinaryWriter()[type](val)).toThrow(err);
});
it.each([0xffffffff + 1, -1, 3.142])(
"should error for value out of range %s",
(val) => {
expect(() => new BinaryWriter()[type](val)).toThrow(
/^invalid uint32: .*/,
);
// @ts-expect-error test string
expect(() => new BinaryWriter()[type](val.toString())).toThrow(
/^invalid uint32: .*/,
);
},
);
});
});

describe("BinaryReader", () => {
describe("skip", () => {
it("should skip group", () => {
const reader = new BinaryReader(
new BinaryWriter()
.tag(1, WireType.StartGroup)
.tag(33, WireType.Varint)
.bool(true)
.tag(1, WireType.EndGroup)
.finish(),
);
const [fieldNo, wireType] = reader.tag();
expect(fieldNo).toBe(1);
expect(wireType).toBe(WireType.StartGroup);
reader.skip(WireType.StartGroup, 1);
expect(reader.pos).toBe(reader.len);
});
it("should skip nested group", () => {
const reader = new BinaryReader(
new BinaryWriter()
.tag(1, WireType.StartGroup)
.tag(1, WireType.StartGroup)
.tag(1, WireType.EndGroup)
.tag(1, WireType.EndGroup)
.finish(),
);
const [fieldNo, wireType] = reader.tag();
expect(fieldNo).toBe(1);
expect(wireType).toBe(WireType.StartGroup);
reader.skip(WireType.StartGroup, 1);
expect(reader.pos).toBe(reader.len);
});
it("should error on unexpected end group field number", () => {
const reader = new BinaryReader(
new BinaryWriter()
.tag(1, WireType.StartGroup)
.tag(2, WireType.EndGroup)
.finish(),
);
const [fieldNo, wireType] = reader.tag();
expect(fieldNo).toBe(1);
expect(wireType).toBe(WireType.StartGroup);
expect(() => {
reader.skip(WireType.StartGroup, 1);
}).toThrow(/^invalid end group tag$/);
});
it("should return skipped group data", () => {
const reader = new BinaryReader(
new BinaryWriter()
.tag(1, WireType.StartGroup)
.tag(33, WireType.Varint)
.bool(true)
.tag(1, WireType.EndGroup)
.finish(),
);
reader.tag();
const skipped = reader.skip(WireType.StartGroup, 1);
const sr = new BinaryReader(skipped);
{
const [fieldNo, wireType] = sr.tag();
expect(fieldNo).toBe(33);
expect(wireType).toBe(WireType.Varint);
const bool = sr.bool();
expect(bool).toBe(true);
}
{
const [fieldNo, wireType] = sr.tag();
expect(fieldNo).toBe(1);
expect(wireType).toBe(WireType.EndGroup);
expect(sr.pos).toBe(sr.len);
}
});
});
});
123 changes: 123 additions & 0 deletions packages/protobuf-test/src/wire/text-encoding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2021-2024 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { beforeEach, describe, expect, test } from "@jest/globals";
import {
getTextEncoding,
configureTextEncoding,
} from "@bufbuild/protobuf/wire";
import { afterEach } from "node:test";

describe("getTextEncoding()", () => {
test("returns TextEncoding", () => {
const te = getTextEncoding();
expect(te).toBeDefined();
});
test("returns same TextEncoding", () => {
const te1 = getTextEncoding();
const te2 = getTextEncoding();
expect(te2).toBe(te1);
});
describe("encodeUtf8()", () => {
test("encodes", () => {
const bytes = getTextEncoding().encodeUtf8("hello 🌍");
expect(bytes).toStrictEqual(
new Uint8Array([104, 101, 108, 108, 111, 32, 240, 159, 140, 141]),
);
});
});
describe("decodeUtf8()", () => {
test("decodes", () => {
const text = getTextEncoding().decodeUtf8(
new Uint8Array([104, 101, 108, 108, 111, 32, 240, 159, 140, 141]),
);
expect(text).toBe("hello 🌍");
});
});
describe("checkUtf8()", () => {
test("returns true for valid", () => {
const valid = "🌍";
const ok = getTextEncoding().checkUtf8(valid);
expect(ok).toBe(true);
});
test("returns false for invalid", () => {
const invalid = "🌍".substring(0, 1);
const ok = getTextEncoding().checkUtf8(invalid);
expect(ok).toBe(false);
});
});
});

describe("configureTextEncoding()", () => {
let backup: ReturnType<typeof getTextEncoding>;
beforeEach(() => {
backup = getTextEncoding();
});
afterEach(() => {
configureTextEncoding(backup);
});
test("configures checkUtf8", () => {
configureTextEncoding({
checkUtf8(text: string): boolean {
if (text === "valid") {
return true;
}
return false;
},
decodeUtf8: backup.decodeUtf8,
encodeUtf8: backup.encodeUtf8,
});
expect(getTextEncoding().checkUtf8("valid")).toBe(true);
expect(getTextEncoding().checkUtf8("no valid")).toBe(false);
});
test("configures checkUtf8", () => {
configureTextEncoding({
checkUtf8: backup.checkUtf8,
decodeUtf8: backup.decodeUtf8,
encodeUtf8: backup.encodeUtf8,
});
expect(getTextEncoding().checkUtf8("valid")).toBe(true);
expect(getTextEncoding().checkUtf8("no valid")).toBe(false);
});
test("configures decodeUtf8", () => {
let arg: Uint8Array | undefined;
configureTextEncoding({
checkUtf8: backup.checkUtf8,
decodeUtf8(bytes: Uint8Array): string {
arg = bytes;
return "custom decodeUtf8";
},
encodeUtf8: backup.encodeUtf8,
});
const bytes = new Uint8Array(10);
const text = getTextEncoding().decodeUtf8(bytes);
expect(text).toBe("custom decodeUtf8");
expect(arg).toBe(bytes);
});
test("configures encodeUtf8", () => {
let arg: string | undefined;
configureTextEncoding({
checkUtf8: backup.checkUtf8,
decodeUtf8: backup.decodeUtf8,
encodeUtf8(text: string): Uint8Array {
arg = text;
return new Uint8Array(10);
},
});
expect(getTextEncoding().encodeUtf8("test")).toStrictEqual(
new Uint8Array(10),
);
expect(arg).toBe("test");
});
});
11 changes: 11 additions & 0 deletions packages/protobuf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./wire": {
"import": "./dist/esm/wire/index.js",
"require": "./dist/cjs/wire/index.js"
}
},
"typesVersions": {
"*": {
"wire": [
"./dist/cjs/wire/index.d.ts"
]
}
},
"devDependencies": {
Expand Down
Loading