json_pure 2.0.4 → 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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +9 -5
  4. data/CHANGES.md +39 -0
  5. data/Gemfile +1 -3
  6. data/LICENSE +56 -0
  7. data/README.md +54 -21
  8. data/Rakefile +19 -93
  9. data/VERSION +1 -1
  10. data/ext/json/ext/generator/generator.c +214 -45
  11. data/ext/json/ext/generator/generator.h +5 -2
  12. data/ext/json/ext/parser/extconf.rb +25 -0
  13. data/ext/json/ext/parser/parser.c +155 -83
  14. data/ext/json/ext/parser/parser.h +2 -0
  15. data/ext/json/ext/parser/parser.rl +79 -7
  16. data/ext/json/extconf.rb +1 -0
  17. data/java/src/json/ext/Generator.java +28 -24
  18. data/java/src/json/ext/GeneratorState.java +30 -0
  19. data/java/src/json/ext/Parser.java +109 -82
  20. data/java/src/json/ext/Parser.rl +39 -12
  21. data/java/src/json/ext/StringEncoder.java +8 -2
  22. data/json-java.gemspec +22 -22
  23. data/json.gemspec +0 -0
  24. data/json_pure.gemspec +9 -14
  25. data/lib/json.rb +549 -29
  26. data/lib/json/add/bigdecimal.rb +2 -2
  27. data/lib/json/add/complex.rb +2 -3
  28. data/lib/json/add/ostruct.rb +1 -1
  29. data/lib/json/add/rational.rb +2 -3
  30. data/lib/json/add/regexp.rb +2 -2
  31. data/lib/json/add/set.rb +29 -0
  32. data/lib/json/common.rb +341 -115
  33. data/lib/json/pure/generator.rb +31 -10
  34. data/lib/json/pure/parser.rb +35 -5
  35. data/lib/json/version.rb +1 -1
  36. data/tests/json_addition_test.rb +6 -0
  37. data/tests/json_common_interface_test.rb +47 -4
  38. data/tests/json_encoding_test.rb +2 -2
  39. data/tests/json_fixtures_test.rb +9 -1
  40. data/tests/json_generator_test.rb +55 -0
  41. data/tests/json_parser_test.rb +43 -12
  42. data/tests/test_helper.rb +3 -7
  43. metadata +17 -13
  44. data/data/example.json +0 -1
  45. data/data/index.html +0 -38
  46. data/data/prototype.js +0 -4184
@@ -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
@@ -250,7 +268,8 @@ module JSON
250
268
  if respond_to?(name)
251
269
  __send__(name)
252
270
  else
253
- instance_variable_get("@#{name}")
271
+ instance_variable_get("@#{name}") if
272
+ instance_variables.include?("@#{name}".to_sym) # avoid warning
254
273
  end
255
274
  end
256
275
 
@@ -313,8 +332,10 @@ module JSON
313
332
  first = false
314
333
  }
315
334
  depth = state.depth -= 1
316
- result << state.object_nl
317
- result << state.indent * depth if indent
335
+ unless first
336
+ result << state.object_nl
337
+ result << state.indent * depth if indent
338
+ end
318
339
  result << '}'
319
340
  result
320
341
  end
@@ -398,13 +419,13 @@ module JSON
398
419
  string = encode(::Encoding::UTF_8)
399
420
  end
400
421
  if state.ascii_only?
401
- '"' << JSON.utf8_to_json_ascii(string) << '"'
422
+ '"' << JSON.utf8_to_json_ascii(string, state.escape_slash) << '"'
402
423
  else
403
- '"' << JSON.utf8_to_json(string) << '"'
424
+ '"' << JSON.utf8_to_json(string, state.escape_slash) << '"'
404
425
  end
405
426
  end
406
427
 
407
- # Module that holds the extinding methods if, the String module is
428
+ # Module that holds the extending methods if, the String module is
408
429
  # included.
409
430
  module Extend
410
431
  # Raw Strings are JSON Objects (the raw bytes are stored in an
@@ -49,7 +49,7 @@ module JSON
49
49
  )+
