Compare commits

...

21 Commits

Author SHA1 Message Date
Aman Gupta
ec99b40364 faster html escaping code from rails master 2013-02-16 17:54:13 -08:00
Aman Gupta
84420c7f12 short-circuit String#blank? when string is empty 2013-02-16 17:06:30 -08:00
Aman Gupta
c57e85fd13 Revert "ignore "invalid byte sequence in UTF-8" from String#=~"
This reverts commit 18e9b2ffc9.
2013-02-16 17:05:59 -08:00
Corey Donohoe
2eca011798 Merge pull request #2 from github/cve-2013-0277
apply patch for cve-2013-0277
2013-02-11 10:51:52 -08:00
Corey Donohoe
f6cf01337f apply patch for cve-2013-0277 2013-02-11 10:47:45 -08:00
Corey Donohoe
0ad86343c6 Merge pull request #1 from github/cve-2013-0333
Backport Patches for CVE-2013-0333
2013-01-28 16:33:32 -08:00
Corey Donohoe
42524c2bf1 backport patches for CVE-2013-0333
https://groups.google.com/forum/?fromgroups=#!topic/rubyonrails-security/1h2DR63ViGo
2013-01-28 13:23:53 -08:00
rick
46f1ddbff9 backport dynamic finder fix (CVE-2012-5664) 2013-01-02 15:02:25 -07:00
Aman Gupta
b18f5c9af1 bump mocha dependency 2012-12-08 20:35:13 -08:00
Aman Gupta
18e9b2ffc9 ignore "invalid byte sequence in UTF-8" from String#=~ 2012-11-15 04:33:45 -08:00
Aman Gupta
9ec3637bc5 backport String#encoding_aware? and String#blank? encoding fixes from rails3 2012-11-13 20:50:29 -08:00
Aman Gupta
ba9248e6e3 Remove call to Kernel#gem 2012-11-09 14:47:38 -08:00
Joshua Peek
a27559cddf Skip primary key check for HABTM inserts 2012-09-05 21:43:48 -05:00
Aman Gupta
e786726603 fix encoding errors inside ActiveSupport::BufferedLogger
http://developer.uservoice.com/blog/2012/03/04/how-to-upgrade-a-rails-2-3-app-to-ruby-1-9-3/
2012-08-17 18:14:52 -07:00
Aman Gupta
a1d2a22047 fix TZInfo on ruby 1.9
http://developer.uservoice.com/blog/2012/03/04/how-to-upgrade-a-rails-2-3-app-to-ruby-1-9-3/
2012-08-16 11:52:43 -07:00
Aman Gupta
d43ecd5b32 fix multiple queries when chaining named scopes
https://rails.lighthouseapp.com/projects/8994/tickets/5410-multiple-database-queries-when-chaining-named-scopes-with-rails-238-and-ruby-192
2012-08-15 17:35:23 -07:00
Aman Gupta
61359bf6ad Use String#encode to do transliteration on ruby 1.9 2012-08-15 11:30:20 -07:00
Aman Gupta
a2beda1177 force binary strings when logging sql statements 2012-08-14 12:52:51 -07:00
Aman Gupta
52c895d565 handle load errors on 1.9
https://github.com/rails/rails/pull/3745
http://groups.google.com/group/rubyonrails-core/browse_thread/thread/81be70a119260e59?pli=1
2012-08-14 12:52:31 -07:00
Aman Gupta
74f90612ec avoid iconv require warning on 1.9 2012-08-14 12:52:01 -07:00
Justin Collins
a6eb61b7e4 Fix SQL injection via nested hashes in conditions 2012-06-12 23:14:10 -07:00
23 changed files with 835 additions and 122 deletions

View File

@@ -31,7 +31,6 @@ rescue LoadError
end
end
gem 'rack', '~> 1.1'
require 'rack'
require 'action_controller/cgi_ext'

View File

@@ -104,7 +104,7 @@ module ActionView
# escape_once("<< Accept & Checkout")
# # => "<< Accept & Checkout"
def escape_once(html)
ActiveSupport::Multibyte.clean(html.to_s).gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] }
ERB::Util.html_escape_once(html)
end
private

