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

Fix Any unpacking to verify types #1023

Merged
merged 2 commits into from
Dec 2, 2024
Merged
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
208 changes: 156 additions & 52 deletions packages/protobuf-test/src/wkt/any.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,86 +13,190 @@
// limitations under the License.

import { describe, expect, test } from "@jest/globals";
import {
create,
createRegistry,
type Message,
toBinary,
} from "@bufbuild/protobuf";
import {
AnySchema,
anyIs,
anyPack,
anyUnpack,
ValueSchema,
type FieldMask,
anyUnpackTo,
FieldMaskSchema,
DurationSchema,
} from "@bufbuild/protobuf/wkt";
import type { Value } from "@bufbuild/protobuf/wkt";
import { create, createRegistry } from "@bufbuild/protobuf";

describe("google.protobuf.Any", () => {
test(`is correctly identifies by message and type name`, () => {
const val = create(ValueSchema, {
kind: { case: "numberValue", value: 1 },
describe("anyIs", () => {
test(`matches standard type URL`, () => {
const any = create(AnySchema, {
typeUrl: "type.googleapis.com/google.protobuf.Value",
});
const any = anyPack(ValueSchema, val);

expect(anyIs(any, ValueSchema)).toBe(true);
expect(anyIs(any, ValueSchema.typeName)).toBe(true);

// The typeUrl set in the Any doesn't have to start with a URL prefix
expect(anyIs(any, "type.googleapis.com/google.protobuf.Value")).toBe(false);
});

test(`matches type name with leading slash`, () => {
test(`matches short type URL`, () => {
const any = create(AnySchema, { typeUrl: "/google.protobuf.Value" });
expect(anyIs(any, ValueSchema)).toBe(true);
});

test(`is returns false for an empty Any`, () => {
test(`matches custom type URL`, () => {
const any = create(AnySchema, {
typeUrl: "example.com/google.protobuf.Value",
});
expect(anyIs(any, ValueSchema)).toBe(true);
});
test("accepts type name string", () => {
const any = create(AnySchema, { typeUrl: "/google.protobuf.Value" });
expect(anyIs(any, "google.protobuf.Value")).toBe(true);
expect(anyIs(any, "google.protobuf.Duration")).toBe(false);
});
test("accepts empty type name string", () => {
const any = create(AnySchema, { typeUrl: "/google.protobuf.Value" });
expect(anyIs(any, "")).toBe(false);
expect(anyIs(create(AnySchema), "")).toBe(false);
});
test("returns false for an empty Any", () => {
const any = create(AnySchema);

expect(anyIs(any, ValueSchema)).toBe(false);
expect(anyIs(any, "google.protobuf.Value")).toBe(false);
expect(anyIs(any, "")).toBe(false);
});

test(`unpack correctly unpacks a message in the registry`, () => {
const typeRegistry = createRegistry(ValueSchema);
const val = create(ValueSchema, {
kind: { case: "numberValue", value: 1 },
test("returns false for different type", () => {
const any = create(AnySchema, {
typeUrl: "type.googleapis.com/google.protobuf.Value",
});
const any = anyPack(ValueSchema, val);

const unpacked = anyUnpack(any, typeRegistry) as Value;

expect(unpacked).toBeDefined();
expect(unpacked.kind.case).toBe("numberValue");
expect(unpacked.kind.value).toBe(1);
expect(anyIs(any, DurationSchema)).toBe(false);
expect(anyIs(any, "google.protobuf.Duration")).toBe(false);
});
});

test(`unpack correctly unpacks a message with a leading slash type url in the registry`, () => {
const typeRegistry = createRegistry(ValueSchema);
const val = create(ValueSchema, {
kind: { case: "numberValue", value: 1 },
describe("anyUnpack()", () => {
describe("with a schema", () => {
test("returns undefined if the Any is empty", () => {
const any = create(AnySchema, {
typeUrl: "",
value: new Uint8Array(),
});
const unpacked: FieldMask | undefined = anyUnpack(any, FieldMaskSchema);
expect(unpacked).toBeUndefined();
});
test("returns undefined if the Any contains a different type", () => {
const any = create(AnySchema, {
typeUrl: "type.googleapis.com/google.protobuf.Duration",
value: toBinary(
DurationSchema,
create(DurationSchema, {
seconds: BigInt(100),
}),
),
});
const unpacked: FieldMask | undefined = anyUnpack(any, FieldMaskSchema);
expect(unpacked).toBeUndefined();
});
test("returns unpacked", () => {
const val = create(FieldMaskSchema, {
paths: ["foo"],
});
const any = create(AnySchema, {
typeUrl: "type.googleapis.com/google.protobuf.FieldMask",
value: toBinary(FieldMaskSchema, val),
});
const unpacked: FieldMask | undefined = anyUnpack(any, FieldMaskSchema);
expect(unpacked).toBeDefined();
expect(unpacked?.paths).toStrictEqual(["foo"]);
});
const { value } = anyPack(ValueSchema, val);
const any = create(AnySchema, { typeUrl: "/google.protobuf.Value", value });

const unpacked = anyUnpack(any, typeRegistry) as Value;

expect(unpacked).toBeDefined();
expect(unpacked.kind.case).toBe("numberValue");
expect(unpacked.kind.value).toBe(1);
});

test(`unpack returns undefined if message not in the registry`, () => {
const typeRegistry = createRegistry();
const val = create(ValueSchema, {
kind: { case: "numberValue", value: 1 },
describe("with a registry", () => {
test("returns undefined if the Any is empty", () => {
const any = create(AnySchema);
const unpacked: Message | undefined = anyUnpack(any, createRegistry());
expect(unpacked).toBeUndefined();
});
test(`returns undefined if message not in the registry`, () => {
const registry = createRegistry();
const val = create(ValueSchema, {
kind: { case: "numberValue", value: 1 },
});
const any = anyPack(ValueSchema, val);
const unpacked = anyUnpack(any, registry);
expect(unpacked).toBeUndefined();
});
test(`returns unpacked`, () => {
const typeRegistry = createRegistry(ValueSchema);
const val = create(ValueSchema, {
kind: { case: "numberValue", value: 1 },
});
const any = anyPack(ValueSchema, val);
const unpacked: Message | undefined = anyUnpack(any, typeRegistry);
expect(unpacked).toStrictEqual(val);
});
const any = anyPack(ValueSchema, val);
const unpacked = anyUnpack(any, typeRegistry);
expect(unpacked).toBeUndefined();
});
});

test(`unpack returns undefined with an empty Any`, () => {
const typeRegistry = createRegistry(ValueSchema);
describe("anyUnpackTo()", () => {
test("returns undefined if the Any is empty", () => {
const any = create(AnySchema);
const unpacked = anyUnpack(any, typeRegistry);
const unpacked: FieldMask | undefined = anyUnpackTo(
any,
FieldMaskSchema,
create(FieldMaskSchema),
);
expect(unpacked).toBeUndefined();
});
test("returns undefined if the Any contains a different type", () => {
const any = create(AnySchema, {
typeUrl: "type.googleapis.com/google.protobuf.Duration",
value: toBinary(
DurationSchema,
create(DurationSchema, {
seconds: BigInt(100),
}),
),
});
const unpacked: FieldMask | undefined = anyUnpackTo(
any,
FieldMaskSchema,
create(FieldMaskSchema),
);
expect(unpacked).toBeUndefined();
});
test("returns unpacked", () => {
const val = create(FieldMaskSchema, {
paths: ["foo"],
});
const any = create(AnySchema, {
typeUrl: "type.googleapis.com/google.protobuf.FieldMask",
value: toBinary(FieldMaskSchema, val),
});
const unpacked: FieldMask | undefined = anyUnpackTo(
any,
FieldMaskSchema,
create(FieldMaskSchema),
);
expect(unpacked).toBeDefined();
expect(unpacked?.paths).toStrictEqual(["foo"]);
});
test("merges into target", () => {
const val = create(FieldMaskSchema, {
paths: ["foo"],
});
const any = create(AnySchema, {
typeUrl: "type.googleapis.com/google.protobuf.FieldMask",
value: toBinary(FieldMaskSchema, val),
});
const target = create(FieldMaskSchema, {
paths: ["bar"],
});
const unpacked: FieldMask | undefined = anyUnpackTo(
any,
FieldMaskSchema,
target,
);
expect(unpacked).toBeDefined();
expect(unpacked?.paths).toStrictEqual(["bar", "foo"]);
expect(target.paths).toStrictEqual(["bar", "foo"]);
});
});
4 changes: 2 additions & 2 deletions packages/protobuf/src/wkt/any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function anyUnpack(
registryOrMessageDesc.kind == "message"
? registryOrMessageDesc
: registryOrMessageDesc.getMessage(typeUrlToName(any.typeUrl));
if (!desc) {
if (!desc || !anyIs(any, desc)) {
return undefined;
}
return fromBinary(desc, any.value);
Expand All @@ -119,7 +119,7 @@ export function anyUnpackTo<Desc extends DescMessage>(
schema: Desc,
message: MessageShape<Desc>,
) {
if (any.typeUrl === "") {
if (!anyIs(any, schema)) {
return undefined;
}
return mergeFromBinary(schema, message, any.value);
Expand Down