Skip to content

Commit

Permalink
Fix Any unpacking to verify types (#1023)
Browse files Browse the repository at this point in the history
  • Loading branch information
timostamm authored Dec 2, 2024
1 parent 70eb682 commit eee4072
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 54 deletions.
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

0 comments on commit eee4072

Please sign in to comment.