View File

@@ -8,8 +8,8 @@ require 'yaml'
require 'stringio'
require 'test/unit'
gem 'mocha', '>= 0.9.7'
require 'mocha'
gem 'mocha', '>= 0.13.1'
require 'mocha/setup'
begin
require 'ruby-debug'

View File

@@ -1,8 +1,8 @@
require 'rubygems'
require 'test/unit'
gem 'mocha', '>= 0.9.3'
require 'mocha'
gem 'mocha', '>= 0.13.1'
require 'mocha/setup'
require 'active_model'
require 'active_model/state_machine'

View File

@@ -49,7 +49,7 @@ module ActiveRecord
alias_method :proxy_respond_to?, :respond_to?
alias_method :proxy_extend, :extend
delegate :to_param, :to => :proxy_target
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^respond_to_missing|^object_id$)/ }
def initialize(owner, reflection)
@owner, @reflection = owner, reflection

View File

@@ -34,17 +34,12 @@ module ActiveRecord
options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
options[:select] ||= (@reflection.options[:select] || '*')
end
def count_records
load_target.size
end
def insert_record(record, force = true, validate = true)
if has_primary_key?
raise ActiveRecord::ConfigurationError,
"Primary key is not allowed in a has_and_belongs_to_many join table (#{@reflection.options[:join_table]})."
end
if record.new_record?
if force
record.save!

View File