50
50
  )mx
51
51
 
52
- UNPARSED = Object.new.freeze
52
+ UNPARSED = Object.new.freeze
53
53
 
54
54
  # Creates a new JSON::Pure::Parser instance for the string _source_.
55
55
  #
@@ -61,15 +61,20 @@ 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
67
69
  # conjunction with the *create_additions* option.
68
70
  # * *create_additions*: If set to true, the Parser creates
69
- # additions when if a matching class and create_id was found. This
71
+ # additions when a matching class and create_id are found. This
70
72
  # option defaults to false.
71
73
  # * *object_class*: Defaults to Hash
72
74
  # * *array_class*: Defaults to Array
75
+ # * *decimal_class*: Specifies which class to use instead of the default
76
+ # (Float) when parsing decimal numbers. This class must accept a single
77
+ # string argument in its constructor.
73
78
  def initialize(source, opts = {})
74
79
  opts ||= {}
75
80
  source = convert_encoding source
@@ -83,6 +88,7 @@ module JSON
83
88
  end
84
89
  @allow_nan = !!opts[:allow_nan]
85
90
  @symbolize_names = !!opts[:symbolize_names]
91
+ @freeze = !!opts[:freeze]
86
92
  if opts.key?(:create_additions)
87
93
  @create_additions = !!opts[:create_additions]
88
94
  else
@@ -94,6 +100,7 @@ module JSON
94
100
  @create_id = @create_additions ? JSON.create_id : nil
95
101
  @object_class = opts[:object_class] || Hash
96
102
  @array_class = opts[:array_class] || Array
103
+ @decimal_class = opts[:decimal_class]
97
104
  @match_string = opts[:match_string]
98
105
  end
99
106
 
@@ -116,6 +123,7 @@ module JSON
116
123
  obj = parse_value
117
124
  UNPARSED.equal?(obj) and raise ParserError,
118
125
  "source is not valid JSON!"
126
+ obj.freeze if @freeze
119
127
  end
120
128
  while !eos? && skip(IGNORE) do end
121
129
  eos? or raise ParserError, "source is not valid JSON!"
@@ -157,6 +165,7 @@ module JSON
157
165
  EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
158
166
  end
159
167
 
168
+ STR_UMINUS = ''.respond_to?(:-@)
160
169
  def parse_string
161
170
  if scan(STRING)
162
171
  return '' if self[1].empty?
@@ -176,6 +185,15 @@ module JSON
176
185
  if string.respond_to?(:force_encoding)
177
186
  string.force_encoding(::Encoding::UTF_8)
178
187
  end
188
+
189
+ if @freeze
190
+ if STR_UMINUS
191
+ string = -string
192
+ else
193
+ string.freeze
194
+ end
195
+ end
196
+
179
197
  if @create_additions and @match_string
180
198
  for (regexp, klass) in @match_string
181
199
  klass.json_creatable? or next
@@ -193,7 +211,15 @@ module JSON
193
211
  def parse_value
194
212
  case
195
213
  when scan(FLOAT)
196
- Float(self[1])
214
+ if @decimal_class then
215
+ if @decimal_class == BigDecimal then
216
+ BigDecimal(self[1])
217
+ else
218
+ @decimal_class.new(self[1]) || Float(self[1])
219
+ end
220
+ else
221
+ Float(self[1])
222
+ end
197
223
  when scan(INTEGER)
198
224
  Integer(self[1])
199
225
  when scan(TRUE)
@@ -230,8 +256,10 @@ module JSON
230
256
  @max_nesting.nonzero? && @current_nesting > @max_nesting
231
257
  result = @array_class.new
232
258
  delim = false
233
- until eos?
259
+ loop do
234
260
  case
261
+ when eos?
262
+ raise ParserError, "unexpected end of string while parsing array"
235
263
  when !UNPARSED.equal?(value = parse_value)
236
264
  delim = false
237
265
  result << value
@@ -262,8 +290,10 @@ module JSON
262
290
  @max_nesting.nonzero? && @current_nesting > @max_nesting
