Skip to content

Commit 735eeb3

Browse files
committed
[Java] Fix JSON escaping of string characters, delimeters, etc.
Previously, there were several problems: - (delimeters) single `char` values were output with single quotes, but this is not valid JSON. - (escaping) escaping was not implemented as per the specification in Section 7 of RFC 8259. For example, special-cased control codes, e.g., `\b` were encoded as `\\` followed by `\b` rather than `\\` followed by `b`. Also, the non-special-cased control characters were not encoded using JSON's mechanism for doing so, e.g., `\u0020`. - (numbers) Section 6 of the specification says "Numeric values that cannot be represented in the grammar below (such as Infinity and NaN) are not permitted." However, these were encoded as expressions of numbers with multiple terms, e.g., `-1/0` for positive infinity. While these are quite logical encodings of such "numbers", it is not valid JSON. Therefore, I have kept the expressions, but enclosed them within quotes. Also, in this commit: - Replaced custom compilation logic with Agrona's CompilerUtil. Note that `CompilerUtil#compileOnDisk` is broken. I attempted to use it to see if I could see classes in IntelliJ rather than just stack frames, but it fails with an NPE.
1 parent ddd42b9 commit 735eeb3

File tree

8 files changed

+303
-249
lines changed

8 files changed

+303
-249
lines changed

sbe-tool/src/main/java/uk/co/real_logic/sbe/json/JsonTokenListener.java

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
*/
1616
package uk.co.real_logic.sbe.json;
1717

18-
import org.agrona.DirectBuffer;
19-
import org.agrona.PrintBufferUtil;
2018
import uk.co.real_logic.sbe.PrimitiveValue;
2119
import uk.co.real_logic.sbe.ir.Encoding;
2220
import uk.co.real_logic.sbe.ir.Token;
2321
import uk.co.real_logic.sbe.otf.TokenListener;
2422
import uk.co.real_logic.sbe.otf.Types;
23+
import org.agrona.DirectBuffer;
24+
import org.agrona.PrintBufferUtil;
2525

