json_pure 2.0.4 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
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