aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/json/decoding.rb
blob: 5eb8c0fd7d44e44eab20fe9260e7a15c42241d01 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
require 'yaml'
require 'strscan'

module ActiveSupport
  module JSON
    class ParseError < StandardError
    end
    
    class << self
      # Converts a JSON string into a Ruby object.
      def decode(json)
        YAML.load(convert_json_to_yaml(json))
      rescue ArgumentError => e
        raise ParseError, "Invalid JSON string"
      end
      
      protected
        # matches YAML-formatted dates
        DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[ \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?)$/

        # Ensure that ":" and "," are always followed by a space
        def convert_json_to_yaml(json) #:nodoc:
          scanner, quoting, marks, pos, times = StringScanner.new(json), false, [], nil, []
          while scanner.scan_until(/(\\['"]|['":,\\]|\\.)/)
            case char = scanner[1]
            when '"', "'"
              if !quoting
                quoting = char
                pos = scanner.pos
              elsif quoting == char
                if json[pos..scanner.pos-2] =~ DATE_REGEX
                  # found a date, track the exact positions of the quotes so we can remove them later.
                  # oh, and increment them for each current mark, each one is an extra padded space that bumps
                  # the position in the final YAML output
                  total_marks = marks.size
                  times << pos+total_marks << scanner.pos+total_marks
                end
                quoting = false
              end
            when ":",","
              marks << scanner.pos - 1 unless quoting
            end
          end

          if marks.empty?
            json.gsub(/\\\//, '/')
          else
            left_pos  = [-1].push(*marks)
            right_pos = marks << scanner.pos + scanner.rest_size
            output    = []
            left_pos.each_with_index do |left, i|
              scanner.pos = left.succ
              output << scanner.peek(right_pos[i] - scanner.pos + 1)
            end
            output = output * " "
            
            times.each { |i| output[i-1] = ' ' }
            output.gsub!(/\\\//, '/')
            output
          end
        end
    end
  end
end