@@ -80,9 +80,11 @@ module ActiveRecord
end
unless instance_method_already_implemented?("#{name}=")
if create_time_zone_conversion_attribute?(name, column)
if self.serialized_attributes[name]
define_write_method_for_serialized_attribute(name)
elsif create_time_zone_conversion_attribute?(name, column)
define_write_method_for_time_zone_conversion(name)
else
else
define_write_method(name.to_sym)
end
end
@@ -184,7 +186,20 @@ module ActiveRecord
def define_write_method(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
end
# Defined for all serialized attributes. Disallows assigning already serialized YAML.
def define_write_method_for_serialized_attribute(attr_name)
method_body = <<-EOV
def #{attr_name}=(value)
if value.is_a?(String) and value =~ /^---/
raise ActiveRecordError, "You tried to assign already serialized content to #{attr_name}. This is disabled due to security issues."
end
write_attribute(:#{attr_name}, value)
end
EOV
evaluate_attribute_method attr_name, method_body, "#{attr_name}="
end
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
def define_write_method_for_time_zone_conversion(attr_name)

View File

@@ -1897,7 +1897,11 @@ module ActiveRecord #:nodoc:
# end
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.#{method_id}(*args)
options = args.extract_options!
options = if args.length > #{attribute_names.size}
args.extract_options!
else
{}
end
attributes = construct_attributes_from_arguments(
[:#{attribute_names.join(',:')}],
args
@@ -2333,17 +2337,17 @@ module ActiveRecord #:nodoc:
# And for value objects on a composed_of relationship:
# { :address => Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, default_table_name = quoted_table_name)
def sanitize_sql_hash_for_conditions(attrs, default_table_name = quoted_table_name, top_level = true)
attrs = expand_hash_conditions_for_aggregates(attrs)
conditions = attrs.map do |attr, value|
table_name = default_table_name
unless value.is_a?(Hash)
if not value.is_a?(Hash)
attr = attr.to_s
# Extract table name from qualified attribute names.
if attr.include?('.')
if attr.include?('.') and top_level
attr_table_name, attr = attr.split('.', 2)
attr_table_name = connection.quote_table_name(attr_table_name)
else
@@ -2351,8 +2355,10 @@ module ActiveRecord #:nodoc:
end
attribute_condition("#{attr_table_name}.#{connection.quote_column_name(attr)}", value)
elsif top_level
sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s), false)
else
sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
raise ActiveRecord::StatementInvalid
end
end.join(' AND ')

View File

@@ -195,6 +195,7 @@ module ActiveRecord
def log_info(sql, name, ms)
if @logger && @logger.debug?
name = '%s (%.1fms)' % [name || 'SQL', ms]
sql.force_encoding 'binary' if sql.respond_to?(:force_encoding)
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
end
end

View File

@@ -1499,6 +1499,12 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil topic.content
end
def test_should_raise_exception_on_assigning_aready_serialized_content
topic = Topic.new
serialized_content = %w[foo bar].to_yaml
assert_raise(ActiveRecord::ActiveRecordError) { topic.content = serialized_content }
end
def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
myobj = MyObject.new('value1', 'value2')
topic = Topic.new(:content => myobj)

View File

@@ -363,6 +363,22 @@ class FinderTest < ActiveRecord::TestCase
}
end
def test_hash_condition_find_with_improper_nested_hashes
assert_raise(ActiveRecord::StatementInvalid) {
Company.find(:first, :conditions => { :name => { :companies => { :id => 1 }}})
}
end
def test_hash_condition_find_with_dot_in_nested_column_name
assert_raise(ActiveRecord::StatementInvalid) {
Company.find(:first, :conditions => { :name => { "companies.id" => 1 }})
}
end
def test_hash_condition_find_with_dot_in_column_name_okay
assert Company.find(:first, :conditions => { "companies.id" => 1 })
end
def test_hash_condition_find_with_escaped_characters
Company.create("name" => "Ain't noth'n like' \#stuff")
assert Company.find(:first, :conditions => { :name => "Ain't noth'n like' \#stuff" })

View File

@@ -63,7 +63,11 @@ module ActiveSupport
# If a newline is necessary then create a new message ending with a newline.
# Ensures that the original message is not mutated.
message = "#{message}\n" unless message[-1] == ?\n
buffer << message
if message.respond_to?(:force_encoding)
buffer << message.force_encoding(Encoding.default_external)
else
buffer << message
end
auto_flush
message
end

View File

@@ -20,7 +20,8 @@ class MissingSourceFile < LoadError #:nodoc:
REGEXPS = [
[/^no such file to load -- (.+)$/i, 1],
[/^Missing \w+ (file\s*)?([^\s]+.rb)$/i, 2],
[/^Missing API definition file in (.+)$/i, 1]
[/^Missing API definition file in (.+)$/i, 1],
[/^cannot load such file -- (.+)$/i, 1]
] unless defined?(REGEXPS)
end

View File

@@ -1,3 +1,6 @@
# encoding: utf-8
require 'active_support/core_ext/string/encoding'
class Object
# An object is blank if it's false, empty, or a whitespace string.
# For example, "", " ", +nil+, [], and {} are blank.
@@ -63,9 +66,26 @@ class Hash #:nodoc:
alias_method :blank?, :empty?
end
class String #:nodoc:
class String
# 0x3000: fullwidth whitespace
NON_WHITESPACE_REGEXP = %r![^\s#{[0x3000].pack("U")}]!
# A string is blank if it's empty or contains whitespaces only:
#
# "".blank? # => true
# " ".blank? # => true
# " ".blank? # => true
# " something here ".blank? # => false
#
def blank?
self !~ /\S/
return true if empty?
# 1.8 does not takes [:space:] properly
if encoding_aware?
self !~ /[^[:space:]]/
else
self !~ NON_WHITESPACE_REGEXP
end
end
end

View File

@@ -0,0 +1,11 @@
class String
if defined?(Encoding) && "".respond_to?(:encode)
def encoding_aware?
true
end
else
def encoding_aware?
false
end
end
end

View File

@@ -2,8 +2,10 @@ require 'erb'
class ERB
module Util
HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' }
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
JSON_ESCAPE_REGEXP = /[&"><]/
# A utility method for escaping HTML tag characters.
# This method is also aliased as <tt>h</tt>.
@@ -14,12 +16,23 @@ class ERB
# ==== Example:
# puts html_escape("is a > 0 & a < 10?")
# # => is a &gt; 0 &amp; a &lt; 10?
def html_escape(s)
s = s.to_s
if s.html_safe?
s
else
s.to_s.gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;").html_safe
if RUBY_VERSION > '1.9'
def html_escape(s)
s = s.to_s
if s.html_safe?
s
else
s.gsub(/[&"'><]/, HTML_ESCAPE).html_safe
end
end
else
def html_escape(s)
s = s.to_s
if s.html_safe?
s
else
s.gsub(/[&"'><]/){ |x| HTML_ESCAPE[x] }.html_safe
end
end
end
@@ -29,21 +42,50 @@ class ERB
module_function :html_escape
module_function :h
# A utility method for escaping HTML entities in JSON strings.
# This method is also aliased as <tt>j</tt>.
# A utility method for escaping HTML without affecting existing escaped entities.
#
# In your ERb templates, use this method to escape any HTML entities:
# <%=j @person.to_json %>
# html_escape_once('1 < 2 &amp; 3')
# # => "1 &lt; 2 &amp; 3"
#
# ==== Example:
# puts json_escape("is a > 0 & a < 10?")
# # => is a \u003E 0 \u0026 a \u003C 10?
def json_escape(s)
s.to_s.gsub(/[&"><]/) { |special| JSON_ESCAPE[special] }
# html_escape_once('&lt;&lt; Accept & Checkout')
# # => "&lt;&lt; Accept &amp; Checkout"
if RUBY_VERSION > '1.9'
def html_escape_once(s)
result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
s.html_safe? ? result.html_safe : result
end
else
def html_escape_once(s)
result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP) { |special| HTML_ESCAPE[special] }
s.html_safe? ? result.html_safe : result
end
end
module_function :html_escape_once
# A utility method for escaping HTML entities in JSON strings
# using \uXXXX JavaScript escape sequences for string literals:
#
# json_escape('is a > 0 & a < 10?')
# # => is a \u003E 0 \u0026 a \u003C 10?
#
# Note that after this operation is performed the output is not
# valid JSON. In particular double quotes are removed:
#
# json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}')
# # => {name:john,created_at:2010-04-28T01:39:31Z,id:1}
if RUBY_VERSION > '1.9'
def json_escape(s)
result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
s.html_safe? ? result.html_safe : result
end
else
def json_escape(s)
result = s.to_s.gsub(JSON_ESCAPE_REGEXP) { |special| JSON_ESCAPE[special] }
s.html_safe? ? result.html_safe : result
end
end
alias j json_escape
module_function :j
module_function :json_escape
end
end
@@ -54,7 +96,7 @@ class Object
end
end
class Fixnum
class Numeric
def html_safe?
true
end

View File

@@ -1,6 +1,6 @@
# encoding: utf-8
require 'singleton'
require 'iconv'
require 'iconv' if RUBY_VERSION < '1.9'
require 'kconv'
module ActiveSupport
@@ -283,8 +283,7 @@ module ActiveSupport
if RUBY_VERSION >= '1.9'
undef_method :transliterate
def transliterate(string)
warn "Ruby 1.9 doesn't support Unicode normalization yet"
string.dup
string.encode("us-ascii", :undef => :replace, :invalid => :replace, :replace => "")
end
# The iconv transliteration code doesn't function correctly

View File

@@ -0,0 +1,644 @@
module ActiveSupport
# Include OkJson as a replacement for the Yaml backend
# encoding: UTF-8
#
# Copyright 2011, 2012 Keith Rarick
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# See https://github.com/kr/okjson for updates.
require 'stringio'
# Some parts adapted from
# http://golang.org/src/pkg/json/decode.go and
# http://golang.org/src/pkg/utf8/utf8.go
module OkJson
Upstream = 'LTD7LBKLZWFF7OZK'
extend self
# Decodes a json document in string s and
# returns the corresponding ruby value.
# String s must be valid UTF-8. If you have
# a string in some other encoding, convert
# it first.
#
# String values in the resulting structure
# will be UTF-8.
def decode(s)
ts = lex(s)
v, ts = textparse(ts)
if ts.length > 0
raise Error, 'trailing garbage'
end
v
end
# Parses a "json text" in the sense of RFC 4627.
# Returns the parsed value and any trailing tokens.
# Note: this is almost the same as valparse,
# except that it does not accept atomic values.
def textparse(ts)
if ts.length < 0
raise Error, 'empty'
end
typ, _, val = ts[0]
case typ
when '{' then objparse(ts)
when '[' then arrparse(ts)
else
raise Error, "unexpected #{val.inspect}"
end
end
# Parses a "value" in the sense of RFC 4627.
# Returns the parsed value and any trailing tokens.
def valparse(ts)
if ts.length < 0
raise Error, 'empty'
end
typ, _, val = ts[0]
case typ
when '{' then objparse(ts)
when '[' then arrparse(ts)
when :val,:str then [val, ts[1..-1]]
else
raise Error, "unexpected #{val.inspect}"
end
end
# Parses an "object" in the sense of RFC 4627.
# Returns the parsed value and any trailing tokens.
def objparse(ts)
ts = eat('{', ts)
obj = {}
if ts[0][0] == '}'
return obj, ts[1..-1]
end
k, v, ts = pairparse(ts)
obj[k] = v
if ts[0][0] == '}'
return obj, ts[1..-1]
end
loop do
ts = eat(',', ts)
k, v, ts = pairparse(ts)
obj[k] = v
if ts[0][0] == '}'
return obj, ts[1..-1]
end
end
end
# Parses a "member" in the sense of RFC 4627.
# Returns the parsed values and any trailing tokens.
def pairparse(ts)
(typ, _, k), ts = ts[0], ts[1..-1]
if typ != :str
raise Error, "unexpected #{k.inspect}"
end
ts = eat(':', ts)
v, ts = valparse(ts)
[k, v, ts]
end
# Parses an "array" in the sense of RFC 4627.
# Returns the parsed value and any trailing tokens.
def arrparse(ts)
ts = eat('[', ts)
arr = []
if ts[0][0] == ']'
return arr, ts[1..-1]
end
v, ts = valparse(ts)
arr << v
if ts[0][0] == ']'
return arr, ts[1..-1]
end
loop do
ts = eat(',', ts)
v, ts = valparse(ts)
arr << v
if ts[0][0] == ']'
return arr, ts[1..-1]
end
end
end
def eat(typ, ts)
if ts[0][0] != typ
raise Error, "expected #{typ} (got #{ts[0].inspect})"
end
ts[1..-1]
end
# Scans s and returns a list of json tokens,
# excluding white space (as defined in RFC 4627).
def lex(s)
ts = []
while s.length > 0
typ, lexeme, val = tok(s)
if typ == nil
raise Error, "invalid character at #{s[0,10].inspect}"
end
if typ != :space
ts << [typ, lexeme, val]
end
s = s[lexeme.length..-1]
end
ts
end
# Scans the first token in s and
# returns a 3-element list, or nil
# if s does not begin with a valid token.
#
# The first list element is one of
# '{', '}', ':', ',', '[', ']',
# :val, :str, and :space.
#
# The second element is the lexeme.
#
# The third element is the value of the
# token for :val and :str, otherwise
# it is the lexeme.
def tok(s)
case s[0]
when ?{ then ['{', s[0,1], s[0,1]]
when ?} then ['}', s[0,1], s[0,1]]
when ?: then [':', s[0,1], s[0,1]]
when ?, then [',', s[0,1], s[0,1]]
when ?[ then ['[', s[0,1], s[0,1]]
when ?] then [']', s[0,1], s[0,1]]
when ?n then nulltok(s)
when ?t then truetok(s)
when ?f then falsetok(s)
when ?" then strtok(s)
when Spc then [:space, s[0,1], s[0,1]]
when ?\t then [:space, s[0,1], s[0,1]]
when ?\n then [:space, s[0,1], s[0,1]]
when ?\r then [:space, s[0,1], s[0,1]]
else numtok(s)
end
end
def nulltok(s); s[0,4] == 'null' ? [:val, 'null', nil] : [] end
def truetok(s); s[0,4] == 'true' ? [:val, 'true', true] : [] end
def falsetok(s); s[0,5] == 'false' ? [:val, 'false', false] : [] end
def numtok(s)
m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s)
if m && m.begin(0) == 0
if m[3] && !m[2]
[:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))]
elsif m[2]
[:val, m[0], Float(m[0])]
else
[:val, m[0], Integer(m[0])]
end
else
[]
end
end
def strtok(s)
m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s)
if ! m
raise Error, "invalid string literal at #{abbrev(s)}"
end
[:str, m[0], unquote(m[0])]
end
def abbrev(s)
t = s[0,10]
p = t['`']
t = t[0,p] if p
t = t + '...' if t.length < s.length
'`' + t + '`'
end
# Converts a quoted json string literal q into a UTF-8-encoded string.
# The rules are different than for Ruby, so we cannot use eval.
# Unquote will raise an error if q contains control characters.
def unquote(q)
q = q[1...-1]
a = q.dup # allocate a big enough string
rubydoesenc = false
# In ruby >= 1.9, a[w] is a codepoint, not a byte.
if a.class.method_defined?(:force_encoding)
a.force_encoding('UTF-8')
rubydoesenc = true
end
r, w = 0, 0
while r < q.length
c = q[r]
case true
when c == ?\\
r += 1
if r >= q.length
raise Error, "string literal ends with a \"\\\": \"#{q}\""
end
case q[r]
when ?",?\\,?/,?'
a[w] = q[r]
r += 1
w += 1
when ?b,?f,?n,?r,?t
a[w] = Unesc[q[r]]
r += 1
w += 1
when ?u
r += 1
uchar = begin
hexdec4(q[r,4])
rescue RuntimeError => e
raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}"
end
r += 4
if surrogate? uchar
if q.length >= r+6
uchar1 = hexdec4(q[r+2,4])
uchar = subst(uchar, uchar1)
if uchar != Ucharerr
# A valid pair; consume.
r += 6
end
end
end
if rubydoesenc
a[w] = '' << uchar
w += 1
else
w += ucharenc(a, w, uchar)
end
else
raise Error, "invalid escape char #{q[r]} in \"#{q}\""
end
when c == ?", c < Spc
raise Error, "invalid character in string literal \"#{q}\""
else
# Copy anything else byte-for-byte.
# Valid UTF-8 will remain valid UTF-8.
# Invalid UTF-8 will remain invalid UTF-8.
# In ruby >= 1.9, c is a codepoint, not a byte,
# in which case this is still what we want.
a[w] = c
r += 1
w += 1
end
end
a[0,w]
end
# Encodes unicode character u as UTF-8
# bytes in string a at position i.
# Returns the number of bytes written.
def ucharenc(a, i, u)
case true
when u <= Uchar1max
a[i] = (u & 0xff).chr
1
when u <= Uchar2max
a[i+0] = (Utag2 | ((u>>6)&0xff)).chr
a[i+1] = (Utagx | (u&Umaskx)).chr
2
when u <= Uchar3max
a[i+0] = (Utag3 | ((u>>12)&0xff)).chr
a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr
a[i+2] = (Utagx | (u&Umaskx)).chr
3
else
a[i+0] = (Utag4 | ((u>>18)&0xff)).chr
a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr
a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr
a[i+3] = (Utagx | (u&Umaskx)).chr
4
end
end
def hexdec4(s)
if s.length != 4
raise Error, 'short'
end
(nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3])
end
def subst(u1, u2)
if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3
return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself
end
return Ucharerr
end
def surrogate?(u)
Usurr1 <= u && u < Usurr3
end
def nibble(c)
case true
when ?0 <= c && c <= ?9 then c.ord - ?0.ord
when ?a <= c && c <= ?z then c.ord - ?a.ord + 10
when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10
else
raise Error, "invalid hex code #{c}"
end
end
# Encodes x into a json text. It may contain only
# Array, Hash, String, Numeric, true, false, nil.
# (Note, this list excludes Symbol.)
# X itself must be an Array or a Hash.
# No other value can be encoded, and an error will
# be raised if x contains any other value, such as
# Nan, Infinity, Symbol, and Proc, or if a Hash key
# is not a String.
# Strings contained in x must be valid UTF-8.
def encode(x)
case x
when Hash then objenc(x)
when Array then arrenc(x)
else
raise Error, 'root value must be an Array or a Hash'
end
end
def valenc(x)
case x
when Hash then objenc(x)
when Array then arrenc(x)
when String then strenc(x)
when Numeric then numenc(x)
when true then "true"
when false then "false"
when nil then "null"
else
raise Error, "cannot encode #{x.class}: #{x.inspect}"
end
end
def objenc(x)
'{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}'
end
def arrenc(a)
'[' + a.map{|x| valenc(x)}.join(',') + ']'
end
def keyenc(k)
case k
when String then strenc(k)
else
raise Error, "Hash key is not a string: #{k.inspect}"
end
end
def strenc(s)
t = StringIO.new
t.putc(?")
r = 0
# In ruby >= 1.9, s[r] is a codepoint, not a byte.
rubydoesenc = s.class.method_defined?(:encoding)
while r < s.length
case s[r]
when ?" then t.print('\\"')
when ?\\ then t.print('\\\\')
when ?\b then t.print('\\b')
when ?\f then t.print('\\f')
when ?\n then t.print('\\n')
when ?\r then t.print('\\r')
when ?\t then t.print('\\t')
else
c = s[r]
case true
when rubydoesenc
begin
c.ord # will raise an error if c is invalid UTF-8
t.write(c)
rescue
t.write(Ustrerr)
end
when Spc <= c && c <= ?~
t.putc(c)
else
n = ucharcopy(t, s, r) # ensure valid UTF-8 output
r += n - 1 # r is incremented below
end
end
r += 1
end
t.putc(?")
t.string
end
def numenc(x)
if ((x.nan? || x.infinite?) rescue false)
raise Error, "Numeric cannot be represented: #{x}"
end
"#{x}"
end
# Copies the valid UTF-8 bytes of a single character
# from string s at position i to I/O object t, and
# returns the number of bytes copied.
# If no valid UTF-8 char exists at position i,
# ucharcopy writes Ustrerr and returns 1.
def ucharcopy(t, s, i)
n = s.length - i
raise Utf8Error if n < 1
c0 = s[i].ord
# 1-byte, 7-bit sequence?
if c0 < Utagx
t.putc(c0)
return 1
end
raise Utf8Error if c0 < Utag2 # unexpected continuation byte?
raise Utf8Error if n < 2 # need continuation byte
c1 = s[i+1].ord
raise Utf8Error if c1 < Utagx || Utag2 <= c1
# 2-byte, 11-bit sequence?
if c0 < Utag3
raise Utf8Error if ((c0&Umask2)<<6 | (c1&Umaskx)) <= Uchar1max
t.putc(c0)
t.putc(c1)
return 2
end
# need second continuation byte
raise Utf8Error if n < 3
c2 = s[i+2].ord
raise Utf8Error if c2 < Utagx || Utag2 <= c2
# 3-byte, 16-bit sequence?
if c0 < Utag4
u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx)
raise Utf8Error if u <= Uchar2max
t.putc(c0)
t.putc(c1)
t.putc(c2)
return 3
end
# need third continuation byte
raise Utf8Error if n < 4
c3 = s[i+3].ord
raise Utf8Error if c3 < Utagx || Utag2 <= c3
# 4-byte, 21-bit sequence?
if c0 < Utag5
u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx)
raise Utf8Error if u <= Uchar3max
t.putc(c0)
t.putc(c1)
t.putc(c2)
t.putc(c3)
return 4
end
raise Utf8Error
rescue Utf8Error
t.write(Ustrerr)
return 1
end
class Utf8Error < ::StandardError
end
class Error < ::StandardError
end
Utagx = 0x80 # 1000 0000
Utag2 = 0xc0 # 1100 0000
Utag3 = 0xe0 # 1110 0000
Utag4 = 0xf0 # 1111 0000
Utag5 = 0xF8 # 1111 1000
Umaskx = 0x3f # 0011 1111
Umask2 = 0x1f # 0001 1111
Umask3 = 0x0f # 0000 1111
Umask4 = 0x07 # 0000 0111
Uchar1max = (1<<7) - 1
Uchar2max = (1<<11) - 1
Uchar3max = (1<<16) - 1
Ucharerr = 0xFFFD # unicode "replacement char"
Ustrerr = "\xef\xbf\xbd" # unicode "replacement char"
Usurrself = 0x10000
Usurr1 = 0xd800
Usurr2 = 0xdc00
Usurr3 = 0xe000
Spc = ' '[0]
Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t}
end
module JSON
module Backends
module OkJson
ParseError = ::ActiveSupport::OkJson::Error
extend self
# Parses a JSON string or IO and convert it into an object
def decode(json)
if json.respond_to?(:read)
json = json.read
end
data = ActiveSupport::OkJson.decode(json)
if ActiveSupport.parse_json_times
convert_dates_from(data)
else
data
end
end
private
def convert_dates_from(data)
case data
when nil
nil
when DATE_REGEX
begin
DateTime.parse(data)
rescue ArgumentError
data
end
when Array
data.map! { |d| convert_dates_from(d) }
when Hash
data.each do |key, value|
data[key] = convert_dates_from(value)
end
else
data
end
end
end
end
end
end

View File

@@ -7,79 +7,12 @@ module ActiveSupport
ParseError = ::StandardError
extend 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"
raise "The Yaml backend has been deprecated due to security risks, you should set ActiveSupport::JSON.backend = 'OkJson'"
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 json[pos..scanner.pos-2] =~ DATE_REGEX
# found a date, track the exact positions of the quotes so we can
# overwrite them with spaces later.
times << pos << scanner.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')
[ustr[1..-1].to_i(16)].pack("U")
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)
# overwrite the quotes found around the dates with spaces
while times.size > 0 && times[0] <= right_pos[i]
chunk[times.shift - scanner.pos - 1] = ' '
end
chunk.gsub!(/\\([\\\/]|u[[:xdigit:]]{4})/) do
ustr = $1
if ustr.start_with?('u')
[ustr[1..-1].to_i(16)].pack("U")
elsif ustr == '\\'
'\\\\'
else
ustr
end
end
output << chunk
end
output = output * " "
output.gsub!(/\\\//, '/')
output
end
end
end
end
end

View File

@@ -6,7 +6,7 @@ module ActiveSupport
module JSON
# Listed in order of preference.
DECODERS = %w(Yajl Yaml)
DECODERS = %w(Yajl OkJson)
class << self
attr_reader :parse_error

View File

@@ -5,8 +5,8 @@ require 'active_support/testing/deprecation'
require 'active_support/testing/declarative'
begin
gem 'mocha', ">= 0.9.7"
require 'mocha'
gem 'mocha', ">= 0.13.1"
require 'mocha/setup'
rescue LoadError
# Fake Mocha::ExpectationError so we can rescue it in #run. Bleh.
Object.const_set :Mocha, Module.new

View File

@@ -47,10 +47,31 @@ module TZInfo
def self.datetime_new!(ajd = 0, of = 0, sg = Date::ITALY)
DateTime.new!(ajd, of, sg)
end
else
elsif DateTime.respond_to? :new0
def self.datetime_new!(ajd = 0, of = 0, sg = Date::ITALY)
DateTime.new0(ajd, of, sg)
end
else
HALF_DAYS_IN_DAY = rational_new!(1, 2)
def self.datetime_new!(ajd = 0, of = 0, sg = Date::ITALY)
# Convert from an Astronomical Julian Day number to a civil Julian Day number.
jd = ajd + of + HALF_DAYS_IN_DAY
# Ruby trunk revision 31862 changed the behaviour of DateTime.jd so that it will no
# longer accept a fractional civil Julian Day number if further arguments are specified.
# Calculate the hours, minutes and seconds to pass to jd.
jd_i = jd.to_i
jd_i -= 1 if jd < 0
hours = (jd - jd_i) * 24
hours_i = hours.to_i
minutes = (hours - hours_i) * 60
minutes_i = minutes.to_i
seconds = (minutes - minutes_i) * 60
DateTime.jd(jd_i, hours_i, minutes_i, seconds, of, sg)
end
end
end
end
end

View File

@@ -42,9 +42,9 @@ class TestJSONDecoding < ActiveSupport::TestCase
}
# load the default JSON backend
ActiveSupport::JSON.backend = 'Yaml'
ActiveSupport::JSON.backend = 'OkJson'
backends = %w(Yaml)
backends = %w(OkJson)
backends << "JSONGem" if defined?(::JSON)
backends << "Yajl" if defined?(::Yajl)