From 10c9cfea11b2bde3e3c0096cadcd03522c0d1ed7 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 27 Jul 2023 17:11:51 +0900 Subject: [PATCH 01/33] Bump version --- lib/rexml/rexml.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rexml/rexml.rb b/lib/rexml/rexml.rb index 0d18559a..0315a2db 100644 --- a/lib/rexml/rexml.rb +++ b/lib/rexml/rexml.rb @@ -31,7 +31,7 @@ module REXML COPYRIGHT = "Copyright © 2001-2008 Sean Russell " DATE = "2008/019" - VERSION = "3.2.6" + VERSION = "3.2.7" REVISION = "" Copyright = COPYRIGHT From 9c694933d5f983004d543db394da16718e694e2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 08:53:46 +0900 Subject: [PATCH 02/33] build(deps): bump actions/checkout from 3 to 4 (#101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
Release notes

Sourced from actions/checkout's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v4.0.0

v3.6.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3.5.3...v3.6.0

v3.5.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v3.5.3

v3.5.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v3.5.1...v3.5.2

v3.5.1

What's Changed

New Contributors

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v4.0.0

v3.6.0

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

v3.0.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2755192a..20ff87e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Extract release note run: | ruby \ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e7df009..a96885a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: # - runs-on: ubuntu-latest # ruby-version: truffleruby steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} @@ -47,7 +47,7 @@ jobs: - "3.0" - head steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} @@ -65,7 +65,7 @@ jobs: name: "Document" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.7 @@ -75,7 +75,7 @@ jobs: - name: Build document run: | bundle exec rake warning:error rdoc - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: | github.event_name == 'push' with: From 5ff20266416b9830e9531912d6eaf9682b5d070a Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 5 Jan 2024 10:02:08 +0900 Subject: [PATCH 03/33] CI: Add ruby-3.3 (#102) I'd like to run tests on both ruby-3.3. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a96885a6..5bf3a654 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: - "3.0" - "3.1" - "3.2" + - "3.3" - jruby # include: # - runs-on: ubuntu-latest From 6a0dd497d8435398dec566b4d52330eb79b75173 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 5 Jan 2024 11:22:34 +0900 Subject: [PATCH 04/33] Use reusing workflow for Ruby versions (#103) This automatically add new version of Ruby for GitHub Actiosn. --- .github/workflows/test.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5bf3a654..94a116a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,14 @@ on: - push - pull_request jobs: + ruby-versions: + uses: ruby/actions/.github/workflows/ruby_versions.yml@master + with: + engine: cruby-jruby + min_version: 2.5 + inplace: + needs: ruby-versions name: "Inplace: ${{ matrix.ruby-version }} on ${{ matrix.runs-on }}" runs-on: ${{ matrix.runs-on }} strategy: @@ -13,15 +20,7 @@ jobs: - ubuntu-latest - macos-latest - windows-latest - ruby-version: - - "2.5" - - "2.6" - - "2.7" - - "3.0" - - "3.1" - - "3.2" - - "3.3" - - jruby + ruby-version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} # include: # - runs-on: ubuntu-latest # ruby-version: truffleruby From 72a26d616fc1bfaad00f1422f17f5fad38f40e1f Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sun, 7 Jan 2024 07:58:40 +0900 Subject: [PATCH 05/33] Add parse benchmark (#104) I want to improve the parsing process and would like to add a parsing benchmark. The benchmark process just parses the XML from beginning to end. Since performance differs depending on whether YJIT is ON or OFF, both are measured. --- .github/workflows/benchmark.yml | 29 +++++++++++++++++ Rakefile | 39 ++++++++++++++++++++++ benchmark/parse.yaml | 57 +++++++++++++++++++++++++++++++++ rexml.gemspec | 1 + 4 files changed, 126 insertions(+) create mode 100644 .github/workflows/benchmark.yml create mode 100644 benchmark/parse.yaml diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..52349b44 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,29 @@ +name: Benchmark + +on: + - push + - pull_request + +jobs: + benchmark: + name: "Benchmark: Ruby ${{ matrix.ruby-version }}: ${{ matrix.runs-on }}" + strategy: + fail-fast: false + matrix: + ruby-version: + - '3.3' + runs-on: + - ubuntu-latest + runs-on: ${{ matrix.runs-on }} + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + - name: Install dependencies + run: | + bundle install + gem install rexml -v 3.2.6 + - name: Benchmark + run: | + rake benchmark diff --git a/Rakefile b/Rakefile index 7143e754..76a56296 100644 --- a/Rakefile +++ b/Rakefile @@ -28,3 +28,42 @@ RDoc::Task.new do |rdoc| end load "#{__dir__}/tasks/tocs.rake" + +benchmark_tasks = [] +namespace :benchmark do + Dir.glob("benchmark/*.yaml").sort.each do |yaml| + name = File.basename(yaml, ".*") + env = { + "RUBYLIB" => nil, + "BUNDLER_ORIG_RUBYLIB" => nil, + } + command_line = [ + RbConfig.ruby, "-v", "-S", "benchmark-driver", File.expand_path(yaml), + ] + + desc "Run #{name} benchmark" + task name do + puts("```") + sh(env, *command_line) + puts("```") + end + benchmark_tasks << "benchmark:#{name}" + + case name + when /\Aparse/ + namespace name do + desc "Run #{name} benchmark: small" + task :small do + puts("```") + sh(env.merge("N_ELEMENTS" => "500", "N_ATTRIBUTES" => "1"), + *command_line) + puts("```") + end + benchmark_tasks << "benchmark:#{name}:small" + end + end + end +end + +desc "Run all benchmarks" +task :benchmark => benchmark_tasks diff --git a/benchmark/parse.yaml b/benchmark/parse.yaml new file mode 100644 index 00000000..e7066fcb --- /dev/null +++ b/benchmark/parse.yaml @@ -0,0 +1,57 @@ +loop_count: 100 +contexts: + - gems: + rexml: 3.2.6 + require: false + prelude: require 'rexml' + - name: master + prelude: | + $LOAD_PATH.unshift(File.expand_path("lib")) + require 'rexml' + - name: 3.2.6(YJIT) + gems: + rexml: 3.2.6 + require: false + prelude: | + require 'rexml' + RubyVM::YJIT.enable + - name: master(YJIT) + prelude: | + $LOAD_PATH.unshift(File.expand_path("lib")) + require 'rexml' + RubyVM::YJIT.enable + +prelude: | + require 'rexml/document' + require 'rexml/parsers/sax2parser' + require 'rexml/parsers/pullparser' + require 'rexml/parsers/streamparser' + require 'rexml/streamlistener' + + n_elements = Integer(ENV.fetch("N_ELEMENTS", "5000"), 10) + n_attributes = Integer(ENV.fetch("N_ATTRIBUTES", "2"), 10) + + def build_xml(n_elements, n_attributes) + xml = '' + n_elements.times do |i| + xml << '' + end + xml << '' + end + xml = build_xml(n_elements, n_attributes) + + class Listener + include REXML::StreamListener + end + +benchmark: + 'dom' : REXML::Document.new(xml).elements.each("root/child") {|_|} + 'sax' : REXML::Parsers::SAX2Parser.new(xml).parse + 'pull' : | + parser = REXML::Parsers::PullParser.new(xml) + while parser.has_next? + parser.pull + end + 'stream' : REXML::Parsers::StreamParser.new(xml, Listener.new).parse diff --git a/rexml.gemspec b/rexml.gemspec index ceb77047..b51df33b 100644 --- a/rexml.gemspec +++ b/rexml.gemspec @@ -55,6 +55,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.0' + spec.add_development_dependency "benchmark_driver" spec.add_development_dependency "bundler" spec.add_development_dependency "rake" spec.add_development_dependency "test-unit" From 810d2285235d5501a0a124f300832e6e9515da3c Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Wed, 17 Jan 2024 15:32:57 +0900 Subject: [PATCH 06/33] Use string scanner with baseparser (#105) Using StringScanner reduces the string copying process and speeds up the process. And I removed unnecessary methods. https://github.com/ruby/rexml/actions/runs/7549990000/job/20554906140?pr=105 ``` ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux] Calculating ------------------------------------- rexml 3.2.6 master 3.2.6(YJIT) master(YJIT) dom 4.868 5.077 8.137 8.303 i/s - 100.000 times in 20.540529s 19.696590s 12.288900s 12.043666s sax 13.597 13.953 19.206 20.948 i/s - 100.000 times in 7.354343s 7.167142s 5.206745s 4.773765s pull 15.641 16.918 22.266 25.378 i/s - 100.000 times in 6.393424s 5.910955s 4.491201s 3.940471s stream 14.339 15.844 19.810 22.206 i/s - 100.000 times in 6.973856s 6.311350s 5.047957s 4.503244s Comparison: dom master(YJIT): 8.3 i/s 3.2.6(YJIT): 8.1 i/s - 1.02x slower master: 5.1 i/s - 1.64x slower rexml 3.2.6: 4.9 i/s - 1.71x slower sax master(YJIT): 20.9 i/s 3.2.6(YJIT): 19.2 i/s - 1.09x slower master: 14.0 i/s - 1.50x slower rexml 3.2.6: 13.6 i/s - 1.54x slower pull master(YJIT): 25.4 i/s 3.2.6(YJIT): 22.3 i/s - 1.14x slower master: 16.9 i/s - 1.50x slower rexml 3.2.6: 15.6 i/s - 1.62x slower stream master(YJIT): 22.2 i/s 3.2.6(YJIT): 19.8 i/s - 1.12x slower master: 15.8 i/s - 1.40x slower rexml 3.2.6: 14.3 i/s - 1.55x slower ``` - YJIT=ON : 1.02x - 1.14x faster - YJIT=OFF : 1.02x - 1.10x faster --------- Co-authored-by: Sutou Kouhei --- benchmark/parse.yaml | 4 + lib/rexml/parsers/baseparser.rb | 21 ++-- lib/rexml/source.rb | 149 ++++++++------------------ rexml.gemspec | 2 + test/parse/test_entity_declaration.rb | 36 +++++++ test/test_core.rb | 2 +- 6 files changed, 99 insertions(+), 115 deletions(-) create mode 100644 test/parse/test_entity_declaration.rb diff --git a/benchmark/parse.yaml b/benchmark/parse.yaml index e7066fcb..8818b50c 100644 --- a/benchmark/parse.yaml +++ b/benchmark/parse.yaml @@ -5,6 +5,8 @@ contexts: require: false prelude: require 'rexml' - name: master + gems: + strscan: 3.0.8 prelude: | $LOAD_PATH.unshift(File.expand_path("lib")) require 'rexml' @@ -16,6 +18,8 @@ contexts: require 'rexml' RubyVM::YJIT.enable - name: master(YJIT) + gems: + strscan: 3.0.8 prelude: | $LOAD_PATH.unshift(File.expand_path("lib")) require 'rexml' diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index 305b1207..65bad260 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -96,7 +96,7 @@ class BaseParser ENTITYDEF = "(?:#{ENTITYVALUE}|(?:#{EXTERNALID}(#{NDATADECL})?))" PEDECL = "" GEDECL = "" - ENTITYDECL = /\s*(?:#{GEDECL})|(?:#{PEDECL})/um + ENTITYDECL = /\s*(?:#{GEDECL})|\s*(?:#{PEDECL})/um NOTATIONDECL_START = /\A\s*0 - rv - end - def read end - def consume( pattern ) - @buffer = $' if pattern.match( @buffer ) - end - - def match_to( char, pattern ) - return pattern.match(@buffer) - end - - def match_to_consume( char, pattern ) - md = pattern.match(@buffer) - @buffer = $' - return md - end - def match(pattern, cons=false) - md = pattern.match(@buffer) - @buffer = $' if cons and md - return md + if cons + @scanner.scan(pattern).nil? ? nil : @scanner + else + @scanner.check(pattern).nil? ? nil : @scanner + end end # @return true if the Source is exhausted def empty? - @buffer == "" - end - - def position - @orig.index( @buffer ) + @scanner.eos? end # @return the current line in the source def current_line lines = @orig.split - res = lines.grep @buffer[0..30] + res = lines.grep @scanner.rest[0..30] res = res[-1] if res.kind_of? Array lines.index( res ) if res end private + def detect_encoding - buffer_encoding = @buffer.encoding + scanner_encoding = @scanner.rest.encoding detected_encoding = "UTF-8" begin - @buffer.force_encoding("ASCII-8BIT") - if @buffer[0, 2] == "\xfe\xff" - @buffer[0, 2] = "" + @scanner.string.force_encoding("ASCII-8BIT") + if @scanner.scan(/\xfe\xff/n) detected_encoding = "UTF-16BE" - elsif @buffer[0, 2] == "\xff\xfe" - @buffer[0, 2] = "" + elsif @scanner.scan(/\xff\xfe/n) detected_encoding = "UTF-16LE" - elsif @buffer[0, 3] == "\xef\xbb\xbf" - @buffer[0, 3] = "" + elsif @scanner.scan(/\xef\xbb\xbf/n) detected_encoding = "UTF-8" end ensure - @buffer.force_encoding(buffer_encoding) + @scanner.string.force_encoding(scanner_encoding) end self.encoding = detected_encoding end def encoding_updated if @encoding != 'UTF-8' - @buffer = decode(@buffer) + @scanner.string = decode(@scanner.rest) @to_utf = true else @to_utf = false - @buffer.force_encoding ::Encoding::UTF_8 + @scanner.string.force_encoding(::Encoding::UTF_8) end end end @@ -172,7 +138,7 @@ def initialize(arg, block_size=500, encoding=nil) end if !@to_utf and - @buffer.respond_to?(:force_encoding) and + @orig.respond_to?(:force_encoding) and @source.respond_to?(:external_encoding) and @source.external_encoding != ::Encoding::UTF_8 @force_utf8 = true @@ -181,65 +147,44 @@ def initialize(arg, block_size=500, encoding=nil) end end - def scan(pattern, cons=false) - rv = super - # You'll notice that this next section is very similar to the same - # section in match(), but just a liiittle different. This is - # because it is a touch faster to do it this way with scan() - # than the way match() does it; enough faster to warrant duplicating - # some code - if rv.size == 0 - until @buffer =~ pattern or @source.nil? - begin - @buffer << readline - rescue Iconv::IllegalSequence - raise - rescue - @source = nil - end - end - rv = super - end - rv.taint if RUBY_VERSION < '2.7' - rv - end - def read begin - @buffer << readline + # NOTE: `@scanner << readline` does not free memory, so when parsing huge XML in JRuby's DOM, + # out-of-memory error `Java::JavaLang::OutOfMemoryError: Java heap space` occurs. + # `@scanner.string = @scanner.rest + readline` frees memory that is already consumed + # and avoids this problem. + @scanner.string = @scanner.rest + readline rescue Exception, NameError @source = nil end end - def consume( pattern ) - match( pattern, true ) - end - def match( pattern, cons=false ) - rv = pattern.match(@buffer) - @buffer = $' if cons and rv - while !rv and @source + if cons + md = @scanner.scan(pattern) + else + md = @scanner.check(pattern) + end + while md.nil? and @source begin - @buffer << readline - rv = pattern.match(@buffer) - @buffer = $' if cons and rv + @scanner << readline + if cons + md = @scanner.scan(pattern) + else + md = @scanner.check(pattern) + end rescue @source = nil end end - rv.taint if RUBY_VERSION < '2.7' - rv + + md.nil? ? nil : @scanner end def empty? super and ( @source.nil? || @source.eof? ) end - def position - @er_source.pos rescue 0 - end - # @return the current line in the source def current_line begin @@ -290,7 +235,7 @@ def encoding_updated @source.set_encoding(@encoding, @encoding) end @line_break = encode(">") - @pending_buffer, @buffer = @buffer, "" + @pending_buffer, @scanner.string = @scanner.rest, "" @pending_buffer.force_encoding(@encoding) super end diff --git a/rexml.gemspec b/rexml.gemspec index b51df33b..2ba1c64d 100644 --- a/rexml.gemspec +++ b/rexml.gemspec @@ -55,6 +55,8 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.0' + spec.add_runtime_dependency("strscan", ">= 3.0.8") + spec.add_development_dependency "benchmark_driver" spec.add_development_dependency "bundler" spec.add_development_dependency "rake" diff --git a/test/parse/test_entity_declaration.rb b/test/parse/test_entity_declaration.rb new file mode 100644 index 00000000..e15deec6 --- /dev/null +++ b/test/parse/test_entity_declaration.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: false +require 'test/unit' +require 'rexml/document' + +module REXMLTests + class TestParseEntityDeclaration < Test::Unit::TestCase + private + def xml(internal_subset) + <<-XML + + + XML + end + + def parse(internal_subset) + REXML::Document.new(xml(internal_subset)).doctype + end + + def test_empty + exception = assert_raise(REXML::ParseException) do + parse(<<-INTERNAL_SUBSET) + + INTERNAL_SUBSET + end + assert_equal(<<-DETAIL.chomp, exception.to_s) +Malformed notation declaration: name is missing +Line: 5 +Position: 72 +Last 80 unconsumed characters: + ]> + DETAIL + end + end +end diff --git a/test/test_core.rb b/test/test_core.rb index 7c18c03f..8c33d834 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -727,7 +727,7 @@ def test_iso_8859_1_output_function koln_iso_8859_1 = "K\xF6ln" koln_utf8 = "K\xc3\xb6ln" source = Source.new( koln_iso_8859_1, 'iso-8859-1' ) - results = source.scan(/.*/)[0] + results = source.match(/.*/)[0] koln_utf8.force_encoding('UTF-8') if koln_utf8.respond_to?(:force_encoding) assert_equal koln_utf8, results output << results From 83ca5c4b0f76cf7b307dd1be1dc934e1e8199863 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sun, 21 Jan 2024 06:11:42 +0900 Subject: [PATCH 07/33] Reduce calls to `Source#buffer`(`StringScanner#rest`) (#106) Reduce calls to `Source#buffer`(`StringScanner#rest`) ## Why `Source#buffer` calling `StringScanner#rest`. `StringScanner#rest` is slow. Reduce calls to `Source#buffer`. ## Benchmark ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.0/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22] Calculating ------------------------------------- before after before(YJIT) after(YJIT) dom 10.639 10.985 16.213 16.221 i/s - 100.000 times in 9.399033s 9.103461s 6.167962s 6.164794s sax 28.357 29.440 42.900 44.375 i/s - 100.000 times in 3.526479s 3.396688s 2.331024s 2.253511s pull 32.852 34.210 48.976 51.273 i/s - 100.000 times in 3.043965s 2.923140s 2.041816s 1.950344s stream 30.821 31.908 43.953 44.697 i/s - 100.000 times in 3.244539s 3.134020s 2.275172s 2.237310s Comparison: dom after(YJIT): 16.2 i/s before(YJIT): 16.2 i/s - 1.00x slower after: 11.0 i/s - 1.48x slower before: 10.6 i/s - 1.52x slower sax after(YJIT): 44.4 i/s before(YJIT): 42.9 i/s - 1.03x slower after: 29.4 i/s - 1.51x slower before: 28.4 i/s - 1.56x slower pull after(YJIT): 51.3 i/s before(YJIT): 49.0 i/s - 1.05x slower after: 34.2 i/s - 1.50x slower before: 32.9 i/s - 1.56x slower stream after(YJIT): 44.7 i/s before(YJIT): 44.0 i/s - 1.02x slower after: 31.9 i/s - 1.40x slower before: 30.8 i/s - 1.45x slower ``` - YJIT=ON : 1.00x - 1.05x faster - YJIT=OFF : 1.03x - 1.04x faster --- lib/rexml/parsers/baseparser.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index 65bad260..7126a12d 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -348,9 +348,13 @@ def pull_event @source.match(/\A\s*/um, true) end begin - @source.read if @source.buffer.size<2 - if @source.buffer[0] == ?< - if @source.buffer[1] == ?/ + next_data = @source.buffer + if next_data.size < 2 + @source.read + next_data = @source.buffer + end + if next_data[0] == ?< + if next_data[1] == ?/ @nsstack.shift last_tag = @tags.pop md = @source.match( CLOSE_MATCH, true ) @@ -364,7 +368,7 @@ def pull_event raise REXML::ParseException.new(message, @source) end return [ :end_element, last_tag ] - elsif @source.buffer[1] == ?! + elsif next_data[1] == ?! md = @source.match(/\A(\s*[^>]*>)/um) #STDERR.puts "SOURCE BUFFER = #{source.buffer}, #{source.buffer.size}" raise REXML::ParseException.new("Malformed node", @source) unless md @@ -383,7 +387,7 @@ def pull_event end raise REXML::ParseException.new( "Declarations can only occur "+ "in the doctype declaration.", @source) - elsif @source.buffer[1] == ?? + elsif next_data[1] == ?? return process_instruction else # Get the next tag From 77128555476cb0db798e2912fb3a07d6411dc320 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sun, 21 Jan 2024 20:02:00 +0900 Subject: [PATCH 08/33] Use `@scanner << readline` instead of `@scanner.string = @scanner.rest + readline` (#107) ## Why JRuby's `StringScanner#<<` and `StringScanner#scan` OutOfMemoryError has been resolved in strscan gem 3.0.9. https://github.com/ruby/strscan/issues/83 ## Benchmark ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.0/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22] Calculating ------------------------------------- before after before(YJIT) after(YJIT) dom 10.958 11.044 16.615 16.783 i/s - 100.000 times in 9.126104s 9.055023s 6.018799s 5.958437s sax 29.624 29.609 44.390 45.370 i/s - 100.000 times in 3.375641s 3.377372s 2.252774s 2.204080s pull 33.868 34.695 51.173 53.492 i/s - 100.000 times in 2.952679s 2.882229s 1.954138s 1.869422s stream 31.719 32.351 43.604 45.403 i/s - 100.000 times in 3.152713s 3.091052s 2.293356s 2.202514s Comparison: dom after(YJIT): 16.8 i/s before(YJIT): 16.6 i/s - 1.01x slower after: 11.0 i/s - 1.52x slower before: 11.0 i/s - 1.53x slower sax after(YJIT): 45.4 i/s before(YJIT): 44.4 i/s - 1.02x slower before: 29.6 i/s - 1.53x slower after: 29.6 i/s - 1.53x slower pull after(YJIT): 53.5 i/s before(YJIT): 51.2 i/s - 1.05x slower after: 34.7 i/s - 1.54x slower before: 33.9 i/s - 1.58x slower stream after(YJIT): 45.4 i/s before(YJIT): 43.6 i/s - 1.04x slower after: 32.4 i/s - 1.40x slower before: 31.7 i/s - 1.43x slower ``` - YJIT=ON : 1.01x - 1.05x faster - YJIT=OFF : 1.00x - 1.02x faster --- benchmark/parse.yaml | 4 ++-- lib/rexml/source.rb | 6 +----- rexml.gemspec | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/benchmark/parse.yaml b/benchmark/parse.yaml index 8818b50c..8c85ed17 100644 --- a/benchmark/parse.yaml +++ b/benchmark/parse.yaml @@ -6,7 +6,7 @@ contexts: prelude: require 'rexml' - name: master gems: - strscan: 3.0.8 + strscan: 3.0.9 prelude: | $LOAD_PATH.unshift(File.expand_path("lib")) require 'rexml' @@ -19,7 +19,7 @@ contexts: RubyVM::YJIT.enable - name: master(YJIT) gems: - strscan: 3.0.8 + strscan: 3.0.9 prelude: | $LOAD_PATH.unshift(File.expand_path("lib")) require 'rexml' diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb index 71b08f99..db78a124 100644 --- a/lib/rexml/source.rb +++ b/lib/rexml/source.rb @@ -149,11 +149,7 @@ def initialize(arg, block_size=500, encoding=nil) def read begin - # NOTE: `@scanner << readline` does not free memory, so when parsing huge XML in JRuby's DOM, - # out-of-memory error `Java::JavaLang::OutOfMemoryError: Java heap space` occurs. - # `@scanner.string = @scanner.rest + readline` frees memory that is already consumed - # and avoids this problem. - @scanner.string = @scanner.rest + readline + @scanner << readline rescue Exception, NameError @source = nil end diff --git a/rexml.gemspec b/rexml.gemspec index 2ba1c64d..c76bedbe 100644 --- a/rexml.gemspec +++ b/rexml.gemspec @@ -55,7 +55,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.0' - spec.add_runtime_dependency("strscan", ">= 3.0.8") + spec.add_runtime_dependency("strscan", ">= 3.0.9") spec.add_development_dependency "benchmark_driver" spec.add_development_dependency "bundler" From 51217dbcc64ecc34aa70f126b103bedf07e153fc Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Wed, 31 Jan 2024 16:35:55 +0900 Subject: [PATCH 09/33] Reduce calls to StringScanner.new() (#108) ## Why `StringScanner.new()` instances can be reused within parse_attributes, reducing initialization costs. ## Benchmark ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.0/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22] Calculating ------------------------------------- before after before(YJIT) after(YJIT) dom 11.018 11.207 17.059 16.660 i/s - 100.000 times in 9.075992s 8.923280s 5.861969s 6.002555s sax 29.843 30.821 45.518 47.505 i/s - 100.000 times in 3.350909s 3.244524s 2.196940s 2.105037s pull 34.480 35.937 52.816 57.098 i/s - 100.000 times in 2.900205s 2.782632s 1.893370s 1.751378s stream 32.430 33.516 46.247 48.412 i/s - 100.000 times in 3.083536s 2.983607s 2.162288s 2.065584s Comparison: dom before(YJIT): 17.1 i/s after(YJIT): 16.7 i/s - 1.02x slower after: 11.2 i/s - 1.52x slower before: 11.0 i/s - 1.55x slower sax after(YJIT): 47.5 i/s before(YJIT): 45.5 i/s - 1.04x slower after: 30.8 i/s - 1.54x slower before: 29.8 i/s - 1.59x slower pull after(YJIT): 57.1 i/s before(YJIT): 52.8 i/s - 1.08x slower after: 35.9 i/s - 1.59x slower before: 34.5 i/s - 1.66x slower stream after(YJIT): 48.4 i/s before(YJIT): 46.2 i/s - 1.05x slower after: 33.5 i/s - 1.44x slower before: 32.4 i/s - 1.49x slower ``` - YJIT=ON : 1.02x - 1.08x faster - YJIT=OFF : 1.01x - 1.04x faster --- lib/rexml/parsers/baseparser.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index 7126a12d..b66b0ede 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -115,6 +115,7 @@ class BaseParser def initialize( source ) self.stream = source @listeners = [] + @attributes_scanner = StringScanner.new('') end def add_listener( listener ) @@ -601,7 +602,8 @@ def parse_attributes(prefixes, curr_ns) return attributes, closed if raw_attributes.nil? return attributes, closed if raw_attributes.empty? - scanner = StringScanner.new(raw_attributes) + @attributes_scanner.string = raw_attributes + scanner = @attributes_scanner until scanner.eos? if scanner.scan(/\s+/) break if scanner.eos? From 7e4049f6a68c99c4efec2df117057ee080680c9f Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Wed, 31 Jan 2024 17:17:51 +0900 Subject: [PATCH 10/33] Change loop in parse_attributes to `while true`. (#109) ## Why loop is slower than `while true`. ## Benchmark ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.0/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22] Calculating ------------------------------------- before after before(YJIT) after(YJIT) dom 11.186 11.304 17.395 17.450 i/s - 100.000 times in 8.940144s 8.846590s 5.748718s 5.730793s sax 30.811 31.629 47.352 48.040 i/s - 100.000 times in 3.245601s 3.161619s 2.111854s 2.081594s pull 35.793 36.621 56.924 57.313 i/s - 100.000 times in 2.793829s 2.730693s 1.756732s 1.744812s stream 33.157 34.757 46.792 50.536 i/s - 100.000 times in 3.015940s 2.877088s 2.137106s 1.978787s Comparison: dom after(YJIT): 17.4 i/s before(YJIT): 17.4 i/s - 1.00x slower after: 11.3 i/s - 1.54x slower before: 11.2 i/s - 1.56x slower sax after(YJIT): 48.0 i/s before(YJIT): 47.4 i/s - 1.01x slower after: 31.6 i/s - 1.52x slower before: 30.8 i/s - 1.56x slower pull after(YJIT): 57.3 i/s before(YJIT): 56.9 i/s - 1.01x slower after: 36.6 i/s - 1.57x slower before: 35.8 i/s - 1.60x slower stream after(YJIT): 50.5 i/s before(YJIT): 46.8 i/s - 1.08x slower after: 34.8 i/s - 1.45x slower before: 33.2 i/s - 1.52x slower ``` - YJIT=ON : 1.00x - 1.08x faster - YJIT=OFF : 1.01x - 1.04x faster --- lib/rexml/parsers/baseparser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index b66b0ede..3fe5c291 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -610,7 +610,7 @@ def parse_attributes(prefixes, curr_ns) end pos = scanner.pos - loop do + while true break if scanner.scan(ATTRIBUTE_PATTERN) unless scanner.scan(QNAME) message = "Invalid attribute name: <#{scanner.rest}>" From 444c9ce7449d3c5a75ae50087555ec73ae1963a8 Mon Sep 17 00:00:00 2001 From: flatisland Date: Thu, 8 Feb 2024 14:59:30 +0900 Subject: [PATCH 11/33] xpath: Fix normalize_space(array) case (#111) GitHub: fix GH-110 Fixed a bug in `REXML::Functions.normalize_space(array)` and introduced test cases for it: - Corrected a typo in the variable name within the collect block (`string` -> `x`). - Added `test_normalize_space_strings` to `test/functions/test_base.rb`. --------- Co-authored-by: Sutou Kouhei --- lib/rexml/functions.rb | 3 +-- test/functions/test_base.rb | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/rexml/functions.rb b/lib/rexml/functions.rb index 77926bf2..4c114616 100644 --- a/lib/rexml/functions.rb +++ b/lib/rexml/functions.rb @@ -262,11 +262,10 @@ def Functions::string_length( string ) string(string).length end - # UNTESTED def Functions::normalize_space( string=nil ) string = string(@@context[:node]) if string.nil? if string.kind_of? Array - string.collect{|x| string.to_s.strip.gsub(/\s+/um, ' ') if string} + string.collect{|x| x.to_s.strip.gsub(/\s+/um, ' ') if x} else string.to_s.strip.gsub(/\s+/um, ' ') end diff --git a/test/functions/test_base.rb b/test/functions/test_base.rb index 74dc1a31..9ba3ed24 100644 --- a/test/functions/test_base.rb +++ b/test/functions/test_base.rb @@ -229,6 +229,28 @@ def test_normalize_space assert_equal( [REXML::Comment.new("COMMENT A")], m ) end + def test_normalize_space_strings + source = <<-XML +breakfast boosts\t\t + +concentration +Coffee beans + aroma + + + + Dessert + \t\t after dinner + XML + normalized_texts = REXML::XPath.each(REXML::Document.new(source), "normalize-space(//text())").to_a + assert_equal([ + "breakfast boosts concentration", + "Coffee beans aroma", + "Dessert after dinner", + ], + normalized_texts) + end + def test_string_nil_without_context doc = REXML::Document.new(<<-XML) From fc6cad570b849692a28f26a963ceb58edc282bbc Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 16 Feb 2024 04:51:16 +0900 Subject: [PATCH 12/33] Remove unnecessary checks in baseparser (#112) ## Why https://github.com/ruby/rexml/blob/444c9ce7449d3c5a75ae50087555ec73ae1963a8/lib/rexml/parsers/baseparser.rb#L352-L425 ``` next_data = @source.buffer if next_data.size < 2 @source.read next_data = @source.buffer end if next_data[0] == ?< : (omit) : else # next_data is a string of one or more characters other than '<'. md = @source.match( TEXT_PATTERN, true ) # TEXT_PATTERN = /\A([^<]*)/um text = md[1] if md[0].length == 0 # md[0].length is greater than or equal to 1. @source.match( /(\s+)/, true ) end ``` This is an unnecessary check because md[0].length is greater than or equal to 1. --- lib/rexml/parsers/baseparser.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index 3fe5c291..595669c9 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -420,9 +420,6 @@ def pull_event else md = @source.match( TEXT_PATTERN, true ) text = md[1] - if md[0].length == 0 - @source.match( /(\s+)/, true ) - end return [ :text, text ] end rescue REXML::UndefinedNamespaceException From 372daf1a1c93b0a47d174d85feb911d63b501665 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 16 Feb 2024 04:53:36 +0900 Subject: [PATCH 13/33] Stop specifying the gem version of strscan in benchmarks. (#113) ## [Why] Because benchmarks are broken when new strscan is released. https://github.com/ruby/rexml/actions/runs/7825513689/job/21349811563 ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /opt/hostedtoolcache/Ruby/3.3.0/x64/bin/ruby -v -S benchmark-driver /home/runner/work/rexml/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux] Calculating ------------------------------------- rexml 3.2.6 master 3.2.6(YJIT) master(YJIT) /opt/hostedtoolcache/Ruby/3.3.0/x64/lib/ruby/3.3.0/rubygems/dependency.rb:315:in `to_specs': Could not find 'strscan' (= 3.0.9) - did find: [strscan-3.1.0,strscan-3.0.7] (Gem::MissingSpecVersionError) Checked in 'GEM_PATH=/home/runner/.local/share/gem/ruby/3.3.0:/opt/hostedtoolcache/Ruby/3.3.0/x64/lib/ruby/gems/3.3.0' , execute `gem env` for more information from /opt/hostedtoolcache/Ruby/3.3.0/x64/lib/ruby/3.3.0/rubygems/dependency.rb:325:in `to_spec' from /opt/hostedtoolcache/Ruby/3.3.0/x64/lib/ruby/3.3.0/rubygems/core_ext/kernel_gem.rb:56:in `gem' from /tmp/benchmark_driver-20240208-1790-njwk6u.rb:1:in `
' ``` --- benchmark/parse.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/benchmark/parse.yaml b/benchmark/parse.yaml index 8c85ed17..e7066fcb 100644 --- a/benchmark/parse.yaml +++ b/benchmark/parse.yaml @@ -5,8 +5,6 @@ contexts: require: false prelude: require 'rexml' - name: master - gems: - strscan: 3.0.9 prelude: | $LOAD_PATH.unshift(File.expand_path("lib")) require 'rexml' @@ -18,8 +16,6 @@ contexts: require 'rexml' RubyVM::YJIT.enable - name: master(YJIT) - gems: - strscan: 3.0.9 prelude: | $LOAD_PATH.unshift(File.expand_path("lib")) require 'rexml' From fb7ba27594ce15e2a0a566c837355cb4beb4db14 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Wed, 21 Feb 2024 06:17:35 +0900 Subject: [PATCH 14/33] test: Fix invalid XML with spaces before the XML declaration (#115) ## Why? XML declaration allowed only at the start of the document. https://www.w3.org/TR/2006/REC-xml11-20060816/#document ``` [1] document ::= ( prolog element Misc* ) - ( Char* RestrictedChar Char* ) ``` It doesn't have `S*` before `prolog`. https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-prolog ``` [22] prolog ::= XMLDecl Misc* (doctypedecl Misc*)? ``` It doesn't have `S*` before `XMLdecl`. https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-XMLDecl ``` [23] XMLDecl ::= '' ``` It doesn't have `S*` before `' diff --git a/test/test_contrib.rb b/test/test_contrib.rb index f3ad0b6c..23ee35b1 100644 --- a/test/test_contrib.rb +++ b/test/test_contrib.rb @@ -80,7 +80,7 @@ def test_bad_doctype_Tobias # Peter Verhage def test_namespace_Peter - source = <<-EOF + source = <<~EOF @@ -377,7 +377,7 @@ def test_various_xpath end def test_entities_Holden_Glova - document = <<-EOL + document = <<~EOL diff --git a/test/test_core.rb b/test/test_core.rb index 8c33d834..5668b934 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -15,7 +15,7 @@ class Tester < Test::Unit::TestCase include Helper::Fixture include REXML def setup - @xsa_source = <<-EOL + @xsa_source = <<~EOL /um, true)[1] ] + elsif @source.match("DOCTYPE", true) + base_error_message = "Malformed DOCTYPE" + unless @source.match(/\s+/um, true) + if @source.match(">") + message = "#{base_error_message}: name is missing" + else + message = "#{base_error_message}: invalid name" + end + @source.string = "/um, true) + elsif @source.match(/\s*>/um, true) + id = [nil, nil, nil] @document_status = :after_doctype else - message = "#{base_error_message}: garbage after external ID" - raise REXML::ParseException.new(message, @source) + id = parse_id(base_error_message, + accept_external_id: true, + accept_public_id: false) + if id[0] == "SYSTEM" + # For backward compatibility + id[1], id[2] = id[2], nil + end + if @source.match(/\s*\[/um, true) + @document_status = :in_doctype + elsif @source.match(/\s*>/um, true) + @document_status = :after_doctype + else + message = "#{base_error_message}: garbage after external ID" + raise REXML::ParseException.new(message, @source) + end end - end - args = [:start_doctype, name, *id] - if @document_status == :after_doctype - @source.match(/\A\s*/um, true) - @stack << [ :end_doctype ] - end - return args - when /\A\s+/ - else - @document_status = :after_doctype - if @source.encoding == "UTF-8" - @source.buffer_encoding = ::Encoding::UTF_8 + args = [:start_doctype, name, *id] + if @document_status == :after_doctype + @source.match(/\s*/um, true) + @stack << [ :end_doctype ] + end + return args + else + message = "Invalid XML" + raise REXML::ParseException.new(message, @source) end end end if @document_status == :in_doctype - md = @source.match(/\A\s*(.*?>)/um) - case md[1] - when SYSTEMENTITY - match = @source.match( SYSTEMENTITY, true )[1] - return [ :externalentity, match ] - - when ELEMENTDECL_START - return [ :elementdecl, @source.match( ELEMENTDECL_PATTERN, true )[1] ] - - when ENTITY_START - match = [:entitydecl, *@source.match( ENTITYDECL, true ).captures.compact] - ref = false - if match[1] == '%' - ref = true - match.delete_at 1 - end - # Now we have to sort out what kind of entity reference this is - if match[2] == 'SYSTEM' - # External reference - match[3] = match[3][1..-2] # PUBID - match.delete_at(4) if match.size > 4 # Chop out NDATA decl - # match is [ :entity, name, SYSTEM, pubid(, ndata)? ] - elsif match[2] == 'PUBLIC' - # External reference - match[3] = match[3][1..-2] # PUBID - match[4] = match[4][1..-2] # HREF - match.delete_at(5) if match.size > 5 # Chop out NDATA decl - # match is [ :entity, name, PUBLIC, pubid, href(, ndata)? ] - else - match[2] = match[2][1..-2] - match.pop if match.size == 4 - # match is [ :entity, name, value ] - end - match << '%' if ref - return match - when ATTLISTDECL_START - md = @source.match( ATTLISTDECL_PATTERN, true ) - raise REXML::ParseException.new( "Bad ATTLIST declaration!", @source ) if md.nil? - element = md[1] - contents = md[0] - - pairs = {} - values = md[0].scan( ATTDEF_RE ) - values.each do |attdef| - unless attdef[3] == "#IMPLIED" - attdef.compact! - val = attdef[3] - val = attdef[4] if val == "#FIXED " - pairs[attdef[0]] = val - if attdef[0] =~ /^xmlns:(.*)/ - @nsstack[0] << $1 - end + @source.match(/\s*/um, true) # skip spaces + if @source.match("/um, true) + raise REXML::ParseException.new( "Bad ELEMENT declaration!", @source ) if md.nil? + return [ :elementdecl, "/um) - message = "#{base_error_message}: name is missing" + # Now we have to sort out what kind of entity reference this is + if match[2] == 'SYSTEM' + # External reference + match[3] = match[3][1..-2] # PUBID + match.delete_at(4) if match.size > 4 # Chop out NDATA decl + # match is [ :entity, name, SYSTEM, pubid(, ndata)? ] + elsif match[2] == 'PUBLIC' + # External reference + match[3] = match[3][1..-2] # PUBID + match[4] = match[4][1..-2] # HREF + match.delete_at(5) if match.size > 5 # Chop out NDATA decl + # match is [ :entity, name, PUBLIC, pubid, href(, ndata)? ] else - message = "#{base_error_message}: invalid declaration name" + match[2] = match[2][1..-2] + match.pop if match.size == 4 + # match is [ :entity, name, value ] end - raise REXML::ParseException.new(message, @source) - end - name = parse_name(base_error_message) - id = parse_id(base_error_message, - accept_external_id: true, - accept_public_id: true) - unless @source.match(/\A\s*>/um, true) - message = "#{base_error_message}: garbage before end >" - raise REXML::ParseException.new(message, @source) + match << '%' if ref + return match + elsif @source.match("ATTLIST", true) + md = @source.match(ATTLISTDECL_END, true) + raise REXML::ParseException.new( "Bad ATTLIST declaration!", @source ) if md.nil? + element = md[1] + contents = md[0] + + pairs = {} + values = md[0].scan( ATTDEF_RE ) + values.each do |attdef| + unless attdef[3] == "#IMPLIED" + attdef.compact! + val = attdef[3] + val = attdef[4] if val == "#FIXED " + pairs[attdef[0]] = val + if attdef[0] =~ /^xmlns:(.*)/ + @nsstack[0] << $1 + end + end + end + return [ :attlistdecl, element, pairs, contents ] + elsif @source.match("NOTATION", true) + base_error_message = "Malformed notation declaration" + unless @source.match(/\s+/um, true) + if @source.match(">") + message = "#{base_error_message}: name is missing" + else + message = "#{base_error_message}: invalid name" + end + @source.string = " /um, true) + message = "#{base_error_message}: garbage before end >" + raise REXML::ParseException.new(message, @source) + end + return [:notationdecl, name, *id] + elsif md = @source.match(/--(.*?)-->/um, true) + case md[1] + when /--/, /-\z/ + raise REXML::ParseException.new("Malformed comment", @source) + end + return [ :comment, md[1] ] if md end - return [:notationdecl, name, *id] - when DOCTYPE_END + elsif match = @source.match(/(%.*?;)\s*/um, true) + return [ :externalentity, match[1] ] + elsif @source.match(/\]\s*>/um, true) @document_status = :after_doctype - @source.match( DOCTYPE_END, true ) return [ :end_doctype ] end end if @document_status == :after_doctype - @source.match(/\A\s*/um, true) + @source.match(/\s*/um, true) end begin - next_data = @source.buffer - if next_data.size < 2 - @source.read - next_data = @source.buffer - end - if next_data[0] == ?< - if next_data[1] == ?/ + if @source.match("<", true) + if @source.match("/", true) @nsstack.shift last_tag = @tags.pop - md = @source.match( CLOSE_MATCH, true ) + md = @source.match(CLOSE_PATTERN, true) if md and !last_tag message = "Unexpected top-level end tag (got '#{md[1]}')" raise REXML::ParseException.new(message, @source) end if md.nil? or last_tag != md[1] message = "Missing end tag for '#{last_tag}'" - message << " (got '#{md[1]}')" if md + message += " (got '#{md[1]}')" if md + @source.string = "]*>)/um) + elsif @source.match("!", true) + md = @source.match(/([^>]*>)/um) #STDERR.puts "SOURCE BUFFER = #{source.buffer}, #{source.buffer.size}" raise REXML::ParseException.new("Malformed node", @source) unless md - if md[0][2] == ?- - md = @source.match( COMMENT_PATTERN, true ) + if md[0][0] == ?- + md = @source.match(/--(.*?)-->/um, true) case md[1] when /--/, /-\z/ @@ -383,17 +385,18 @@ def pull_event return [ :comment, md[1] ] if md else - md = @source.match( CDATA_PATTERN, true ) + md = @source.match(/\[CDATA\[(.*?)\]\]>/um, true) return [ :cdata, md[1] ] if md end raise REXML::ParseException.new( "Declarations can only occur "+ "in the doctype declaration.", @source) - elsif next_data[1] == ?? + elsif @source.match("?", true) return process_instruction else # Get the next tag - md = @source.match(TAG_MATCH, true) + md = @source.match(TAG_PATTERN, true) unless md + @source.string = "<" + @source.buffer raise REXML::ParseException.new("malformed XML: missing tag start", @source) end tag = md[1] @@ -418,7 +421,7 @@ def pull_event return [ :start_element, tag, attributes ] end else - md = @source.match( TEXT_PATTERN, true ) + md = @source.match(/([^<]*)/um, true) text = md[1] return [ :text, text ] end @@ -462,8 +465,7 @@ def normalize( input, entities=nil, entity_filter=nil ) # Unescapes all possible entities def unnormalize( string, entities=nil, filter=nil ) - rv = string.clone - rv.gsub!( /\r\n?/, "\n" ) + rv = string.gsub( /\r\n?/, "\n" ) matches = rv.scan( REFERENCE_RE ) return rv if matches.size == 0 rv.gsub!( /�*((?:\d+)|(?:x[a-fA-F0-9]+));/ ) { @@ -498,9 +500,9 @@ def need_source_encoding_update?(xml_declaration_encoding) end def parse_name(base_error_message) - md = @source.match(/\A\s*#{NAME}/um, true) + md = @source.match(NAME_PATTERN, true) unless md - if @source.match(/\A\s*\S/um) + if @source.match(/\s*\S/um) message = "#{base_error_message}: invalid name" else message = "#{base_error_message}: name is missing" @@ -577,11 +579,28 @@ def parse_id_invalid_details(accept_external_id:, end def process_instruction - match_data = @source.match(INSTRUCTION_PATTERN, true) + match_data = @source.match(INSTRUCTION_END, true) unless match_data message = "Invalid processing instruction node" + @source.string = " DETAIL end + + def test_no_name + exception = assert_raise(REXML::ParseException) do + parse(<<-DOCTYPE) + + DOCTYPE + end + assert_equal(<<-DETAIL.chomp, exception.to_s) +Malformed DOCTYPE: name is missing +Line: 3 +Position: 17 +Last 80 unconsumed characters: + + DETAIL + end end class TestExternalID < self From 19975fea162ca5b31ac8218087ea2924aee90e5d Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Sun, 3 Mar 2024 18:36:34 +0900 Subject: [PATCH 16/33] source: Remove unnecessary string length comparisons in the case of string comparisons (#116) ## Why https://github.com/ruby/rexml/blob/370666e314816b57ecd5878e757224c3b6bc93f5/lib/rexml/source.rb#L208-L234 Because `@line_break = encode(">")`, the end of `@scanner << readline` is one of the following. 1. ">" 2. "X>" 3. "X" (eof) This will not be matched by additional reads in the following cases. - `@source.match(">")` - `@source.match(">X")` ## Benchmark ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.0/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22] Calculating ------------------------------------- before after before(YJIT) after(YJIT) dom 10.689 10.736 18.484 18.108 i/s - 100.000 times in 9.355754s 9.314792s 5.409984s 5.522527s sax 30.793 31.583 52.965 52.641 i/s - 100.000 times in 3.247486s 3.166258s 1.888036s 1.899660s pull 36.308 37.182 63.773 64.669 i/s - 100.000 times in 2.754203s 2.689440s 1.568069s 1.546325s stream 34.936 35.991 56.830 57.729 i/s - 100.000 times in 2.862361s 2.778467s 1.759632s 1.732238s Comparison: dom before(YJIT): 18.5 i/s after(YJIT): 18.1 i/s - 1.02x slower after: 10.7 i/s - 1.72x slower before: 10.7 i/s - 1.73x slower sax before(YJIT): 53.0 i/s after(YJIT): 52.6 i/s - 1.01x slower after: 31.6 i/s - 1.68x slower before: 30.8 i/s - 1.72x slower pull after(YJIT): 64.7 i/s before(YJIT): 63.8 i/s - 1.01x slower after: 37.2 i/s - 1.74x slower before: 36.3 i/s - 1.78x slower stream after(YJIT): 57.7 i/s before(YJIT): 56.8 i/s - 1.02x slower after: 36.0 i/s - 1.60x slower before: 34.9 i/s - 1.65x slower ``` - YJIT=ON : 0.98x - 1.02x faster - YJIT=OFF : 1.00x - 1.03x faster --- lib/rexml/source.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb index 4111d1d3..9eeba273 100644 --- a/lib/rexml/source.rb +++ b/lib/rexml/source.rb @@ -161,6 +161,9 @@ def read end end + # Note: When specifying a string for 'pattern', it must not include '>' except in the following formats: + # - ">" + # - "XXX>" (X is any string excluding '>') def match( pattern, cons=false ) read if @scanner.eos? && @source while true @@ -170,7 +173,7 @@ def match( pattern, cons=false ) md = @scanner.check(pattern) end break if md - return nil if pattern.is_a?(String) && pattern.bytesize <= @scanner.rest_size + return nil if pattern.is_a?(String) return nil if @source.nil? return nil unless read end From d146162e9a61574499d10428bc0065754cd26601 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Mon, 4 Mar 2024 05:24:53 +0900 Subject: [PATCH 17/33] Remove `Source#string=` method (#117) ## Why? We want to just change scan pointer. https://github.com/ruby/rexml/pull/114#discussion_r1501773803 > I want to just change scan pointer (`StringScanner#pos=`) instead of changing `@scanner.string`. --- lib/rexml/parsers/baseparser.rb | 23 +++++++++++++---------- lib/rexml/source.rb | 8 ++++++-- test/parse/test_notation_declaration.rb | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index bc59bcdc..c79de0eb 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -211,8 +211,9 @@ def pull_event #STDERR.puts @source.encoding #STDERR.puts "BUFFER = #{@source.buffer.inspect}" if @document_status == nil + start_position = @source.position if @source.match("/um, true)[1] ] @@ -224,7 +225,7 @@ def pull_event else message = "#{base_error_message}: invalid name" end - @source.string = "/um, true) @@ -325,7 +327,7 @@ def pull_event else message = "#{base_error_message}: invalid name" end - @source.string = " " scanner << match_data[1] - scanner.pos = pos + scanner.pos = start_position closed = !match_data[2].nil? next end diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb index 9eeba273..81d96451 100644 --- a/lib/rexml/source.rb +++ b/lib/rexml/source.rb @@ -76,8 +76,12 @@ def match(pattern, cons=false) end end - def string=(string) - @scanner.string = string + def position + @scanner.pos + end + + def position=(pos) + @scanner.pos = pos end # @return true if the Source is exhausted diff --git a/test/parse/test_notation_declaration.rb b/test/parse/test_notation_declaration.rb index 19a0536d..9e81b6a4 100644 --- a/test/parse/test_notation_declaration.rb +++ b/test/parse/test_notation_declaration.rb @@ -35,7 +35,7 @@ def test_no_name Line: 5 Position: 72 Last 80 unconsumed characters: - ]> + ]> DETAIL end From 77cb0dcf0af1b31acf7fc813315c7c3defac23f8 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Thu, 7 Mar 2024 07:02:34 +0900 Subject: [PATCH 18/33] Separate `IOSource#ensure_buffer` from `IOSource#match`. (#118) ## Why? It would affect performance to do a read check in `IOSource#match` every time, Separate read processing from `IOSource#ensure_buffer`. Use `IOSource#ensure_buffer` in the following cases where `@source.buffer` is empty. 1. at the start of pull_event 2. If a trailing `'>'` pattern matches, as in `@source.match(/\s*>/um)`. ## Benchmark ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.0/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22] Calculating ------------------------------------- before after before(YJIT) after(YJIT) dom 10.278 10.986 16.430 16.941 i/s - 100.000 times in 9.729858s 9.102574s 6.086579s 5.902885s sax 30.166 30.496 49.851 51.596 i/s - 100.000 times in 3.315008s 3.279069s 2.005961s 1.938123s pull 35.459 36.380 60.266 63.134 i/s - 100.000 times in 2.820181s 2.748745s 1.659301s 1.583928s stream 33.762 34.636 55.173 55.859 i/s - 100.000 times in 2.961948s 2.887131s 1.812485s 1.790218s Comparison: dom after(YJIT): 16.9 i/s before(YJIT): 16.4 i/s - 1.03x slower after: 11.0 i/s - 1.54x slower before: 10.3 i/s - 1.65x slower sax after(YJIT): 51.6 i/s before(YJIT): 49.9 i/s - 1.04x slower after: 30.5 i/s - 1.69x slower before: 30.2 i/s - 1.71x slower pull after(YJIT): 63.1 i/s before(YJIT): 60.3 i/s - 1.05x slower after: 36.4 i/s - 1.74x slower before: 35.5 i/s - 1.78x slower stream after(YJIT): 55.9 i/s before(YJIT): 55.2 i/s - 1.01x slower after: 34.6 i/s - 1.61x slower before: 33.8 i/s - 1.65x slower ``` - YJIT=ON : 1.01x - 1.05x faster - YJIT=OFF : 1.01x - 1.06x faster --- lib/rexml/parsers/baseparser.rb | 5 +++++ lib/rexml/source.rb | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index c79de0eb..c01b087b 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -210,6 +210,8 @@ def pull_event return @stack.shift if @stack.size > 0 #STDERR.puts @source.encoding #STDERR.puts "BUFFER = #{@source.buffer.inspect}" + + @source.ensure_buffer if @document_status == nil start_position = @source.position if @source.match("/um, true) id = [nil, nil, nil] @document_status = :after_doctype + @source.ensure_buffer else id = parse_id(base_error_message, accept_external_id: true, @@ -248,6 +251,7 @@ def pull_event @document_status = :in_doctype elsif @source.match(/\s*>/um, true) @document_status = :after_doctype + @source.ensure_buffer else message = "#{base_error_message}: garbage after external ID" raise REXML::ParseException.new(message, @source) @@ -646,6 +650,7 @@ def parse_attributes(prefixes, curr_ns) raise REXML::ParseException.new(message, @source) end unless scanner.scan(/.*#{Regexp.escape(quote)}/um) + @source.ensure_buffer match_data = @source.match(/^(.*?)(\/)?>/um, true) if match_data scanner << "/" if closed diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb index 81d96451..7f47c2be 100644 --- a/lib/rexml/source.rb +++ b/lib/rexml/source.rb @@ -68,6 +68,9 @@ def encoding=(enc) def read end + def ensure_buffer + end + def match(pattern, cons=false) if cons @scanner.scan(pattern).nil? ? nil : @scanner @@ -165,11 +168,14 @@ def read end end + def ensure_buffer + read if @scanner.eos? && @source + end + # Note: When specifying a string for 'pattern', it must not include '>' except in the following formats: # - ">" # - "XXX>" (X is any string excluding '>') def match( pattern, cons=false ) - read if @scanner.eos? && @source while true if cons md = @scanner.scan(pattern) From d4e79f2f45e1a0fe111cf2974ea6496045c9eb5d Mon Sep 17 00:00:00 2001 From: Jean byroot Boussier Date: Fri, 15 Mar 2024 14:31:07 +0100 Subject: [PATCH 19/33] Make the test suite compatible with `--enable-frozen-string-literal` (#120) Ref: https://bugs.ruby-lang.org/issues/20205 Since `rexml` is tested as part of ruby-core CI, it needs to be compatible with the `--enable-frozen-string-literal` option. Co-authored-by: Jean Boussier --- .github/workflows/test.yml | 12 ++++++++++++ test/formatter/test_default.rb | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 94a116a2..7fe53d82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,18 @@ jobs: - name: Test run: bundle exec rake test + frozen-string-literal: + name: frozen-string-literal + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler-cache: true + - name: Test + run: bundle exec rake test RUBYOPT="--enable-frozen-string-literal" + gem: name: "Gem: ${{ matrix.ruby-version }} on ${{ matrix.runs-on }}" runs-on: ${{ matrix.runs-on }} diff --git a/test/formatter/test_default.rb b/test/formatter/test_default.rb index 321d8180..aa403dbe 100644 --- a/test/formatter/test_default.rb +++ b/test/formatter/test_default.rb @@ -2,7 +2,7 @@ module REXMLTests class DefaultFormatterTest < Test::Unit::TestCase def format(node) formatter = REXML::Formatters::Default.new - output = "" + output = +"" formatter.write(node, output) output end From 0496940d5998ccbc50d16fb734993ab50fc60c2d Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Mon, 18 Mar 2024 23:30:47 +0900 Subject: [PATCH 20/33] Optimize the parse_attributes method to use `Source#match` to parse XML. (#119) ## Why? Improve maintainability by consolidating processing into `Source#match`. ## Benchmark ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.0/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22] Calculating ------------------------------------- before after before(YJIT) after(YJIT) dom 10.891 10.622 16.356 17.403 i/s - 100.000 times in 9.182130s 9.414177s 6.113806s 5.746133s sax 30.335 29.845 49.749 54.877 i/s - 100.000 times in 3.296483s 3.350595s 2.010071s 1.822259s pull 35.514 34.801 61.123 66.908 i/s - 100.000 times in 2.815793s 2.873484s 1.636041s 1.494591s stream 35.141 34.475 52.110 56.836 i/s - 100.000 times in 2.845646s 2.900638s 1.919017s 1.759456s Comparison: dom after(YJIT): 17.4 i/s before(YJIT): 16.4 i/s - 1.06x slower before: 10.9 i/s - 1.60x slower after: 10.6 i/s - 1.64x slower sax after(YJIT): 54.9 i/s before(YJIT): 49.7 i/s - 1.10x slower before: 30.3 i/s - 1.81x slower after: 29.8 i/s - 1.84x slower pull after(YJIT): 66.9 i/s before(YJIT): 61.1 i/s - 1.09x slower before: 35.5 i/s - 1.88x slower after: 34.8 i/s - 1.92x slower stream after(YJIT): 56.8 i/s before(YJIT): 52.1 i/s - 1.09x slower before: 35.1 i/s - 1.62x slower after: 34.5 i/s - 1.65x slower ``` - YJIT=ON : 1.06x - 1.10x faster - YJIT=OFF : 0.97x - 0.98x faster --- lib/rexml/parsers/baseparser.rb | 116 ++++++++++++-------------------- test/parse/test_element.rb | 4 +- test/test_core.rb | 20 +++++- 3 files changed, 64 insertions(+), 76 deletions(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index c01b087b..f66b968f 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -114,7 +114,7 @@ class BaseParser module Private INSTRUCTION_END = /#{NAME}(\s+.*?)?\?>/um - TAG_PATTERN = /((?>#{QNAME_STR}))/um + TAG_PATTERN = /((?>#{QNAME_STR}))\s*/um CLOSE_PATTERN = /(#{QNAME_STR})\s*>/um ATTLISTDECL_END = /\s+#{NAME}(?:#{ATTDEF})*\s*>/um NAME_PATTERN = /\s*#{NAME}/um @@ -128,7 +128,6 @@ module Private def initialize( source ) self.stream = source @listeners = [] - @attributes_scanner = StringScanner.new('') end def add_listener( listener ) @@ -614,87 +613,60 @@ def process_instruction(start_position) def parse_attributes(prefixes, curr_ns) attributes = {} closed = false - match_data = @source.match(/^(.*?)(\/)?>/um, true) - if match_data.nil? - message = "Start tag isn't ended" - raise REXML::ParseException.new(message, @source) - end - - raw_attributes = match_data[1] - closed = !match_data[2].nil? - return attributes, closed if raw_attributes.nil? - return attributes, closed if raw_attributes.empty? - - @attributes_scanner.string = raw_attributes - scanner = @attributes_scanner - until scanner.eos? - if scanner.scan(/\s+/) - break if scanner.eos? - end - - start_position = scanner.pos - while true - break if scanner.scan(ATTRIBUTE_PATTERN) - unless scanner.scan(QNAME) - message = "Invalid attribute name: <#{scanner.rest}>" - raise REXML::ParseException.new(message, @source) - end - name = scanner[0] - unless scanner.scan(/\s*=\s*/um) + while true + if @source.match(">", true) + return attributes, closed + elsif @source.match("/>", true) + closed = true + return attributes, closed + elsif match = @source.match(QNAME, true) + name = match[1] + prefix = match[2] + local_part = match[3] + + unless @source.match(/\s*=\s*/um, true) message = "Missing attribute equal: <#{name}>" raise REXML::ParseException.new(message, @source) end - quote = scanner.scan(/['"]/) - unless quote - message = "Missing attribute value start quote: <#{name}>" - raise REXML::ParseException.new(message, @source) - end - unless scanner.scan(/.*#{Regexp.escape(quote)}/um) - @source.ensure_buffer - match_data = @source.match(/^(.*?)(\/)?>/um, true) - if match_data - scanner << "/" if closed - scanner << ">" - scanner << match_data[1] - scanner.pos = start_position - closed = !match_data[2].nil? - next + unless match = @source.match(/(['"])(.*?)\1\s*/um, true) + if match = @source.match(/(['"])/, true) + message = + "Missing attribute value end quote: <#{name}>: <#{match[1]}>" + raise REXML::ParseException.new(message, @source) + else + message = "Missing attribute value start quote: <#{name}>" + raise REXML::ParseException.new(message, @source) end - message = - "Missing attribute value end quote: <#{name}>: <#{quote}>" - raise REXML::ParseException.new(message, @source) end - end - name = scanner[1] - prefix = scanner[2] - local_part = scanner[3] - # quote = scanner[4] - value = scanner[5] - if prefix == "xmlns" - if local_part == "xml" - if value != "http://www.w3.org/XML/1998/namespace" - msg = "The 'xml' prefix must not be bound to any other namespace "+ + value = match[2] + if prefix == "xmlns" + if local_part == "xml" + if value != "http://www.w3.org/XML/1998/namespace" + msg = "The 'xml' prefix must not be bound to any other namespace "+ + "(http://www.w3.org/TR/REC-xml-names/#ns-decl)" + raise REXML::ParseException.new( msg, @source, self ) + end + elsif local_part == "xmlns" + msg = "The 'xmlns' prefix must not be declared "+ "(http://www.w3.org/TR/REC-xml-names/#ns-decl)" - raise REXML::ParseException.new( msg, @source, self ) + raise REXML::ParseException.new( msg, @source, self) end - elsif local_part == "xmlns" - msg = "The 'xmlns' prefix must not be declared "+ - "(http://www.w3.org/TR/REC-xml-names/#ns-decl)" - raise REXML::ParseException.new( msg, @source, self) + curr_ns << local_part + elsif prefix + prefixes << prefix unless prefix == "xml" end - curr_ns << local_part - elsif prefix - prefixes << prefix unless prefix == "xml" - end - if attributes.has_key?(name) - msg = "Duplicate attribute #{name.inspect}" - raise REXML::ParseException.new(msg, @source, self) - end + if attributes.has_key?(name) + msg = "Duplicate attribute #{name.inspect}" + raise REXML::ParseException.new(msg, @source, self) + end - attributes[name] = value + attributes[name] = value + else + message = "Invalid attribute name: <#{@source.buffer.split(%r{[/>\s]}).first}>" + raise REXML::ParseException.new(message, @source) + end end - return attributes, closed end end end diff --git a/test/parse/test_element.rb b/test/parse/test_element.rb index 9f172a28..987214f3 100644 --- a/test/parse/test_element.rb +++ b/test/parse/test_element.rb @@ -41,9 +41,9 @@ def test_empty_namespace_attribute_name assert_equal(<<-DETAIL.chomp, exception.to_s) Invalid attribute name: <:a=""> Line: 1 -Position: 9 +Position: 13 Last 80 unconsumed characters: - +:a=""> DETAIL end diff --git a/test/test_core.rb b/test/test_core.rb index 5668b934..44e2e7ea 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -116,11 +116,12 @@ def test_attribute def test_attribute_namespace_conflict # https://www.w3.org/TR/xml-names/#uniqAttrs - message = <<-MESSAGE + message = <<-MESSAGE.chomp Duplicate attribute "a" Line: 4 Position: 140 Last 80 unconsumed characters: +/> MESSAGE assert_raise(REXML::ParseException.new(message)) do Document.new(<<-XML) @@ -1323,11 +1324,26 @@ def test_ticket_21 exception = assert_raise(ParseException) do Document.new(src) end - assert_equal(<<-DETAIL, exception.to_s) + assert_equal(<<-DETAIL.chomp, exception.to_s) Missing attribute value start quote: Line: 1 Position: 16 Last 80 unconsumed characters: +value/> + DETAIL + end + + def test_parse_exception_on_missing_attribute_end_quote + src = ' +Line: 1 +Position: 17 +Last 80 unconsumed characters: +value/> DETAIL end From 030bfb4cf91f218a481de5c661c7a689f48971d5 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 22 Mar 2024 22:28:00 +0900 Subject: [PATCH 21/33] Change `attribute.has_key?(name)` to ` attributes[name]`. (#121) ## Why? `attributes[name]` is faster than `attribute.has_key?(name)` in Micro Benchmark. However, the Benchmark did not show a significant difference. Would like to merge if possible, how about it? See: https://github.com/ruby/rexml/pull/119#discussion_r1525611640 ## Micro Benchmark ``` $ cat benchmark/attributes.yaml loop_count: 100000 contexts: - name: No YJIT prelude: | $LOAD_PATH.unshift(File.expand_path("lib")) require 'rexml' - name: YJIT prelude: | $LOAD_PATH.unshift(File.expand_path("lib")) require 'rexml' RubyVM::YJIT.enable prelude: | attributes = {} name = :a benchmark: 'attributes[name]' : attributes[name] 'attributes.has_key?(name)' : attributes.has_key?(name) ``` ``` $ benchmark-driver benchmark/attributes.yaml Calculating ------------------------------------- No YJIT YJIT attributes[name] 53.362M 53.562M i/s - 100.000k times in 0.001874s 0.001867s attributes.has_key?(name) 45.025M 45.005M i/s - 100.000k times in 0.002221s 0.002222s Comparison: attributes[name] YJIT: 53561863.6 i/s No YJIT: 53361791.1 i/s - 1.00x slower attributes.has_key?(name) No YJIT: 45024765.3 i/s YJIT: 45004502.0 i/s - 1.00x slower ``` ## Benchmark ``` RUBYLIB= BUNDLER_ORIG_RUBYLIB= /Users/naitoh/.rbenv/versions/3.3.0/bin/ruby -v -S benchmark-driver /Users/naitoh/ghq/github.com/naitoh/rexml/benchmark/parse.yaml ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22] Calculating ------------------------------------- before after before(YJIT) after(YJIT) dom 10.786 10.783 18.196 17.959 i/s - 100.000 times in 9.270908s 9.273657s 5.495854s 5.568326s sax 30.213 30.430 57.030 56.672 i/s - 100.000 times in 3.309845s 3.286240s 1.753459s 1.764551s pull 35.211 35.259 70.817 70.784 i/s - 100.000 times in 2.840056s 2.836136s 1.412098s 1.412754s stream 34.281 34.475 63.084 62.978 i/s - 100.000 times in 2.917067s 2.900689s 1.585196s 1.587860s Comparison: dom before(YJIT): 18.2 i/s after(YJIT): 18.0 i/s - 1.01x slower before: 10.8 i/s - 1.69x slower after: 10.8 i/s - 1.69x slower sax before(YJIT): 57.0 i/s after(YJIT): 56.7 i/s - 1.01x slower after: 30.4 i/s - 1.87x slower before: 30.2 i/s - 1.89x slower pull before(YJIT): 70.8 i/s after(YJIT): 70.8 i/s - 1.00x slower after: 35.3 i/s - 2.01x slower before: 35.2 i/s - 2.01x slower stream before(YJIT): 63.1 i/s after(YJIT): 63.0 i/s - 1.00x slower after: 34.5 i/s - 1.83x slower before: 34.3 i/s - 1.84x slower ``` - YJIT=ON : 0.98x - 1.00x faster - YJIT=OFF : 1.00x - 1.00x faster --- lib/rexml/parsers/baseparser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index f66b968f..8d62391c 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -656,7 +656,7 @@ def parse_attributes(prefixes, curr_ns) prefixes << prefix unless prefix == "xml" end - if attributes.has_key?(name) + if attributes[name] msg = "Duplicate attribute #{name.inspect}" raise REXML::ParseException.new(msg, @source, self) end From 06be5cfd081533f3bbf691717f51eb76268a5896 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 3 May 2024 00:29:57 +0900 Subject: [PATCH 22/33] xpath: Fix wrong position with nested path (#122) ## Why? Fixed incorrect calculation of position in node set. Fix GH-25 Reported by jcavalieri. Thanks!!! --- lib/rexml/xpath_parser.rb | 10 +++++++--- test/xpath/test_base.rb | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/lib/rexml/xpath_parser.rb b/lib/rexml/xpath_parser.rb index d8b88e7a..5eb1e5a9 100644 --- a/lib/rexml/xpath_parser.rb +++ b/lib/rexml/xpath_parser.rb @@ -590,6 +590,7 @@ def filter_nodeset(nodeset) def evaluate_predicate(expression, nodesets) enter(:predicate, expression, nodesets) if @debug + new_nodeset_count = 0 new_nodesets = nodesets.collect do |nodeset| new_nodeset = [] subcontext = { :size => nodeset.size } @@ -606,17 +607,20 @@ def evaluate_predicate(expression, nodesets) result = result[0] if result.kind_of? Array and result.length == 1 if result.kind_of? Numeric if result == node.position - new_nodeset << XPathNode.new(node, position: new_nodeset.size + 1) + new_nodeset_count += 1 + new_nodeset << XPathNode.new(node, position: new_nodeset_count) end elsif result.instance_of? Array if result.size > 0 and result.inject(false) {|k,s| s or k} if result.size > 0 - new_nodeset << XPathNode.new(node, position: new_nodeset.size + 1) + new_nodeset_count += 1 + new_nodeset << XPathNode.new(node, position: new_nodeset_count) end end else if result - new_nodeset << XPathNode.new(node, position: new_nodeset.size + 1) + new_nodeset_count += 1 + new_nodeset << XPathNode.new(node, position: new_nodeset_count) end end end diff --git a/test/xpath/test_base.rb b/test/xpath/test_base.rb index 5156bbbe..68b33ab7 100644 --- a/test/xpath/test_base.rb +++ b/test/xpath/test_base.rb @@ -451,6 +451,46 @@ def test_following # puts results #end + def test_nested_predicates + doc = Document.new <<-EOF +
+
+ ab + cd +
+
+ ef + gh +
+
+ hi +
+
+ EOF + + matches = XPath.match(doc, '(/div/div/test[0])').map(&:text) + assert_equal [], matches + matches = XPath.match(doc, '(/div/div/test[1])').map(&:text) + assert_equal ["ab", "ef", "hi"], matches + matches = XPath.match(doc, '(/div/div/test[2])').map(&:text) + assert_equal ["cd", "gh"], matches + matches = XPath.match(doc, '(/div/div/test[3])').map(&:text) + assert_equal [], matches + + matches = XPath.match(doc, '(/div/div/test[1])[1]').map(&:text) + assert_equal ["ab"], matches + matches = XPath.match(doc, '(/div/div/test[1])[2]').map(&:text) + assert_equal ["ef"], matches + matches = XPath.match(doc, '(/div/div/test[1])[3]').map(&:text) + assert_equal ["hi"], matches + matches = XPath.match(doc, '(/div/div/test[2])[1]').map(&:text) + assert_equal ["cd"], matches + matches = XPath.match(doc, '(/div/div/test[2])[2]').map(&:text) + assert_equal ["gh"], matches + matches = XPath.match(doc, '(/div/div/test[2])[3]').map(&:text) + assert_equal [], matches + end + # Contributed by Mike Stok def test_starts_with source = <<-EOF From d78118dcfc6c5604dcf8dd5b5d19462993a34c12 Mon Sep 17 00:00:00 2001 From: NAITOH Jun Date: Fri, 3 May 2024 23:46:18 +0900 Subject: [PATCH 23/33] Fix a problem that parse exception message can't be generated for invalid encoding XML (#123) ## Why? If the XML tag contains Unicode characters and an error is occurred for the tag, an incompatible encoding error is raised. Because our parse exception message parts have an UTF-8 part (that includes the target tag information) and an ASCII-8BIT part (that includes error context input). Fix GH-29 Reported by DuKewu. Thanks!!! --- lib/rexml/parseexception.rb | 1 + test/parse/test_element.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/rexml/parseexception.rb b/lib/rexml/parseexception.rb index 7b16cd1a..e57d05fd 100644 --- a/lib/rexml/parseexception.rb +++ b/lib/rexml/parseexception.rb @@ -29,6 +29,7 @@ def to_s err << "\nLine: #{line}\n" err << "Position: #{position}\n" err << "Last 80 unconsumed characters:\n" + err.force_encoding("ASCII-8BIT") err << @source.buffer[0..80].force_encoding("ASCII-8BIT").gsub(/\n/, ' ') end diff --git a/test/parse/test_element.rb b/test/parse/test_element.rb index 987214f3..14d0703a 100644 --- a/test/parse/test_element.rb +++ b/test/parse/test_element.rb @@ -47,6 +47,19 @@ def test_empty_namespace_attribute_name DETAIL end + def test_empty_namespace_attribute_name_with_utf8_character + exception = assert_raise(REXML::ParseException) do + parse("") # U+200B ZERO WIDTH SPACE + end + assert_equal(<<-DETAIL.chomp.force_encoding("ASCII-8BIT"), exception.to_s) +Invalid attribute name: <:\xE2\x80\x8B> +Line: 1 +Position: 8 +Last 80 unconsumed characters: +:\xE2\x80\x8B> + DETAIL + end + def test_garbage_less_than_before_root_element_at_line_start exception = assert_raise(REXML::ParseException) do parse("<\n") From bf2c8edb5facb206c25a62952aa37218793283e6 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 6 May 2024 06:31:33 +0900 Subject: [PATCH 24/33] Move development dependencies to Gemfile (#124) --- Gemfile | 7 +++++++ rexml.gemspec | 5 ----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 54da2c0c..042ef8ac 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,10 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in rexml.gemspec gemspec + +group :development do + gem "benchmark_driver" + gem "bundler" + gem "rake" + gem "test-unit" +end diff --git a/rexml.gemspec b/rexml.gemspec index c76bedbe..97eac657 100644 --- a/rexml.gemspec +++ b/rexml.gemspec @@ -56,9 +56,4 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.5.0' spec.add_runtime_dependency("strscan", ">= 3.0.9") - - spec.add_development_dependency "benchmark_driver" - spec.add_development_dependency "bundler" - spec.add_development_dependency "rake" - spec.add_development_dependency "test-unit" end From e77365e2d1c9cdb822c7e09b05fc5a4903d92c23 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 6 May 2024 11:25:18 +0900 Subject: [PATCH 25/33] Exclude older than 2.6 on macos-14 --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fe53d82..ac95c6f0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,8 @@ jobs: - macos-latest - windows-latest ruby-version: ${{ fromJson(needs.ruby-versions.outputs.versions) }} + exclude: + - {runs-on: macos-latest, ruby-version: 2.5} # include: # - runs-on: ubuntu-latest # ruby-version: truffleruby From 4325835f92f3f142ebd91a3fdba4e1f1ab7f1cfb Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 16 May 2024 11:26:51 +0900 Subject: [PATCH 26/33] Read quoted attributes in chunks (#126) --- Gemfile | 1 + lib/rexml/parsers/baseparser.rb | 20 ++++++++++---------- lib/rexml/source.rb | 29 ++++++++++++++++++++++++----- test/test_document.rb | 11 +++++++++++ 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/Gemfile b/Gemfile index 042ef8ac..f78cc861 100644 --- a/Gemfile +++ b/Gemfile @@ -10,4 +10,5 @@ group :development do gem "bundler" gem "rake" gem "test-unit" + gem "test-unit-ruby-core" end diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb index 8d62391c..d09237c5 100644 --- a/lib/rexml/parsers/baseparser.rb +++ b/lib/rexml/parsers/baseparser.rb @@ -628,17 +628,17 @@ def parse_attributes(prefixes, curr_ns) message = "Missing attribute equal: <#{name}>" raise REXML::ParseException.new(message, @source) end - unless match = @source.match(/(['"])(.*?)\1\s*/um, true) - if match = @source.match(/(['"])/, true) - message = - "Missing attribute value end quote: <#{name}>: <#{match[1]}>" - raise REXML::ParseException.new(message, @source) - else - message = "Missing attribute value start quote: <#{name}>" - raise REXML::ParseException.new(message, @source) - end + unless match = @source.match(/(['"])/, true) + message = "Missing attribute value start quote: <#{name}>" + raise REXML::ParseException.new(message, @source) + end + quote = match[1] + value = @source.read_until(quote) + unless value.chomp!(quote) + message = "Missing attribute value end quote: <#{name}>: <#{quote}>" + raise REXML::ParseException.new(message, @source) end - value = match[2] + @source.match(/\s*/um, true) if prefix == "xmlns" if local_part == "xml" if value != "http://www.w3.org/XML/1998/namespace" diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb index 7f47c2be..999751b4 100644 --- a/lib/rexml/source.rb +++ b/lib/rexml/source.rb @@ -65,7 +65,11 @@ def encoding=(enc) encoding_updated end - def read + def read(term = nil) + end + + def read_until(term) + @scanner.scan_until(Regexp.union(term)) or @scanner.rest end def ensure_buffer @@ -158,9 +162,9 @@ def initialize(arg, block_size=500, encoding=nil) end end - def read + def read(term = nil) begin - @scanner << readline + @scanner << readline(term) true rescue Exception, NameError @source = nil @@ -168,6 +172,21 @@ def read end end + def read_until(term) + pattern = Regexp.union(term) + data = [] + begin + until str = @scanner.scan_until(pattern) + @scanner << readline(term) + end + rescue EOFError + @scanner.rest + else + read if @scanner.eos? and !@source.eof? + str + end + end + def ensure_buffer read if @scanner.eos? && @source end @@ -218,8 +237,8 @@ def current_line end private - def readline - str = @source.readline(@line_break) + def readline(term = nil) + str = @source.readline(term || @line_break) if @pending_buffer if str.nil? str = @pending_buffer diff --git a/test/test_document.rb b/test/test_document.rb index 953656f8..f96bfd5d 100644 --- a/test/test_document.rb +++ b/test/test_document.rb @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- # frozen_string_literal: false +require 'core_assertions' + module REXMLTests class TestDocument < Test::Unit::TestCase + include Test::Unit::CoreAssertions + def test_version_attributes_to_s doc = REXML::Document.new(<<~eoxml) @@ -198,6 +202,13 @@ def test_xml_declaration_standalone assert_equal('no', doc.stand_alone?, bug2539) end + def test_gt_linear_performance + seq = [10000, 50000, 100000, 150000, 200000] + assert_linear_performance(seq) do |n| + REXML::Document.new('" * n + '">') + end + end + class WriteTest < Test::Unit::TestCase def setup @document = REXML::Document.new(<<-EOX) From 085def07425561862d8329001168d8bc9c75ae8f Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 16 May 2024 11:34:38 +0900 Subject: [PATCH 27/33] Add 3.2.7 entry --- NEWS.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 271c303b..63b50c33 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,58 @@ # News -## 3.2.6 - 2023-07-27 {#version-3-2-6} +## 3.2.7 - 2024-05-16 {#version-3-2-7} + +### Improvements + + * Improve parse performance by using `StringScanner`. + + * GH-106 + * GH-107 + * GH-108 + * GH-109 + * GH-112 + * GH-113 + * GH-114 + * GH-115 + * GH-116 + * GH-117 + * GH-118 + * GH-119 + * GH-121 + + * Patch by NAITOH Jun. + + * Improved parse performance when an attribute has many `<`s. + + * GH-124 + +### Fixes + + * XPath: Fixed a bug of `normalize_space(array)`. + + * GH-110 + * GH-111 + + * Patch by flatisland. + + * XPath: Fixed a bug that wrong position is used with nested path. + + * GH-110 + * GH-122 + + * Reported by jcavalieri. + * Patch by NAITOH Jun. + + * Fixed a bug that an exception message can't be generated for + invalid encoding XML. + + * GH-29 + * GH-123 + + * Reported by DuKewu. + * Patch by NAITOH Jun. + +w## 3.2.6 - 2023-07-27 {#version-3-2-6} ### Improvements From 9ba35f9f032c07c39b8c86536ac13a9cb313bef2 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 16 May 2024 11:35:55 +0900 Subject: [PATCH 28/33] Bump version --- lib/rexml/rexml.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rexml/rexml.rb b/lib/rexml/rexml.rb index 0315a2db..191932b8 100644 --- a/lib/rexml/rexml.rb +++ b/lib/rexml/rexml.rb @@ -31,7 +31,7 @@ module REXML COPYRIGHT = "Copyright © 2001-2008 Sean Russell " DATE = "2008/019" - VERSION = "3.2.7" + VERSION = "3.2.8" REVISION = "" Copyright = COPYRIGHT From 4670f8fc187c89d0504d027ea997959287143453 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 16 May 2024 11:43:21 +0900 Subject: [PATCH 29/33] Add missing Thanks section --- NEWS.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 63b50c33..00976d84 100644 --- a/NEWS.md +++ b/NEWS.md @@ -52,7 +52,15 @@ * Reported by DuKewu. * Patch by NAITOH Jun. -w## 3.2.6 - 2023-07-27 {#version-3-2-6} +### Thanks + + * NAITOH Jun + * flatisland + * jcavalieri + * DuKewu + + +## 3.2.6 - 2023-07-27 {#version-3-2-6} ### Improvements From d574ba5fe1c40adbafbf16e47533f4eb32b43e60 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 16 May 2024 14:28:13 +0900 Subject: [PATCH 30/33] ci: install only gems required for running tests (#129) --- .github/workflows/test.yml | 4 ++++ Gemfile | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac95c6f0..fd26b9ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,8 +66,12 @@ jobs: with: ruby-version: ${{ matrix.ruby-version }} - name: Install as gem + env: + BUNDLE_PATH__SYSTEM: "true" + BUNDLE_WITHOUT: "benchmark:development" run: | rake install + bundle install - name: Test run: | ruby -run -e mkdir -- tmp diff --git a/Gemfile b/Gemfile index f78cc861..67f21dfb 100644 --- a/Gemfile +++ b/Gemfile @@ -6,9 +6,15 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gemspec group :development do - gem "benchmark_driver" gem "bundler" gem "rake" +end + +group :benchmark do + gem "benchmark_driver" +end + +group :test do gem "test-unit" gem "test-unit-ruby-core" end From 94e180e939baff8f7e328a287bb96ebbd99db6eb Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 16 May 2024 14:30:35 +0900 Subject: [PATCH 31/33] Suppress a warning --- lib/rexml/source.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb index 999751b4..0f3c5011 100644 --- a/lib/rexml/source.rb +++ b/lib/rexml/source.rb @@ -174,7 +174,6 @@ def read(term = nil) def read_until(term) pattern = Regexp.union(term) - data = [] begin until str = @scanner.scan_until(pattern) @scanner << readline(term) From b67081caa807fad48d31983137b7ed8711e7f0df Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 16 May 2024 14:31:50 +0900 Subject: [PATCH 32/33] Remove an unused variable (#128) Fix up #126. From 1cf37bab79d61d6183bbda8bf525ed587012b718 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Thu, 16 May 2024 14:32:59 +0900 Subject: [PATCH 33/33] Add 3.2.8 entry --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index 00976d84..013409e6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # News +## 3.2.8 - 2024-05-16 {#version-3-2-8} + +### Fixes + + * Suppressed a warning + ## 3.2.7 - 2024-05-16 {#version-3-2-7} ### Improvements