2626
import java.io.UnsupportedEncodingException;
2727
import java.util.List;
@@ -236,7 +236,7 @@ public void onVarData(
236236
final String str = charsetName != null ? new String(tempBuffer, 0, length, charsetName) :
237237
PrintBufferUtil.hexDump(tempBuffer);
238238

239-
escape(str);
239+
Types.jsonEscape(str, output);
240240

241241
doubleQuote();
242242
next();
@@ -290,7 +290,7 @@ private void appendEncodingAsString(
290290
final long longValue = constOrNotPresentValue.longValue();
291291
if (PrimitiveValue.NULL_VALUE_CHAR != longValue)
292292
{
293-
escape(new String(new byte[]{ (byte)longValue }, characterEncoding));
293+
Types.jsonEscape(new String(new byte[] {(byte)longValue}, characterEncoding), output);
294294
}
295295
}
296296
catch (final UnsupportedEncodingException ex)
@@ -300,7 +300,7 @@ private void appendEncodingAsString(
300300
}
301301
else
302302
{
303-
escape(constOrNotPresentValue.toString());
303+
Types.jsonEscape(constOrNotPresentValue.toString(), output);
304304
}
305305

306306
doubleQuote();
@@ -371,7 +371,7 @@ private void escapePrintableChar(final DirectBuffer buffer, final int index, fin
371371
final byte c = buffer.getByte(index + (i * elementSize));
372372
if (c > 0)
373373
{
374-
escape((char)c);
374+
Types.jsonEscape((char)c, output);
375375
}
376376
else
377377
{
@@ -467,22 +467,4 @@ private static long readEncodingAsLong(
467467

468468
return Types.getLong(buffer, bufferIndex, typeToken.encoding());
469469
}
470-
471-
private void escape(final String str)
472-
{
473-
for (int i = 0, length = str.length(); i < length; i++)
474-
{
475-
escape(str.charAt(i));
476-
}
477-
}
478-
479-
private void escape(final char c)
480-
{
481-
if ('"' == c || '\\' == c || '\b' == c || '\f' == c || '\n' == c || '\r' == c || '\t' == c)
482-
{
483-
output.append('\\');
484-
}
485-
486-
output.append(c);
487-
}
488470
}

sbe-tool/src/main/java/uk/co/real_logic/sbe/otf/Types.java

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
*/
1616
package uk.co.real_logic.sbe.otf;
1717

18-
import org.agrona.DirectBuffer;
1918
import uk.co.real_logic.sbe.PrimitiveType;
2019
import uk.co.real_logic.sbe.PrimitiveValue;
2120
import uk.co.real_logic.sbe.ir.Encoding;
21+
import org.agrona.DirectBuffer;
2222

2323
import java.nio.ByteOrder;
2424

@@ -27,6 +27,11 @@
2727
*/
2828
public class Types
2929
{
30+
private static final char[] HEX_DIGIT = {
31+
'0', '1', '2', '3', '4', '5', '6', '7',
32+
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
33+
};
34+
3035
/**
3136
* Get an integer value from a buffer at a given index for a {@link PrimitiveType}.
3237
*
@@ -187,7 +192,9 @@ public static void appendAsJsonString(
187192
switch (encoding.primitiveType())
188193
{
189194
case CHAR:
190-
sb.append('\'').append((char)buffer.getByte(index)).append('\'');
195+
sb.append('\"');
196+
jsonEscape((char)buffer.getByte(index), sb);
197+
sb.append('\"');
191198
break;
192199

193200
case INT8:
@@ -227,15 +234,15 @@ public static void appendAsJsonString(
227234
final float value = buffer.getFloat(index, encoding.byteOrder());
228235
if (Float.isNaN(value))
229236
{
230-
sb.append("0/0");
237+
sb.append("\"0/0\"");
231238
}
232239
else if (value == Float.POSITIVE_INFINITY)
233240
{
234-
sb.append("1/0");
241+
sb.append("\"1/0\"");
235242
}
236243
else if (value == Float.NEGATIVE_INFINITY)
237244
{
238-
sb.append("-1/0");
245+
sb.append("\"-1/0\"");
239246
}
240247
else
241248
{
@@ -249,15 +256,15 @@ else if (value == Float.NEGATIVE_INFINITY)
249256
final double value = buffer.getDouble(index, encoding.byteOrder());
250257
if (Double.isNaN(value))
251258
{
252-
sb.append("0/0");
259+
sb.append("\"0/0\"");
253260
}
254261
else if (value == Double.POSITIVE_INFINITY)
255262
{
256-
sb.append("1/0");
263+
sb.append("\"1/0\"");
257264
}
258265
else if (value == Double.NEGATIVE_INFINITY)
259266
{
260-
sb.append("-1/0");
267+
sb.append("\"-1/0\"");
261268
}
262269
else
263270
{
@@ -280,7 +287,9 @@ public static void appendAsJsonString(final StringBuilder sb, final PrimitiveVal
280287
switch (encoding.primitiveType())
281288
{
282289
case CHAR:
283-
sb.append('\'').append((char)value.longValue()).append('\'');
290+
sb.append('\"');
291+
jsonEscape((char)value.longValue(), sb);
292+
sb.append('\"');
284293
break;
285294

286295
case INT8:
@@ -299,15 +308,15 @@ public static void appendAsJsonString(final StringBuilder sb, final PrimitiveVal
299308
final float floatValue = (float)value.doubleValue();
300309
if (Float.isNaN(floatValue))
301310
{
302-
sb.append("0/0");
311+
sb.append("\"0/0\"");
303312
}
304313
else if (floatValue == Float.POSITIVE_INFINITY)
305314
{
306-
sb.append("1/0");
315+
sb.append("\"1/0\"");
307316
}
308317
else if (floatValue == Float.NEGATIVE_INFINITY)
309318
{
310-
sb.append("-1/0");
319+
sb.append("\"-1/0\"");
311320
}
312321
else
313322
{
@@ -321,15 +330,15 @@ else if (floatValue == Float.NEGATIVE_INFINITY)
321330
final double doubleValue = value.doubleValue();
322331
if (Double.isNaN(doubleValue))
323332
{
324-
sb.append("0/0");
333+
sb.append("\"0/0\"");
325334
}
326335
else if (doubleValue == Double.POSITIVE_INFINITY)
327336
{
328-
sb.append("1/0");
337+
sb.append("\"1/0\"");
329338
}
330339
else if (doubleValue == Double.NEGATIVE_INFINITY)
331340
{
332-
sb.append("-1/0");
341+
sb.append("\"-1/0\"");
333342
}
334343
else
335344
{
@@ -339,4 +348,70 @@ else if (doubleValue == Double.NEGATIVE_INFINITY)
339348
}
340349
}
341350
}
351+
352+
/**
353+
* Escape a string for use in a JSON string.
354+
*
355+
* @param str the string to escape
356+
* @param output to append the escaped string to
357+
*/
358+
public static void jsonEscape(final String str, final StringBuilder output)
359+
{
360+
for (int i = 0, length = str.length(); i < length; i++)
361+
{
362+
jsonEscape(str.charAt(i), output);
363+
}
364+
}
365+
366+
/**
367+
* Escape a character for use in a JSON string.
368+
*
369+
* @param c the character to escape
370+
* @param output to append the escaped character to
371+
*/
372+
public static void jsonEscape(final char c, final StringBuilder output)
373+
{
374+
if ('"' == c || '\\' == c)
375+
{
376+
output.append('\\');
377+
output.append(c);
378+
}
379+
else if ('\b' == c)
380+
{
381+
output.append("\\b");
382+
}
383+
else if ('\f' == c)
384+
{
385+
output.append("\\f");
386+
}
387+
else if ('\n' == c)
388+
{
389+
output.append("\\n");
390+
}
391+
else if ('\r' == c)
392+
{
393+
output.append("\\r");
394+
}
395+
else if ('\t' == c)
396+
{
397+
output.append("\\t");
398+
}
399+
else if (c <= 0x1F || Character.isHighSurrogate(c) || Character.isLowSurrogate(c))
400+
{
401+
jsonUnicodeEncode(c, output);
402+
}
403+
else
404+
{
405+
output.append(c);
406+
}
407+
}
408+
409+
private static void jsonUnicodeEncode(final char c, final StringBuilder output)
410+
{
411+
output.append('\\').append('u')
412+
.append(HEX_DIGIT[(c >>> 12) & 0x0F])
413+
.append(HEX_DIGIT[(c >>> 8) & 0x0F])
414+
.append(HEX_DIGIT[(c >>> 4) & 0x0F])
415+
.append(HEX_DIGIT[c & 0x0F]);
416+
}
342417
}

0 commit comments

Comments
 (0)