263
291
  result = @object_class.new
264
292
  delim = false
265
- until eos?
293
+ loop do
266
294
  case
295
+ when eos?
296
+ raise ParserError, "unexpected end of string while parsing object"
267
297
  when !UNPARSED.equal?(string = parse_string)
268
298
  skip(IGNORE)
269
299
  unless scan(PAIR_DELIMITER)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
  module JSON
3
3
  # JSON version
4
- VERSION = '2.0.4'
4
+ VERSION = '2.3.1'
5
5
  VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
6
6
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
7
7
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
@@ -5,6 +5,7 @@ require 'json/add/complex'
5
5
  require 'json/add/rational'
6
6
  require 'json/add/bigdecimal'
7
7
  require 'json/add/ostruct'
8
+ require 'json/add/set'
8
9
  require 'date'
9
10
 
10
11
  class JSONAdditionTest < Test::Unit::TestCase
@@ -190,4 +191,9 @@ class JSONAdditionTest < Test::Unit::TestCase
190
191
  o.foo = { 'bar' => true }
191
192
  assert_equal o, parse(JSON(o), :create_additions => true)
192
193
  end
194
+
195
+ def test_set
196
+ s = Set.new([:a, :b, :c, :a])
197
+ assert_equal s, JSON.parse(JSON(s), :create_additions => true)
198
+ end
193
199
  end
@@ -27,15 +27,15 @@ class JSONCommonInterfaceTest < Test::Unit::TestCase
27
27
  end
28
28
 
29
29
  def test_parser
30
- assert_match /::Parser\z/, JSON.parser.name
30
+ assert_match(/::Parser\z/, JSON.parser.name)
31
31
  end
32
32
 
33
33
  def test_generator
34
- assert_match /::Generator\z/, JSON.generator.name
34
+ assert_match(/::Generator\z/, JSON.generator.name)
35
35
  end
36
36
 
37
37
  def test_state
38
- assert_match /::Generator::State\z/, JSON.state.name
38
+ assert_match(/::Generator::State\z/, JSON.state.name)
39
39
  end
40
40
 
41
41
  def test_create_id
@@ -56,7 +56,7 @@ class JSONCommonInterfaceTest < Test::Unit::TestCase
56
56
  end
57
57
 
58
58
  def test_parse_bang
59
- assert_equal [ 1, NaN, 3, ], JSON.parse!('[ 1, NaN, 3 ]')
59
+ assert_equal [ 1, Infinity, 3, ], JSON.parse!('[ 1, Infinity, 3 ]')
60
60
  end
61
61
 
62
62
  def test_generate
@@ -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
@@ -79,8 +79,8 @@ class JSONEncodingTest < Test::Unit::TestCase
79
79
  json = '["\ud840\udc01"]'
80
80
  assert_equal json, generate(utf8, :ascii_only => true)
81
81
  assert_equal utf8, parse(json)
82
- assert_raises(JSON::ParserError) { parse('"\u"') }
83
- assert_raises(JSON::ParserError) { parse('"\ud800"') }
82
+ assert_raise(JSON::ParserError) { parse('"\u"') }
83
+ assert_raise(JSON::ParserError) { parse('"\ud800"') }
84
84
  end
85
85
 
86
86
  def test_chars
@@ -3,13 +3,14 @@ require 'test_helper'
3
3
 
4
4
  class JSONFixturesTest < Test::Unit::TestCase
5
5
  def setup
6
- fixtures = File.join(File.dirname(__FILE__), 'fixtures/{fail,pass}.json')
6
+ fixtures = File.join(File.dirname(__FILE__), 'fixtures/{fail,pass}*.json')
7
7
  passed, failed = Dir[fixtures].partition { |f| f['pass'] }
8
8
  @passed = passed.inject([]) { |a, f| a << [ f, File.read(f) ] }.sort
9
9
  @failed = failed.inject([]) { |a, f| a << [ f, File.read(f) ] }.sort
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
@@ -29,4 +32,9 @@ class JSONFixturesTest < Test::Unit::TestCase
29
32
  end
