Skip to content

Commit df64a92

Browse files
merge from upstream
Signed-off-by: Anthony Petrov <anthony@swirldslabs.com>
2 parents 4b876bc + 756dcbb commit df64a92

13 files changed

Lines changed: 266 additions & 65 deletions

File tree

pbj-core/hiero-dependency-versions/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ dependencies.constraints {
6060
api("com.google.protobuf:protobuf-java:$protobuf") { because("com.google.protobuf") }
6161
api("com.google.protobuf:protobuf-java-util:$protobuf") { because("com.google.protobuf.util") }
6262
api("net.bytebuddy:byte-buddy:1.17.6") { because("net.bytebuddy") }
63-
api("org.assertj:assertj-core:3.27.3") { because("org.assertj.core") }
63+
api("org.assertj:assertj-core:3.27.6") { because("org.assertj.core") }
6464
api("org.junit.jupiter:junit-jupiter-api:$junit5") { because("org.junit.jupiter.api") }
6565
api("org.junit.jupiter:junit-jupiter-engine:$junit5") { because("org.junit.jupiter.engine") }
6666

pbj-core/pbj-compiler/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,15 @@ immutable.
142142
### Generated Unit Tests
143143

144144
For each generated model object there is a unit test generated that tests the protobuf and JSON codecs against the
145-
*protoc* generated code to make sure they are 100% byte for byte binary compatible.
145+
*protoc* generated code to make sure they are 100% byte for byte binary compatible. This requires a dependency on
146+
Google Protobuf libraries which may not be always desirable. To disable tests generation and avoid having to add
147+
a dependency on Google Protobuf libraries, add the following configuration to your `build.gradle.kts`:
148+
149+
```kotlin
150+
pbj {
151+
generateTestClasses = false
152+
}
153+
```
146154

147155
## Generated Code Formatting
148156

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/Field.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ public interface Field {
2323
/** Annotation to add to fields that can't be set to null */
2424
String NON_NULL_ANNOTATION = "@NonNull";
2525

26-
/** The default maximum size of a repeated or length-encoded field (Bytes, String, Message, etc.).
27-
* The size should not be increased beyond the current limit because of the safety concerns.
28-
*/
29-
long DEFAULT_MAX_SIZE = 2 * 1024 * 1024;
30-
3126
/**
3227
* Is this field a repeated field. Repeated fields are lists of values rather than a single value.
3328
*
@@ -38,9 +33,14 @@ public interface Field {
3833
/**
3934
* Returns the field's max size relevant to repeated or length-encoded fields.
4035
* The returned value has no meaning for scalar fields (BOOL, INT, etc.).
36+
* A negative value means that the parser is free to enforce any generic limit it may be using for all fields.
37+
* A non-negative value would override the generic limit used by the parser for this particular field.
38+
* Note that PBJ currently doesn't support setting maxSize for individual fields,
39+
* so currently the method returns -1 and the parser always uses the generic limit
40+
* (see `Codec.parse(..., int maxSize)` for details.)
4141
*/
4242
default long maxSize() {
43-
return DEFAULT_MAX_SIZE;
43+
return -1;
4444
}
4545

4646
/**

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/generators/TestGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ private static String generateTestMethod(final String modelClassName, final Stri
421421
assertEquals(charBuffer2, charBuffer);
422422
423423
// Test JSON Reading
424-
final $modelClassName jsonReadPbj = $modelClassName.JSON.parse(JsonTools.parseJson(charBuffer), false, Integer.MAX_VALUE);
424+
final $modelClassName jsonReadPbj = $modelClassName.JSON.parse(JsonTools.parseJson(charBuffer), false, Integer.MAX_VALUE, Integer.MAX_VALUE);
425425
assertEquals(modelObj, jsonReadPbj);
426426
}
427427

pbj-core/pbj-compiler/src/main/java/com/hedera/pbj/compiler/impl/generators/json/JsonCodecParseMethodGenerator.java

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,26 @@ static String generateParseObjectMethod(final String modelClassName, final List<
4949
/**
5050
* Parses a HashObject object from JSON parse tree for object JSONParser.ObjContext.
5151
* Throws an UnknownFieldException wrapped in a ParseException if in strict mode ONLY.
52+
* <p>
53+
* The {@code maxSize} specifies a custom value for the default `Codec.DEFAULT_MAX_SIZE` limit. IMPORTANT:
54+
* specifying a value larger than the default one can put the application at risk because a maliciously-crafted
55+
* payload can cause the parser to allocate too much memory which can result in OutOfMemory and/or crashes.
56+
* It's important to carefully estimate the maximum size limit that a particular protobuf model type should support,
57+
* and then pass that value as a parameter. Note that the estimated limit should apply to the **type** as a whole,
58+
* rather than to individual instances of the model. In other words, this value should be a constant, or a config
59+
* value that is controlled by the application, rather than come from the input that the application reads.
60+
* When in doubt, use the other overloaded versions of this method that use the default `Codec.DEFAULT_MAX_SIZE`.
5261
*
5362
* @param root The JSON parsed object tree to parse data from
63+
* @param maxSize a ParseException will be thrown if the size of a delimited field exceeds the limit
5464
* @return Parsed HashObject model object or null if data input was null or empty
5565
* @throws ParseException If parsing fails
5666
*/
5767
public @NonNull $modelClassName parse(
5868
@Nullable final JSONParser.ObjContext root,
5969
final boolean strictMode,
60-
final int maxDepth) throws ParseException {
70+
final int maxDepth,
71+
final int maxSize) throws ParseException {
6172
if (maxDepth < 0) {
6273
throw new ParseException("Reached maximum allowed depth of nested messages");
6374
}
@@ -146,24 +157,40 @@ private static void generateFieldCaseStatement(
146157
final StringBuilder origSB, final Field field, final String valueGetter) {
147158
final StringBuilder sb = new StringBuilder();
148159
final boolean isMapField = field instanceof SingleField && ((SingleField) field).isMapField();
160+
final boolean isMapFieldOrOneOf = isMapField || field.parent() != null;
149161
if (field.repeated()) {
150162
if (field.type() == Field.FieldType.MESSAGE) {
151-
sb.append("parseObjArray($valueGetter.arr(), " + field.messageType() + ".JSON, maxDepth - 1)");
163+
sb.append(("parseObjArray(checkSize(\"$fieldName\", $valueGetter.arr().value(), $maxSize), "
164+
+ field.messageType() + ".JSON, maxDepth - 1, $maxSize)")
165+
.replace("$maxSize", field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize")
166+
.replace("$fieldName", field.name()));
152167
} else {
153-
sb.append("$valueGetter.arr().value().stream().map(v -> ");
168+
sb.append("checkSize(\"$fieldName\", $valueGetter.arr().value(), $maxSize).stream().map(v -> "
169+
.replace("$maxSize", field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize")
170+
.replace("$fieldName", field.name()));
154171
switch (field.type()) {
155172
case ENUM -> sb.append(field.messageType() + ".fromString(v.STRING().getText())");
156173
case INT32, UINT32, SINT32, FIXED32, SFIXED32 -> sb.append("parseInteger(v)");
157174
case INT64, UINT64, SINT64, FIXED64, SFIXED64 -> sb.append("parseLong(v)");
158175
case FLOAT -> sb.append("parseFloat(v)");
159176
case DOUBLE -> sb.append("parseDouble(v)");
160177
case STRING ->
161-
sb.append(
162-
isMapField || field.parent() != null
163-
? "unescape(v.STRING().getText())"
164-
: "toUtf8Bytes(unescape(v.STRING().getText()))");
178+
sb.append((isMapFieldOrOneOf ? "toUtf8Bytes(" : "") +
179+
"unescape(checkSize(\"$fieldName\", v.STRING().getText(), $maxSize))"
180+
.replace("$maxSize", field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize")
181+
.replace("$fieldName", field.name()) +
182+
(isMapFieldOrOneOf ? ")" : "")
183+
);
165184
case BOOL -> sb.append("parseBoolean(v)");
166-
case BYTES -> sb.append("Bytes.fromBase64(v.STRING().getText())");
185+
186+
// maxSize * 2 - because Base64. The *2 math isn't precise, but it's good enough for our purposes.
187+
case BYTES ->
188+
sb.append(
189+
"Bytes.fromBase64(checkSize(\"$fieldName\", v.STRING().getText(), $maxSize < (Integer.MAX_VALUE / 2) ? $maxSize * 2 : Integer.MAX_VALUE))"
190+
.replace(
191+
"$maxSize",
192+
field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize")
193+
.replace("$fieldName", field.name()));
167194
default -> throw new RuntimeException("Unknown field type [" + field.type() + "]");
168195
}
169196
sb.append(").toList()");
@@ -175,12 +202,22 @@ private static void generateFieldCaseStatement(
175202
case "FloatValue" -> sb.append("parseFloat($valueGetter)");
176203
case "DoubleValue" -> sb.append("parseDouble($valueGetter)");
177204
case "StringValue" ->
178-
sb.append(
179-
isMapField || field.parent() != null
180-
? "unescape($valueGetter.STRING().getText())"
181-
: "toUtf8Bytes(unescape($valueGetter.STRING().getText()))");
205+
sb.append((isMapFieldOrOneOf ? "toUtf8Bytes(" : "") +
206+
"unescape(checkSize(\"$fieldName\", $valueGetter.STRING().getText(), $maxSize))"
207+
.replace("$maxSize", field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize")
208+
.replace("$fieldName", field.name()) +
209+
(isMapFieldOrOneOf ? ")" : "")
210+
);
182211
case "BoolValue" -> sb.append("parseBoolean($valueGetter)");
183-
case "BytesValue" -> sb.append("Bytes.fromBase64($valueGetter.STRING().getText())");
212+
213+
// maxSize * 2 - because Base64. The *2 math isn't precise, but it's good enough for our purposes:
214+
case "BytesValue" ->
215+
sb.append(
216+
"Bytes.fromBase64(checkSize(\"$fieldName\", $valueGetter.STRING().getText(), $maxSize < (Integer.MAX_VALUE / 2) ? $maxSize * 2 : Integer.MAX_VALUE))"
217+
.replace(
218+
"$maxSize",
219+
field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize")
220+
.replace("$fieldName", field.name()));
184221
default -> throw new RuntimeException("Unknown message type [" + field.messageType() + "]");
185222
}
186223
} else if (field.type() == Field.FieldType.MAP) {
@@ -204,21 +241,33 @@ private static void generateFieldCaseStatement(
204241
} else {
205242
switch (field.type()) {
206243
case MESSAGE ->
207-
sb.append(
208-
field.javaFieldType()
209-
+ ".JSON.parse($valueGetter.getChild(JSONParser.ObjContext.class, 0), false, maxDepth - 1)");
244+
sb.append(field.javaFieldType()
245+
+ ".JSON.parse($valueGetter.getChild(JSONParser.ObjContext.class, 0), false, maxDepth - 1, $maxSize)"
246+
.replace(
247+
"$maxSize",
248+
field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize"));
210249
case ENUM -> sb.append(field.javaFieldType() + ".fromString($valueGetter.STRING().getText())");
211250
case INT32, UINT32, SINT32, FIXED32, SFIXED32 -> sb.append("parseInteger($valueGetter)");
212251
case INT64, UINT64, SINT64, FIXED64, SFIXED64 -> sb.append("parseLong($valueGetter)");
213252
case FLOAT -> sb.append("parseFloat($valueGetter)");
214253
case DOUBLE -> sb.append("parseDouble($valueGetter)");
215254
case STRING ->
216-
sb.append(
217-
isMapField || field.parent() != null
218-
? "unescape($valueGetter.STRING().getText())"
219-
: "toUtf8Bytes(unescape($valueGetter.STRING().getText()))");
255+
sb.append((isMapFieldOrOneOf ? "toUtf8Bytes(" : "") +
256+
"unescape(checkSize(\"$fieldName\", $valueGetter.STRING().getText(), $maxSize))"
257+
.replace("$maxSize", field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize")
258+
.replace("$fieldName", field.name()) +
259+
(isMapFieldOrOneOf ? ")" : "")
260+
);
220261
case BOOL -> sb.append("parseBoolean($valueGetter)");
221-
case BYTES -> sb.append("Bytes.fromBase64($valueGetter.STRING().getText())");
262+
263+
// maxSize * 2 - because Base64. The *2 math isn't precise, but it's good enough for our purposes:
264+
case BYTES ->
265+
sb.append(
266+
"Bytes.fromBase64(checkSize(\"$fieldName\", $valueGetter.STRING().getText(), $maxSize < (Integer.MAX_VALUE / 2) ? $maxSize * 2 : Integer.MAX_VALUE))"
267+
.replace(
268+
"$maxSize",
269+
field.maxSize() >= 0 ? String.valueOf(field.maxSize()) : "maxSize")
270+
.replace("$fieldName", field.name()));
222271
default -> throw new RuntimeException("Unknown field type [" + field.type() + "]");
223272
}
224273
}

0 commit comments

Comments
 (0)