@@ -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