30
33
  end
31
34
  end
35
+
36
+ def test_sanity
37
+ assert(@passed.size > 5)
38
+ assert(@failed.size > 20)
39
+ end
32
40
  end
@@ -40,6 +40,43 @@ class JSONGeneratorTest < Test::Unit::TestCase
40
40
  EOT
41
41
  end
42
42
 
43
+ def silence
44
+ v = $VERBOSE
45
+ $VERBOSE = nil
46
+ yield
47
+ ensure
48
+ $VERBOSE = v
49
+ end
50
+
51
+ def test_remove_const_segv
52
+ stress = GC.stress
53
+ const = JSON::SAFE_STATE_PROTOTYPE.dup
54
+
55
+ bignum_too_long_to_embed_as_string = 1234567890123456789012345
56
+ expect = bignum_too_long_to_embed_as_string.to_s
57
+ GC.stress = true
58
+
59
+ 10.times do |i|
60
+ tmp = bignum_too_long_to_embed_as_string.to_json
61
+ raise "'\#{expect}' is expected, but '\#{tmp}'" unless tmp == expect
62
+ end
63
+
64
+ silence do
65
+ JSON.const_set :SAFE_STATE_PROTOTYPE, nil
66
+ end
67
+
68
+ 10.times do |i|
69
+ assert_raise TypeError do
70
+ bignum_too_long_to_embed_as_string.to_json
71
+ end
72
+ end
73
+ ensure
74
+ GC.stress = stress
75
+ silence do
76
+ JSON.const_set :SAFE_STATE_PROTOTYPE, const
77
+ end
78
+ end if JSON.const_defined?("Ext") && RUBY_ENGINE != 'jruby'
79
+
43
80
  def test_generate
44
81
  json = generate(@hash)
45
82
  assert_equal(parse(@json2), parse(json))
@@ -55,6 +92,11 @@ EOT
55
92
  end
56
93
 
57
94
  def test_generate_pretty
95
+ json = pretty_generate({})
96
+ assert_equal(<<'EOT'.chomp, json)
97
+ {
98
+ }
99
+ EOT
58
100
  json = pretty_generate(@hash)
59
101
  # hashes aren't (insertion) ordered on every ruby implementation
60
102
  # assert_equal(@json3, json)
@@ -136,6 +178,7 @@ EOT
136
178
  :ascii_only => false,
137
179
  :buffer_initial_length => 1024,
138
180
  :depth => 0,
181
+ :escape_slash => false,
139
182
  :indent => " ",
140
183
  :max_nesting => 100,
141
184
  :object_nl => "\n",
@@ -152,6 +195,7 @@ EOT
152
195
  :ascii_only => false,
153
196
  :buffer_initial_length => 1024,
154
197
  :depth => 0,
198
+ :escape_slash => false,
155
199
  :indent => "",
156
200
  :max_nesting => 100,
157
201
  :object_nl => "",
@@ -168,6 +212,7 @@ EOT
168
212
  :ascii_only => false,
169
213
  :buffer_initial_length => 1024,
170
214
  :depth => 0,
215
+ :escape_slash => false,
171
216
  :indent => "",
172
217
  :max_nesting => 0,
173
218
  :object_nl => "",
@@ -356,6 +401,10 @@ EOT
356
401
  json = '["/"]'
357
402
  assert_equal json, generate(data)
358
403
  #
404
+ data = [ '/' ]
405
+ json = '["\/"]'
406
+ assert_equal json, generate(data, :escape_slash => true)
407
+ #
359
408
  data = ['"']
360
409
  json = '["\""]'
361
410
  assert_equal json, generate(data)
@@ -374,4 +423,10 @@ EOT
374
423
  assert_equal '["foo"]', JSON.generate([s.new('foo')])
375
424
  end
376
425
  end
426
+
427
+ if defined?(Encoding)
428
+ def test_nonutf8_encoding
429
+ assert_equal("\"5\u{b0}\"", "5\xb0".force_encoding("iso-8859-1").to_json)
430
+ end
431
+ end
377
432
  end