From 97ec68437f2f26960abfadb2f1e8c975bb02c8bf Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 5 Nov 2024 15:41:10 +0100 Subject: [PATCH] Turn `json_pure` into an empty gem Fix: https://github.com/ruby/json/issues/650 Closes: https://github.com/ruby/json/pull/682 --- Gemfile | 6 +- README.md | 12 +- Rakefile | 51 +- json_pure.gemspec | 23 +- lib/json.rb | 7 +- lib/json/ext.rb | 6 +- lib/json/ext/truffle_ruby_generator.rb | 619 ++++++++++++++++++++++++ lib/json/pure.rb | 16 +- lib/json/pure/generator.rb | 621 ------------------------- lib/json/pure/parser.rb | 356 -------------- test/json/test_helper.rb | 20 +- 11 files changed, 642 insertions(+), 1095 deletions(-) create mode 100644 lib/json/ext/truffle_ruby_generator.rb delete mode 100644 lib/json/pure/generator.rb delete mode 100644 lib/json/pure/parser.rb diff --git a/Gemfile b/Gemfile index ef2cf7fa0..4a76a6f91 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,6 @@ source 'https://rubygems.org' -if ENV['JSON'] == 'pure' - gemspec name: 'json_pure' -else - gemspec name: 'json' -end +gemspec name: 'json' group :development do gem "ruby_memcheck" if RUBY_PLATFORM =~ /linux/i diff --git a/README.md b/README.md index 65f284249..29624e8c2 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,10 @@ ## Description This is an implementation of the JSON specification according to RFC 7159 -http://www.ietf.org/rfc/rfc7159.txt . There is two variants available: +http://www.ietf.org/rfc/rfc7159.txt . -* A pure ruby variant, that relies on the `strscan` extensions, which is - part of the ruby standard library. -* The quite a bit faster native extension variant, which is in parts - implemented in C or Java and comes with a parser generated by the [Ragel] - state machine compiler. - -Both variants of the JSON generator generate UTF-8 character sequences by -default. If an :ascii\_only option with a true value is given, they escape all +The JSON generator generate UTF-8 character sequences by default. +If an :ascii\_only option with a true value is given, they escape all non-ASCII and control characters with \uXXXX escape sequences, and support UTF-16 surrogate pairs in order to be able to generate the whole range of unicode code points. diff --git a/Rakefile b/Rakefile index 7a013eb0d..e16fc8d3d 100644 --- a/Rakefile +++ b/Rakefile @@ -56,12 +56,8 @@ else RAGEL_DOTGEN = %w[rlgen-dot rlgen-cd ragel].find(&which) end -desc "Installing library (pure)" -task :install_pure do - ruby 'install.rb' -end - -task :install_ext_really do +desc "Installing library (extension)" +task :install => [ :compile ] do sitearchdir = CONFIG["sitearchdir"] cd 'ext' do for file in Dir["json/ext/*.#{CONFIG['DLEXT']}"] @@ -73,30 +69,6 @@ task :install_ext_really do end end -desc "Installing library (extension)" -task :install_ext => [ :compile, :install_pure, :install_ext_really ] - -desc "Installing library (extension)" -task :install => :install_ext - -task :check_env do - ENV.key?('JSON') or fail "JSON env var is required" -end - -desc "Testing library (pure ruby)" -task :test_pure => [ :set_env_pure, :check_env, :do_test_pure ] -task(:set_env_pure) { ENV['JSON'] = 'pure' } - -UndocumentedTestTask.new do |t| - t.name = 'do_test_pure' - t.test_files = FileList['test/json/*_test.rb'] - t.verbose = true - t.options = '-v' -end - -desc "Testing library (pure ruby and extension)" -task :test => [ :test_pure, :test_ext ] - namespace :gems do desc 'Install all development gems' task :install do @@ -177,16 +149,14 @@ if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'jruby' sh "gem build -o pkg/json-#{PKG_VERSION}-java.gem json.gemspec" end - desc "Testing library (jruby)" - task :test_ext => [ :set_env_ext, :create_jar, :check_env, :do_test_ext ] - task(:set_env_ext) { ENV['JSON'] = 'ext' } - UndocumentedTestTask.new do |t| - t.name = 'do_test_ext' + t.name = :test t.test_files = FileList['test/json/*_test.rb'] t.verbose = true t.options = '-v' end + desc "Testing library (jruby)" + task :test => [:create_jar ] file JRUBY_PARSER_JAR => :compile do cd 'java/src' do @@ -239,20 +209,19 @@ else task :compile => [ :ragel, EXT_PARSER_DL, EXT_GENERATOR_DL ] end - desc "Testing library (extension)" - task :test_ext => [ :set_env_ext, :check_env, :compile, :do_test_ext ] - task(:set_env_ext) { ENV['JSON'] = 'ext' } - UndocumentedTestTask.new do |t| - t.name = 'do_test_ext' + t.name = :test t.test_files = FileList['test/json/*_test.rb'] t.verbose = true t.options = '-v' end + desc "Testing library (extension)" + task :test => [ :compile ] + begin require "ruby_memcheck" - RubyMemcheck::TestTask.new(valgrind: [ :set_env_ext, :check_env, :compile, :do_test_ext ]) do |t| + RubyMemcheck::TestTask.new(valgrind: [ :compile, :test ]) do |t| t.test_files = FileList['test/json/*_test.rb'] t.verbose = true t.options = '-v' diff --git a/json_pure.gemspec b/json_pure.gemspec index 37b437c4a..21d39d024 100644 --- a/json_pure.gemspec +++ b/json_pure.gemspec @@ -23,28 +23,7 @@ Gem::Specification.new do |s| "LEGAL", "README.md", "json_pure.gemspec", - "lib/json.rb", - "lib/json/add/bigdecimal.rb", - "lib/json/add/complex.rb", - "lib/json/add/core.rb", - "lib/json/add/date.rb", - "lib/json/add/date_time.rb", - "lib/json/add/exception.rb", - "lib/json/add/ostruct.rb", - "lib/json/add/range.rb", - "lib/json/add/rational.rb", - "lib/json/add/regexp.rb", - "lib/json/add/set.rb", - "lib/json/add/struct.rb", - "lib/json/add/symbol.rb", - "lib/json/add/time.rb", - "lib/json/common.rb", - "lib/json/ext.rb", - "lib/json/generic_object.rb", "lib/json/pure.rb", - "lib/json/pure/generator.rb", - "lib/json/pure/parser.rb", - "lib/json/version.rb", ] s.homepage = "https://ruby.github.io/json" s.metadata = { @@ -56,5 +35,7 @@ Gem::Specification.new do |s| 'wiki_uri' => 'https://github.com/ruby/json/wiki' } + s.add_dependency "json" + s.required_ruby_version = Gem::Requirement.new(">= 2.7") end diff --git a/lib/json.rb b/lib/json.rb index c28e853e1..dfd9b7dfc 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -583,10 +583,5 @@ # module JSON require 'json/version' - - begin - require 'json/ext' - rescue LoadError - require 'json/pure' - end + require 'json/ext' end diff --git a/lib/json/ext.rb b/lib/json/ext.rb index 92ef61eae..175427d1f 100644 --- a/lib/json/ext.rb +++ b/lib/json/ext.rb @@ -8,14 +8,12 @@ module JSON module Ext if RUBY_ENGINE == 'truffleruby' require 'json/ext/parser' - require 'json/pure' - $DEBUG and warn "Using Ext extension for JSON parser and Pure library for JSON generator." + require 'json/ext/truffle_ruby_generator' JSON.parser = Parser - JSON.generator = JSON::Pure::Generator + JSON.generator = TruffleRubyGenerator else require 'json/ext/parser' require 'json/ext/generator' - $DEBUG and warn "Using Ext extension for JSON." JSON.parser = Parser JSON.generator = Generator end diff --git a/lib/json/ext/truffle_ruby_generator.rb b/lib/json/ext/truffle_ruby_generator.rb new file mode 100644 index 000000000..2fa1e8e05 --- /dev/null +++ b/lib/json/ext/truffle_ruby_generator.rb @@ -0,0 +1,619 @@ +# frozen_string_literal: true +module JSON + module TruffleRubyGenerator + MAP = { + "\x0" => '\u0000', + "\x1" => '\u0001', + "\x2" => '\u0002', + "\x3" => '\u0003', + "\x4" => '\u0004', + "\x5" => '\u0005', + "\x6" => '\u0006', + "\x7" => '\u0007', + "\b" => '\b', + "\t" => '\t', + "\n" => '\n', + "\xb" => '\u000b', + "\f" => '\f', + "\r" => '\r', + "\xe" => '\u000e', + "\xf" => '\u000f', + "\x10" => '\u0010', + "\x11" => '\u0011', + "\x12" => '\u0012', + "\x13" => '\u0013', + "\x14" => '\u0014', + "\x15" => '\u0015', + "\x16" => '\u0016', + "\x17" => '\u0017', + "\x18" => '\u0018', + "\x19" => '\u0019', + "\x1a" => '\u001a', + "\x1b" => '\u001b', + "\x1c" => '\u001c', + "\x1d" => '\u001d', + "\x1e" => '\u001e', + "\x1f" => '\u001f', + '"' => '\"', + '\\' => '\\\\', + }.freeze # :nodoc: + + ESCAPE_PATTERN = /[\/"\\\x0-\x1f]/n # :nodoc: + + SCRIPT_SAFE_MAP = MAP.merge( + '/' => '\\/', + "\u2028".b => '\u2028', + "\u2029".b => '\u2029', + ).freeze + + SCRIPT_SAFE_ESCAPE_PATTERN = Regexp.union(ESCAPE_PATTERN, "\u2028".b, "\u2029".b) + + # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with + # UTF16 big endian characters as \u????, and return it. + def utf8_to_json(string, script_safe = false) # :nodoc: + string = string.b + if script_safe + string.gsub!(SCRIPT_SAFE_ESCAPE_PATTERN) { SCRIPT_SAFE_MAP[$&] || $& } + else + string.gsub!(ESCAPE_PATTERN) { MAP[$&] || $& } + end + string.force_encoding(::Encoding::UTF_8) + string + end + + def utf8_to_json_ascii(string, script_safe = false) # :nodoc: + string = string.b + map = script_safe ? SCRIPT_SAFE_MAP : MAP + string.gsub!(/[\/"\\\x0-\x1f]/n) { map[$&] || $& } + string.gsub!(/( + (?: + [\xc2-\xdf][\x80-\xbf] | + [\xe0-\xef][\x80-\xbf]{2} | + [\xf0-\xf4][\x80-\xbf]{3} + )+ | + [\x80-\xc1\xf5-\xff] # invalid + )/nx) { |c| + c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" + s = c.encode(::Encoding::UTF_16BE, ::Encoding::UTF_8).unpack('H*')[0] + s.force_encoding(::Encoding::BINARY) + s.gsub!(/.{4}/n, '\\\\u\&') + s.force_encoding(::Encoding::UTF_8) + } + string.force_encoding(::Encoding::UTF_8) + string + rescue => e + raise GeneratorError.wrap(e) + end + + def valid_utf8?(string) + encoding = string.encoding + (encoding == Encoding::UTF_8 || encoding == Encoding::ASCII) && + string.valid_encoding? + end + module_function :utf8_to_json, :utf8_to_json_ascii, :valid_utf8? + + # This class is used to create State instances, that are use to hold data + # while generating a JSON text from a Ruby data structure. + class State + def self.generate(obj, opts = nil) + new(opts).generate(obj) + end + + # Creates a State object from _opts_, which ought to be Hash to create + # a new State instance configured by _opts_, something else to create + # an unconfigured instance. If _opts_ is a State object, it is just + # returned. + def self.from_state(opts) + case + when self === opts + opts + when opts.respond_to?(:to_hash) + new(opts.to_hash) + when opts.respond_to?(:to_h) + new(opts.to_h) + else + SAFE_STATE_PROTOTYPE.dup + end + end + + # Instantiates a new State object, configured by _opts_. + # + # _opts_ can have the following keys: + # + # * *indent*: a string used to indent levels (default: ''), + # * *space*: a string that is put after, a : or , delimiter (default: ''), + # * *space_before*: a string that is put before a : pair delimiter (default: ''), + # * *object_nl*: a string that is put at the end of a JSON object (default: ''), + # * *array_nl*: a string that is put at the end of a JSON array (default: ''), + # * *script_safe*: true if U+2028, U+2029 and forward slash (/) should be escaped + # as to make the JSON object safe to interpolate in a script tag (default: false). + # * *check_circular*: is deprecated now, use the :max_nesting option instead, + # * *max_nesting*: sets the maximum level of data structure nesting in + # the generated JSON, max_nesting = 0 if no maximum should be checked. + # * *allow_nan*: true if NaN, Infinity, and -Infinity should be + # generated, otherwise an exception is thrown, if these values are + # encountered. This options defaults to false. + def initialize(opts = nil) + @indent = '' + @space = '' + @space_before = '' + @object_nl = '' + @array_nl = '' + @allow_nan = false + @ascii_only = false + @depth = 0 + @buffer_initial_length = 1024 + @script_safe = false + @strict = false + @max_nesting = 100 + configure(opts) if opts + end + + # This string is used to indent levels in the JSON text. + attr_accessor :indent + + # This string is used to insert a space between the tokens in a JSON + # string. + attr_accessor :space + + # This string is used to insert a space before the ':' in JSON objects. + attr_accessor :space_before + + # This string is put at the end of a line that holds a JSON object (or + # Hash). + attr_accessor :object_nl + + # This string is put at the end of a line that holds a JSON array. + attr_accessor :array_nl + + # This integer returns the maximum level of data structure nesting in + # the generated JSON, max_nesting = 0 if no maximum is checked. + attr_accessor :max_nesting + + # If this attribute is set to true, forward slashes will be escaped in + # all json strings. + attr_accessor :script_safe + + # If this attribute is set to true, attempting to serialize types not + # supported by the JSON spec will raise a JSON::GeneratorError + attr_accessor :strict + + # :stopdoc: + attr_reader :buffer_initial_length + + def buffer_initial_length=(length) + if length > 0 + @buffer_initial_length = length + end + end + # :startdoc: + + # This integer returns the current depth data structure nesting in the + # generated JSON. + attr_accessor :depth + + def check_max_nesting # :nodoc: + return if @max_nesting.zero? + current_nesting = depth + 1 + current_nesting > @max_nesting and + raise NestingError, "nesting of #{current_nesting} is too deep" + end + + # Returns true, if circular data structures are checked, + # otherwise returns false. + def check_circular? + !@max_nesting.zero? + end + + # Returns true if NaN, Infinity, and -Infinity should be considered as + # valid JSON and output. + def allow_nan? + @allow_nan + end + + # Returns true, if only ASCII characters should be generated. Otherwise + # returns false. + def ascii_only? + @ascii_only + end + + # Returns true, if forward slashes are escaped. Otherwise returns false. + def script_safe? + @script_safe + end + + # Returns true, if strict mode is enabled. Otherwise returns false. + # Strict mode only allow serializing JSON native types: Hash, Array, + # String, Integer, Float, true, false and nil. + def strict? + @strict + end + + # Configure this State instance with the Hash _opts_, and return + # itself. + def configure(opts) + if opts.respond_to?(:to_hash) + opts = opts.to_hash + elsif opts.respond_to?(:to_h) + opts = opts.to_h + else + raise TypeError, "can't convert #{opts.class} into Hash" + end + opts.each do |key, value| + instance_variable_set "@#{key}", value + end + + # NOTE: If adding new instance variables here, check whether #generate should check them for #generate_json + @indent = opts[:indent] || '' if opts.key?(:indent) + @space = opts[:space] || '' if opts.key?(:space) + @space_before = opts[:space_before] || '' if opts.key?(:space_before) + @object_nl = opts[:object_nl] || '' if opts.key?(:object_nl) + @array_nl = opts[:array_nl] || '' if opts.key?(:array_nl) + @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) + @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only) + @depth = opts[:depth] || 0 + @buffer_initial_length ||= opts[:buffer_initial_length] + + @script_safe = if opts.key?(:script_safe) + !!opts[:script_safe] + elsif opts.key?(:escape_slash) + !!opts[:escape_slash] + else + false + end + + @strict = !!opts[:strict] if opts.key?(:strict) + + if !opts.key?(:max_nesting) # defaults to 100 + @max_nesting = 100 + elsif opts[:max_nesting] + @max_nesting = opts[:max_nesting] + else + @max_nesting = 0 + end + self + end + alias merge configure + + # Returns the configuration instance variables as a hash, that can be + # passed to the configure method. + def to_h + result = {} + instance_variables.each do |iv| + iv = iv.to_s[1..-1] + result[iv.to_sym] = self[iv] + end + result + end + + alias to_hash to_h + + # Generates a valid JSON document from object +obj+ and + # returns the result. If no valid JSON document can be + # created this method raises a + # GeneratorError exception. + def generate(obj) + if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and + !@ascii_only and !@script_safe and @max_nesting == 0 and !@strict + result = generate_json(obj, ''.dup) + else + result = obj.to_json(self) + end + JSON::TruffleRubyGenerator.valid_utf8?(result) or raise GeneratorError, + "source sequence #{result.inspect} is illegal/malformed utf-8" + result + end + + # Handles @allow_nan, @buffer_initial_length, other ivars must be the default value (see above) + private def generate_json(obj, buf) + case obj + when Hash + buf << '{' + first = true + obj.each_pair do |k,v| + buf << ',' unless first + + key_str = k.to_s + if key_str.class == String + fast_serialize_string(key_str, buf) + elsif key_str.is_a?(String) + generate_json(key_str, buf) + else + raise TypeError, "#{k.class}#to_s returns an instance of #{key_str.class}, expected a String" + end + + buf << ':' + generate_json(v, buf) + first = false + end + buf << '}' + when Array + buf << '[' + first = true + obj.each do |e| + buf << ',' unless first + generate_json(e, buf) + first = false + end + buf << ']' + when String + if obj.class == String + fast_serialize_string(obj, buf) + else + buf << obj.to_json(self) + end + when Integer + buf << obj.to_s + else + # Note: Float is handled this way since Float#to_s is slow anyway + buf << obj.to_json(self) + end + end + + # Assumes !@ascii_only, !@script_safe + private def fast_serialize_string(string, buf) # :nodoc: + buf << '"' + unless string.encoding == ::Encoding::UTF_8 + begin + string = string.encode(::Encoding::UTF_8) + rescue Encoding::UndefinedConversionError => error + raise GeneratorError, error.message + end + end + raise GeneratorError, "source sequence is illegal/malformed utf-8" unless string.valid_encoding? + + if /["\\\x0-\x1f]/n.match?(string) + buf << string.gsub(/["\\\x0-\x1f]/n, MAP) + else + buf << string + end + buf << '"' + end + + # Return the value returned by method +name+. + def [](name) + if respond_to?(name) + __send__(name) + else + instance_variable_get("@#{name}") if + instance_variables.include?("@#{name}".to_sym) # avoid warning + end + end + + def []=(name, value) + if respond_to?(name_writer = "#{name}=") + __send__ name_writer, value + else + instance_variable_set "@#{name}", value + end + end + end + + module GeneratorMethods + module Object + # Converts this object to a string (calling #to_s), converts + # it to a JSON string, and returns the result. This is a fallback, if no + # special method #to_json was defined for some object. + def to_json(state = nil, *) + if state && State.from_state(state).strict? + raise GeneratorError, "#{self.class} not allowed in JSON" + else + to_s.to_json + end + end + end + + module Hash + # Returns a JSON string containing a JSON object, that is unparsed from + # this Hash instance. + # _state_ is a JSON::State object, that can also be used to configure the + # produced JSON string output further. + # _depth_ is used to find out nesting depth, to indent accordingly. + def to_json(state = nil, *) + state = State.from_state(state) + state.check_max_nesting + json_transform(state) + end + + private + + def json_shift(state) + state.object_nl.empty? or return '' + state.indent * state.depth + end + + def json_transform(state) + depth = state.depth += 1 + + if empty? + state.depth -= 1 + return '{}' + end + + delim = ",#{state.object_nl}" + result = +"{#{state.object_nl}" + first = true + indent = !state.object_nl.empty? + each { |key, value| + result << delim unless first + result << state.indent * depth if indent + + key_str = key.to_s + if key_str.is_a?(String) + key_json = key_str.to_json(state) + else + raise TypeError, "#{key.class}#to_s returns an instance of #{key_str.class}, expected a String" + end + + result = +"#{result}#{key_json}#{state.space_before}:#{state.space}" + if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value) + raise GeneratorError, "#{value.class} not allowed in JSON" + elsif value.respond_to?(:to_json) + result << value.to_json(state) + else + result << %{"#{String(value)}"} + end + first = false + } + depth = state.depth -= 1 + unless first + result << state.object_nl + result << state.indent * depth if indent + end + result << '}' + result + end + end + + module Array + # Returns a JSON string containing a JSON array, that is unparsed from + # this Array instance. + # _state_ is a JSON::State object, that can also be used to configure the + # produced JSON string output further. + def to_json(state = nil, *) + state = State.from_state(state) + state.check_max_nesting + json_transform(state) + end + + private + + def json_transform(state) + depth = state.depth += 1 + + if empty? + state.depth -= 1 + return '[]' + end + + result = '['.dup + if state.array_nl.empty? + delim = "," + else + result << state.array_nl + delim = ",#{state.array_nl}" + end + + first = true + indent = !state.array_nl.empty? + each { |value| + result << delim unless first + result << state.indent * depth if indent + if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value) + raise GeneratorError, "#{value.class} not allowed in JSON" + elsif value.respond_to?(:to_json) + result << value.to_json(state) + else + result << %{"#{String(value)}"} + end + first = false + } + depth = state.depth -= 1 + result << state.array_nl + result << state.indent * depth if indent + result << ']' + end + end + + module Integer + # Returns a JSON string representation for this Integer number. + def to_json(*) to_s end + end + + module Float + # Returns a JSON string representation for this Float number. + def to_json(state = nil, *) + state = State.from_state(state) + case + when infinite? + if state.allow_nan? + to_s + else + raise GeneratorError, "#{self} not allowed in JSON" + end + when nan? + if state.allow_nan? + to_s + else + raise GeneratorError, "#{self} not allowed in JSON" + end + else + to_s + end + end + end + + module String + # This string should be encoded with UTF-8 A call to this method + # returns a JSON string encoded with UTF16 big endian characters as + # \u????. + def to_json(state = nil, *args) + state = State.from_state(state) + if encoding == ::Encoding::UTF_8 + unless valid_encoding? + raise GeneratorError, "source sequence is illegal/malformed utf-8" + end + string = self + else + string = encode(::Encoding::UTF_8) + end + if state.ascii_only? + %("#{JSON::TruffleRubyGenerator.utf8_to_json_ascii(string, state.script_safe)}") + else + %("#{JSON::TruffleRubyGenerator.utf8_to_json(string, state.script_safe)}") + end + rescue Encoding::UndefinedConversionError => error + raise ::JSON::GeneratorError, error.message + end + + # Module that holds the extending methods if, the String module is + # included. + module Extend + # Raw Strings are JSON Objects (the raw bytes are stored in an + # array for the key "raw"). The Ruby String can be created by this + # module method. + def json_create(o) + o['raw'].pack('C*') + end + end + + # Extends _modul_ with the String::Extend module. + def self.included(modul) + modul.extend Extend + end + + # This method creates a raw object hash, that can be nested into + # other data structures and will be unparsed as a raw string. This + # method should be used, if you want to convert raw strings to JSON + # instead of UTF-8 strings, e. g. binary data. + def to_json_raw_object + { + JSON.create_id => self.class.name, + 'raw' => self.unpack('C*'), + } + end + + # This method creates a JSON text from the result of + # a call to to_json_raw_object of this String. + def to_json_raw(*args) + to_json_raw_object.to_json(*args) + end + end + + module TrueClass + # Returns a JSON string for true: 'true'. + def to_json(*) 'true' end + end + + module FalseClass + # Returns a JSON string for false: 'false'. + def to_json(*) 'false' end + end + + module NilClass + # Returns a JSON string for nil: 'null'. + def to_json(*) 'null' end + end + end + end +end diff --git a/lib/json/pure.rb b/lib/json/pure.rb index 69d2256d1..78a6d9dce 100644 --- a/lib/json/pure.rb +++ b/lib/json/pure.rb @@ -1,16 +1,4 @@ # frozen_string_literal: true -require 'json/common' -module JSON - # This module holds all the modules/classes that implement JSON's - # functionality in pure ruby. - module Pure - require 'json/pure/parser' - require 'json/pure/generator' - $DEBUG and warn "Using Pure library for JSON." - JSON.parser = Parser - JSON.generator = Generator - end - - JSON_LOADED = true unless defined?(::JSON::JSON_LOADED) -end +warn "`json_pure` is deprecated and has no effect, just use `json`" +require "json" diff --git a/lib/json/pure/generator.rb b/lib/json/pure/generator.rb deleted file mode 100644 index 5b4c83255..000000000 --- a/lib/json/pure/generator.rb +++ /dev/null @@ -1,621 +0,0 @@ -# frozen_string_literal: true -module JSON - MAP = { - "\x0" => '\u0000', - "\x1" => '\u0001', - "\x2" => '\u0002', - "\x3" => '\u0003', - "\x4" => '\u0004', - "\x5" => '\u0005', - "\x6" => '\u0006', - "\x7" => '\u0007', - "\b" => '\b', - "\t" => '\t', - "\n" => '\n', - "\xb" => '\u000b', - "\f" => '\f', - "\r" => '\r', - "\xe" => '\u000e', - "\xf" => '\u000f', - "\x10" => '\u0010', - "\x11" => '\u0011', - "\x12" => '\u0012', - "\x13" => '\u0013', - "\x14" => '\u0014', - "\x15" => '\u0015', - "\x16" => '\u0016', - "\x17" => '\u0017', - "\x18" => '\u0018', - "\x19" => '\u0019', - "\x1a" => '\u001a', - "\x1b" => '\u001b', - "\x1c" => '\u001c', - "\x1d" => '\u001d', - "\x1e" => '\u001e', - "\x1f" => '\u001f', - '"' => '\"', - '\\' => '\\\\', - }.freeze # :nodoc: - - ESCAPE_PATTERN = /[\/"\\\x0-\x1f]/n # :nodoc: - - SCRIPT_SAFE_MAP = MAP.merge( - '/' => '\\/', - "\u2028".b => '\u2028', - "\u2029".b => '\u2029', - ).freeze - - SCRIPT_SAFE_ESCAPE_PATTERN = Regexp.union(ESCAPE_PATTERN, "\u2028".b, "\u2029".b) - - # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with - # UTF16 big endian characters as \u????, and return it. - def utf8_to_json(string, script_safe = false) # :nodoc: - string = string.b - if script_safe - string.gsub!(SCRIPT_SAFE_ESCAPE_PATTERN) { SCRIPT_SAFE_MAP[$&] || $& } - else - string.gsub!(ESCAPE_PATTERN) { MAP[$&] || $& } - end - string.force_encoding(::Encoding::UTF_8) - string - end - - def utf8_to_json_ascii(string, script_safe = false) # :nodoc: - string = string.b - map = script_safe ? SCRIPT_SAFE_MAP : MAP - string.gsub!(/[\/"\\\x0-\x1f]/n) { map[$&] || $& } - string.gsub!(/( - (?: - [\xc2-\xdf][\x80-\xbf] | - [\xe0-\xef][\x80-\xbf]{2} | - [\xf0-\xf4][\x80-\xbf]{3} - )+ | - [\x80-\xc1\xf5-\xff] # invalid - )/nx) { |c| - c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" - s = c.encode(::Encoding::UTF_16BE, ::Encoding::UTF_8).unpack('H*')[0] - s.force_encoding(::Encoding::BINARY) - s.gsub!(/.{4}/n, '\\\\u\&') - s.force_encoding(::Encoding::UTF_8) - } - string.force_encoding(::Encoding::UTF_8) - string - rescue => e - raise GeneratorError.wrap(e) - end - - def valid_utf8?(string) - encoding = string.encoding - (encoding == Encoding::UTF_8 || encoding == Encoding::ASCII) && - string.valid_encoding? - end - module_function :utf8_to_json, :utf8_to_json_ascii, :valid_utf8? - - module Pure - module Generator - # This class is used to create State instances, that are use to hold data - # while generating a JSON text from a Ruby data structure. - class State - def self.generate(obj, opts = nil) - new(opts).generate(obj) - end - - # Creates a State object from _opts_, which ought to be Hash to create - # a new State instance configured by _opts_, something else to create - # an unconfigured instance. If _opts_ is a State object, it is just - # returned. - def self.from_state(opts) - case - when self === opts - opts - when opts.respond_to?(:to_hash) - new(opts.to_hash) - when opts.respond_to?(:to_h) - new(opts.to_h) - else - SAFE_STATE_PROTOTYPE.dup - end - end - - # Instantiates a new State object, configured by _opts_. - # - # _opts_ can have the following keys: - # - # * *indent*: a string used to indent levels (default: ''), - # * *space*: a string that is put after, a : or , delimiter (default: ''), - # * *space_before*: a string that is put before a : pair delimiter (default: ''), - # * *object_nl*: a string that is put at the end of a JSON object (default: ''), - # * *array_nl*: a string that is put at the end of a JSON array (default: ''), - # * *script_safe*: true if U+2028, U+2029 and forward slash (/) should be escaped - # as to make the JSON object safe to interpolate in a script tag (default: false). - # * *check_circular*: is deprecated now, use the :max_nesting option instead, - # * *max_nesting*: sets the maximum level of data structure nesting in - # the generated JSON, max_nesting = 0 if no maximum should be checked. - # * *allow_nan*: true if NaN, Infinity, and -Infinity should be - # generated, otherwise an exception is thrown, if these values are - # encountered. This options defaults to false. - def initialize(opts = nil) - @indent = '' - @space = '' - @space_before = '' - @object_nl = '' - @array_nl = '' - @allow_nan = false - @ascii_only = false - @depth = 0 - @buffer_initial_length = 1024 - @script_safe = false - @strict = false - @max_nesting = 100 - configure(opts) if opts - end - - # This string is used to indent levels in the JSON text. - attr_accessor :indent - - # This string is used to insert a space between the tokens in a JSON - # string. - attr_accessor :space - - # This string is used to insert a space before the ':' in JSON objects. - attr_accessor :space_before - - # This string is put at the end of a line that holds a JSON object (or - # Hash). - attr_accessor :object_nl - - # This string is put at the end of a line that holds a JSON array. - attr_accessor :array_nl - - # This integer returns the maximum level of data structure nesting in - # the generated JSON, max_nesting = 0 if no maximum is checked. - attr_accessor :max_nesting - - # If this attribute is set to true, forward slashes will be escaped in - # all json strings. - attr_accessor :script_safe - - # If this attribute is set to true, attempting to serialize types not - # supported by the JSON spec will raise a JSON::GeneratorError - attr_accessor :strict - - # :stopdoc: - attr_reader :buffer_initial_length - - def buffer_initial_length=(length) - if length > 0 - @buffer_initial_length = length - end - end - # :startdoc: - - # This integer returns the current depth data structure nesting in the - # generated JSON. - attr_accessor :depth - - def check_max_nesting # :nodoc: - return if @max_nesting.zero? - current_nesting = depth + 1 - current_nesting > @max_nesting and - raise NestingError, "nesting of #{current_nesting} is too deep" - end - - # Returns true, if circular data structures are checked, - # otherwise returns false. - def check_circular? - !@max_nesting.zero? - end - - # Returns true if NaN, Infinity, and -Infinity should be considered as - # valid JSON and output. - def allow_nan? - @allow_nan - end - - # Returns true, if only ASCII characters should be generated. Otherwise - # returns false. - def ascii_only? - @ascii_only - end - - # Returns true, if forward slashes are escaped. Otherwise returns false. - def script_safe? - @script_safe - end - - # Returns true, if strict mode is enabled. Otherwise returns false. - # Strict mode only allow serializing JSON native types: Hash, Array, - # String, Integer, Float, true, false and nil. - def strict? - @strict - end - - # Configure this State instance with the Hash _opts_, and return - # itself. - def configure(opts) - if opts.respond_to?(:to_hash) - opts = opts.to_hash - elsif opts.respond_to?(:to_h) - opts = opts.to_h - else - raise TypeError, "can't convert #{opts.class} into Hash" - end - opts.each do |key, value| - instance_variable_set "@#{key}", value - end - - # NOTE: If adding new instance variables here, check whether #generate should check them for #generate_json - @indent = opts[:indent] || '' if opts.key?(:indent) - @space = opts[:space] || '' if opts.key?(:space) - @space_before = opts[:space_before] || '' if opts.key?(:space_before) - @object_nl = opts[:object_nl] || '' if opts.key?(:object_nl) - @array_nl = opts[:array_nl] || '' if opts.key?(:array_nl) - @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) - @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only) - @depth = opts[:depth] || 0 - @buffer_initial_length ||= opts[:buffer_initial_length] - - @script_safe = if opts.key?(:script_safe) - !!opts[:script_safe] - elsif opts.key?(:escape_slash) - !!opts[:escape_slash] - else - false - end - - @strict = !!opts[:strict] if opts.key?(:strict) - - if !opts.key?(:max_nesting) # defaults to 100 - @max_nesting = 100 - elsif opts[:max_nesting] - @max_nesting = opts[:max_nesting] - else - @max_nesting = 0 - end - self - end - alias merge configure - - # Returns the configuration instance variables as a hash, that can be - # passed to the configure method. - def to_h - result = {} - instance_variables.each do |iv| - iv = iv.to_s[1..-1] - result[iv.to_sym] = self[iv] - end - result - end - - alias to_hash to_h - - # Generates a valid JSON document from object +obj+ and - # returns the result. If no valid JSON document can be - # created this method raises a - # GeneratorError exception. - def generate(obj) - if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and - !@ascii_only and !@script_safe and @max_nesting == 0 and !@strict - result = generate_json(obj, ''.dup) - else - result = obj.to_json(self) - end - JSON.valid_utf8?(result) or raise GeneratorError, - "source sequence #{result.inspect} is illegal/malformed utf-8" - result - end - - # Handles @allow_nan, @buffer_initial_length, other ivars must be the default value (see above) - private def generate_json(obj, buf) - case obj - when Hash - buf << '{' - first = true - obj.each_pair do |k,v| - buf << ',' unless first - - key_str = k.to_s - if key_str.class == String - fast_serialize_string(key_str, buf) - elsif key_str.is_a?(String) - generate_json(key_str, buf) - else - raise TypeError, "#{k.class}#to_s returns an instance of #{key_str.class}, expected a String" - end - - buf << ':' - generate_json(v, buf) - first = false - end - buf << '}' - when Array - buf << '[' - first = true - obj.each do |e| - buf << ',' unless first - generate_json(e, buf) - first = false - end - buf << ']' - when String - if obj.class == String - fast_serialize_string(obj, buf) - else - buf << obj.to_json(self) - end - when Integer - buf << obj.to_s - else - # Note: Float is handled this way since Float#to_s is slow anyway - buf << obj.to_json(self) - end - end - - # Assumes !@ascii_only, !@script_safe - private def fast_serialize_string(string, buf) # :nodoc: - buf << '"' - unless string.encoding == ::Encoding::UTF_8 - begin - string = string.encode(::Encoding::UTF_8) - rescue Encoding::UndefinedConversionError => error - raise GeneratorError, error.message - end - end - raise GeneratorError, "source sequence is illegal/malformed utf-8" unless string.valid_encoding? - - if /["\\\x0-\x1f]/n.match?(string) - buf << string.gsub(/["\\\x0-\x1f]/n, MAP) - else - buf << string - end - buf << '"' - end - - # Return the value returned by method +name+. - def [](name) - if respond_to?(name) - __send__(name) - else - instance_variable_get("@#{name}") if - instance_variables.include?("@#{name}".to_sym) # avoid warning - end - end - - def []=(name, value) - if respond_to?(name_writer = "#{name}=") - __send__ name_writer, value - else - instance_variable_set "@#{name}", value - end - end - end - - module GeneratorMethods - module Object - # Converts this object to a string (calling #to_s), converts - # it to a JSON string, and returns the result. This is a fallback, if no - # special method #to_json was defined for some object. - def to_json(state = nil, *) - if state && State.from_state(state).strict? - raise GeneratorError, "#{self.class} not allowed in JSON" - else - to_s.to_json - end - end - end - - module Hash - # Returns a JSON string containing a JSON object, that is unparsed from - # this Hash instance. - # _state_ is a JSON::State object, that can also be used to configure the - # produced JSON string output further. - # _depth_ is used to find out nesting depth, to indent accordingly. - def to_json(state = nil, *) - state = State.from_state(state) - state.check_max_nesting - json_transform(state) - end - - private - - def json_shift(state) - state.object_nl.empty? or return '' - state.indent * state.depth - end - - def json_transform(state) - depth = state.depth += 1 - - if empty? - state.depth -= 1 - return '{}' - end - - delim = ",#{state.object_nl}" - result = +"{#{state.object_nl}" - first = true - indent = !state.object_nl.empty? - each { |key, value| - result << delim unless first - result << state.indent * depth if indent - - key_str = key.to_s - if key_str.is_a?(String) - key_json = key_str.to_json(state) - else - raise TypeError, "#{key.class}#to_s returns an instance of #{key_str.class}, expected a String" - end - - result = +"#{result}#{key_json}#{state.space_before}:#{state.space}" - if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value) - raise GeneratorError, "#{value.class} not allowed in JSON" - elsif value.respond_to?(:to_json) - result << value.to_json(state) - else - result << %{"#{String(value)}"} - end - first = false - } - depth = state.depth -= 1 - unless first - result << state.object_nl - result << state.indent * depth if indent - end - result << '}' - result - end - end - - module Array - # Returns a JSON string containing a JSON array, that is unparsed from - # this Array instance. - # _state_ is a JSON::State object, that can also be used to configure the - # produced JSON string output further. - def to_json(state = nil, *) - state = State.from_state(state) - state.check_max_nesting - json_transform(state) - end - - private - - def json_transform(state) - depth = state.depth += 1 - - if empty? - state.depth -= 1 - return '[]' - end - - result = '['.dup - if state.array_nl.empty? - delim = "," - else - result << state.array_nl - delim = ",#{state.array_nl}" - end - - first = true - indent = !state.array_nl.empty? - each { |value| - result << delim unless first - result << state.indent * depth if indent - if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value) - raise GeneratorError, "#{value.class} not allowed in JSON" - elsif value.respond_to?(:to_json) - result << value.to_json(state) - else - result << %{"#{String(value)}"} - end - first = false - } - depth = state.depth -= 1 - result << state.array_nl - result << state.indent * depth if indent - result << ']' - end - end - - module Integer - # Returns a JSON string representation for this Integer number. - def to_json(*) to_s end - end - - module Float - # Returns a JSON string representation for this Float number. - def to_json(state = nil, *) - state = State.from_state(state) - case - when infinite? - if state.allow_nan? - to_s - else - raise GeneratorError, "#{self} not allowed in JSON" - end - when nan? - if state.allow_nan? - to_s - else - raise GeneratorError, "#{self} not allowed in JSON" - end - else - to_s - end - end - end - - module String - # This string should be encoded with UTF-8 A call to this method - # returns a JSON string encoded with UTF16 big endian characters as - # \u????. - def to_json(state = nil, *args) - state = State.from_state(state) - if encoding == ::Encoding::UTF_8 - unless valid_encoding? - raise GeneratorError, "source sequence is illegal/malformed utf-8" - end - string = self - else - string = encode(::Encoding::UTF_8) - end - if state.ascii_only? - %("#{JSON.utf8_to_json_ascii(string, state.script_safe)}") - else - %("#{JSON.utf8_to_json(string, state.script_safe)}") - end - rescue Encoding::UndefinedConversionError => error - raise ::JSON::GeneratorError, error.message - end - - # Module that holds the extending methods if, the String module is - # included. - module Extend - # Raw Strings are JSON Objects (the raw bytes are stored in an - # array for the key "raw"). The Ruby String can be created by this - # module method. - def json_create(o) - o['raw'].pack('C*') - end - end - - # Extends _modul_ with the String::Extend module. - def self.included(modul) - modul.extend Extend - end - - # This method creates a raw object hash, that can be nested into - # other data structures and will be unparsed as a raw string. This - # method should be used, if you want to convert raw strings to JSON - # instead of UTF-8 strings, e. g. binary data. - def to_json_raw_object - { - JSON.create_id => self.class.name, - 'raw' => self.unpack('C*'), - } - end - - # This method creates a JSON text from the result of - # a call to to_json_raw_object of this String. - def to_json_raw(*args) - to_json_raw_object.to_json(*args) - end - end - - module TrueClass - # Returns a JSON string for true: 'true'. - def to_json(*) 'true' end - end - - module FalseClass - # Returns a JSON string for false: 'false'. - def to_json(*) 'false' end - end - - module NilClass - # Returns a JSON string for nil: 'null'. - def to_json(*) 'null' end - end - end - end - end -end diff --git a/lib/json/pure/parser.rb b/lib/json/pure/parser.rb deleted file mode 100644 index 36ef75ca5..000000000 --- a/lib/json/pure/parser.rb +++ /dev/null @@ -1,356 +0,0 @@ -#frozen_string_literal: true -require 'strscan' - -module JSON - module Pure - # This class implements the JSON parser that is used to parse a JSON string - # into a Ruby data structure. - class Parser < StringScanner - STRING = /" ((?:[^\x0-\x1f"\\] | - # escaped special characters: - \\["\\\/bfnrt] | - \\u[0-9a-fA-F]{4} | - # match all but escaped special characters: - \\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*) - "/nx - INTEGER = /(-?0|-?[1-9]\d*)/ - FLOAT = /(-? - (?:0|[1-9]\d*) - (?: - \.\d+(?i:e[+-]?\d+) | - \.\d+ | - (?i:e[+-]?\d+) - ) - )/x - NAN = /NaN/ - INFINITY = /Infinity/ - MINUS_INFINITY = /-Infinity/ - OBJECT_OPEN = /\{/ - OBJECT_CLOSE = /\}/ - ARRAY_OPEN = /\[/ - ARRAY_CLOSE = /\]/ - PAIR_DELIMITER = /:/ - COLLECTION_DELIMITER = /,/ - TRUE = /true/ - FALSE = /false/ - NULL = /null/ - IGNORE = %r( - (?: - //[^\n\r]*[\n\r]| # line comments - /\* # c-style comments - (?: - [\s\S]*? # any char, repeated lazily - ) - \*/ # the End of this comment - |[ \t\r\n]+ # whitespaces: space, horizontal tab, lf, cr - )+ - )mx - - UNPARSED = Object.new.freeze - - class << self - def parse(source, opts = nil) - if opts.nil? - new(source).parse - else - # NB: The ** shouldn't be required, but we have to deal with - # different versions of the `json` and `json_pure` gems being - # loaded concurrently. - # Prior to 2.7.3, `JSON::Ext::Parser` would only take kwargs. - # Ref: https://github.com/ruby/json/issues/650 - new(source, **opts).parse - end - end - end - - # Creates a new JSON::Pure::Parser instance for the string _source_. - # - # It will be configured by the _opts_ hash. _opts_ can have the following - # keys: - # * *max_nesting*: The maximum depth of nesting allowed in the parsed data - # structures. Disable depth checking with :max_nesting => false|nil|0, - # it defaults to 100. - # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in - # defiance of RFC 7159 to be parsed by the Parser. This option defaults - # to false. - # * *allow_trailing_comma*: If set to true, allow arrays and objects with a - # trailing comma in defiance of RFC 7159 to be parsed by the Parser. - # This option defaults to false. - # * *freeze*: If set to true, all parsed objects will be frozen. Parsed - # string will be deduplicated if possible. - # * *symbolize_names*: If set to true, returns symbols for the names - # (keys) in a JSON object. Otherwise strings are returned, which is - # also the default. It's not possible to use this option in - # conjunction with the *create_additions* option. - # * *create_additions*: If set to true, the Parser creates - # additions when a matching class and create_id are found. This - # option defaults to false. - # * *object_class*: Defaults to Hash. If another type is provided, it will be used - # instead of Hash to represent JSON objects. The type must respond to - # +new+ without arguments, and return an object that respond to +[]=+. - # * *array_class*: Defaults to Array If another type is provided, it will be used - # instead of Hash to represent JSON arrays. The type must respond to - # +new+ without arguments, and return an object that respond to +<<+. - # * *decimal_class*: Specifies which class to use instead of the default - # (Float) when parsing decimal numbers. This class must accept a single - # string argument in its constructor. - def initialize(source, opts = nil) - opts ||= {} - source = convert_encoding source - super source - if !opts.key?(:max_nesting) # defaults to 100 - @max_nesting = 100 - elsif opts[:max_nesting] - @max_nesting = opts[:max_nesting] - else - @max_nesting = 0 - end - @allow_nan = !!opts[:allow_nan] - @allow_trailing_comma = !!opts[:allow_trailing_comma] - @symbolize_names = !!opts[:symbolize_names] - @freeze = !!opts[:freeze] - - @deprecated_create_additions = false - @create_additions = opts.fetch(:create_additions, false) - if @create_additions.nil? - @create_additions = true - @deprecated_create_additions = true - end - - @symbolize_names && @create_additions and raise ArgumentError, - 'options :symbolize_names and :create_additions cannot be used '\ - 'in conjunction' - @create_id = @create_additions ? JSON.create_id : nil - @object_class = opts[:object_class] || Hash - @array_class = opts[:array_class] || Array - @decimal_class = opts[:decimal_class] - @match_string = opts[:match_string] - end - - alias source string - - def reset - super - @current_nesting = 0 - end - - # Parses the current JSON string _source_ and returns the - # complete data structure as a result. - def parse - reset - obj = nil - while !eos? && skip(IGNORE) do end - if eos? - raise ParserError, "source is not valid JSON!" - else - obj = parse_value - UNPARSED.equal?(obj) and raise ParserError, - "source is not valid JSON!" - obj.freeze if @freeze - end - while !eos? && skip(IGNORE) do end - eos? or raise ParserError, "source is not valid JSON!" - obj - end - - private - - def convert_encoding(source) - if source.respond_to?(:to_str) - source = source.to_str - else - raise TypeError, - "#{source.inspect} is not like a string" - end - if source.encoding != ::Encoding::BINARY - source = source.encode(::Encoding::UTF_8) - source.force_encoding(::Encoding::BINARY) - end - source - end - - # Unescape characters in strings. - UNESCAPE_MAP = { - '"' => '"', - '\\' => '\\', - '/' => '/', - 'b' => "\b", - 'f' => "\f", - 'n' => "\n", - 'r' => "\r", - 't' => "\t", - 'u' => nil, - }.freeze - - def parse_string - if scan(STRING) - return '' if self[1].empty? - string = self[1].gsub(%r{(?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff])}n) do |c| - k = $&[1] - if u = UNESCAPE_MAP.fetch(k) { k.chr } - u - else # \uXXXX - bytes = ''.b - i = 0 - while c[6 * i] == ?\\ && c[6 * i + 1] == ?u - bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16) - i += 1 - end - bytes.encode(Encoding::UTF_8, Encoding::UTF_16BE).force_encoding(::Encoding::BINARY) - end - end - string.force_encoding(::Encoding::UTF_8) - - if @freeze - string = -string - end - - if @create_additions and @match_string - for (regexp, klass) in @match_string - if klass.json_creatable? and string.match?(regexp) - if @deprecated_create_additions - warn "JSON.load implicit support for `create_additions: true` is deprecated and will be removed in 3.0, use JSON.unsafe_load or explicitly pass `create_additions: true`" - end - - return klass.json_create(string) - end - end - end - string - else - UNPARSED - end - rescue => e - raise ParserError, "Caught #{e.class} at '#{peek(20)}': #{e}" - end - - def parse_value - case - when scan(FLOAT) - if @decimal_class then - if @decimal_class == BigDecimal then - BigDecimal(self[1]) - else - @decimal_class.new(self[1]) || Float(self[1]) - end - else - Float(self[1]) - end - when scan(INTEGER) - Integer(self[1]) - when scan(TRUE) - true - when scan(FALSE) - false - when scan(NULL) - nil - when !UNPARSED.equal?(string = parse_string) - string - when scan(ARRAY_OPEN) - @current_nesting += 1 - ary = parse_array - @current_nesting -= 1 - ary - when scan(OBJECT_OPEN) - @current_nesting += 1 - obj = parse_object - @current_nesting -= 1 - obj - when @allow_nan && scan(NAN) - NaN - when @allow_nan && scan(INFINITY) - Infinity - when @allow_nan && scan(MINUS_INFINITY) - MinusInfinity - else - UNPARSED - end - end - - def parse_array - raise NestingError, "nesting of #@current_nesting is too deep" if - @max_nesting.nonzero? && @current_nesting > @max_nesting - result = @array_class.new - delim = false - loop do - case - when eos? - raise ParserError, "unexpected end of string while parsing array" - when !UNPARSED.equal?(value = parse_value) - delim = false - result << value - skip(IGNORE) - if scan(COLLECTION_DELIMITER) - delim = true - elsif match?(ARRAY_CLOSE) - ; - else - raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!" - end - when scan(ARRAY_CLOSE) - if delim && !@allow_trailing_comma - raise ParserError, "expected next element in array at '#{peek(20)}'!" - end - break - when skip(IGNORE) - ; - else - raise ParserError, "unexpected token in array at '#{peek(20)}'!" - end - end - result - end - - def parse_object - raise NestingError, "nesting of #@current_nesting is too deep" if - @max_nesting.nonzero? && @current_nesting > @max_nesting - result = @object_class.new - delim = false - loop do - case - when eos? - raise ParserError, "unexpected end of string while parsing object" - when !UNPARSED.equal?(string = parse_string) - skip(IGNORE) - unless scan(PAIR_DELIMITER) - raise ParserError, "expected ':' in object at '#{peek(20)}'!" - end - skip(IGNORE) - unless UNPARSED.equal?(value = parse_value) - result[@symbolize_names ? string.to_sym : string] = value - delim = false - skip(IGNORE) - if scan(COLLECTION_DELIMITER) - delim = true - elsif match?(OBJECT_CLOSE) - ; - else - raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!" - end - else - raise ParserError, "expected value in object at '#{peek(20)}'!" - end - when scan(OBJECT_CLOSE) - if delim && !@allow_trailing_comma - raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!" - end - if @create_additions and klassname = result[@create_id] - klass = JSON.deep_const_get(klassname) - break unless klass and klass.json_creatable? - if @deprecated_create_additions - warn "JSON.load implicit support for `create_additions: true` is deprecated and will be removed in 3.0, use JSON.unsafe_load or explicitly pass `create_additions: true`" - end - result = klass.json_create(result) - end - break - when skip(IGNORE) - ; - else - raise ParserError, "unexpected token in object at '#{peek(20)}'!" - end - end - result - end - end - end -end diff --git a/test/json/test_helper.rb b/test/json/test_helper.rb index f81eeec10..11bb8ba8c 100644 --- a/test/json/test_helper.rb +++ b/test/json/test_helper.rb @@ -1,23 +1,7 @@ -case ENV['JSON'] -when 'pure' - $LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__)) - $stderr.puts("Testing JSON::Pure") - require 'json/pure' -when 'ext' - $stderr.puts("Testing JSON::Ext") - $LOAD_PATH.unshift(File.expand_path('../../../ext', __FILE__), File.expand_path('../../../lib', __FILE__)) - require 'json/ext' -else - $LOAD_PATH.unshift(File.expand_path('../../../ext', __FILE__), File.expand_path('../../../lib', __FILE__)) - $stderr.puts("Testing JSON") - require 'json' -end +$LOAD_PATH.unshift(File.expand_path('../../../ext', __FILE__), File.expand_path('../../../lib', __FILE__)) +require 'json' require 'test/unit' -begin - require 'byebug' -rescue LoadError -end if GC.respond_to?(:verify_compaction_references) # This method was added in Ruby 3.0.0. Calling it this way asks the GC to