json-repair 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c130bbea1b9299e31e5bfa8db873b09fd911715b7125fda6ee60a101353be5f
4
- data.tar.gz: 80f6e2fe16669210505e45c99a443eaa3ce6b8b3c10e3967633885f91a9d057b
3
+ metadata.gz: 2d7b2f30c2451f62471beabed342ff8360f644e4ae703fa3e0f5490e44d4f0d1
4
+ data.tar.gz: 254abaa4ab104a0cc6650d02f099ebb00b0600ef213b00866da266159b6c1a37
5
5
  SHA512:
6
- metadata.gz: 4e2c05c45ebf1cf149705021faa611c22e6e2e0de48d15d2496da4e935291abe6bf185cde756263c6aad210f73ca2e54e4c965826b14bdbb0fa9ffa47bf1684f
7
- data.tar.gz: 180b477cea0b27c813b664bfcda75eb28b59453d1fe4ce90781cf298c870aa8518411e689a37ddc30882aa5f376c351ff718600411d97df845f67fd87b593d92
6
+ metadata.gz: bfd417574888a7ff44a03870f48ab6e02c7bb7ae80189262e093425ba52e3943982f6ab36170c1cac6a3871e3d3379469b8319bbaa6defe8a0d473a7be6fe2a9
7
+ data.tar.gz: 208611ef2a09d4e3a5c91afe98334dfc7443a3db56de9624cfa82a3dea1e0c0480de18227646a96dac3c46f64af526e229f70dd9de4cd41cc1160a703b132a0b
data/.rubocop.yml CHANGED
@@ -1,5 +1,15 @@
1
+ # Merge our Exclude lists with RuboCop's defaults (vendor/**/*, tmp/**/*, …)
2
+ # instead of replacing them — CI vendors gems into vendor/bundle, which
3
+ # RuboCop must keep skipping.
4
+ inherit_mode:
5
+ merge:
6
+ - Exclude
7
+
1
8
  AllCops:
2
9
  TargetRubyVersion: 3.0
10
+ Exclude:
11
+ # gitignored local planning notes and scratch tooling (see CLAUDE.md)
12
+ - docs/**/*
3
13
 
4
14
  Style/Documentation:
5
15
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changes
2
2
 
