diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..7b2e23ed --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,42 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '41 19 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'ruby' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..b8f1d1d8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,161 @@ +name: CI + +on: [push, pull_request, workflow_dispatch] + +env: + BUNDLE_JOBS: 4 + +# jobs defined in the order we want them listed in the Actions UI +jobs: + profile: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + ruby: [2.7] + idna_mode: [native, pure] + os: [ubuntu-20.04] + env: + IDNA_MODE: ${{ matrix.idna_mode }} + steps: + - uses: actions/checkout@v3 + + - name: Install libidn + run: sudo apt-get install libidn11-dev + + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: false + ruby-version: ${{ matrix.ruby }} + + - name: Install gems + run: bundle install + + - name: >- + Profile Memory Allocation with ${{ matrix.idna_mode }} IDNA during Addressable::URI#parse + run: bundle exec rake profile:memory + + - name: >- + Profile Memory Allocation with ${{ matrix.idna_mode }} IDNA during Addressable::Template#match + run: bundle exec rake profile:template_match_memory + + coverage: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + ruby: [2.7] + os: [ubuntu-20.04] + env: + BUNDLE_WITHOUT: development + COVERALLS_SERVICE_NAME: github + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_DEBUG: true + CI_BUILD_NUMBER: ${{ github.run_id }} + steps: + - uses: actions/checkout@v3 + + - name: Install libidn + run: sudo apt-get install libidn11-dev + + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: false + ruby-version: ${{ matrix.ruby }} + + - name: Install gems + run: bundle install + + - name: Run specs and report coverage + run: bundle exec rake + + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # the job name is composed by these attributes + ruby: + - 2.2 + - 2.3 + - 2.4 + - 2.5 + - 2.6 + - 2.7 + # quotes because of YAML gotcha: https://github.com/actions/runner/issues/849 + - '3.0' + - 3.1 + - head + - jruby-9.1 + - jruby-9.2 + - jruby-9.3 + - truffleruby-21.3 + - truffleruby-22.1 + os: + - ubuntu-20.04 + gemfile: + - Gemfile + include: + - { os: ubuntu-20.04, ruby: 2.7, gemfile: gemfiles/public_suffix_2.rb } + - { os: ubuntu-20.04, ruby: 2.7, gemfile: gemfiles/public_suffix_3.rb } + - { os: ubuntu-20.04, ruby: 2.7, gemfile: gemfiles/public_suffix_4.rb } + # Ubuntu + - { os: ubuntu-22.04, ruby: 3.1 } + # macOS + - { os: macos-11, ruby: 3.1 } + - { os: macos-12, ruby: 3.1 } + # Windows + - { os: windows-2019, ruby: 3.1 } + - { os: windows-2022, ruby: 3.1 } + - { os: windows-2022, ruby: jruby-9.3 } + # allowed to fail + - { os: ubuntu-20.04, ruby: jruby-head, gemfile: Gemfile, allow-failure: true } + - { os: ubuntu-20.04, ruby: truffleruby-head, gemfile: Gemfile, allow-failure: true } + env: + BUNDLE_GEMFILE: ${{ matrix.gemfile }} + BUNDLE_WITHOUT: development:coverage + # Workaround for Windows JRuby JDK issue + # https://github.com/ruby/setup-ruby/issues/339 + # https://github.com/jruby/jruby/issues/7182#issuecomment-1112953015 + JAVA_OPTS: -Djdk.io.File.enableADS=true + steps: + - uses: actions/checkout@v3 + + - name: Install libidn (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + run: sudo apt-get install libidn11-dev + + - name: Install libidn (macOS) + if: startsWith(matrix.os, 'macos') + run: brew install libidn + + - name: Setup ruby + continue-on-error: ${{ matrix.allow-failure || false }} + id: setupruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: false + ruby-version: ${{ matrix.ruby }} + + - name: Install gems + continue-on-error: ${{ matrix.allow-failure || false }} + id: bundle + run: bundle install + + - name: Run specs + continue-on-error: ${{ matrix.allow-failure || false }} + id: specs + run: bundle exec rake spec + + # because continue-on-error marks the steps as pass if they fail + - name: >- + Setup ruby outcome: ${{ steps.setupruby.outcome }} + run: echo NOOP + - name: >- + Install gems outcome: ${{ steps.bundle.outcome }} + run: echo NOOP + - name: >- + Run specs outcome: ${{ steps.specs.outcome }} + run: echo NOOP diff --git a/.gitignore b/.gitignore index 874e3a0b..b53c0229 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ doc heckling pkg specdoc +tmp/ +vendor/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4b280f7e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -bundler_args: --without development --retry=3 --jobs=3 -cache: bundler -language: ruby -rvm: - - jruby-9.0.5.0 - - jruby-9.1.16.0 - - jruby-9.2.5.0 - - ruby-head - - ruby-head-clang - - 2.0.0 - - 2.1.10 - - 2.2.10 - - 2.3.8 - - 2.4.5 - - 2.5.3 - - 2.6.0 -matrix: - include: - - gemfile: gemfiles/Gemfile.public_suffix_2 - rvm: 2.5.0 - allow_failures: - - rvm: ruby-head - - rvm: ruby-head-clang - fast_finish: true -before_install: - - gem update bundler -# - apt-get update -# - apt-get install idn -sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 90531e1f..c15168dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +# Addressable 2.8.1 +- refactor `Addressable::URI.normalize_path` to address linter offenses ([#430](https://github.com/sporkmonger/addressable/pull/430)) +- remove redundant colon in `Addressable::URI::CharacterClasses::AUTHORITY` regex ([#438](https://github.com/sporkmonger/addressable/pull/438)) +- update gemspec to reflect supported Ruby versions ([#466], [#464], [#463]) +- compatibility w/ public_suffix 5.x ([#466], [#465], [#460]) +- fixes "invalid byte sequence in UTF-8" exception when unencoding URLs containing non UTF-8 characters ([#459](https://github.com/sporkmonger/addressable/pull/459)) +- `Ractor` compatibility ([#449](https://github.com/sporkmonger/addressable/pull/449)) +- use the whole string instead of a single line for template match ([#431](https://github.com/sporkmonger/addressable/pull/431)) +- force UTF-8 encoding only if needed ([#341](https://github.com/sporkmonger/addressable/pull/341)) + +[#460]: https://github.com/sporkmonger/addressable/pull/460 +[#463]: https://github.com/sporkmonger/addressable/pull/463 +[#464]: https://github.com/sporkmonger/addressable/pull/464 +[#465]: https://github.com/sporkmonger/addressable/pull/465 +[#466]: https://github.com/sporkmonger/addressable/pull/466 + +# Addressable 2.8.0 +- fixes ReDoS vulnerability in Addressable::Template#match +- no longer replaces `+` with spaces in queries for non-http(s) schemes +- fixed encoding ipv6 literals +- the `:compacted` flag for `normalized_query` now dedupes parameters +- fix broken `escape_component` alias +- dropping support for Ruby 2.0 and 2.1 +- adding Ruby 3.0 compatibility for development tasks +- drop support for `rack-mount` and remove Addressable::Template#generate +- performance improvements +- switch CI/CD to GitHub Actions + +# Addressable 2.7.0 +- added `:compacted` flag to `normalized_query` +- `heuristic_parse` handles `mailto:` more intuitively +- dropped explicit support for JRuby 9.0.5.0 +- compatibility w/ public_suffix 4.x +- performance improvements + # Addressable 2.6.0 - added `tld=` method to allow assignment to the public suffix - most `heuristic_parse` patterns are now case-insensitive diff --git a/Gemfile b/Gemfile index 9ca55cab..0d36ffb5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,17 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec group :test do - gem 'rspec', '~> 3.5' - gem 'rspec-its', '~> 1.1' + gem 'rspec', '~> 3.8' + gem 'rspec-its', '~> 1.3' +end + +group :coverage do + gem "coveralls", "> 0.7", require: false, platforms: :mri + gem "simplecov", require: false end group :development do @@ -14,19 +21,10 @@ group :development do end group :test, :development do - gem 'rake', '> 10.0', '< 12' - gem 'simplecov', :require => false - gem 'coveralls', :require => false, :platforms => [ - :ruby_20, :ruby_21, :ruby_22, :ruby_23 - ] - # Used to test compatibility. - gem 'rack-mount', git: 'https://github.com/sporkmonger/rack-mount.git', require: 'rack/mount' - - if RUBY_VERSION.start_with?('2.0', '2.1') - gem 'rack', '< 2', :require => false - else - gem 'rack', :require => false - end + gem 'memory_profiler' + gem "rake", ">= 12.3.3" end -gem 'idn-ruby', :platform => [:mri_20, :mri_21, :mri_22, :mri_23, :mri_24] +unless ENV["IDNA_MODE"] == "pure" + gem "idn-ruby", platform: :mri +end diff --git a/README.md b/README.md index fa65c288..9892f615 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,23 @@
License
Apache 2.0
-[![Gem Version](http://img.shields.io/gem/dt/addressable.svg)][gem] -[![Build Status](https://secure.travis-ci.org/sporkmonger/addressable.svg?branch=master)][travis] +[![Gem Version](https://img.shields.io/gem/dt/addressable.svg)][gem] +[![Build Status](https://github.com/sporkmonger/addressable/workflows/CI/badge.svg)][actions] [![Test Coverage Status](https://img.shields.io/coveralls/sporkmonger/addressable.svg)][coveralls] -[![Documentation Coverage Status](http://inch-ci.org/github/sporkmonger/addressable.svg?branch=master)][inch] +[![Documentation Coverage Status](https://inch-ci.org/github/sporkmonger/addressable.svg?branch=master)][inch] [gem]: https://rubygems.org/gems/addressable -[travis]: http://travis-ci.org/sporkmonger/addressable +[actions]: https://github.com/sporkmonger/addressable/actions [coveralls]: https://coveralls.io/r/sporkmonger/addressable -[inch]: http://inch-ci.org/github/sporkmonger/addressable +[inch]: https://inch-ci.org/github/sporkmonger/addressable # Description -Addressable is a replacement for the URI implementation that is part of -Ruby's standard library. It more closely conforms to RFC 3986, RFC 3987, and -RFC 6570 (level 4), providing support for IRIs and URI templates. +Addressable is an alternative implementation to the URI implementation +that is part of Ruby's standard library. It is flexible, offers heuristic +parsing, and additionally provides extensive support for IRIs and URI templates. + +Addressable closely conforms to RFC 3986, RFC 3987, and RFC 6570 (level 4). # Reference @@ -96,7 +98,7 @@ You may optionally turn on native IDN support by installing libidn and the idn gem: ```console -$ sudo apt-get install idn # Debian/Ubuntu +$ sudo apt-get install libidn11-dev # Debian/Ubuntu $ brew install libidn # OS X $ gem install idn-ruby ``` @@ -108,7 +110,7 @@ dependency using a pessimistic version constraint covering the major and minor values: ```ruby -spec.add_dependency 'addressable', '~> 2.5' +spec.add_dependency 'addressable', '~> 2.7' ``` If you need a specific bug fix, you can also specify minimum tiny versions diff --git a/Rakefile b/Rakefile index 64b9deac..b7e0ff31 100644 --- a/Rakefile +++ b/Rakefile @@ -14,9 +14,9 @@ RELEASE_NAME = "REL #{PKG_VERSION}" PKG_SUMMARY = "URI Implementation" PKG_DESCRIPTION = <<-TEXT -Addressable is a replacement for the URI implementation that is part of -Ruby's standard library. It more closely conforms to the relevant RFCs and -adds support for IRIs and URI templates. +Addressable is an alternative implementation to the URI implementation that is +part of Ruby's standard library. It is flexible, offers heuristic parsing, and +additionally provides extensive support for IRIs and URI templates. TEXT PKG_FILES = FileList[ diff --git a/addressable.gemspec b/addressable.gemspec index 83936c1b..d51f4655 100644 --- a/addressable.gemspec +++ b/addressable.gemspec @@ -1,37 +1,35 @@ # -*- encoding: utf-8 -*- -# stub: addressable 2.6.0 ruby lib +# stub: addressable 2.8.1 ruby lib Gem::Specification.new do |s| - s.name = "addressable" - s.version = "2.6.0" + s.name = "addressable".freeze + s.version = "2.8.1" - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.require_paths = ["lib"] - s.authors = ["Bob Aman"] - s.date = "2019-01-18" - s.description = "Addressable is a replacement for the URI implementation that is part of\nRuby's standard library. It more closely conforms to the relevant RFCs and\nadds support for IRIs and URI templates.\n" - s.email = "bob@sporkmonger.com" - s.extra_rdoc_files = ["README.md"] - s.files = ["CHANGELOG.md", "Gemfile", "LICENSE.txt", "README.md", "Rakefile", "data/unicode.data", "lib/addressable", "lib/addressable.rb", "lib/addressable/idna", "lib/addressable/idna.rb", "lib/addressable/idna/native.rb", "lib/addressable/idna/pure.rb", "lib/addressable/template.rb", "lib/addressable/uri.rb", "lib/addressable/version.rb", "spec/addressable", "spec/addressable/idna_spec.rb", "spec/addressable/net_http_compat_spec.rb", "spec/addressable/rack_mount_compat_spec.rb", "spec/addressable/security_spec.rb", "spec/addressable/template_spec.rb", "spec/addressable/uri_spec.rb", "spec/spec_helper.rb", "tasks/clobber.rake", "tasks/gem.rake", "tasks/git.rake", "tasks/metrics.rake", "tasks/rspec.rake", "tasks/yard.rake"] - s.homepage = "https://github.com/sporkmonger/addressable" - s.licenses = ["Apache-2.0"] - s.rdoc_options = ["--main", "README.md"] - s.required_ruby_version = Gem::Requirement.new(">= 2.0") - s.rubygems_version = "2.5.1" - s.summary = "URI Implementation" + s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= + s.metadata = { "changelog_uri" => "https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md" } if s.respond_to? :metadata= + s.require_paths = ["lib".freeze] + s.authors = ["Bob Aman".freeze] + s.date = "2022-08-19" + s.description = "Addressable is an alternative implementation to the URI implementation that is\npart of Ruby's standard library. It is flexible, offers heuristic parsing, and\nadditionally provides extensive support for IRIs and URI templates.\n".freeze + s.email = "bob@sporkmonger.com".freeze + s.extra_rdoc_files = ["README.md".freeze] + s.files = ["CHANGELOG.md".freeze, "Gemfile".freeze, "LICENSE.txt".freeze, "README.md".freeze, "Rakefile".freeze, "data/unicode.data".freeze, "lib/addressable".freeze, "lib/addressable.rb".freeze, "lib/addressable/idna".freeze, "lib/addressable/idna.rb".freeze, "lib/addressable/idna/native.rb".freeze, "lib/addressable/idna/pure.rb".freeze, "lib/addressable/template.rb".freeze, "lib/addressable/uri.rb".freeze, "lib/addressable/version.rb".freeze, "spec/addressable".freeze, "spec/addressable/idna_spec.rb".freeze, "spec/addressable/net_http_compat_spec.rb".freeze, "spec/addressable/security_spec.rb".freeze, "spec/addressable/template_spec.rb".freeze, "spec/addressable/uri_spec.rb".freeze, "spec/spec_helper.rb".freeze, "tasks/clobber.rake".freeze, "tasks/gem.rake".freeze, "tasks/git.rake".freeze, "tasks/metrics.rake".freeze, "tasks/profile.rake".freeze, "tasks/rspec.rake".freeze, "tasks/yard.rake".freeze] + s.homepage = "https://github.com/sporkmonger/addressable".freeze + s.licenses = ["Apache-2.0".freeze] + s.rdoc_options = ["--main".freeze, "README.md".freeze] + s.required_ruby_version = Gem::Requirement.new(">= 2.2".freeze) + s.rubygems_version = "3.3.7".freeze + s.summary = "URI Implementation".freeze if s.respond_to? :specification_version then s.specification_version = 4 + end - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, ["< 4.0", ">= 2.0.2"]) - s.add_development_dependency(%q, ["< 3.0", ">= 1.0"]) - else - s.add_dependency(%q, ["< 4.0", ">= 2.0.2"]) - s.add_dependency(%q, ["< 3.0", ">= 1.0"]) - end + if s.respond_to? :add_runtime_dependency then + s.add_runtime_dependency(%q.freeze, [">= 2.0.2", "< 6.0"]) + s.add_development_dependency(%q.freeze, [">= 1.0", "< 3.0"]) else - s.add_dependency(%q, ["< 4.0", ">= 2.0.2"]) - s.add_dependency(%q, ["< 3.0", ">= 1.0"]) + s.add_dependency(%q.freeze, [">= 2.0.2", "< 6.0"]) + s.add_dependency(%q.freeze, [">= 1.0", "< 3.0"]) end end diff --git a/gemfiles/Gemfile.public_suffix_2 b/gemfiles/Gemfile.public_suffix_2 deleted file mode 100644 index 21767ac8..00000000 --- a/gemfiles/Gemfile.public_suffix_2 +++ /dev/null @@ -1,34 +0,0 @@ -source 'https://rubygems.org' - -gemspec path: ".." - -gem 'public_suffix', '>= 2.0.2', '~> 2.0' - -group :test do - gem 'rspec', '~> 3.0' - gem 'rspec-its', '~> 1.1' -end - -group :development do - gem 'launchy', '~> 2.4', '>= 2.4.3' - gem 'redcarpet', :platform => :mri_19 - gem 'yard' -end - -group :test, :development do - gem 'rake', '> 10.0', '< 12' - gem 'simplecov', :require => false - gem 'coveralls', :require => false, :platforms => [ - :ruby_20, :ruby_21, :ruby_22, :ruby_23 - ] - # Used to test compatibility. - gem 'rack-mount', git: 'https://github.com/sporkmonger/rack-mount.git', require: 'rack/mount' - - if RUBY_VERSION.start_with?('2.0', '2.1') - gem 'rack', '< 2', :require => false - else - gem 'rack', :require => false - end -end - -gem 'idn-ruby', :platform => [:mri_20, :mri_21, :mri_22] diff --git a/gemfiles/public_suffix_2.rb b/gemfiles/public_suffix_2.rb new file mode 100644 index 00000000..1db989ab --- /dev/null +++ b/gemfiles/public_suffix_2.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Assumes this gemfile is used from the project root +eval_gemfile "../Gemfile" + +gem "public_suffix", ">= 2.0.2", "~> 2.0" diff --git a/gemfiles/public_suffix_3.rb b/gemfiles/public_suffix_3.rb new file mode 100644 index 00000000..86d07aca --- /dev/null +++ b/gemfiles/public_suffix_3.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Assumes this gemfile is used from the project root +eval_gemfile "../Gemfile" + +gem "public_suffix", "~> 3.0" diff --git a/gemfiles/public_suffix_4.rb b/gemfiles/public_suffix_4.rb new file mode 100644 index 00000000..84936d2c --- /dev/null +++ b/gemfiles/public_suffix_4.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Assumes this gemfile is used from the project root +eval_gemfile "../Gemfile" + +gem "public_suffix", "~> 4.0" diff --git a/lib/addressable/idna.rb b/lib/addressable/idna.rb index e41c1f5d..2dbd3934 100644 --- a/lib/addressable/idna.rb +++ b/lib/addressable/idna.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# encoding:utf-8 #-- # Copyright (C) Bob Aman # diff --git a/lib/addressable/idna/native.rb b/lib/addressable/idna/native.rb index 84de8e8c..302e1b0c 100644 --- a/lib/addressable/idna/native.rb +++ b/lib/addressable/idna/native.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# encoding:utf-8 #-- # Copyright (C) Bob Aman # diff --git a/lib/addressable/idna/pure.rb b/lib/addressable/idna/pure.rb index 519094da..a7c796e3 100644 --- a/lib/addressable/idna/pure.rb +++ b/lib/addressable/idna/pure.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# encoding:utf-8 #-- # Copyright (C) Bob Aman # @@ -135,7 +134,7 @@ def self.unicode_downcase(input) unpacked.map! { |codepoint| lookup_unicode_lowercase(codepoint) } return unpacked.pack("U*") end - (class <= HANGUL_LBASE && ch_one < HANGUL_LBASE + HANGUL_LCOUNT && @@ -178,43 +177,45 @@ def self.unicode_compose_pair(ch_one, ch_two) end p = [] - ucs4_to_utf8 = lambda do |ch| - if ch < 128 - p << ch - elsif ch < 2048 - p << (ch >> 6 | 192) - p << (ch & 63 | 128) - elsif ch < 0x10000 - p << (ch >> 12 | 224) - p << (ch >> 6 & 63 | 128) - p << (ch & 63 | 128) - elsif ch < 0x200000 - p << (ch >> 18 | 240) - p << (ch >> 12 & 63 | 128) - p << (ch >> 6 & 63 | 128) - p << (ch & 63 | 128) - elsif ch < 0x4000000 - p << (ch >> 24 | 248) - p << (ch >> 18 & 63 | 128) - p << (ch >> 12 & 63 | 128) - p << (ch >> 6 & 63 | 128) - p << (ch & 63 | 128) - elsif ch < 0x80000000 - p << (ch >> 30 | 252) - p << (ch >> 24 & 63 | 128) - p << (ch >> 18 & 63 | 128) - p << (ch >> 12 & 63 | 128) - p << (ch >> 6 & 63 | 128) - p << (ch & 63 | 128) - end - end - ucs4_to_utf8.call(ch_one) - ucs4_to_utf8.call(ch_two) + ucs4_to_utf8(ch_one, p) + ucs4_to_utf8(ch_two, p) return lookup_unicode_composition(p) end - (class <> 6 | 192) + buffer << (char & 63 | 128) + elsif char < 0x10000 + buffer << (char >> 12 | 224) + buffer << (char >> 6 & 63 | 128) + buffer << (char & 63 | 128) + elsif char < 0x200000 + buffer << (char >> 18 | 240) + buffer << (char >> 12 & 63 | 128) + buffer << (char >> 6 & 63 | 128) + buffer << (char & 63 | 128) + elsif char < 0x4000000 + buffer << (char >> 24 | 248) + buffer << (char >> 18 & 63 | 128) + buffer << (char >> 12 & 63 | 128) + buffer << (char >> 6 & 63 | 128) + buffer << (char & 63 | 128) + elsif char < 0x80000000 + buffer << (char >> 30 | 252) + buffer << (char >> 24 & 63 | 128) + buffer << (char >> 18 & 63 | 128) + buffer << (char >> 12 & 63 | 128) + buffer << (char >> 6 & 63 | 128) + buffer << (char & 63 | 128) + end + end + private_class_method :ucs4_to_utf8 def self.unicode_sort_canonical(unpacked) unpacked = unpacked.dup @@ -238,7 +239,7 @@ def self.unicode_sort_canonical(unpacked) end return unpacked end - (class <(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)" RESERVED = "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])" UNRESERVED = @@ -412,7 +411,7 @@ def extract(uri, processor=nil) # match.captures # #=> ["a", ["b", "c"]] def match(uri, processor=nil) - uri = Addressable::URI.parse(uri) + uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI) mapping = {} # First, we need to process the pattern, and extract the values. @@ -653,50 +652,16 @@ def named_captures self.to_regexp.named_captures end - ## - # Generates a route result for a given set of parameters. - # Should only be used by rack-mount. - # - # @param params [Hash] The set of parameters used to expand the template. - # @param recall [Hash] Default parameters used to expand the template. - # @param options [Hash] Either a `:processor` or a `:parameterize` block. - # - # @api private - def generate(params={}, recall={}, options={}) - merged = recall.merge(params) - if options[:processor] - processor = options[:processor] - elsif options[:parameterize] - # TODO: This is sending me into fits trying to shoe-horn this into - # the existing API. I think I've got this backwards and processors - # should be a set of 4 optional blocks named :validate, :transform, - # :match, and :restore. Having to use a singleton here is a huge - # code smell. - processor = Object.new - class <Regexp that parses a template pattern. Memoizes the + # value if template processor not set (processors may not be deterministic) + # + # @param [String] pattern The URI template pattern. + # @param [#match] processor The template processor to use. + # + # @return [Array, Regexp] + # An array of expansion variables nad a regular expression which may be + # used to parse a template pattern + def parse_template_pattern(pattern, processor = nil) + if processor.nil? && pattern == @pattern + @cached_template_parse ||= + parse_new_template_pattern(pattern, processor) + else + parse_new_template_pattern(pattern, processor) + end + end + ## # Generates the Regexp that parses a template pattern. # # @param [String] pattern The URI template pattern. # @param [#match] processor The template processor to use. # - # @return [Regexp] - # A regular expression which may be used to parse a template pattern. - def parse_template_pattern(pattern, processor=nil) + # @return [Array, Regexp] + # An array of expansion variables nad a regular expression which may be + # used to parse a template pattern + def parse_new_template_pattern(pattern, processor = nil) # Escape the pattern. The two gsubs restore the escaped curly braces # back to their original form. Basically, escape everything that isn't # within an expansion. @@ -1037,7 +1022,7 @@ def parse_template_pattern(pattern, processor=nil) end # Ensure that the regular expression matches the whole URI. - regexp_string = "^#{regexp_string}$" + regexp_string = "\\A#{regexp_string}\\z" return expansions, Regexp.new(regexp_string) end diff --git a/lib/addressable/uri.rb b/lib/addressable/uri.rb index 9109256c..14b92530 100644 --- a/lib/addressable/uri.rb +++ b/lib/addressable/uri.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# encoding:utf-8 #-- # Copyright (C) Bob Aman # @@ -38,20 +37,35 @@ class InvalidURIError < StandardError ## # Container for the character classes specified in # RFC 3986. + # + # Note: Concatenated and interpolated `String`s are not affected by the + # `frozen_string_literal` directive and must be frozen explicitly. + # + # Interpolated `String`s *were* frozen this way before Ruby 3.0: + # https://bugs.ruby-lang.org/issues/17104 module CharacterClasses ALPHA = "a-zA-Z" DIGIT = "0-9" GEN_DELIMS = "\\:\\/\\?\\#\\[\\]\\@" SUB_DELIMS = "\\!\\$\\&\\'\\(\\)\\*\\+\\,\\;\\=" - RESERVED = GEN_DELIMS + SUB_DELIMS - UNRESERVED = ALPHA + DIGIT + "\\-\\.\\_\\~" - PCHAR = UNRESERVED + SUB_DELIMS + "\\:\\@" - SCHEME = ALPHA + DIGIT + "\\-\\+\\." - HOST = UNRESERVED + SUB_DELIMS + "\\[\\:\\]" - AUTHORITY = PCHAR - PATH = PCHAR + "\\/" - QUERY = PCHAR + "\\/\\?" - FRAGMENT = PCHAR + "\\/\\?" + RESERVED = (GEN_DELIMS + SUB_DELIMS).freeze + UNRESERVED = (ALPHA + DIGIT + "\\-\\.\\_\\~").freeze + PCHAR = (UNRESERVED + SUB_DELIMS + "\\:\\@").freeze + SCHEME = (ALPHA + DIGIT + "\\-\\+\\.").freeze + HOST = (UNRESERVED + SUB_DELIMS + "\\[\\:\\]").freeze + AUTHORITY = (PCHAR + "\\[\\:\\]").freeze + PATH = (PCHAR + "\\/").freeze + QUERY = (PCHAR + "\\/\\?").freeze + FRAGMENT = (PCHAR + "\\/\\?").freeze + end + + module NormalizeCharacterClasses + HOST = /[^#{CharacterClasses::HOST}]/ + UNRESERVED = /[^#{CharacterClasses::UNRESERVED}]/ + PCHAR = /[^#{CharacterClasses::PCHAR}]/ + SCHEME = /[^#{CharacterClasses::SCHEME}]/ + FRAGMENT = /[^#{CharacterClasses::FRAGMENT}]/ + QUERY = %r{[^a-zA-Z0-9\-\.\_\~\!\$\'\(\)\*\+\,\=\:\@\/\?%]|%(?!2B|2b)} end SLASH = '/' @@ -73,7 +87,7 @@ module CharacterClasses "wais" => 210, "ldap" => 389, "prospero" => 1525 - } + }.freeze ## # Returns a URI object based on the parsed string. @@ -207,7 +221,7 @@ def self.heuristic_parse(uri, hints={}) fragments = match.captures authority = fragments[3] if authority && authority.length > 0 - new_authority = authority.gsub(/\\/, '/').gsub(/ /, '%20') + new_authority = authority.tr("\\", "/").gsub(" ", "%20") # NOTE: We want offset 4, not 3! offset = match.offset(4) uri = uri.dup @@ -218,8 +232,9 @@ def self.heuristic_parse(uri, hints={}) parsed = self.parse(hints[:scheme] + "://" + uri) end if parsed.path.include?(".") - new_host = parsed.path[/^([^\/]+\.[^\/]*)/, 1] - if new_host + if parsed.path[/\b@\b/] + parsed.scheme = "mailto" unless parsed.scheme + elsif new_host = parsed.path[/^([^\/]+\.[^\/]*)/, 1] parsed.defer_validation do new_path = parsed.path.sub( Regexp.new("^" + Regexp.escape(new_host)), EMPTY_STR) @@ -281,15 +296,15 @@ def self.convert_path(path) uri.path.sub!(/^\/?([a-zA-Z])[\|:][\\\/]/) do "/#{$1.downcase}:/" end - uri.path.gsub!(/\\/, SLASH) + uri.path.tr!("\\", SLASH) if File.exist?(uri.path) && File.stat(uri.path).directory? - uri.path.sub!(/\/$/, EMPTY_STR) + uri.path.chomp!(SLASH) uri.path = uri.path + '/' end # If the path is absolute, set the scheme and host. - if uri.path =~ /^\// + if uri.path.start_with?(SLASH) uri.scheme = "file" uri.host = EMPTY_STR end @@ -326,6 +341,21 @@ def self.join(*uris) return result end + ## + # Tables used to optimize encoding operations in `self.encode_component` + # and `self.normalize_component` + SEQUENCE_ENCODING_TABLE = Hash.new do |hash, sequence| + hash[sequence] = sequence.unpack("C*").map do |c| + format("%02x", c) + end.join + end + + SEQUENCE_UPCASED_PERCENT_ENCODING_TABLE = Hash.new do |hash, sequence| + hash[sequence] = sequence.unpack("C*").map do |c| + format("%%%02X", c) + end.join + end + ## # Percent encodes a URI component. # @@ -392,18 +422,20 @@ def self.encode_component(component, character_class= component.force_encoding(Encoding::ASCII_8BIT) # Avoiding gsub! because there are edge cases with frozen strings component = component.gsub(character_class) do |sequence| - (sequence.unpack('C*').map { |c| "%" + ("%02x" % c).upcase }).join + SEQUENCE_UPCASED_PERCENT_ENCODING_TABLE[sequence] end if upcase_encoded.length > 0 - component = component.gsub(/%(#{upcase_encoded.chars.map do |char| - char.unpack('C*').map { |c| '%02x' % c }.join - end.join('|')})/i) { |s| s.upcase } + upcase_encoded_chars = upcase_encoded.chars.map do |char| + SEQUENCE_ENCODING_TABLE[char] + end + component = component.gsub(/%(#{upcase_encoded_chars.join('|')})/, + &:upcase) end return component end class << self - alias_method :encode_component, :encode_component + alias_method :escape_component, :encode_component end ## @@ -442,15 +474,13 @@ def self.unencode(uri, return_type=String, leave_encoded='') "Expected Class (String or Addressable::URI), " + "got #{return_type.inspect}" end - uri = uri.dup - # Seriously, only use UTF-8. I'm really not kidding! - uri.force_encoding("utf-8") - leave_encoded = leave_encoded.dup.force_encoding("utf-8") - result = uri.gsub(/%[0-9a-f]{2}/iu) do |sequence| + + result = uri.gsub(/%[0-9a-f]{2}/i) do |sequence| c = sequence[1..3].to_i(16).chr - c.force_encoding("utf-8") + c.force_encoding(sequence.encoding) leave_encoded.include?(c) ? sequence : c end + result.force_encoding("utf-8") if return_type == String return result @@ -530,13 +560,17 @@ def self.normalize_component(component, character_class= leave_re = if leave_encoded.length > 0 character_class = "#{character_class}%" unless character_class.include?('%') - "|%(?!#{leave_encoded.chars.map do |char| - seq = char.unpack('C*').map { |c| '%02x' % c }.join + "|%(?!#{leave_encoded.chars.flat_map do |char| + seq = SEQUENCE_ENCODING_TABLE[char] [seq.upcase, seq.downcase] - end.flatten.join('|')})" + end.join('|')})" end - character_class = /[^#{character_class}]#{leave_re}/ + character_class = if leave_re + /[^#{character_class}]#{leave_re}/ + else + /[^#{character_class}]/ + end end # We can't perform regexps on invalid UTF sequences, but # here we need to, so switch to ASCII. @@ -860,12 +894,12 @@ def normalized_scheme else Addressable::URI.normalize_component( self.scheme.strip.downcase, - Addressable::URI::CharacterClasses::SCHEME + Addressable::URI::NormalizeCharacterClasses::SCHEME ) end end # All normalized values should be UTF-8 - @normalized_scheme.force_encoding(Encoding::UTF_8) if @normalized_scheme + force_utf8_encoding_if_needed(@normalized_scheme) @normalized_scheme end @@ -880,7 +914,7 @@ def scheme=(new_scheme) new_scheme = new_scheme.to_str end if new_scheme && new_scheme !~ /\A[a-z][a-z0-9\.\+\-]*\z/i - raise InvalidURIError, "Invalid scheme format: #{new_scheme}" + raise InvalidURIError, "Invalid scheme format: '#{new_scheme}'" end @scheme = new_scheme @scheme = nil if @scheme.to_s.strip.empty? @@ -915,12 +949,12 @@ def normalized_user else Addressable::URI.normalize_component( self.user.strip, - Addressable::URI::CharacterClasses::UNRESERVED + Addressable::URI::NormalizeCharacterClasses::UNRESERVED ) end end # All normalized values should be UTF-8 - @normalized_user.force_encoding(Encoding::UTF_8) if @normalized_user + force_utf8_encoding_if_needed(@normalized_user) @normalized_user end @@ -972,14 +1006,12 @@ def normalized_password else Addressable::URI.normalize_component( self.password.strip, - Addressable::URI::CharacterClasses::UNRESERVED + Addressable::URI::NormalizeCharacterClasses::UNRESERVED ) end end # All normalized values should be UTF-8 - if @normalized_password - @normalized_password.force_encoding(Encoding::UTF_8) - end + force_utf8_encoding_if_needed(@normalized_password) @normalized_password end @@ -1047,9 +1079,7 @@ def normalized_userinfo end end # All normalized values should be UTF-8 - if @normalized_userinfo - @normalized_userinfo.force_encoding(Encoding::UTF_8) - end + force_utf8_encoding_if_needed(@normalized_userinfo) @normalized_userinfo end @@ -1096,6 +1126,7 @@ def host # @return [String] The host component, normalized. def normalized_host return nil unless self.host + @normalized_host ||= begin if !self.host.strip.empty? result = ::Addressable::IDNA.to_ascii( @@ -1107,14 +1138,15 @@ def normalized_host end result = Addressable::URI.normalize_component( result, - CharacterClasses::HOST) + NormalizeCharacterClasses::HOST + ) result else EMPTY_STR.dup end end # All normalized values should be UTF-8 - @normalized_host.force_encoding(Encoding::UTF_8) if @normalized_host + force_utf8_encoding_if_needed(@normalized_host) @normalized_host end @@ -1172,7 +1204,7 @@ def hostname=(new_hostname) # Returns the top-level domain for this host. # # @example - # Addressable::URI.parse("www.example.co.uk").tld # => "co.uk" + # Addressable::URI.parse("http://www.example.co.uk").tld # => "co.uk" def tld PublicSuffix.parse(self.host, ignore_private: true).tld end @@ -1182,7 +1214,7 @@ def tld # # @param [String, #to_str] new_tld The new top-level domain. def tld=(new_tld) - replaced_tld = domain.sub(/#{tld}\z/, new_tld) + replaced_tld = host.sub(/#{tld}\z/, new_tld) self.host = PublicSuffix::Domain.new(replaced_tld).to_s end @@ -1190,7 +1222,7 @@ def tld=(new_tld) # Returns the public suffix domain for this host. # # @example - # Addressable::URI.parse("www.example.co.uk").domain # => "example.co.uk" + # Addressable::URI.parse("http://www.example.co.uk").domain # => "example.co.uk" def domain PublicSuffix.domain(self.host, ignore_private: true) end @@ -1232,9 +1264,7 @@ def normalized_authority authority end # All normalized values should be UTF-8 - if @normalized_authority - @normalized_authority.force_encoding(Encoding::UTF_8) - end + force_utf8_encoding_if_needed(@normalized_authority) @normalized_authority end @@ -1468,7 +1498,7 @@ def normalized_site site_string end # All normalized values should be UTF-8 - @normalized_site.force_encoding(Encoding::UTF_8) if @normalized_site + force_utf8_encoding_if_needed(@normalized_site) @normalized_site end @@ -1519,7 +1549,7 @@ def normalized_path result = path.strip.split(SLASH, -1).map do |segment| Addressable::URI.normalize_component( segment, - Addressable::URI::CharacterClasses::PCHAR + Addressable::URI::NormalizeCharacterClasses::PCHAR ) end.join(SLASH) @@ -1531,7 +1561,7 @@ def normalized_path result end # All normalized values should be UTF-8 - @normalized_path.force_encoding(Encoding::UTF_8) if @normalized_path + force_utf8_encoding_if_needed(@normalized_path) @normalized_path end @@ -1594,15 +1624,20 @@ def normalized_query(*flags) modified_query_class = Addressable::URI::CharacterClasses::QUERY.dup # Make sure possible key-value pair delimiters are escaped. modified_query_class.sub!("\\&", "").sub!("\\;", "") - pairs = (self.query || "").split("&", -1) + pairs = (query || "").split("&", -1) + pairs.delete_if(&:empty?).uniq! if flags.include?(:compacted) pairs.sort! if flags.include?(:sorted) component = pairs.map do |pair| - Addressable::URI.normalize_component(pair, modified_query_class, "+") + Addressable::URI.normalize_component( + pair, + Addressable::URI::NormalizeCharacterClasses::QUERY, + "+" + ) end.join("&") component == "" ? nil : component end # All normalized values should be UTF-8 - @normalized_query.force_encoding(Encoding::UTF_8) if @normalized_query + force_utf8_encoding_if_needed(@normalized_query) @normalized_query end @@ -1656,11 +1691,13 @@ def query_values(return_type=Hash) # so it's best to make all changes in-place. pair[0] = URI.unencode_component(pair[0]) if pair[1].respond_to?(:to_str) + value = pair[1].to_str # I loathe the fact that I have to do this. Stupid HTML 4.01. # Treating '+' as a space was just an unbelievably bad idea. # There was nothing wrong with '%20'! # If it ain't broke, don't fix it! - pair[1] = URI.unencode_component(pair[1].to_str.gsub(/\+/, " ")) + value = value.tr("+", " ") if ["http", "https", nil].include?(scheme) + pair[1] = URI.unencode_component(value) end if return_type == Hash accu[pair[0]] = pair[1] @@ -1791,14 +1828,12 @@ def normalized_fragment @normalized_fragment ||= begin component = Addressable::URI.normalize_component( self.fragment, - Addressable::URI::CharacterClasses::FRAGMENT + Addressable::URI::NormalizeCharacterClasses::FRAGMENT ) component == "" ? nil : component end # All normalized values should be UTF-8 - if @normalized_fragment - @normalized_fragment.force_encoding(Encoding::UTF_8) - end + force_utf8_encoding_if_needed(@normalized_fragment) @normalized_fragment end @@ -1917,7 +1952,7 @@ def join(uri) # Section 5.2.3 of RFC 3986 # # Removes the right-most path segment from the base path. - if base_path =~ /\// + if base_path.include?(SLASH) base_path.sub!(/\/[^\/]+$/, SLASH) else base_path = EMPTY_STR @@ -2367,10 +2402,10 @@ def inspect # # @param [Proc] block # A set of operations to perform on a given URI. - def defer_validation(&block) - raise LocalJumpError, "No block given." unless block + def defer_validation + raise LocalJumpError, "No block given." unless block_given? @validation_deferred = true - block.call() + yield @validation_deferred = false validate return nil @@ -2394,30 +2429,35 @@ def defer_validation(&block) def self.normalize_path(path) # Section 5.2.4 of RFC 3986 - return nil if path.nil? + return if path.nil? normalized_path = path.dup - begin - mod = nil + loop do mod ||= normalized_path.gsub!(RULE_2A, SLASH) pair = normalized_path.match(RULE_2B_2C) - parent, current = pair[1], pair[2] if pair + if pair + parent = pair[1] + current = pair[2] + else + parent = nil + current = nil + end + + regexp = "/#{Regexp.escape(parent.to_s)}/\\.\\./|" + regexp += "(/#{Regexp.escape(current.to_s)}/\\.\\.$)" + if pair && ((parent != SELF_REF && parent != PARENT) || (current != SELF_REF && current != PARENT)) - mod ||= normalized_path.gsub!( - Regexp.new( - "/#{Regexp.escape(parent.to_s)}/\\.\\./|" + - "(/#{Regexp.escape(current.to_s)}/\\.\\.$)" - ), SLASH - ) + mod ||= normalized_path.gsub!(Regexp.new(regexp), SLASH) end mod ||= normalized_path.gsub!(RULE_2D, EMPTY_STR) # Non-standard, removes prefixed dotted segments from path. mod ||= normalized_path.gsub!(RULE_PREFIXED_PARENT, SLASH) - end until mod.nil? + break if mod.nil? + end - return normalized_path + normalized_path end ## @@ -2506,5 +2546,15 @@ def remove_composite_values remove_instance_variable(:@uri_string) if defined?(@uri_string) remove_instance_variable(:@hash) if defined?(@hash) end + + ## + # Converts the string to be UTF-8 if it is not already UTF-8 + # + # @api private + def force_utf8_encoding_if_needed(str) + if str && str.encoding != Encoding::UTF_8 + str.force_encoding(Encoding::UTF_8) + end + end end end diff --git a/lib/addressable/version.rb b/lib/addressable/version.rb index 445d2823..d8e1644b 100644 --- a/lib/addressable/version.rb +++ b/lib/addressable/version.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# encoding:utf-8 #-- # Copyright (C) Bob Aman # @@ -23,8 +22,8 @@ module Addressable module VERSION MAJOR = 2 - MINOR = 6 - TINY = 0 + MINOR = 8 + TINY = 1 STRING = [MAJOR, MINOR, TINY].join('.') end diff --git a/spec/addressable/idna_spec.rb b/spec/addressable/idna_spec.rb index 651f75af..b1509d22 100644 --- a/spec/addressable/idna_spec.rb +++ b/spec/addressable/idna_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# coding: utf-8 # Copyright (C) Bob Aman # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -294,7 +293,9 @@ it_should_behave_like "converting from unicode to ASCII" it_should_behave_like "converting from ASCII to unicode" end -rescue LoadError +rescue LoadError => error + raise error if ENV["CI"] && TestHelper.native_supported? + # Cannot test the native implementation without libidn support. warn('Could not load native IDN implementation.') end diff --git a/spec/addressable/net_http_compat_spec.rb b/spec/addressable/net_http_compat_spec.rb index 8663a867..d07a43e5 100644 --- a/spec/addressable/net_http_compat_spec.rb +++ b/spec/addressable/net_http_compat_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# coding: utf-8 # Copyright (C) Bob Aman # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/spec/addressable/rack_mount_compat_spec.rb b/spec/addressable/rack_mount_compat_spec.rb deleted file mode 100644 index 7b02cb76..00000000 --- a/spec/addressable/rack_mount_compat_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -# coding: utf-8 -# Copyright (C) Bob Aman -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -require "spec_helper" - -require "addressable/uri" -require "addressable/template" -require "rack/mount" - -describe Rack::Mount do - let(:app_one) do - proc { |env| [200, {'Content-Type' => 'text/plain'}, 'Route 1'] } - end - let(:app_two) do - proc { |env| [200, {'Content-Type' => 'text/plain'}, 'Route 2'] } - end - let(:app_three) do - proc { |env| [200, {'Content-Type' => 'text/plain'}, 'Route 3'] } - end - let(:routes) do - s = Rack::Mount::RouteSet.new do |set| - set.add_route(app_one, { - :request_method => 'GET', - :path_info => Addressable::Template.new('/one/{id}/') - }, {:id => 'unidentified'}, :one) - set.add_route(app_two, { - :request_method => 'GET', - :path_info => Addressable::Template.new('/two/') - }, {:id => 'unidentified'}, :two) - set.add_route(app_three, { - :request_method => 'GET', - :path_info => Addressable::Template.new('/three/{id}/').to_regexp - }, {:id => 'unidentified'}, :three) - end - s.rehash - s - end - - it "should generate from routes with Addressable::Template" do - path, _ = routes.generate(:path_info, :one, {:id => '123'}) - expect(path).to eq '/one/123/' - end - - it "should generate from routes with Addressable::Template using defaults" do - path, _ = routes.generate(:path_info, :one, {}) - expect(path).to eq '/one/unidentified/' - end - - it "should recognize routes with Addressable::Template" do - request = Rack::Request.new( - 'REQUEST_METHOD' => 'GET', - 'PATH_INFO' => '/one/123/' - ) - route, _, params = routes.recognize(request) - expect(route).not_to be_nil - expect(route.app).to eq app_one - expect(params).to eq({id: '123'}) - end - - it "should generate from routes with Addressable::Template" do - path, _ = routes.generate(:path_info, :two, {:id => '654'}) - expect(path).to eq '/two/' - end - - it "should generate from routes with Addressable::Template using defaults" do - path, _ = routes.generate(:path_info, :two, {}) - expect(path).to eq '/two/' - end - - it "should recognize routes with Addressable::Template" do - request = Rack::Request.new( - 'REQUEST_METHOD' => 'GET', - 'PATH_INFO' => '/two/' - ) - route, _, params = routes.recognize(request) - expect(route).not_to be_nil - expect(route.app).to eq app_two - expect(params).to eq({id: 'unidentified'}) - end - - it "should recognize routes with derived Regexp" do - request = Rack::Request.new( - 'REQUEST_METHOD' => 'GET', - 'PATH_INFO' => '/three/789/' - ) - route, _, params = routes.recognize(request) - expect(route).not_to be_nil - expect(route.app).to eq app_three - expect(params).to eq({id: '789'}) - end -end diff --git a/spec/addressable/security_spec.rb b/spec/addressable/security_spec.rb index 601e8088..3bf90a20 100644 --- a/spec/addressable/security_spec.rb +++ b/spec/addressable/security_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# coding: utf-8 # Copyright (C) Bob Aman # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/spec/addressable/template_spec.rb b/spec/addressable/template_spec.rb index a0191652..f7b0994c 100644 --- a/spec/addressable/template_spec.rb +++ b/spec/addressable/template_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# coding: utf-8 # Copyright (C) Bob Aman # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +18,7 @@ require "spec_helper" require "bigdecimal" +require "timeout" require "addressable/template" shared_examples_for 'expands' do |tests| @@ -77,6 +77,15 @@ end end +describe "#to_regexp" do + it "does not match the first line of multiline strings" do + uri = "https://www.example.com/bar" + template = Addressable::Template.new(uri) + expect(template.match(uri)).not_to be_nil + expect(template.match("#{uri}\ngarbage")).to be_nil + end +end + describe "Type conversion" do subject { { @@ -1340,6 +1349,14 @@ def self.match(name) expect(subject).not_to match("foo_bar*") expect(subject).not_to match("foo_bar:20") end + + it 'should parse in a reasonable time' do + expect do + Timeout.timeout(0.1) do + expect(subject).not_to match("0"*25 + "!") + end + end.not_to raise_error + end end context "VARIABLE_LIST" do subject { Addressable::Template::VARIABLE_LIST } diff --git a/spec/addressable/uri_spec.rb b/spec/addressable/uri_spec.rb index 7cecd0c9..b1f95417 100644 --- a/spec/addressable/uri_spec.rb +++ b/spec/addressable/uri_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# coding: utf-8 # Copyright (C) Bob Aman # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -65,116 +64,116 @@ def to_s describe Addressable::URI, "when created with a non-numeric port number" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:port => "bogus") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with a invalid encoded port number" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:port => "%eb") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with a non-string scheme" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:scheme => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a non-string user" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:user => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a non-string password" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:password => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a non-string userinfo" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:userinfo => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a non-string host" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:host => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a non-string authority" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:authority => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a non-string path" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:path => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a non-string query" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:query => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a non-string fragment" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:fragment => :bogus) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when created with a scheme but no hierarchical " + "segment" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.parse("http:") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "quote handling" do describe 'in host name' do it "should raise an error for single quote" do - expect(lambda do + expect do Addressable::URI.parse("http://local\"host/") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end end describe Addressable::URI, "newline normalization" do it "should not accept newlines in scheme" do - expect(lambda do + expect do Addressable::URI.parse("ht%0atp://localhost/") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should not unescape newline in path" do @@ -199,47 +198,47 @@ def to_s it "should not accept newline in hostname" do uri = Addressable::URI.parse("http://localhost/") - expect(lambda do + expect do uri.host = "local\nhost" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with ambiguous path" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.parse("::http") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with an invalid host" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:host => "") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with a host consisting of " + "sub-delims characters" do it "should not raise an error" do - expect(lambda do + expect do Addressable::URI.new( :host => Addressable::URI::CharacterClasses::SUB_DELIMS.gsub(/\\/, '') ) - end).not_to raise_error + end.not_to raise_error end end describe Addressable::URI, "when created with a host consisting of " + "unreserved characters" do it "should not raise an error" do - expect(lambda do + expect do Addressable::URI.new( :host => Addressable::URI::CharacterClasses::UNRESERVED.gsub(/\\/, '') ) - end).not_to raise_error + end.not_to raise_error end end @@ -269,83 +268,83 @@ def to_s end it "should raise an error if the scheme is set to whitespace" do - expect(lambda do + expect do @uri.scheme = "\t \n" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError, /'\t \n'/) end it "should raise an error if the scheme is set to all digits" do - expect(lambda do + expect do @uri.scheme = "123" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError, /'123'/) end it "should raise an error if the scheme begins with a digit" do - expect(lambda do + expect do @uri.scheme = "1scheme" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError, /'1scheme'/) end it "should raise an error if the scheme begins with a plus" do - expect(lambda do + expect do @uri.scheme = "+scheme" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError, /'\+scheme'/) end it "should raise an error if the scheme begins with a dot" do - expect(lambda do + expect do @uri.scheme = ".scheme" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError, /'\.scheme'/) end it "should raise an error if the scheme begins with a dash" do - expect(lambda do + expect do @uri.scheme = "-scheme" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError, /'-scheme'/) end it "should raise an error if the scheme contains an illegal character" do - expect(lambda do + expect do @uri.scheme = "scheme!" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError, /'scheme!'/) end it "should raise an error if the scheme contains whitespace" do - expect(lambda do + expect do @uri.scheme = "sch eme" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError, /'sch eme'/) end it "should raise an error if the scheme contains a newline" do - expect(lambda do + expect do @uri.scheme = "sch\neme" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should raise an error if set into an invalid state" do - expect(lambda do + expect do @uri.user = "user" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should raise an error if set into an invalid state" do - expect(lambda do + expect do @uri.password = "pass" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should raise an error if set into an invalid state" do - expect(lambda do + expect do @uri.scheme = "http" @uri.fragment = "fragment" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should raise an error if set into an invalid state" do - expect(lambda do + expect do @uri.fragment = "fragment" @uri.scheme = "http" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end @@ -999,6 +998,72 @@ def to_s end end +describe Addressable::URI, "when normalized and then deeply frozen" do + before do + @uri = Addressable::URI.parse( + "http://user:password@example.com:8080/path?query=value#fragment" + ).normalize! + + @uri.instance_variables.each do |var| + @uri.instance_variable_set(var, @uri.instance_variable_get(var).freeze) + end + + @uri.freeze + end + + it "#normalized_scheme should not error" do + expect { @uri.normalized_scheme }.not_to raise_error + end + + it "#normalized_user should not error" do + expect { @uri.normalized_user }.not_to raise_error + end + + it "#normalized_password should not error" do + expect { @uri.normalized_password }.not_to raise_error + end + + it "#normalized_userinfo should not error" do + expect { @uri.normalized_userinfo }.not_to raise_error + end + + it "#normalized_host should not error" do + expect { @uri.normalized_host }.not_to raise_error + end + + it "#normalized_authority should not error" do + expect { @uri.normalized_authority }.not_to raise_error + end + + it "#normalized_port should not error" do + expect { @uri.normalized_port }.not_to raise_error + end + + it "#normalized_site should not error" do + expect { @uri.normalized_site }.not_to raise_error + end + + it "#normalized_path should not error" do + expect { @uri.normalized_path }.not_to raise_error + end + + it "#normalized_query should not error" do + expect { @uri.normalized_query }.not_to raise_error + end + + it "#normalized_fragment should not error" do + expect { @uri.normalized_fragment }.not_to raise_error + end + + it "should be frozen" do + expect(@uri).to be_frozen + end + + it "should not allow destructive operations" do + expect { @uri.normalize! }.to raise_error(RuntimeError) + end +end + describe Addressable::URI, "when created from string components" do before do @uri = Addressable::URI.new( @@ -1015,31 +1080,31 @@ def to_s end it "should raise an error if invalid components omitted" do - expect(lambda do + expect do @uri.omit(:bogus) - end).to raise_error(ArgumentError) - expect(lambda do + end.to raise_error(ArgumentError) + expect do @uri.omit(:scheme, :bogus, :path) - end).to raise_error(ArgumentError) + end.to raise_error(ArgumentError) end end describe Addressable::URI, "when created with a nil host but " + "non-nil authority components" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:user => "user", :password => "pass", :port => 80) - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with both an authority and a user" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new( :user => "user", :authority => "user@example.com:80" ) - end).to raise_error(ArgumentError) + end.to raise_error(ArgumentError) end end @@ -1077,33 +1142,33 @@ def to_s describe Addressable::URI, "when created with a host with a backslash" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:authority => "example\\example") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with a host with a slash" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:authority => "example/example") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with a host with a space" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:authority => "example example") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end describe Addressable::URI, "when created with both a userinfo and a user" do it "should raise an error" do - expect(lambda do + expect do Addressable::URI.new(:user => "user", :userinfo => "user:pass") - end).to raise_error(ArgumentError) + end.to raise_error(ArgumentError) end end @@ -1195,18 +1260,18 @@ def to_s "like a URI object" do it "should parse without error" do uri = Addressable::URI.parse(Fake::URI::HTTP.new("http://example.com/")) - expect(lambda do + expect do Addressable::URI.parse(uri) - end).not_to raise_error + end.not_to raise_error end end describe Addressable::URI, "when parsed from a standard library URI object" do it "should parse without error" do uri = Addressable::URI.parse(URI.parse("http://example.com/")) - expect(lambda do + expect do Addressable::URI.parse(uri) - end).not_to raise_error + end.not_to raise_error end end @@ -1366,9 +1431,9 @@ def to_s end it "should not allow request URI assignment" do - expect(lambda do + expect do @uri.request_uri = "/" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should have a query of 'objectClass?one'" do @@ -1390,9 +1455,9 @@ def to_s end it "should raise an error if omission would create an invalid URI" do - expect(lambda do + expect do @uri.omit(:authority, :path) - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should have an origin of 'ldap://[2001:db8::7]'" do @@ -1778,9 +1843,9 @@ def to_s it "should not be roughly equal to the string " + "'http://example.com:bogus/'" do - expect(lambda do + expect do expect(@uri === "http://example.com:bogus/").to eq(false) - end).not_to raise_error + end.not_to raise_error end it "should result in itself when joined with itself" do @@ -1810,21 +1875,21 @@ def to_s end it "should not allow origin assignment without scheme" do - expect(lambda do + expect do @uri.origin = "example.com" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should not allow origin assignment without host" do - expect(lambda do + expect do @uri.origin = "http://" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should not allow origin assignment with bogus type" do - expect(lambda do + expect do @uri.origin = :bogus - end).to raise_error(TypeError) + end.to raise_error(TypeError) end # Section 6.2.3 of RFC 3986 @@ -1880,9 +1945,9 @@ def to_s end it "when joined with a bogus object a TypeError should be raised" do - expect(lambda do + expect do @uri.join(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should have the correct username after assignment" do @@ -2015,15 +2080,15 @@ def to_s end it "should raise an error if the site value is set to something bogus" do - expect(lambda do + expect do @uri.site = 42 - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise an error if the request URI is set to something bogus" do - expect(lambda do + expect do @uri.request_uri = 42 - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should correctly convert to a hash" do @@ -2072,9 +2137,9 @@ def to_s it "should raise an error for " + "'http://[]/'" do - expect(lambda do + expect do Addressable::URI.parse("http://[]/") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end @@ -2100,9 +2165,9 @@ def to_s it "should not allow to set bare IPv6 address as host" do uri = Addressable::URI.parse("http://[::1]/") skip "not checked" - expect(lambda do + expect do uri.host = '3ffe:1900:4545:3:200:f8ff:fe21:67cf' - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end @@ -2134,9 +2199,9 @@ def to_s it "should raise an error for " + "'http://[v0.]/'" do - expect(lambda do + expect do Addressable::URI.parse("http://[v0.]/") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end end @@ -2480,9 +2545,9 @@ def to_s end it "should not raise an exception when normalized" do - expect(lambda do + expect do @uri.normalize - end).not_to raise_error + end.not_to raise_error end it "should be considered to be in normal form" do @@ -2534,9 +2599,9 @@ def to_s end it "should not raise an exception when normalized" do - expect(lambda do + expect do @uri.normalize - end).not_to raise_error + end.not_to raise_error end it "should be considered to be in normal form" do @@ -2559,9 +2624,9 @@ def to_s end it "should not raise an exception when normalized" do - expect(lambda do + expect do @uri.normalize - end).not_to raise_error + end.not_to raise_error end it "should be considered to be in normal form" do @@ -2597,9 +2662,9 @@ def to_s end it "should raise an error if encoding with an unexpected return type" do - expect(lambda do + expect do Addressable::URI.normalized_encode(@uri, Integer) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "if percent encoded should be 'http://example.com/C%25CC%25A7'" do @@ -2615,9 +2680,9 @@ def to_s end it "should raise an error if encoding with an unexpected return type" do - expect(lambda do + expect do Addressable::URI.encode(@uri, Integer) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should be identical to its duplicate" do @@ -2752,9 +2817,9 @@ def to_s it "should not be roughly equal to the string " + "'http://example.com:bogus/'" do - expect(lambda do + expect do expect(@uri === "http://example.com:bogus/").to eq(false) - end).not_to raise_error + end.not_to raise_error end it "should result in itself when joined with itself" do @@ -3100,9 +3165,9 @@ def to_s end it "should become invalid when normalized" do - expect(lambda do + expect do @uri.normalize - end).to raise_error(Addressable::URI::InvalidURIError, /authority/) + end.to raise_error(Addressable::URI::InvalidURIError, /authority/) end it "should have a path of '/..//example.com'" do @@ -3340,12 +3405,12 @@ def to_s end it "should raise an error if routing is attempted" do - expect(lambda do + expect do @uri.route_to("http://example.com/") - end).to raise_error(ArgumentError, /relative\/path\/to\/resource/) - expect(lambda do + end.to raise_error(ArgumentError, /relative\/path\/to\/resource/) + expect do @uri.route_from("http://example.com/") - end).to raise_error(ArgumentError, /relative\/path\/to\/resource/) + end.to raise_error(ArgumentError, /relative\/path\/to\/resource/) end it "when joined with 'another/relative/path' should be " + @@ -3942,9 +4007,9 @@ def to_s end it "should raise an error if assigning a bogus object to the hostname" do - expect(lambda do + expect do @uri.hostname = Object.new - end).to raise_error + end.to raise_error(TypeError) end it "should have the correct port after assignment" do @@ -4023,9 +4088,9 @@ def to_s end it "should raise an error if query values are set to a bogus type" do - expect(lambda do + expect do @uri.query_values = "bogus" - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should have the correct fragment after assignment" do @@ -4097,39 +4162,39 @@ def to_s end it "should fail to merge with bogus values" do - expect(lambda do + expect do @uri.merge(:port => "bogus") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should fail to merge with bogus values" do - expect(lambda do + expect do @uri.merge(:authority => "bar@baz:bogus") - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should fail to merge with bogus parameters" do - expect(lambda do + expect do @uri.merge(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should fail to merge with bogus parameters" do - expect(lambda do + expect do @uri.merge("http://example.com/") - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should fail to merge with both authority and subcomponents" do - expect(lambda do + expect do @uri.merge(:authority => "foo:bar@baz:42", :port => "42") - end).to raise_error(ArgumentError) + end.to raise_error(ArgumentError) end it "should fail to merge with both userinfo and subcomponents" do - expect(lambda do + expect do @uri.merge(:userinfo => "foo:bar", :user => "foo") - end).to raise_error(ArgumentError) + end.to raise_error(ArgumentError) end it "should be identical to its duplicate" do @@ -4262,6 +4327,36 @@ def to_s end end +describe Addressable::URI, "when parsed from 'https://example.com/?q=a+b'" do + before do + @uri = Addressable::URI.parse("https://example.com/?q=a+b") + end + + it "should have query_values of {'q' => 'a b'}" do + expect(@uri.query_values).to eq("q" => "a b") + end +end + +describe Addressable::URI, "when parsed from 'example.com?q=a+b'" do + before do + @uri = Addressable::URI.parse("example.com?q=a+b") + end + + it "should have query_values of {'q' => 'a b'}" do + expect(@uri.query_values).to eq("q" => "a b") + end +end + +describe Addressable::URI, "when parsed from 'mailto:?q=a+b'" do + before do + @uri = Addressable::URI.parse("mailto:?q=a+b") + end + + it "should have query_values of {'q' => 'a+b'}" do + expect(@uri.query_values).to eq("q" => "a+b") + end +end + describe Addressable::URI, "when parsed from " + "'http://example.com/?q=a%2bb'" do before do @@ -4303,6 +4398,46 @@ def to_s end end +describe Addressable::URI, "when parsed from 'http://example/?b=1&a=2&c=3'" do + before do + @uri = Addressable::URI.parse("http://example/?b=1&a=2&c=3") + end + + it "should have a sorted normalized query of 'a=2&b=1&c=3'" do + expect(@uri.normalized_query(:sorted)).to eq("a=2&b=1&c=3") + end +end + +describe Addressable::URI, "when parsed from 'http://example/?&a&&c&'" do + before do + @uri = Addressable::URI.parse("http://example/?&a&&c&") + end + + it "should have a compacted normalized query of 'a&c'" do + expect(@uri.normalized_query(:compacted)).to eq("a&c") + end +end + +describe Addressable::URI, "when parsed from 'http://example.com/?a=1&a=1'" do + before do + @uri = Addressable::URI.parse("http://example.com/?a=1&a=1") + end + + it "should have a compacted normalized query of 'a=1'" do + expect(@uri.normalized_query(:compacted)).to eq("a=1") + end +end + +describe Addressable::URI, "when parsed from 'http://example.com/?a=1&a=2'" do + before do + @uri = Addressable::URI.parse("http://example.com/?a=1&a=2") + end + + it "should have a compacted normalized query of 'a=1&a=2'" do + expect(@uri.normalized_query(:compacted)).to eq("a=1&a=2") + end +end + describe Addressable::URI, "when parsed from " + "'http://example.com/sound%2bvision'" do before do @@ -4414,10 +4549,10 @@ def to_s end it "should raise an error after nil assignment of authority segment" do - expect(lambda do + expect do # This would create an invalid URI @uri.authority = nil - end).to raise_error + end.to raise_error(Addressable::URI::InvalidURIError) end end @@ -4646,12 +4781,12 @@ def to_s end it "should raise an error if routing is attempted" do - expect(lambda do + expect do @uri.route_to("http://example.com/") - end).to raise_error(ArgumentError, /\/\/example.com\//) - expect(lambda do + end.to raise_error(ArgumentError, /\/\/example.com\//) + expect do @uri.route_from("http://example.com/") - end).to raise_error(ArgumentError, /\/\/example.com\//) + end.to raise_error(ArgumentError, /\/\/example.com\//) end it "should have a 'null' origin" do @@ -4745,9 +4880,9 @@ def to_s describe Addressable::URI, "when parsed from " + "'http://under_score.example.com/'" do it "should not cause an error" do - expect(lambda do + expect do Addressable::URI.parse("http://under_score.example.com/") - end).not_to raise_error + end.not_to raise_error end end @@ -4819,9 +4954,9 @@ def to_s end it "should raise an error for invalid return type values" do - expect(lambda do - @uri.query_values(Fixnum) - end).to raise_error(ArgumentError) + expect do + @uri.query_values(Integer) + end.to raise_error(ArgumentError) end it "should have the correct array query values" do @@ -5422,9 +5557,9 @@ def to_s end it "when joined with a bogus object a TypeError should be raised" do - expect(lambda do + expect do Addressable::URI.join(@uri, 42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end @@ -5451,9 +5586,9 @@ def to_s describe Addressable::URI, "when converting a bogus path" do it "should raise a TypeError" do - expect(lambda do + expect do Addressable::URI.convert_path(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end @@ -5515,18 +5650,18 @@ def to_s end context "which " do - let (:uri) { Addressable::URI.parse("http://comrade.net/path/to/source/") } + let (:uri) { Addressable::URI.parse("http://www.comrade.net/path/to/source/") } it "contains a subdomain" do uri.tld = "co.uk" - expect(uri.to_s).to eq("http://comrade.co.uk/path/to/source/") + expect(uri.to_s).to eq("http://www.comrade.co.uk/path/to/source/") end it "is part of the domain" do uri.tld = "com" - expect(uri.to_s).to eq("http://comrade.com/path/to/source/") + expect(uri.to_s).to eq("http://www.comrade.com/path/to/source/") end end end @@ -5648,9 +5783,9 @@ def to_str end it "should raise a TypeError for objects than cannot be converted" do - expect(lambda do + expect do Addressable::URI.parse(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should correctly parse heuristically anything with a 'to_str' method" do @@ -5658,9 +5793,9 @@ def to_str end it "should raise a TypeError for objects than cannot be converted" do - expect(lambda do + expect do Addressable::URI.heuristic_parse(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end @@ -5704,9 +5839,9 @@ def to_str describe Addressable::URI, "when form encoding a non-Array object" do it "should raise a TypeError for objects than cannot be converted" do - expect(lambda do + expect do Addressable::URI.form_encode(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end @@ -5772,9 +5907,9 @@ def to_str end it "should raise a TypeError for objects than cannot be converted" do - expect(lambda do + expect do Addressable::URI.form_unencode(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end @@ -5784,15 +5919,15 @@ def to_str end it "should raise a TypeError for objects than cannot be converted" do - expect(lambda do + expect do Addressable::URI.normalize_component(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise a TypeError for objects than cannot be converted" do - expect(lambda do + expect do Addressable::URI.normalize_component("component", 42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end @@ -5864,6 +5999,18 @@ def to_str end end +describe Addressable::URI, "when encoding IP literals" do + it "should work for IPv4" do + input = "http://127.0.0.1/" + expect(Addressable::URI.encode(input)).to eq(input) + end + + it "should work for IPv6" do + input = "http://[fe80::200:f8ff:fe21:67cf]/" + expect(Addressable::URI.encode(input)).to eq(input) + end +end + describe Addressable::URI, "when encoding a string with existing encodings to upcase" do it "should result in correct percent encoded sequence" do expect(Addressable::URI.encode_component("JK%4c", "0-9A-IKM-Za-z%", "L")).to eq("%4AK%4C") @@ -5911,6 +6058,11 @@ def to_str expect(Addressable::URI.unencode_component("ski=%BA%DAɫ")).to eq("ski=\xBA\xDAɫ") end + it "should not fail with UTF-8 incompatible string" do + url = "/M%E9/\xE9?p=\xFC".b + expect(Addressable::URI.unencode_component(url)).to eq("/M\xE9/\xE9?p=\xFC") + end + it "should result in correct percent encoded sequence as a URI" do expect(Addressable::URI.unencode( "/path?g%C3%BCnther", ::Addressable::URI @@ -5936,41 +6088,41 @@ def to_str describe Addressable::URI, "when unencoding a bogus object" do it "should raise a TypeError" do - expect(lambda do + expect do Addressable::URI.unencode_component(42) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise a TypeError" do - expect(lambda do + expect do Addressable::URI.unencode("/path?g%C3%BCnther", Integer) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end describe Addressable::URI, "when encoding a bogus object" do it "should raise a TypeError" do - expect(lambda do + expect do Addressable::URI.encode(Object.new) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise a TypeError" do - expect(lambda do + expect do Addressable::URI.normalized_encode(Object.new) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise a TypeError" do - expect(lambda do + expect do Addressable::URI.encode_component("günther", Object.new) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise a TypeError" do - expect(lambda do + expect do Addressable::URI.encode_component(Object.new) - end).to raise_error(TypeError) + end.to raise_error(TypeError) end end @@ -5986,9 +6138,9 @@ def to_str end it "should not raise error when frozen" do - expect(lambda do + expect do Addressable::URI.heuristic_parse(@input).freeze.to_s - end).not_to raise_error + end.not_to raise_error end end @@ -6352,6 +6504,44 @@ def to_str end end +describe Addressable::URI, "when given the input: 'user@domain.com'" do + before do + @input = "user@domain.com" + end + + context "for heuristic parse" do + it "should remain 'mailto:user@domain.com'" do + uri = Addressable::URI.heuristic_parse("mailto:#{@input}") + expect(uri.to_s).to eq("mailto:user@domain.com") + end + + it "should have a scheme of 'mailto'" do + uri = Addressable::URI.heuristic_parse(@input) + expect(uri.to_s).to eq("mailto:user@domain.com") + expect(uri.scheme).to eq("mailto") + end + + it "should remain 'acct:user@domain.com'" do + uri = Addressable::URI.heuristic_parse("acct:#{@input}") + expect(uri.to_s).to eq("acct:user@domain.com") + end + + context "HTTP" do + before do + @uri = Addressable::URI.heuristic_parse("http://#{@input}/") + end + + it "should remain 'http://user@domain.com/'" do + expect(@uri.to_s).to eq("http://user@domain.com/") + end + + it "should have the username 'user' for HTTP basic authentication" do + expect(@uri.user).to eq("user") + end + end + end +end + describe Addressable::URI, "when assigning query values" do before do @uri = Addressable::URI.new @@ -6363,54 +6553,54 @@ def to_str end it "should raise an error attempting to assign {'a' => {'b' => ['c']}}" do - expect(lambda do + expect do @uri.query_values = { 'a' => {'b' => ['c'] } } - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise an error attempting to assign " + "{:b => '2', :a => {:c => '1'}}" do - expect(lambda do + expect do @uri.query_values = {:b => '2', :a => {:c => '1'}} - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise an error attempting to assign " + "{:a => 'a', :b => [{:c => 'c', :d => 'd'}, " + "{:e => 'e', :f => 'f'}]}" do - expect(lambda do + expect do @uri.query_values = { :a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}] } - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise an error attempting to assign " + "{:a => 'a', :b => [{:c => true, :d => 'd'}, " + "{:e => 'e', :f => 'f'}]}" do - expect(lambda do + expect do @uri.query_values = { :a => 'a', :b => [{:c => true, :d => 'd'}, {:e => 'e', :f => 'f'}] } - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise an error attempting to assign " + "{:a => 'a', :b => {:c => true, :d => 'd'}}" do - expect(lambda do + expect do @uri.query_values = { :a => 'a', :b => {:c => true, :d => 'd'} } - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should raise an error attempting to assign " + "{:a => 'a', :b => {:c => true, :d => 'd'}}" do - expect(lambda do + expect do @uri.query_values = { :a => 'a', :b => {:c => true, :d => 'd'} } - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should correctly assign {:a => 1, :b => 1.5}" do @@ -6421,13 +6611,13 @@ def to_str it "should raise an error attempting to assign " + "{:z => 1, :f => [2, {999.1 => [3,'4']}, ['h', 'i']], " + ":a => {:b => ['c', 'd'], :e => true, :y => 0.5}}" do - expect(lambda do + expect do @uri.query_values = { :z => 1, :f => [ 2, {999.1 => [3,'4']}, ['h', 'i'] ], :a => { :b => ['c', 'd'], :e => true, :y => 0.5 } } - end).to raise_error(TypeError) + end.to raise_error(TypeError) end it "should correctly assign {}" do @@ -6477,7 +6667,7 @@ def to_str @uri.path = "acct:bob@sporkmonger.com" expect(@uri.path).to eq("acct:bob@sporkmonger.com") expect(@uri.normalize.to_str).to eq("acct%2Fbob@sporkmonger.com") - expect(lambda { @uri.to_s }).to raise_error( + expect { @uri.to_s }.to raise_error( Addressable::URI::InvalidURIError ) end @@ -6495,26 +6685,26 @@ def to_str end it "should not allow relative paths to be assigned on absolute URIs" do - expect(lambda do + expect do @uri.scheme = "http" @uri.host = "example.com" @uri.path = "acct:bob@sporkmonger.com" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should not allow relative paths to be assigned on absolute URIs" do - expect(lambda do + expect do @uri.path = "acct:bob@sporkmonger.com" @uri.scheme = "http" @uri.host = "example.com" - end).to raise_error(Addressable::URI::InvalidURIError) + end.to raise_error(Addressable::URI::InvalidURIError) end it "should not allow relative paths to be assigned on absolute URIs" do - expect(lambda do + expect do @uri.path = "uuid:0b3ecf60-3f93-11df-a9c3-001f5bfffe12" @uri.scheme = "urn" - end).not_to raise_error + end.not_to raise_error end end @@ -6543,3 +6733,13 @@ def to_str expect(@uri.class).to eq(@uri.join('path').class) end end + +describe Addressable::URI, "when initialized in a non-main `Ractor`" do + it "should have the same value as if used in the main `Ractor`" do + pending("Ruby 3.0+ for `Ractor` support") unless defined?(Ractor) + main = Addressable::URI.parse("http://example.com") + expect( + Ractor.new { Addressable::URI.parse("http://example.com") }.take + ).to eq(main) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4427f214..bd8e3958 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,6 +16,15 @@ add_filter "spec/" add_filter "vendor/" end +end if Gem.loaded_specs.key?("simplecov") + +class TestHelper + def self.native_supported? + mri = RUBY_ENGINE == "ruby" + windows = RUBY_PLATFORM.include?("mingw") + + mri && !windows + end end RSpec.configure do |config| diff --git a/tasks/gem.rake b/tasks/gem.rake index 6ca09eb3..24d9714b 100644 --- a/tasks/gem.rake +++ b/tasks/gem.rake @@ -11,7 +11,6 @@ namespace :gem do s.files = PKG_FILES.to_a - s.has_rdoc = true s.extra_rdoc_files = %w( README.md ) s.rdoc_options.concat ["--main", "README.md"] @@ -20,10 +19,10 @@ namespace :gem do exit(1) end - s.required_ruby_version = '>= 2.0' + s.required_ruby_version = ">= 2.2" - s.add_runtime_dependency 'public_suffix', '>= 2.0.2', '< 4.0' - s.add_development_dependency 'bundler', '>= 1.0', '< 3.0' + s.add_runtime_dependency "public_suffix", ">= 2.0.2", "< 6.0" + s.add_development_dependency "bundler", ">= 1.0", "< 3.0" s.require_path = "lib" @@ -31,6 +30,9 @@ namespace :gem do s.email = "bob@sporkmonger.com" s.homepage = "https://github.com/sporkmonger/addressable" s.license = "Apache-2.0" + s.metadata = { + "changelog_uri" => "https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md" + } end Gem::PackageTask.new(GEM_SPEC) do |p| @@ -42,7 +44,7 @@ namespace :gem do desc "Generates .gemspec file" task :gemspec do spec_string = GEM_SPEC.to_ruby - File.open("#{GEM_SPEC.name}.gemspec", 'w') do |file| + File.open("#{GEM_SPEC.name}.gemspec", "w") do |file| file.write spec_string end end @@ -72,9 +74,9 @@ namespace :gem do desc "Reinstall the gem" task :reinstall => [:uninstall, :install] - desc 'Package for release' + desc "Package for release" task :release => ["gem:package", "gem:gemspec"] do |t| - v = ENV['VERSION'] or abort 'Must supply VERSION=x.y.z' + v = ENV["VERSION"] or abort "Must supply VERSION=x.y.z" abort "Versions don't match #{v} vs #{PROJ.version}" if v != PKG_VERSION pkg = "pkg/#{GEM_SPEC.full_name}" diff --git a/tasks/profile.rake b/tasks/profile.rake new file mode 100644 index 00000000..b697d489 --- /dev/null +++ b/tasks/profile.rake @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +namespace :profile do + desc "Profile Template match memory allocations" + task :template_match_memory do + require "memory_profiler" + require "addressable/template" + + start_at = Time.now.to_f + template = Addressable::Template.new("http://example.com/{?one,two,three}") + report = MemoryProfiler.report do + 30_000.times do + template.match( + "http://example.com/?one=one&two=floo&three=me" + ) + end + end + end_at = Time.now.to_f + print_options = { scale_bytes: true, normalize_paths: true } + puts "\n\n" + + if ENV["CI"] + report.pretty_print(print_options) + else + t_allocated = report.scale_bytes(report.total_allocated_memsize) + t_retained = report.scale_bytes(report.total_retained_memsize) + + puts "Total allocated: #{t_allocated} (#{report.total_allocated} objects)" + puts "Total retained: #{t_retained} (#{report.total_retained} objects)" + puts "Took #{end_at - start_at} seconds" + + FileUtils.mkdir_p("tmp") + report.pretty_print(to_file: "tmp/memprof.txt", **print_options) + end + end + + desc "Profile URI parse memory allocations" + task :memory do + require "memory_profiler" + require "addressable/uri" + if ENV["IDNA_MODE"] == "pure" + Addressable.send(:remove_const, :IDNA) + load "addressable/idna/pure.rb" + end + + start_at = Time.now.to_f + report = MemoryProfiler.report do + 30_000.times do + Addressable::URI.parse( + "http://google.com/stuff/../?with_lots=of¶ms=asdff#!stuff" + ).normalize + end + end + end_at = Time.now.to_f + print_options = { scale_bytes: true, normalize_paths: true } + puts "\n\n" + + if ENV["CI"] + report.pretty_print(**print_options) + else + t_allocated = report.scale_bytes(report.total_allocated_memsize) + t_retained = report.scale_bytes(report.total_retained_memsize) + + puts "Total allocated: #{t_allocated} (#{report.total_allocated} objects)" + puts "Total retained: #{t_retained} (#{report.total_retained} objects)" + puts "Took #{end_at - start_at} seconds" + + FileUtils.mkdir_p("tmp") + report.pretty_print(to_file: "tmp/memprof.txt", **print_options) + end + end +end diff --git a/tasks/rspec.rake b/tasks/rspec.rake index 85288438..e3d9f014 100644 --- a/tasks/rspec.rake +++ b/tasks/rspec.rake @@ -5,7 +5,7 @@ require "rspec/core/rake_task" namespace :spec do RSpec::Core::RakeTask.new(:simplecov) do |t| t.pattern = FileList['spec/**/*_spec.rb'] - t.rspec_opts = ['--color', '--format', 'documentation'] + t.rspec_opts = %w[--color --format documentation] unless ENV["CI"] end namespace :simplecov do