diff options
Diffstat (limited to 'activesupport')
-rw-r--r-- | activesupport/CHANGELOG | 3 | ||||
-rw-r--r-- | activesupport/lib/active_support/core_ext/hash/indifferent_access.rb | 6 | ||||
-rw-r--r-- | activesupport/lib/active_support/inflector.rb | 18 | ||||
-rw-r--r-- | activesupport/lib/active_support/json/decoding.rb | 22 | ||||
-rw-r--r-- | activesupport/lib/active_support/testing/setup_and_teardown.rb | 7 | ||||
-rw-r--r-- | activesupport/lib/active_support/xml_mini.rb | 19 | ||||
-rw-r--r-- | activesupport/lib/active_support/xml_mini/libxml.rb | 16 | ||||
-rw-r--r-- | activesupport/lib/active_support/xml_mini/nokogiri.rb | 77 | ||||
-rw-r--r-- | activesupport/test/core_ext/hash_ext_test.rb | 14 | ||||
-rw-r--r-- | activesupport/test/inflector_test.rb | 6 | ||||
-rw-r--r-- | activesupport/test/json/decoding_test.rb | 6 | ||||
-rw-r--r-- | activesupport/test/xml_mini/nokogiri_engine_test.rb | 157 | ||||
-rw-r--r-- | activesupport/test/xml_mini/rexml_engine_test.rb | 15 |
13 files changed, 342 insertions, 24 deletions
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 1d42000867..8c6e724606 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,6 +1,7 @@ *Edge* -* XmlMini supports libxml if available. #2084 [Bart ten Brinke] +* XmlMini supports LibXML and Nokogiri backends. #2084, #2190 [Bart ten Brinke, Aaron Patterson] + Example: XmlMini.backend = 'Nokogiri' *2.3.1 [RC2] (March 5, 2009)* diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index c96c5160b3..34ba8a005d 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -91,6 +91,12 @@ class HashWithIndifferentAccess < Hash self.dup.update(hash) end + # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second. + # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess. + def reverse_merge(other_hash) + super other_hash.with_indifferent_access + end + # Removes a specified key from the hash. def delete(key) super(convert_key(key)) diff --git a/activesupport/lib/active_support/inflector.rb b/activesupport/lib/active_support/inflector.rb index 2e2a63c3e7..3ed30bdf56 100644 --- a/activesupport/lib/active_support/inflector.rb +++ b/activesupport/lib/active_support/inflector.rb @@ -257,20 +257,22 @@ module ActiveSupport # <%= link_to(@person.name, person_path(@person)) %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> def parameterize(string, sep = '-') - re_sep = Regexp.escape(sep) - # Replace accented chars with their ASCII equivalents. + # replace accented chars with ther ascii equivalents parameterized_string = transliterate(string) - # Turn unwanted chars into the separator. + # Turn unwanted chars into the seperator parameterized_string.gsub!(/[^a-z0-9\-_\+]+/i, sep) - # No more than one of the separator in a row. - parameterized_string.squeeze!(sep) - # Remove leading/trailing separator. - parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') + unless sep.blank? + re_sep = Regexp.escape(sep) + # No more than one of the separator in a row. + parameterized_string.gsub!(/#{re_sep}{2,}/, sep) + # Remove leading/trailing separator. + parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') + end parameterized_string.downcase end - # Replaces accented characters with their ASCII equivalents. + # Replaces accented characters with their ascii equivalents. def transliterate(string) Iconv.iconv('ascii//ignore//translit', 'utf-8', string).to_s end diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 5eb8c0fd7d..0e079341ff 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -43,14 +43,32 @@ module ActiveSupport end if marks.empty? - json.gsub(/\\\//, '/') + json.gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do + ustr = $1 + if ustr.starts_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 - output << scanner.peek(right_pos[i] - scanner.pos + 1) + output << scanner.peek(right_pos[i] - scanner.pos + 1).gsub(/\\([\\\/]|u[[:xdigit:]]{4})/) do + ustr = $1 + if ustr.starts_with?('u') + [ustr[1..-1].to_i(16)].pack("U") + elsif ustr == '\\' + '\\\\' + else + ustr + end + end end output = output * " " diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 7edf6fdb32..aaf9f8f42c 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -45,7 +45,12 @@ module ActiveSupport return if @method_name.to_s == "default_test" if using_mocha = respond_to?(:mocha_verify) - assertion_counter = Mocha::TestCaseAdapter::AssertionCounter.new(result) + assertion_counter_klass = if defined?(Mocha::TestCaseAdapter::AssertionCounter) + Mocha::TestCaseAdapter::AssertionCounter + else + Mocha::Integration::TestUnit::AssertionCounter + end + assertion_counter = assertion_counter_klass.new(result) end yield(Test::Unit::TestCase::STARTED, name) diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index 99158e4ff7..ccd1349491 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -6,11 +6,24 @@ module ActiveSupport # XmlMini.backend = 'LibXML' module XmlMini extend self - delegate :parse, :to => :@backend + + attr_reader :backend + delegate :parse, :to => :backend def backend=(name) - require "active_support/xml_mini/#{name.to_s.downcase}.rb" - @backend = ActiveSupport.const_get("XmlMini_#{name}") + if name.is_a?(Module) + @backend = name + else + require "active_support/xml_mini/#{name.to_s.downcase}.rb" + @backend = ActiveSupport.const_get("XmlMini_#{name}") + end + end + + def with_backend(name) + old_backend, self.backend = backend, name + yield + ensure + self.backend = old_backend end end diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index e1549d8c58..3586b24a6b 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -1,4 +1,6 @@ -# = XML Mini Libxml implementation +require 'libxml' + +# = XmlMini LibXML implementation module ActiveSupport module XmlMini_LibXML #:nodoc: extend self @@ -7,19 +9,19 @@ module ActiveSupport # string:: # XML Document string to parse def parse(string) - XML.default_keep_blanks = false + LibXML::XML.default_keep_blanks = false if string.blank? {} else - XML::Parser.string(string.strip).parse.to_hash + LibXML::XML::Parser.string(string.strip).parse.to_hash end end end end -module XML +module LibXML module Conversions module Document def to_hash @@ -37,7 +39,7 @@ module XML # Hash to merge the converted element into. def to_hash(hash={}) if text? - raise RuntimeError if content.length >= LIB_XML_LIMIT + raise LibXML::XML::Error if content.length >= LIB_XML_LIMIT hash[CONTENT_ROOT] = content else sub_hash = insert_name_into_hash(hash, name) @@ -127,5 +129,5 @@ module XML end end -XML::Document.send(:include, XML::Conversions::Document) -XML::Node.send(:include, XML::Conversions::Node) +LibXML::XML::Document.send(:include, LibXML::Conversions::Document) +LibXML::XML::Node.send(:include, LibXML::Conversions::Node) diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb new file mode 100644 index 0000000000..10281584fc --- /dev/null +++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb @@ -0,0 +1,77 @@ +require 'nokogiri' + +# = XmlMini Nokogiri implementation +module ActiveSupport + module XmlMini_Nokogiri #:nodoc: + extend self + + # Parse an XML Document string into a simple hash using libxml / nokogiri. + # string:: + # XML Document string to parse + def parse(string) + if string.blank? + {} + else + doc = Nokogiri::XML(string) + raise doc.errors.first if doc.errors.length > 0 + doc.to_hash + end + end + + module Conversions + module Document + def to_hash + root.to_hash + end + end + + module Node + CONTENT_ROOT = '__content__' + + # Convert XML document to hash + # + # hash:: + # Hash to merge the converted element into. + def to_hash(hash = {}) + hash[name] ||= attributes_as_hash + + walker = lambda { |memo, parent, child, callback| + next if child.blank? && 'file' != parent['type'] + + if child.text? + (memo[CONTENT_ROOT] ||= '') << child.content + next + end + + name = child.name + + child_hash = child.attributes_as_hash + if memo[name] + memo[name] = [memo[name]].flatten + memo[name] << child_hash + else + memo[name] = child_hash + end + + # Recusively walk children + child.children.each { |c| + callback.call(child_hash, child, c, callback) + } + } + + children.each { |c| walker.call(hash[name], self, c, walker) } + hash + end + + def attributes_as_hash + Hash[*(attribute_nodes.map { |node| + [node.node_name, node.value] + }.flatten)] + end + end + end + + Nokogiri::XML::Document.send(:include, Conversions::Document) + Nokogiri::XML::Node.send(:include, Conversions::Node) + end +end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 2285d5a724..482ae57830 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -174,6 +174,13 @@ class HashExtTest < Test::Unit::TestCase assert_equal 2, hash['b'] end + def test_indifferent_reverse_merging + hash = HashWithIndifferentAccess.new('some' => 'value', 'other' => 'value') + hash.reverse_merge!(:some => 'noclobber', :another => 'clobber') + assert_equal 'value', hash[:some] + assert_equal 'clobber', hash[:another] + end + def test_indifferent_deleting get_hash = proc{ { :a => 'foo' }.with_indifferent_access } hash = get_hash.call @@ -884,7 +891,12 @@ class QueryTest < Test::Unit::TestCase end def test_expansion_count_is_limited - expected = defined?(LibXML) ? LibXML::XML::Error : RuntimeError + expected = { + 'ActiveSupport::XmlMini_REXML' => 'RuntimeError', + 'ActiveSupport::XmlMini_Nokogiri' => 'Nokogiri::XML::SyntaxError', + 'ActiveSupport::XmlMini_LibXML' => 'LibXML::XML::Error', + }[ActiveSupport::XmlMini.backend.name].constantize + assert_raise expected do attack_xml = <<-EOT <?xml version="1.0" encoding="UTF-8"?> diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 948b6d9bb0..6b9fbd3156 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -116,6 +116,12 @@ class InflectorTest < Test::Unit::TestCase end end + def test_parameterize_with_multi_character_separator + StringToParameterized.each do |some_string, parameterized_string| + assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__')) + end + end + def test_classify ClassNameToTableName.each do |class_name, table_name| assert_equal(class_name, ActiveSupport::Inflector.classify(table_name)) diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index b88a00e584..8fe40557d6 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -28,7 +28,11 @@ class TestJSONDecoding < Test::Unit::TestCase %(null) => nil, %(true) => true, %(false) => false, - %q("http:\/\/test.host\/posts\/1") => "http://test.host/posts/1" + %q("http:\/\/test.host\/posts\/1") => "http://test.host/posts/1", + %q("\u003cunicode\u0020escape\u003e") => "<unicode escape>", + %q("\\\\u0020skip double backslashes") => "\\u0020skip double backslashes", + %q({a: "\u003cbr /\u003e"}) => {'a' => "<br />"}, + %q({b:["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => {'b' => ["<i>","<b>","<u>"]} } TESTS.each do |json, expected| diff --git a/activesupport/test/xml_mini/nokogiri_engine_test.rb b/activesupport/test/xml_mini/nokogiri_engine_test.rb new file mode 100644 index 0000000000..e5174a0b57 --- /dev/null +++ b/activesupport/test/xml_mini/nokogiri_engine_test.rb @@ -0,0 +1,157 @@ +require 'abstract_unit' +require 'active_support/xml_mini' + +begin + gem 'nokogiri', '>= 1.1.1' +rescue Gem::LoadError + # Skip nokogiri tests +else + +require 'nokogiri' + +class NokogiriEngineTest < Test::Unit::TestCase + include ActiveSupport + + def setup + @default_backend = XmlMini.backend + XmlMini.backend = 'Nokogiri' + end + + def teardown + XmlMini.backend = @default_backend + end + + def test_file_from_xml + hash = Hash.from_xml(<<-eoxml) + <blog> + <logo type="file" name="logo.png" content_type="image/png"> + </logo> + </blog> + eoxml + assert hash.has_key?('blog') + assert hash['blog'].has_key?('logo') + + file = hash['blog']['logo'] + assert_equal 'logo.png', file.original_filename + assert_equal 'image/png', file.content_type + end + + def test_exception_thrown_on_expansion_attack + assert_raise Nokogiri::XML::SyntaxError do + attack_xml = <<-EOT + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE member [ + <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"> + <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;"> + <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;"> + <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;"> + <!ENTITY e "&f;&f;&f;&f;&f;&f;&f;&f;&f;&f;"> + <!ENTITY f "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;"> + <!ENTITY g "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"> + ]> + <member> + &a; + </member> + EOT + Hash.from_xml(attack_xml) + end + end + + def test_setting_nokogiri_as_backend + XmlMini.backend = 'Nokogiri' + assert_equal XmlMini_Nokogiri, XmlMini.backend + end + + def test_blank_returns_empty_hash + assert_equal({}, XmlMini.parse(nil)) + assert_equal({}, XmlMini.parse('')) + end + + def test_array_type_makes_an_array + assert_equal_rexml(<<-eoxml) + <blog> + <posts type="array"> + <post>a post</post> + <post>another post</post> + </posts> + </blog> + eoxml + end + + def test_one_node_document_as_hash + assert_equal_rexml(<<-eoxml) + <products/> + eoxml + end + + def test_one_node_with_attributes_document_as_hash + assert_equal_rexml(<<-eoxml) + <products foo="bar"/> + eoxml + end + + def test_products_node_with_book_node_as_hash + assert_equal_rexml(<<-eoxml) + <products> + <book name="awesome" id="12345" /> + </products> + eoxml + end + + def test_products_node_with_two_book_nodes_as_hash + assert_equal_rexml(<<-eoxml) + <products> + <book name="awesome" id="12345" /> + <book name="america" id="67890" /> + </products> + eoxml + end + + def test_single_node_with_content_as_hash + assert_equal_rexml(<<-eoxml) + <products> + hello world + </products> + eoxml + end + + def test_children_with_children + assert_equal_rexml(<<-eoxml) + <root> + <products> + <book name="america" id="67890" /> + </products> + </root> + eoxml + end + + def test_children_with_text + assert_equal_rexml(<<-eoxml) + <root> + <products> + hello everyone + </products> + </root> + eoxml + end + + def test_children_with_non_adjacent_text + assert_equal_rexml(<<-eoxml) + <root> + good + <products> + hello everyone + </products> + morning + </root> + eoxml + end + + private + def assert_equal_rexml(xml) + hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) } + assert_equal(hash, XmlMini.parse(xml)) + end +end + +end diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb new file mode 100644 index 0000000000..a412d8ca05 --- /dev/null +++ b/activesupport/test/xml_mini/rexml_engine_test.rb @@ -0,0 +1,15 @@ +require 'abstract_unit' +require 'active_support/xml_mini' + +class REXMLEngineTest < Test::Unit::TestCase + include ActiveSupport + + def test_default_is_rexml + assert_equal XmlMini_REXML, XmlMini.backend + end + + def test_set_rexml_as_backend + XmlMini.backend = 'REXML' + assert_equal XmlMini_REXML, XmlMini.backend + end +end |