3
+ ### 2026-06-12 (0.11.2)
4
+
5
+ * Fix the 0.11.0 doubled-colon repair silently mangling objects with a
6
+ stray junk word between pairs. `{"value_1": true, COMMENT "value_2":
7
+ "data"}` returned `{"value_1":true,"COMMENT":"value_2\": \"data"}`
8
+ (the junk word became a key and swallowed the real pair), and
9
+ `{ "key": "value" COMMENT "key2": "value2" }` returned a single
10
+ glued string value. Both shapes now raise "Object key expected"
11
+ again at the same positions as upstream
12
+ [jsonrepair](https://github.com/josdejong/jsonrepair) v3.14.0,
13
+ restoring the pre-0.11.0 behavior: the merge is skipped when the
14
+ pair already needed a missing-colon repair or the value string was
15
+ itself salvaged by the unescaped-quote repair — signals that the
16
+ pair was malformed in a way the merge would compound, not fix. The
17
+ salvage signal survives string concatenation: in
18
+ `{"a": "b" x "c" + "d": "e"}` the `+ "d"` segment no longer clears
19
+ it (caught in review by Copilot). All
20
+ 0.11.0 repairs (canonical, greedy, escaped quotes, unquoted
21
+ keys/values) are unchanged. Go and Python `json_repair` instead
22
+ drop the junk word; we deliberately keep raising rather than
23
+ silently discarding input (see the 0.11.0 note).
24
+
25
+ * Fix a `TypeError` crash on input ending in a lone backslash inside a
26
+ string: `"abc\` now repairs to `"abc"` (likewise `"\` → `""`,
27
+ `["abc\` → `["abc"]`, `{"a": "b\` → `{"a":"b"}`), matching upstream
28
+ [jsonrepair](https://github.com/josdejong/jsonrepair) v3.14.0. This
29
+ was a porting bug — JS `charAt` past EOF returns `''` where Ruby
30
+ `String#[]` returns `nil`, so the invalid-escape repair in
31
+ `parse_string` crashed on `str << nil` instead of ending the string,
32
+ violating the contract that `JSONRepairError` is the only error
33
+ raised. Found by differential fuzzing during the 0.11.0 review.
34
+
3
35
  ### 2026-06-12 (0.11.0)
4
36
 
5
37
  * Repair object string values with unescaped quotes around a colon
data/CLAUDE.md CHANGED
@@ -14,7 +14,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
14
14
  - `bundle exec rake install` / `bundle exec rake release` — local install / publish to rubygems.org (release prompts for a rubygems MFA OTP).
15
15
  - Type checking: `Steepfile` checks `lib/` against `sig/`. `bundle exec steep check` (typecheck) and `bundle exec rbs validate` (sig syntax) both run in CI and as part of the default rake task. `steep` and `rbs` are dev dependencies in the `Gemfile`.
16
16
 
17
- Ruby `>= 3.0.0` is required (per gemspec). CI runs against Ruby 3.3.1.
17
+ Ruby `>= 3.0.0` is required (per gemspec). CI runs against all currently maintained Ruby branches (3.3, 3.4, 4.0).
18
18
 
19
19
  ## Architecture
20
20
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module JSON
4
4
  module Repair
5
- VERSION = '0.11.0'
5
+ VERSION = '0.11.2'
6
6
  end
7
7
  end
data/lib/json/repairer.rb CHANGED
@@ -32,6 +32,7 @@ module JSON
32
32
  @json = json
33
33
  @index = 0
34
34
  @output = +''
35
+ @repaired_unescaped_quote = false
35
36
  end
36
37
 
37
38
  def repair
@@ -68,7 +69,7 @@ module JSON
68
69
  end
69
70
 
70
71
  # repair redundant end quotes
71
- while @json[@index] == CLOSING_BRACE || @json[@index] == CLOSING_BRACKET
72
+ while [CLOSING_BRACE, CLOSING_BRACKET].include?(@json[@index])
72
73
  @index += 1
73
74
  parse_whitespace_and_skip_comments
74
75
  end
@@ -236,7 +237,6 @@ module JSON
236
237
 
237
238
  initial = true
238
239
  while @index < @json.length && @json[@index] != CLOSING_BRACE
239
- processed_comma = true
240
240
  if initial
241
241
  initial = false
242
242
  else
@@ -296,8 +296,14 @@ module JSON
296
296
  end
297
297
 
298
298
  # repair: an object string value with unescaped quotes around a
299
- # colon, like {"a": "b": "c"}
300
- repair_doubled_colon if processed_value
299
+ # colon, like {"a": "b": "c"}. Skipped when this pair already
300
+ # needed a repair that makes the merge compound garbage: a
301
+ # missing colon (the "key" was a stray junk word, like
302
+ # {"v1": true, COMMENT "v2": "data"}) or a value glued together
303
+ # by the unescaped-quote repair (like
304
+ # {"k": "v" COMMENT "k2": "v2"}); both keep raising, matching
305
+ # upstream
306
+ repair_doubled_colon if processed_value && processed_colon && !@repaired_unescaped_quote
301
307
  end
302
308
 
303
309
  if @json[@index] == CLOSING_BRACE
@@ -316,9 +322,13 @@ module JSON
316
322
  # (the unescaped-quotes reading of the input). Greedy: keeps merging
317
323
  # while another `: "..."` follows. Only the string-colon-string
318
324
  # shape is repaired; anything else falls through to the regular
319
- # error paths. Divergence from upstream (which raises "Object key
320
- # expected" as of v3.14.0), matching the Go and Python json-repair
321
- # libraries on the canonical case.
325
+ # error paths. The call site additionally requires the pair's colon
326
+ # to be present in the input and the value string to have parsed
327
+ # without the unescaped-quote repair (@repaired_unescaped_quote) —
328
+ # when either repair already fired, the pair was malformed in a way
329
+ # this merge would compound, not fix. Divergence from upstream
330
+ # (which raises "Object key expected" as of v3.14.0), matching the
331
+ # Go and Python json-repair libraries on the canonical case.
322
332
  def repair_doubled_colon
323
333
  loop do
324
334
  colon = @index
@@ -400,6 +410,9 @@ module JSON
400
410
  # and fixing the string by inserting a quote there, or stopping at a
401
411
  # stop index detected in the first iteration.
402
412
  def parse_string(stop_at_delimiter: false, stop_at_index: -1)
413
+ # fresh parse (the backtracking re-invocations below rebuild the
414
+ # string from scratch, so they reset too); see repair_doubled_colon
415
+ @repaired_unescaped_quote = false
403
416
  skip_escape_chars = @json[@index] == BACKSLASH
404
417
  if skip_escape_chars
405
418
  # repair: remove the first escape character
@@ -509,6 +522,7 @@ module JSON
509
522
 
510
523
  # repair unescaped quote
511
524
  str = "#{str[...o_quote]}\\#{str[o_quote..]}"
525
+ @repaired_unescaped_quote = true
512
526
  elsif stop_at_delimiter && unquoted_string_delimiter?(@json[@index])
513
527
  # we're in the mode to stop the string at the first delimiter
514
528
  # because there is an end quote missing
@@ -531,7 +545,9 @@ module JSON
531
545
  return true
532
546
  elsif @json[@index] == BACKSLASH
533
547
  # handle escaped content like \n or ★
534
- char = @json[@index + 1]
548
+ # nil at EOF: '' mirrors JS charAt, making the invalid-escape
549
+ # repair below a no-op that ends the string
550
+ char = @json[@index + 1] || ''
535
551
  escape_char = ESCAPE_CHARACTERS[char]
536
552
  if escape_char
537
553
  str << @json[@index, 2]
@@ -806,7 +822,12 @@ module JSON
806
822
  # repair: remove the end quote of the first string
807
823
  @output = strip_last_occurrence(@output, '"', strip_remaining_text: true)
808
824
  start = @output.length
825
+ # the segments form one logical string value: keep the doubled-colon
826
+ # guard's flag set when an earlier segment needed the unescaped-quote
827
+ # repair (parse_string resets it on entry)
828
+ repaired_earlier_segment = @repaired_unescaped_quote
809
829
  parsed_str = parse_string
830
+ @repaired_unescaped_quote ||= repaired_earlier_segment
810
831
  @output = if parsed_str
811
832
  # repair: remove the start quote of the second string
812
833
  remove_at_index(@output, start, 1)
@@ -15,6 +15,8 @@ module JSON
15
15
 
16
16
  @output: ::String
17
17
 
18
+ @repaired_unescaped_quote: bool
19
+
18
20
  include Repair::StringUtils
19
21
 
20
22
  CONTROL_CHARACTERS: ::Hash[::String, "\\b" | "\\f" | "\\n" | "\\r" | "\\t"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json-repair
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksandr Zykov