aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/json/backends/yaml.rb
blob: e25e29d36be5d15b98c69a71363b8900f1613984 (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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
require 'active_support/core_ext/string/starts_ends_with'

module ActiveSupport
  module JSON
    module Backends
      module Yaml
        ParseError = ::StandardError
        extend self

        EXCEPTIONS = [::ArgumentError] # :nodoc:
        begin
          require 'psych'
          EXCEPTIONS << Psych::SyntaxError
        rescue LoadError
        end

        # Parses a JSON string or IO and converts it into an object
        def decode(json)
          if json.respond_to?(:read)
            json = json.read
          end
          YAML.load(convert_json_to_yaml(json))
        rescue *EXCEPTIONS => e
          raise ParseError, "Invalid JSON string: '%s'" % json
        end

        protected
          # Ensure that ":" and "," are always followed by a space
          def convert_json_to_yaml(json) #:nodoc:
            require 'strscan' unless defined? ::StringScanner
            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 valid_date?(json[pos..scanner.pos-2])
                    # found a date, track the exact positions of the quotes so we can
                    # overwrite them with spaces later.
                    times << pos
                  end
                  quoting = false
                end
              when ":",",", "]"
                marks << scanner.pos - 1 unless quoting
              when "\\"
                scanner.skip(/\\/)
              end
            end

            if marks.empty?
              json.gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do
                ustr = $1
                if ustr.start_with?('u')
                  char = [ustr[1..-1].to_i(16)].pack("U")
                  # "\n" needs extra escaping due to yaml formatting
                  char == "\n" ? "\\n" : char
                elsif ustr == '\\'
                  '\\\\'
                else
                  ustr
                end
              end
            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
                chunk = scanner.peek(right_pos[i] - scanner.pos + 1)
                if ActiveSupport.parse_json_times
                  # overwrite the quotes found around the dates with spaces
                  while times.size > 0 && times[0] <= right_pos[i]
                    chunk.insert(times.shift - scanner.pos - 1, '! ')
                  end
                end
                chunk.gsub!(/\\([\\\/]|u[[:xdigit:]]{4})/) do
                  ustr = $1
                  if ustr.start_with?('u')
                    char = [ustr[1..-1].to_i(16)].pack("U")
                    # "\n" needs extra escaping due to yaml formatting
                    char == "\n" ? "\\n" : char
                  elsif ustr == '\\'
                    '\\\\'
                  else
                    ustr
                  end
                end
                output << chunk
              end
              output = output * " "

              output.gsub!(/\\\//, '/')
              output
            end
          end

        private
          def valid_date?(date_string)
            begin
              date_string =~ DATE_REGEX && DateTime.parse(date_string)
            rescue ArgumentError
              false
            end
          end

      end
    end
  end
end