Skip to content

Commit f7f2808

Browse files
Dan HudlowDan Hudlowtimostamm
authored
Fix validation for FieldMask (de-)serialization to/from JSON (#1323)
Co-authored-by: Dan Hudlow <hudlow@buf.build> Co-authored-by: Timo Stamm <ts@timostamm.de>
1 parent 8a37beb commit f7f2808

File tree

6 files changed

+46
-16
lines changed

6 files changed

+46
-16
lines changed

packages/bundle-size/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ usually do. We repeat this for an increasing number of files.
1616

1717
| code generator | files | bundle size | minified | compressed |
1818
| ------------------- | ----: | ----------: | --------: | ---------: |
19-
| Protobuf-ES | 1 | 133,010 b | 68,782 b | 15,811 b |
19+
| Protobuf-ES | 1 | 133,010 b | 68,782 b | 15,803 b |
2020
| Protobuf-ES | 4 | 135,199 b | 70,289 b | 16,433 b |
2121
| Protobuf-ES | 8 | 137,961 b | 72,060 b | 16,996 b |
22-
| Protobuf-ES | 16 | 148,411 b | 80,041 b | 19,301 b |
22+
| Protobuf-ES | 16 | 148,411 b | 80,041 b | 19,349 b |
2323
| Protobuf-ES | 32 | 176,202 b | 102,059 b | 24,783 b |
2424
| protobuf-javascript | 1 | 304,887 b | 234,842 b | 35,819 b |
2525
| protobuf-javascript | 4 | 330,904 b | 249,814 b | 37,197 b |

packages/bundle-size/chart.svg

Lines changed: 3 additions & 3 deletions
Loading

packages/protobuf-test/src/reflect/names.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as assert from "node:assert";
1717
import {
1818
safeObjectProperty,
1919
protoCamelCase,
20+
protoSnakeCase,
2021
qualifiedName,
2122
} from "@bufbuild/protobuf/reflect";
2223
import type { DescFile } from "@bufbuild/protobuf";
@@ -155,4 +156,21 @@ void suite("protoCamelCase", () => {
155156
assert.strictEqual(protoCamelCase(name), protocJsonName);
156157
});
157158
}
159+
void suite("irreversible", () => {
160+
testIrreversible("foo_", "foo");
161+
testIrreversible("foo__", "foo");
162+
testIrreversible("foo__bar", "fooBar");
163+
testIrreversible("foo_1", "foo1");
164+
function testIrreversible(snakeCase: string, expectedCamelCase: string) {
165+
void test(`"${snakeCase}" is irreversible`, () => {
166+
assert.strictEqual(protoCamelCase(snakeCase), expectedCamelCase);
167+
assert.notStrictEqual(protoSnakeCase(expectedCamelCase), snakeCase);
168+
});
169+
}
170+
});
171+
});
172+
173+
void test("protoSnakeCase", () => {
174+
assert.strictEqual(protoSnakeCase("fooBar"), "foo_bar");
175+
assert.strictEqual(protoSnakeCase("fooUTF8"), "foo_u_t_f8");
158176
});

packages/protobuf/src/from-json.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
import { reflect } from "./reflect/reflect.js";
3333
import { FieldError, isFieldError } from "./reflect/error.js";
3434
import { formatVal } from "./reflect/reflect-check.js";
35+
import { protoSnakeCase } from "./reflect/names.js";
3536
import type { ScalarValue } from "./reflect/scalar.js";
3637
import type { EnumJsonType, EnumShape, MessageShape } from "./types.js";
3738
import { base64Decode } from "./wire/base64-encoding.js";
@@ -755,16 +756,14 @@ function fieldMaskFromJson(fieldMask: FieldMask, json: JsonValue) {
755756
if (json === "") {
756757
return;
757758
}
758-
function camelToSnake(str: string) {
759-
if (str.includes("_")) {
759+
fieldMask.paths = json.split(",").map((path) => {
760+
if (path.includes("_")) {
760761
throw new Error(
761762
`cannot decode message ${fieldMask.$typeName} from JSON: path names must be lowerCamelCase`,
762763
);
763764
}
764-
const sc = str.replace(/[A-Z]/g, (letter) => "_" + letter.toLowerCase());
765-
return sc[0] === "_" ? sc.substring(1) : sc;
766-
}
767-
fieldMask.paths = json.split(",").map(camelToSnake);
765+
return protoSnakeCase(path);
766+
});
768767
}
769768

770769
function structFromJson(struct: Struct, json: JsonValue) {

packages/protobuf/src/reflect/names.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export function qualifiedName(desc: AnyDesc) {
4747
* used by protoc to convert a field name to a JSON name.
4848
*
4949
* See https://protobuf.com/docs/language-spec#default-json-names
50+
*
51+
* The function protoSnakeCase provides the reverse.
5052
*/
5153
export function protoCamelCase(snakeCase: string): string {
5254
let capNext = false;
@@ -82,6 +84,19 @@ export function protoCamelCase(snakeCase: string): string {
8284
return b.join("");
8385
}
8486

87+
/**
88+
* Converts protoCamelCase to snake_case.
89+
*
90+
* This function is the reverse of function protoCamelCase. Note that some names
91+
* are not reversible - for example, "foo__bar" -> "fooBar" -> "foo_bar".
92+
*/
93+
export function protoSnakeCase(lowerCamelCase: string): string {
94+
return lowerCamelCase.replace(
95+
/[A-Z]/g,
96+
(letter) => "_" + letter.toLowerCase(),
97+
);
98+
}
99+
85100
/**
86101
* Names that cannot be used for object properties because they are reserved
87102
* by built-in JavaScript properties.

packages/protobuf/src/to-json.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
ScalarType,
2121
} from "./descriptors.js";
2222
import type { JsonObject, JsonValue } from "./json-value.js";
23-
import { protoCamelCase } from "./reflect/names.js";
23+
import { protoCamelCase, protoSnakeCase } from "./reflect/names.js";
2424
import { reflect } from "./reflect/reflect.js";
2525
import type { Registry } from "./registry.js";
2626
import type {
@@ -488,11 +488,9 @@ function durationToJson(val: Duration) {
488488
function fieldMaskToJson(val: FieldMask) {
489489
return val.paths
490490
.map((p) => {
491-
if (p.match(/_[0-9]?_/g) || p.match(/[A-Z]/g)) {
491+
if (protoSnakeCase(protoCamelCase(p)) !== p) {
492492
throw new Error(
493-
`cannot encode message ${val.$typeName} to JSON: lowerCamelCase of path name "` +
494-
p +
495-
'" is irreversible',
493+
`cannot encode message ${val.$typeName} to JSON: lowerCamelCase of path name "${p}" is irreversible`,
496494
);
497495
}
498496
return protoCamelCase(p);

0 commit comments

Comments
 (0)