json 2.3.1 → 2.4.0

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.
@@ -37,20 +37,26 @@ module JSON
37
37
  '\\' => '\\\\',
38
38
  } # :nodoc:
39
39
 
40
+ ESCAPE_SLASH_MAP = MAP.merge(
41
+ '/' => '\\/',
42
+ )
43
+
40
44
  # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with
41
45
  # UTF16 big endian characters as \u????, and return it.
42
- def utf8_to_json(string) # :nodoc:
46
+ def utf8_to_json(string, escape_slash = false) # :nodoc:
43
47
  string = string.dup
44
48
  string.force_encoding(::Encoding::ASCII_8BIT)
45
- string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] }
49
+ map = escape_slash ? ESCAPE_SLASH_MAP : MAP
50
+ string.gsub!(/[\/"\\\x0-\x1f]/) { map[$&] || $& }
46
51
  string.force_encoding(::Encoding::UTF_8)
47
52
  string
48
53
  end
49
54
 
50
- def utf8_to_json_ascii(string) # :nodoc:
55
+ def utf8_to_json_ascii(string, escape_slash = false) # :nodoc:
51
56
  string = string.dup
52
57
  string.force_encoding(::Encoding::ASCII_8BIT)
53
- string.gsub!(/["\\\x0-\x1f]/n) { MAP[$&] }
58
+ map = escape_slash ? ESCAPE_SLASH_MAP : MAP
59
+ string.gsub!(/[\/"\\\x0-\x1f]/n) { map[$&] || $& }
54
60
  string.gsub!(/(
55
61
  (?:
56
62
  [\xc2-\xdf][\x80-\xbf] |
@@ -109,6 +115,7 @@ module JSON
109
115
  # * *space_before*: a string that is put before a : pair delimiter (default: ''),
110
116
  # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
111
117
  # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
118
+ # * *escape_slash*: true if forward slash (/) should be escaped (default: false)
112
119
  # * *check_circular*: is deprecated now, use the :max_nesting option instead,
113
120
  # * *max_nesting*: sets the maximum level of data structure nesting in
114
121
  # the generated JSON, max_nesting = 0 if no maximum should be checked.
@@ -123,6 +130,7 @@ module JSON
123
130
  @array_nl = ''
124
131
  @allow_nan = false
125
132
  @ascii_only = false
133
+ @escape_slash = false
126
134
  @buffer_initial_length = 1024
127
135
  configure opts
128
136
  end
@@ -148,6 +156,10 @@ module JSON
148
156
  # the generated JSON, max_nesting = 0 if no maximum is checked.
149
157
  attr_accessor :max_nesting
150
158
 
159
+ # If this attribute is set to true, forward slashes will be escaped in
160
+ # all json strings.
161
+ attr_accessor :escape_slash
162
+
151
163
  # :stopdoc:
152
164
  attr_reader :buffer_initial_length
153
165
 
@@ -187,6 +199,11 @@ module JSON
187
199
  @ascii_only
188
200
  end
189
201
 
202
+ # Returns true, if forward slashes are escaped. Otherwise returns false.
203
+ def escape_slash?
204
+ @escape_slash
205
+ end
206
+
190
207
  # Configure this State instance with the Hash _opts_, and return
191
208
  # itself.
192
209
  def configure(opts)
@@ -209,6 +226,7 @@ module JSON
209
226
  @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only)
210
227
  @depth = opts[:depth] || 0
211
228
  @buffer_initial_length ||= opts[:buffer_initial_length]
229
+ @escape_slash = !!opts[:escape_slash] if opts.key?(:escape_slash)
212
230
 
213
231
  if !opts.key?(:max_nesting) # defaults to 100
214
232
  @max_nesting = 100
@@ -314,8 +332,10 @@ module JSON
314
332
  first = false
315
333
  }
316
334
  depth = state.depth -= 1
317
- result << state.object_nl
318
- result << state.indent * depth if indent
335
+ unless first
336
+ result << state.object_nl
337
+ result << state.indent * depth if indent
338
+ end
319
339
  result << '}'
320
340
  result
321
341
  end
@@ -399,9 +419,9 @@ module JSON
399
419
  string = encode(::Encoding::UTF_8)
400
420
  end
401
421
  if state.ascii_only?
402
- '"' << JSON.utf8_to_json_ascii(string) << '"'
422
+ '"' << JSON.utf8_to_json_ascii(string, state.escape_slash) << '"'
403
423
  else
404
- '"' << JSON.utf8_to_json(string) << '"'
424
+ '"' << JSON.utf8_to_json(string, state.escape_slash) << '"'
405
425
  end
406
426
  end
407
427
 
@@ -61,6 +61,8 @@ module JSON
61
61
  # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
62
62
  # defiance of RFC 7159 to be parsed by the Parser. This option defaults
63
63
  # to false.
64
+ # * *freeze*: If set to true, all parsed objects will be frozen. Parsed
65
+ # string will be deduplicated if possible.
64
66
  # * *symbolize_names*: If set to true, returns symbols for the names
65
67
  # (keys) in a JSON object. Otherwise strings are returned, which is
66
68
  # also the default. It's not possible to use this option in
@@ -86,6 +88,7 @@ module JSON
86
88
  end
87
89
  @allow_nan = !!opts[:allow_nan]
88
90
  @symbolize_names = !!opts[:symbolize_names]
91
+ @freeze = !!opts[:freeze]
89
92
  if opts.key?(:create_additions)
90
93
  @create_additions = !!opts[:create_additions]
91
94
  else
@@ -120,6 +123,7 @@ module JSON
120
123
  obj = parse_value
121
124
  UNPARSED.equal?(obj) and raise ParserError,
122
125
  "source is not valid JSON!"
126
+ obj.freeze if @freeze
123
127
  end
124
128
  while !eos? && skip(IGNORE) do end
125
129
  eos? or raise ParserError, "source is not valid JSON!"
@@ -161,6 +165,7 @@ module JSON
161
165
  EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
162
166
  end
163
167
 
168
+ STR_UMINUS = ''.respond_to?(:-@)
164
169
  def parse_string
165
170
  if scan(STRING)
166
171
  return '' if self[1].empty?
@@ -180,6 +185,15 @@ module JSON
180
185
  if string.respond_to?(:force_encoding)
181
186
  string.force_encoding(::Encoding::UTF_8)
182
187
  end
188
+
189
+ if @freeze
190
+ if STR_UMINUS
191
+ string = -string
192
+ else
193
+ string.freeze
194
+ end
195
+ end
196
+
183
197
  if @create_additions and @match_string
184
198
  for (regexp, klass) in @match_string
185
199
  klass.json_creatable? or next
@@ -242,8 +256,10 @@ module JSON
242
256
  @max_nesting.nonzero? && @current_nesting > @max_nesting
243
257
  result = @array_class.new
244
258
  delim = false
245
- until eos?
259
+ loop do
246
260
  case
261
+ when eos?
262
+ raise ParserError, "unexpected end of string while parsing array"
247
263
  when !UNPARSED.equal?(value = parse_value)
248
264
  delim = false
249
265
  result << value
@@ -274,8 +290,10 @@ module JSON
274
290
  @max_nesting.nonzero? && @current_nesting > @max_nesting
275
291
  result = @object_class.new
276
292
  delim = false
277
- until eos?
293
+ loop do
278
294
  case
295
+ when eos?
296
+ raise ParserError, "unexpected end of string while parsing object"
279
297
  when !UNPARSED.equal?(string = parse_string)
280
298
  skip(IGNORE)
281
299
  unless scan(PAIR_DELIMITER)
@@ -195,9 +195,5 @@ class JSONAdditionTest < Test::Unit::TestCase
195
195
  def test_set
196
196
  s = Set.new([:a, :b, :c, :a])
197
197
  assert_equal s, JSON.parse(JSON(s), :create_additions => true)
198
- ss = SortedSet.new([:d, :b, :a, :c])
199
- ss_again = JSON.parse(JSON(ss), :create_additions => true)
200
- assert_kind_of ss.class, ss_again
201
- assert_equal ss, ss_again
202
198
  end
203
199
  end
@@ -123,4 +123,47 @@ class JSONCommonInterfaceTest < Test::Unit::TestCase
123
123
  assert_equal @json, JSON(@hash)
124
124
  assert_equal @hash, JSON(@json)
125
125
  end
126
+
127
+ def test_load_file
128
+ test_load_shared(:load_file)
129
+ end
130
+
131
+ def test_load_file!
132
+ test_load_shared(:load_file!)
133
+ end
134
+
135
+ def test_load_file_with_option
136
+ test_load_file_with_option_shared(:load_file)
137
+ end
138
+
139
+ def test_load_file_with_option!
140
+ test_load_file_with_option_shared(:load_file!)
141
+ end
142
+
143
+ private
144
+
145
+ def test_load_shared(method_name)
146
+ temp_file_containing(@json) do |filespec|
147
+ assert_equal JSON.public_send(method_name, filespec), @hash
148
+ end
149
+ end
150
+
151
+ def test_load_file_with_option_shared(method_name)
152
+ temp_file_containing(@json) do |filespec|
153
+ parsed_object = JSON.public_send(method_name, filespec, symbolize_names: true)
154
+ key_classes = parsed_object.keys.map(&:class)
155
+ assert_include(key_classes, Symbol)
156
+ assert_not_include(key_classes, String)
157
+ end
158
+ end
159
+
160
+ def temp_file_containing(text, file_prefix = '')
161
+ raise "This method must be called with a code block." unless block_given?
162
+
163
+ Tempfile.create(file_prefix) do |file|
164
+ file << text
165
+ file.close
166
+ yield file.path
167
+ end
168
+ end
126
169
  end
@@ -10,6 +10,7 @@ class JSONFixturesTest < Test::Unit::TestCase
10
10
  end
11
11
 
12
12
  def test_passing
13
+ verbose_bak, $VERBOSE = $VERBOSE, nil
13
14
  for name, source in @passed
14
15
  begin
15
16
  assert JSON.parse(source),
@@ -19,6 +20,8 @@ class JSONFixturesTest < Test::Unit::TestCase
19
20
  raise e
20
21
  end
21
22
  end
23
+ ensure
24
+ $VERBOSE = verbose_bak
22
25
  end
23
26
 
24
27
  def test_failing
@@ -49,7 +49,6 @@ EOT
49
49
  end
50
50
 
51
51
  def test_remove_const_segv
52
- return if RUBY_ENGINE == 'jruby'
53
52
  stress = GC.stress
54
53
  const = JSON::SAFE_STATE_PROTOTYPE.dup
55
54
 
@@ -76,7 +75,7 @@ EOT
76
75
  silence do
77
76
  JSON.const_set :SAFE_STATE_PROTOTYPE, const
78
77
  end
79
- end if JSON.const_defined?("Ext")
78
+ end if JSON.const_defined?("Ext") && RUBY_ENGINE != 'jruby'
80
79
 
81
80
  def test_generate
82
81
  json = generate(@hash)
@@ -93,6 +92,11 @@ EOT
93
92
  end
94
93
 
95
94
  def test_generate_pretty
95
+ json = pretty_generate({})
96
+ assert_equal(<<'EOT'.chomp, json)
97
+ {
98
+ }
99
+ EOT
96
100
  json = pretty_generate(@hash)
97
101
  # hashes aren't (insertion) ordered on every ruby implementation
98
102
  # assert_equal(@json3, json)
@@ -174,6 +178,7 @@ EOT
174
178
  :ascii_only => false,
175
179
  :buffer_initial_length => 1024,
176
180
  :depth => 0,
181
+ :escape_slash => false,
177
182
  :indent => " ",
178
183
  :max_nesting => 100,
179
184
  :object_nl => "\n",
@@ -190,6 +195,7 @@ EOT
190
195
  :ascii_only => false,
191
196
  :buffer_initial_length => 1024,
192
197
  :depth => 0,
198
+ :escape_slash => false,
193
199
  :indent => "",
194
200
  :max_nesting => 100,
195
201
  :object_nl => "",
@@ -206,6 +212,7 @@ EOT
206
212
  :ascii_only => false,
207
213
  :buffer_initial_length => 1024,
208
214
  :depth => 0,
215
+ :escape_slash => false,
209
216
  :indent => "",
210
217
  :max_nesting => 0,
211
218
  :object_nl => "",
@@ -394,6 +401,10 @@ EOT
394
401
  json = '["/"]'
395
402
  assert_equal json, generate(data)
396
403
  #
404
+ data = [ '/' ]
405
+ json = '["\/"]'
406
+ assert_equal json, generate(data, :escape_slash => true)
407
+ #
397
408
  data = ['"']
398
409
  json = '["\""]'
399
410
  assert_equal json, generate(data)
@@ -218,6 +218,17 @@ class JSONParserTest < Test::Unit::TestCase
218
218
  end
219
219
  end
220
220
 
221
+ def test_freeze
222
+ assert_predicate parse('{}', :freeze => true), :frozen?
223
+ assert_predicate parse('[]', :freeze => true), :frozen?
224
+ assert_predicate parse('"foo"', :freeze => true), :frozen?
225
+
226
+ if string_deduplication_available?
227
+ assert_same(-'foo', parse('"foo"', :freeze => true))
228
+ assert_same(-'foo', parse('{"foo": 1}', :freeze => true).keys.first)
229
+ end
230
+ end
231
+
221
232
  def test_parse_comments
222
233
  json = <<EOT
223
234
  {
@@ -293,6 +304,10 @@ EOT
293
304
  json = '["\\\'"]'
294
305
  data = ["'"]
295
306
  assert_equal data, parse(json)
307
+
308
+ json = '["\/"]'
309
+ data = [ '/' ]
310
+ assert_equal data, parse(json)
296
311
  end
297
312
 
298
313
  class SubArray < Array
@@ -464,6 +479,16 @@ EOT
464
479
 
465
480
  private
466
481
 
482
+ def string_deduplication_available?
483
+ r1 = rand.to_s
484
+ r2 = r1.dup
485
+ begin
486
+ (-r1).equal?(-r2)
487
+ rescue NoMethodError
488
+ false # No String#-@
489
+ end
490
+ end
491
+
467
492
  def assert_equal_float(expected, actual, delta = 1e-2)
468
493
  Array === expected and expected = expected.first
469
494
  Array === actual and actual = actual.first
@@ -1,12 +1,12 @@
1
1
  case ENV['JSON']
2
2
  when 'pure'
3
- $:.unshift 'lib'
3
+ $:.unshift File.join(__dir__, '../lib')
4
4
  require 'json/pure'
5
5
  when 'ext'
6
- $:.unshift 'ext', 'lib'
6
+ $:.unshift File.join(__dir__, '../ext'), File.join(__dir__, '../lib')
7
7
  require 'json/ext'
8
8
  else
9
- $:.unshift 'ext', 'lib'
9
+ $:.unshift File.join(__dir__, '../ext'), File.join(__dir__, '../lib')
10
10
  require 'json'
11
11
  end
12
12
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Frank
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-30 00:00:00.000000000 Z
11
+ date: 2020-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -58,6 +58,7 @@ files:
58
58
  - ".travis.yml"
59
59
  - CHANGES.md
60
60
  - Gemfile
61
+ - LICENSE
61
62
  - README-json-jruby.md
62
63
  - README.md
63
64
  - Rakefile
@@ -188,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
189
  - !ruby/object:Gem::Version
189
190
  version: '0'
190
191
  requirements: []
191
- rubygems_version: 3.1.2
192
+ rubygems_version: 3.1.4
192
193
  signing_key:
193
194
  specification_version: 4
194
195
  summary: JSON Implementation for Ruby