Skip to content

Commit bf110df

Browse files
committed
Merge branch 'master' into neon-simd-parser
2 parents 15c265a + 6c41162 commit bf110df

9 files changed

Lines changed: 266 additions & 114 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
### Unreleased
44

5+
* Add bnew `allow_duplicate_key` parsing options. By default a warning is now emitted when a duplicated key is encountered.
6+
In `json 3.0` an error will be raised.
7+
58
### 2025-05-23 (2.12.2)
69

710
* Fix compiler optimization level.

ext/json/ext/generator/generator.c

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ static inline unsigned char search_escape_basic_neon(search_state *search)
311311
if (search->matches_mask > 0) {
312312
return neon_next_match(search);
313313
} else {
314-
// neon_next_match will only advance search->ptr up to the last matching character.
314+
// neon_next_match will only advance search->ptr up to the last matching character.
315315
// Skip over any characters in the last chunk that occur after the last match.
316316
search->has_matches = false;
317317
search->ptr = search->chunk_end;
@@ -320,40 +320,40 @@ static inline unsigned char search_escape_basic_neon(search_state *search)
320320

321321
/*
322322
* The code below implements an SIMD-based algorithm to determine if N bytes at a time
323-
* need to be escaped.
324-
*
323+
* need to be escaped.
324+
*
325325
* Assume the ptr = "Te\sting!" (the double quotes are included in the string)
326-
*
326+
*
327327
* The explanation will be limited to the first 8 bytes of the string for simplicity. However
328328
* the vector insructions may work on larger vectors.
329-
*
329+
*
330330
* First, we load three constants 'lower_bound', 'backslash' and 'dblquote" in vector registers.
331-
*
332-
* lower_bound: [20 20 20 20 20 20 20 20]
333-
* backslash: [5C 5C 5C 5C 5C 5C 5C 5C]
334-
* dblquote: [22 22 22 22 22 22 22 22]
335-
*
336-
* Next we load the first chunk of the ptr:
331+
*
332+
* lower_bound: [20 20 20 20 20 20 20 20]
333+
* backslash: [5C 5C 5C 5C 5C 5C 5C 5C]
334+
* dblquote: [22 22 22 22 22 22 22 22]
335+
*
336+
* Next we load the first chunk of the ptr:
337337
* [22 54 65 5C 73 74 69 6E] (" T e \ s t i n)
338-
*
338+
*
339339
* First we check if any byte in chunk is less than 32 (0x20). This returns the following vector
340340
* as no bytes are less than 32 (0x20):
341341
* [0 0 0 0 0 0 0 0]
342-
*
342+
*
343343
* Next, we check if any byte in chunk is equal to a backslash:
344344
* [0 0 0 FF 0 0 0 0]
345-
*
345+
*
346346
* Finally we check if any byte in chunk is equal to a double quote:
347-
* [FF 0 0 0 0 0 0 0]
348-
*
347+
* [FF 0 0 0 0 0 0 0]
348+
*
349349
* Now we have three vectors where each byte indicates if the corresponding byte in chunk
350350
* needs to be escaped. We combine these vectors with a series of logical OR instructions.
351351
* This is the needs_escape vector and it is equal to:
352-
* [FF 0 0 FF 0 0 0 0]
353-
*
352+
* [FF 0 0 FF 0 0 0 0]
353+
*
354354
* Next we compute the bitwise AND between each byte and 0x1 and compute the horizontal sum of
355355
* the values in the vector. This computes how many bytes need to be escaped within this chunk.
356-
*
356+
*
357357
* Finally we compute a mask that indicates which bytes need to be escaped. If the mask is 0 then,
358358
* no bytes need to be escaped and we can continue to the next chunk. If the mask is not 0 then we
359359
* have at least one byte that needs to be escaped.
@@ -371,16 +371,16 @@ static inline unsigned char search_escape_basic_neon(search_state *search)
371371
search->matches_mask = mask;
372372
return neon_next_match(search);
373373
}
374-
375-
// There are fewer than 16 bytes left.
374+
375+
// There are fewer than 16 bytes left.
376376
unsigned long remaining = (search->end - search->ptr);
377377
if (remaining >= SIMD_MINIMUM_THRESHOLD) {
378378
char *s = copy_remaining_bytes(search, sizeof(uint8x16_t), remaining);
379379

380380
uint64_t mask = compute_chunk_mask_neon(s);
381381

382382
if (!mask) {
383-
// Nothing to escape, ensure search_flush doesn't do anything by setting
383+
// Nothing to escape, ensure search_flush doesn't do anything by setting
384384
// search->cursor to search->ptr.
385385
fbuffer_consumed(search->buffer, remaining);
386386
search->ptr = search->end;
@@ -437,7 +437,7 @@ static inline TARGET_SSE2 FORCE_INLINE unsigned char search_escape_basic_sse2(se
437437
if (search->matches_mask > 0) {
438438
return sse2_next_match(search);
439439
} else {
440-
// sse2_next_match will only advance search->ptr up to the last matching character.
440+
// sse2_next_match will only advance search->ptr up to the last matching character.
441441
// Skip over any characters in the last chunk that occur after the last match.
442442
search->has_matches = false;
443443
if (RB_UNLIKELY(search->chunk_base + sizeof(__m128i) >= search->end)) {
@@ -457,15 +457,15 @@ static inline TARGET_SSE2 FORCE_INLINE unsigned char search_escape_basic_sse2(se
457457
return sse2_next_match(search);
458458
}
459459

460-
// There are fewer than 16 bytes left.
460+
// There are fewer than 16 bytes left.
461461
unsigned long remaining = (search->end - search->ptr);
462462
if (remaining >= SIMD_MINIMUM_THRESHOLD) {
463463
char *s = copy_remaining_bytes(search, sizeof(__m128i), remaining);
464464

465465
int needs_escape_mask = compute_chunk_mask_sse2(s);
466466

467467
if (needs_escape_mask == 0) {
468-
// Nothing to escape, ensure search_flush doesn't do anything by setting
468+
// Nothing to escape, ensure search_flush doesn't do anything by setting
469469
// search->cursor to search->ptr.
470470
fbuffer_consumed(search->buffer, remaining);
471471
search->ptr = search->end;

ext/json/ext/parser/parser.c

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ static ID i_chr, i_aset, i_aref,
3737
i_leftshift, i_new, i_try_convert, i_uminus, i_encode;
3838

3939
static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_symbolize_names, sym_freeze,
40-
sym_decimal_class, sym_on_load;
40+
sym_decimal_class, sym_on_load, sym_allow_duplicate_key;
4141

4242
static int binary_encindex;
4343
static int utf8_encindex;
@@ -365,10 +365,17 @@ static int convert_UTF32_to_UTF8(char *buf, uint32_t ch)
365365
return len;
366366
}
367367

368+
enum duplicate_key_action {
369+
JSON_DEPRECATED = 0,
370+
JSON_IGNORE,
371+
JSON_RAISE,
372+
};
373+
368374
typedef struct JSON_ParserStruct {
369375
VALUE on_load_proc;
370376
VALUE decimal_class;
371377
ID decimal_method_id;
378+
enum duplicate_key_action on_duplicate_key;
372379
int max_nesting;
373380
bool allow_nan;
374381
bool allow_trailing_comma;
@@ -388,15 +395,8 @@ typedef struct JSON_ParserStateStruct {
388395
int current_nesting;
389396
} JSON_ParserState;
390397

391-
392-
#define PARSE_ERROR_FRAGMENT_LEN 32
393-
#ifdef RBIMPL_ATTR_NORETURN
394-
RBIMPL_ATTR_NORETURN()
395-
#endif
396-
static void raise_parse_error(const char *format, JSON_ParserState *state)
398+
static void cursor_position(JSON_ParserState *state, long *line_out, long *column_out)
397399
{
398-
unsigned char buffer[PARSE_ERROR_FRAGMENT_LEN + 3];
399-
400400
const char *cursor = state->cursor;
401401
long column = 0;
402402
long line = 1;
@@ -413,6 +413,27 @@ static void raise_parse_error(const char *format, JSON_ParserState *state)
413413
line++;
414414
}
415415
}
416+
*line_out = line;
417+
*column_out = column;
418+
}
419+
420+
static void emit_parse_warning(const char *message, JSON_ParserState *state)
421+
{
422+
long line, column;
423+
cursor_position(state, &line, &column);
424+
425+
rb_warn("%s at line %ld column %ld", message, line, column);
426+
}
427+
428+
#define PARSE_ERROR_FRAGMENT_LEN 32
429+
#ifdef RBIMPL_ATTR_NORETURN
430+
RBIMPL_ATTR_NORETURN()
431+
#endif
432+
static void raise_parse_error(const char *format, JSON_ParserState *state)
433+
{
434+
unsigned char buffer[PARSE_ERROR_FRAGMENT_LEN + 3];
435+
long line, column;
436+
cursor_position(state, &line, &column);
416437

417438
const char *ptr = "EOF";
418439
if (state->cursor && state->cursor < state->end) {
@@ -809,11 +830,25 @@ static inline VALUE json_decode_array(JSON_ParserState *state, JSON_ParserConfig
809830
return array;
810831
}
811832

812-
static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfig *config, long count)
833+
static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfig *config, size_t count)
813834
{
814-
VALUE object = rb_hash_new_capa(count);
835+
size_t entries_count = count / 2;
836+
VALUE object = rb_hash_new_capa(entries_count);
815837
rb_hash_bulk_insert(count, rvalue_stack_peek(state->stack, count), object);
816838

839+
if (RB_UNLIKELY(RHASH_SIZE(object) < entries_count)) {
840+
switch (config->on_duplicate_key) {
841+
case JSON_IGNORE:
842+
break;
843+
case JSON_DEPRECATED:
844+
emit_parse_warning("detected duplicate keys in JSON object. This will raise an error in json 3.0 unless enabled via `allow_duplicate_key: true`", state);
845+
break;
846+
case JSON_RAISE:
847+
raise_parse_error("duplicate key", state);
848+
break;
849+
}
850+
}
851+
817852
rvalue_stack_pop(state->stack, count);
818853

819854
if (config->freeze) {
@@ -1101,6 +1136,8 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config)
11011136
break;
11021137
}
11031138
case '{': {
1139+
const char *object_start_cursor = state->cursor;
1140+
11041141
state->cursor++;
11051142
json_eat_whitespace(state);
11061143
long stack_head = state->stack->head;
@@ -1135,8 +1172,15 @@ static VALUE json_parse_any(JSON_ParserState *state, JSON_ParserConfig *config)
11351172
if (*state->cursor == '}') {
11361173
state->cursor++;
11371174
state->current_nesting--;
1138-
long count = state->stack->head - stack_head;
1139-
return json_push_value(state, config, json_decode_object(state, config, count));
1175+
size_t count = state->stack->head - stack_head;
1176+
1177+
// Temporary rewind cursor in case an error is raised
1178+
const char *final_cursor = state->cursor;
1179+
state->cursor = object_start_cursor;
1180+
VALUE object = json_decode_object(state, config, count);
1181+
state->cursor = final_cursor;
1182+
1183+
return json_push_value(state, config, object);
11401184
}
11411185

11421186
if (*state->cursor == ',') {
@@ -1225,6 +1269,7 @@ static int parser_config_init_i(VALUE key, VALUE val, VALUE data)
12251269
else if (key == sym_symbolize_names) { config->symbolize_names = RTEST(val); }
12261270
else if (key == sym_freeze) { config->freeze = RTEST(val); }
12271271
else if (key == sym_on_load) { config->on_load_proc = RTEST(val) ? val : Qfalse; }
1272+
else if (key == sym_allow_duplicate_key) { config->on_duplicate_key = RTEST(val) ? JSON_IGNORE : JSON_RAISE; }
12281273
else if (key == sym_decimal_class) {
12291274
if (RTEST(val)) {
12301275
if (rb_respond_to(val, i_try_convert)) {
@@ -1441,6 +1486,7 @@ void Init_parser(void)
14411486
sym_freeze = ID2SYM(rb_intern("freeze"));
14421487
sym_on_load = ID2SYM(rb_intern("on_load"));
14431488
sym_decimal_class = ID2SYM(rb_intern("decimal_class"));
1489+
sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key"));
14441490

14451491
i_chr = rb_intern("chr");
14461492
i_aset = rb_intern("[]=");

java/src/json/ext/OptionsReader.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ IRubyObject get(String key) {
5353
return opts == null ? null : opts.fastARef(runtime.newSymbol(key));
5454
}
5555

56+
boolean hasKey(String key) {
57+
if (opts == null) {
58+
return false;
59+
}
60+
return opts.hasKey(runtime.newSymbol(key));
61+
}
62+
5663
boolean getBool(String key, boolean defaultValue) {
5764
IRubyObject value = get(key);
5865
return value == null ? defaultValue : value.isTrue();

0 commit comments

Comments
 (0)