├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .simplecov ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── Rakefile ├── addressable.gemspec ├── benchmark ├── simple.rb ├── time.gif └── unicode_normalize.rb ├── data └── unicode.data ├── gemfiles ├── public_suffix_2.rb ├── public_suffix_3.rb └── public_suffix_4.rb ├── lib ├── addressable.rb └── addressable │ ├── idna.rb │ ├── idna │ ├── native.rb │ └── pure.rb │ ├── template.rb │ ├── uri.rb │ └── version.rb ├── spec ├── addressable │ ├── idna_spec.rb │ ├── net_http_compat_spec.rb │ ├── security_spec.rb │ ├── template_spec.rb │ └── uri_spec.rb └── spec_helper.rb └── tasks ├── clobber.rake ├── gem.rake ├── git.rake ├── metrics.rake ├── profile.rake ├── rspec.rake └── yard.rake /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "main" ] 9 | schedule: 10 | - cron: '41 19 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'ruby' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 37 | # If this step fails, then you should remove it and run the build manually (see below) 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@v3 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v3 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - addressable-* 7 | 8 | jobs: 9 | release: 10 | if: github.repository == 'sporkmonger/addressable' 11 | runs-on: ubuntu-22.04 12 | permissions: 13 | id-token: write # for trusted publishing 14 | steps: 15 | # tags are named addressable-VERSION, e.g. addressable-2.8.6 16 | - name: Set VERSION from git tag 17 | run: echo "VERSION=$(echo ${{ github.ref }} | cut -d - -f 2)" >> "$GITHUB_ENV" 18 | 19 | - name: Install libidn 20 | run: sudo apt-get install libidn11-dev 21 | 22 | - uses: actions/checkout@v4 23 | 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | bundler-cache: true 27 | ruby-version: ruby 28 | 29 | - uses: rubygems/configure-rubygems-credentials@v1.0.0 30 | 31 | # ensure gem can be built and installed 32 | - run: bundle exec rake gem:install 33 | 34 | # push gem to rubygems.org 35 | - run: gem push --verbose pkg/addressable-${VERSION}.gem 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | env: 6 | BUNDLE_JOBS: 4 7 | 8 | # jobs defined in the order we want them listed in the Actions UI 9 | jobs: 10 | profile: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby: [2.7] 16 | idna_mode: [native, pure] 17 | os: [ubuntu-22.04] 18 | env: 19 | IDNA_MODE: ${{ matrix.idna_mode }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Install libidn 24 | run: sudo apt-get install libidn11-dev 25 | 26 | - name: Setup ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | bundler-cache: false 30 | ruby-version: ${{ matrix.ruby }} 31 | 32 | - name: Install gems 33 | run: bundle install 34 | 35 | - name: >- 36 | Profile Memory Allocation with ${{ matrix.idna_mode }} IDNA during Addressable::URI#parse 37 | run: bundle exec rake profile:memory 38 | 39 | - name: >- 40 | Profile Memory Allocation with ${{ matrix.idna_mode }} IDNA during Addressable::Template#match 41 | run: bundle exec rake profile:template_match_memory 42 | 43 | coverage: 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | ruby: [2.7] 49 | os: [ubuntu-22.04] 50 | env: 51 | BUNDLE_WITHOUT: development 52 | COVERALLS_SERVICE_NAME: github 53 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | COVERALLS_DEBUG: true 55 | CI_BUILD_NUMBER: ${{ github.run_id }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | 59 | - name: Install libidn 60 | run: sudo apt-get install libidn11-dev 61 | 62 | - name: Setup ruby 63 | uses: ruby/setup-ruby@v1 64 | with: 65 | bundler-cache: false 66 | ruby-version: ${{ matrix.ruby }} 67 | 68 | - name: Install gems 69 | run: bundle install 70 | 71 | - name: Run specs and report coverage 72 | run: bundle exec rake 73 | 74 | test: 75 | runs-on: ${{ matrix.os }} 76 | strategy: 77 | fail-fast: false 78 | matrix: 79 | # the job name is composed by these attributes 80 | ruby: 81 | - 2.3 82 | - 2.4 83 | - 2.5 84 | - 2.6 85 | - 2.7 86 | # quotes because of YAML gotcha: https://github.com/actions/runner/issues/849 87 | - '3.0' 88 | - 3.1 89 | - 3.2 90 | - 3.3 91 | - jruby-9.1 92 | - jruby-9.2 93 | - jruby-9.3 94 | - jruby-9.4 95 | - truffleruby-22.2 96 | - truffleruby-22.3 97 | os: 98 | - ubuntu-22.04 99 | gemfile: 100 | - Gemfile 101 | include: 102 | - { os: ubuntu-22.04, ruby: 2.7, gemfile: gemfiles/public_suffix_2.rb } 103 | - { os: ubuntu-22.04, ruby: 2.7, gemfile: gemfiles/public_suffix_3.rb } 104 | - { os: ubuntu-22.04, ruby: 2.7, gemfile: gemfiles/public_suffix_4.rb } 105 | # Ubuntu 106 | - { os: ubuntu-22.04, ruby: 3.1 } 107 | # macOS 108 | - { os: macos-13, ruby: 3.1 } 109 | # Windows 110 | - { os: windows-2019, ruby: 3.1 } 111 | - { os: windows-2022, ruby: 3.1 } 112 | - { os: windows-2022, ruby: jruby-9.3 } 113 | # allowed to fail 114 | - { os: ubuntu-22.04, ruby: head, gemfile: Gemfile, allow-failure: true } 115 | - { os: ubuntu-22.04, ruby: jruby-head, gemfile: Gemfile, allow-failure: true } 116 | - { os: ubuntu-22.04, ruby: truffleruby-head, gemfile: Gemfile, allow-failure: true } 117 | env: 118 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 119 | BUNDLE_WITHOUT: development:coverage 120 | # Workaround for Windows JRuby JDK issue 121 | # https://github.com/ruby/setup-ruby/issues/339 122 | # https://github.com/jruby/jruby/issues/7182#issuecomment-1112953015 123 | JAVA_OPTS: -Djdk.io.File.enableADS=true 124 | steps: 125 | - uses: actions/checkout@v4 126 | 127 | - name: Install libidn (Ubuntu) 128 | if: startsWith(matrix.os, 'ubuntu') 129 | run: sudo apt-get install libidn11-dev 130 | 131 | - name: Install libidn (macOS) 132 | if: startsWith(matrix.os, 'macos') 133 | run: brew install libidn 134 | 135 | - name: Setup ruby 136 | continue-on-error: ${{ matrix.allow-failure || false }} 137 | id: setupruby 138 | uses: ruby/setup-ruby@v1 139 | with: 140 | bundler-cache: false 141 | ruby-version: ${{ matrix.ruby }} 142 | 143 | - name: Install gems 144 | continue-on-error: ${{ matrix.allow-failure || false }} 145 | id: bundle 146 | run: bundle install 147 | 148 | - name: Run specs 149 | continue-on-error: ${{ matrix.allow-failure || false }} 150 | id: specs 151 | run: bundle exec rake spec 152 | 153 | # because continue-on-error marks the steps as pass if they fail 154 | - name: >- 155 | Setup ruby outcome: ${{ steps.setupruby.outcome }} 156 | run: echo NOOP 157 | - name: >- 158 | Install gems outcome: ${{ steps.bundle.outcome }} 159 | run: echo NOOP 160 | - name: >- 161 | Run specs outcome: ${{ steps.specs.outcome }} 162 | run: echo NOOP 163 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .yardoc 3 | .bundle 4 | Gemfile.lock 5 | gemfiles/*.lock 6 | coverage 7 | doc 8 | heckling 9 | pkg 10 | specdoc 11 | tmp/ 12 | vendor/ 13 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | 4 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 5 | SimpleCov.start do 6 | add_filter 'lib/addressable/idna' 7 | add_filter 'lib/addressable/idna.rb' 8 | end 9 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | -o doc/ - CHANGELOG LICENSE 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Addressable 2.8.7 2 | - Allow `public_suffix` 6 ([#535]) 3 | 4 | [#535]: https://github.com/sporkmonger/addressable/pull/535 5 | 6 | # Addressable 2.8.6 7 | - Memoize regexps for common character classes ([#524]) 8 | 9 | [#524]: https://github.com/sporkmonger/addressable/pull/524 10 | 11 | # Addressable 2.8.5 12 | - Fix thread safety issue with encoding tables ([#515]) 13 | - Define URI::NONE as a module to avoid serialization issues ([#509]) 14 | - Fix YAML serialization ([#508]) 15 | 16 | [#508]: https://github.com/sporkmonger/addressable/pull/508 17 | [#509]: https://github.com/sporkmonger/addressable/pull/509 18 | [#515]: https://github.com/sporkmonger/addressable/pull/515 19 | 20 | # Addressable 2.8.4 21 | - Restore `Addressable::IDNA.unicode_normalize_kc` as a deprecated method ([#504]) 22 | 23 | [#504]: https://github.com/sporkmonger/addressable/pull/504 24 | 25 | # Addressable 2.8.3 26 | - Fix template expand level 2 hash support for non-string objects ([#499], [#498]) 27 | 28 | [#499]: https://github.com/sporkmonger/addressable/pull/499 29 | [#498]: https://github.com/sporkmonger/addressable/pull/498 30 | 31 | # Addressable 2.8.2 32 | - Improve cache hits and JIT friendliness ([#486](https://github.com/sporkmonger/addressable/pull/486)) 33 | - Improve code style and test coverage ([#482](https://github.com/sporkmonger/addressable/pull/482)) 34 | - Ensure reset of deferred validation ([#481](https://github.com/sporkmonger/addressable/pull/481)) 35 | - Resolve normalization differences between `IDNA::Native` and `IDNA::Pure` ([#408](https://github.com/sporkmonger/addressable/issues/408), [#492]) 36 | - Remove redundant colon in `Addressable::URI::CharacterClasses::AUTHORITY` regex ([#438](https://github.com/sporkmonger/addressable/pull/438)) (accidentally reverted by [#449] merge but [added back](https://github.com/sporkmonger/addressable/pull/492#discussion_r1105125280) in [#492]) 37 | 38 | [#492]: https://github.com/sporkmonger/addressable/pull/492 39 | 40 | # Addressable 2.8.1 41 | - refactor `Addressable::URI.normalize_path` to address linter offenses ([#430](https://github.com/sporkmonger/addressable/pull/430)) 42 | - update gemspec to reflect supported Ruby versions ([#466], [#464], [#463]) 43 | - compatibility w/ public_suffix 5.x ([#466], [#465], [#460]) 44 | - fixes "invalid byte sequence in UTF-8" exception when unencoding URLs containing non UTF-8 characters ([#459](https://github.com/sporkmonger/addressable/pull/459)) 45 | - `Ractor` compatibility ([#449]) 46 | - use the whole string instead of a single line for template match ([#431](https://github.com/sporkmonger/addressable/pull/431)) 47 | - force UTF-8 encoding only if needed ([#341](https://github.com/sporkmonger/addressable/pull/341)) 48 | 49 | [#449]: https://github.com/sporkmonger/addressable/pull/449 50 | [#460]: https://github.com/sporkmonger/addressable/pull/460 51 | [#463]: https://github.com/sporkmonger/addressable/pull/463 52 | [#464]: https://github.com/sporkmonger/addressable/pull/464 53 | [#465]: https://github.com/sporkmonger/addressable/pull/465 54 | [#466]: https://github.com/sporkmonger/addressable/pull/466 55 | 56 | # Addressable 2.8.0 57 | - fixes ReDoS vulnerability in Addressable::Template#match 58 | - no longer replaces `+` with spaces in queries for non-http(s) schemes 59 | - fixed encoding ipv6 literals 60 | - the `:compacted` flag for `normalized_query` now dedupes parameters 61 | - fix broken `escape_component` alias 62 | - dropping support for Ruby 2.0 and 2.1 63 | - adding Ruby 3.0 compatibility for development tasks 64 | - drop support for `rack-mount` and remove Addressable::Template#generate 65 | - performance improvements 66 | - switch CI/CD to GitHub Actions 67 | 68 | # Addressable 2.7.0 69 | - added `:compacted` flag to `normalized_query` 70 | - `heuristic_parse` handles `mailto:` more intuitively 71 | - dropped explicit support for JRuby 9.0.5.0 72 | - compatibility w/ public_suffix 4.x 73 | - performance improvements 74 | 75 | # Addressable 2.6.0 76 | - added `tld=` method to allow assignment to the public suffix 77 | - most `heuristic_parse` patterns are now case-insensitive 78 | - `heuristic_parse` handles more `file://` URI variations 79 | - fixes bug in `heuristic_parse` when uri starts with digit 80 | - fixes bug in `request_uri=` with query strings 81 | - fixes template issues with `nil` and `?` operator 82 | - `frozen_string_literal` pragmas added 83 | - minor performance improvements in regexps 84 | - fixes to eliminate warnings 85 | 86 | # Addressable 2.5.2 87 | - better support for frozen string literals 88 | - fixed bug w/ uppercase characters in scheme 89 | - IDNA errors w/ emoji URLs 90 | - compatibility w/ public_suffix 3.x 91 | 92 | # Addressable 2.5.1 93 | - allow unicode normalization to be disabled for URI Template expansion 94 | - removed duplicate test 95 | 96 | # Addressable 2.5.0 97 | - dropping support for Ruby 1.9 98 | - adding support for Ruby 2.4 preview 99 | - add support for public suffixes and tld; first runtime dependency 100 | - hostname escaping should match RFC; underscores in hostnames no longer escaped 101 | - paths beginning with // and missing an authority are now considered invalid 102 | - validation now also takes place after setting a path 103 | - handle backslashes in authority more like a browser for `heuristic_parse` 104 | - unescaped backslashes in host now raise an `InvalidURIError` 105 | - `merge!`, `join!`, `omit!` and `normalize!` don't disable deferred validation 106 | - `heuristic_parse` now trims whitespace before parsing 107 | - host parts longer than 63 bytes will be ignored and not passed to libidn 108 | - normalized values always encoded as UTF-8 109 | 110 | # Addressable 2.4.0 111 | - support for 1.8.x dropped 112 | - double quotes in a host now raises an error 113 | - newlines in host will no longer get unescaped during normalization 114 | - stricter handling of bogus scheme values 115 | - stricter handling of encoded port values 116 | - calling `require 'addressable'` will now load both the URI and Template files 117 | - assigning to the `hostname` component with an `IPAddr` object is now supported 118 | - assigning to the `origin` component is now supported 119 | - fixed minor bug where an exception would be thrown for a missing ACE suffix 120 | - better partial expansion of URI templates 121 | 122 | # Addressable 2.3.8 123 | - fix warnings 124 | - update dependency gems 125 | - support for 1.8.x officially deprecated 126 | 127 | # Addressable 2.3.7 128 | - fix scenario in which invalid URIs don't get an exception until inspected 129 | - handle hostnames with two adjacent periods correctly 130 | - upgrade of RSpec 131 | 132 | # Addressable 2.3.6 133 | - normalization drops empty query string 134 | - better handling in template extract for missing values 135 | - template modifier for `'?'` now treated as optional 136 | - fixed issue where character class parameters were modified 137 | - templates can now be tested for equality 138 | - added `:sorted` option to normalization of query strings 139 | - fixed issue with normalization of hosts given in `'example.com.'` form 140 | 141 | # Addressable 2.3.5 142 | - added Addressable::URI#empty? method 143 | - Addressable::URI#hostname methods now strip square brackets from IPv6 hosts 144 | - compatibility with Net::HTTP in Ruby 2.0.0 145 | - Addressable::URI#route_from should always give relative URIs 146 | 147 | # Addressable 2.3.4 148 | - fixed issue with encoding altering its inputs 149 | - query string normalization now leaves ';' characters alone 150 | - FakeFS is detected before attempting to load unicode tables 151 | - additional testing to ensure frozen objects don't cause problems 152 | 153 | # Addressable 2.3.3 154 | - fixed issue with converting common primitives during template expansion 155 | - fixed port encoding issue 156 | - removed a few warnings 157 | - normalize should now ignore %2B in query strings 158 | - the IDNA logic should now be handled by libidn in Ruby 1.9 159 | - no template match should now result in nil instead of an empty MatchData 160 | - added license information to gemspec 161 | 162 | # Addressable 2.3.2 163 | - added Addressable::URI#default_port method 164 | - fixed issue with Marshalling Unicode data on Windows 165 | - improved heuristic parsing to better handle IPv4 addresses 166 | 167 | # Addressable 2.3.1 168 | - fixed missing unicode data file 169 | 170 | # Addressable 2.3.0 171 | - updated Addressable::Template to use RFC 6570, level 4 172 | - fixed compatibility problems with some versions of Ruby 173 | - moved unicode tables into a data file for performance reasons 174 | - removing support for multiple query value notations 175 | 176 | # Addressable 2.2.8 177 | - fixed issues with dot segment removal code 178 | - form encoding can now handle multiple values per key 179 | - updated development environment 180 | 181 | # Addressable 2.2.7 182 | - fixed issues related to Addressable::URI#query_values= 183 | - the Addressable::URI.parse method is now polymorphic 184 | 185 | # Addressable 2.2.6 186 | - changed the way ambiguous paths are handled 187 | - fixed bug with frozen URIs 188 | - https supported in heuristic parsing 189 | 190 | # Addressable 2.2.5 191 | - 'parsing' a pre-parsed URI object is now a dup operation 192 | - introduced conditional support for libidn 193 | - fixed normalization issue on ampersands in query strings 194 | - added additional tests around handling of query strings 195 | 196 | # Addressable 2.2.4 197 | - added origin support from draft-ietf-websec-origin-00 198 | - resolved issue with attempting to navigate below root 199 | - fixed bug with string splitting in query strings 200 | 201 | # Addressable 2.2.3 202 | - added :flat_array notation for query strings 203 | 204 | # Addressable 2.2.2 205 | - fixed issue with percent escaping of '+' character in query strings 206 | 207 | # Addressable 2.2.1 208 | - added support for application/x-www-form-urlencoded. 209 | 210 | # Addressable 2.2.0 211 | - added site methods 212 | - improved documentation 213 | 214 | # Addressable 2.1.2 215 | - added HTTP request URI methods 216 | - better handling of Windows file paths 217 | - validation_deferred boolean replaced with defer_validation block 218 | - normalization of percent-encoded paths should now be correct 219 | - fixed issue with constructing URIs with relative paths 220 | - fixed warnings 221 | 222 | # Addressable 2.1.1 223 | - more type checking changes 224 | - fixed issue with unicode normalization 225 | - added method to find template defaults 226 | - symbolic keys are now allowed in template mappings 227 | - numeric values and symbolic values are now allowed in template mappings 228 | 229 | # Addressable 2.1.0 230 | - refactored URI template support out into its own class 231 | - removed extract method due to being useless and unreliable 232 | - removed Addressable::URI.expand_template 233 | - removed Addressable::URI#extract_mapping 234 | - added partial template expansion 235 | - fixed minor bugs in the parse and heuristic_parse methods 236 | - fixed incompatibility with Ruby 1.9.1 237 | - fixed bottleneck in Addressable::URI#hash and Addressable::URI#to_s 238 | - fixed unicode normalization exception 239 | - updated query_values methods to better handle subscript notation 240 | - worked around issue with freezing URIs 241 | - improved specs 242 | 243 | # Addressable 2.0.2 244 | - fixed issue with URI template expansion 245 | - fixed issue with percent escaping characters 0-15 246 | 247 | # Addressable 2.0.1 248 | - fixed issue with query string assignment 249 | - fixed issue with improperly encoded components 250 | 251 | # Addressable 2.0.0 252 | - the initialize method now takes an options hash as its only parameter 253 | - added query_values method to URI class 254 | - completely replaced IDNA implementation with pure Ruby 255 | - renamed Addressable::ADDRESSABLE_VERSION to Addressable::VERSION 256 | - completely reworked the Rakefile 257 | - changed the behavior of the port method significantly 258 | - Addressable::URI.encode_segment, Addressable::URI.unencode_segment renamed 259 | - documentation is now in YARD format 260 | - more rigorous type checking 261 | - to_str method implemented, implicit conversion to Strings now allowed 262 | - Addressable::URI#omit method added, Addressable::URI#merge method replaced 263 | - updated URI Template code to match v 03 of the draft spec 264 | - added a bunch of new specifications 265 | 266 | # Addressable 1.0.4 267 | - switched to using RSpec's pending system for specs that rely on IDN 268 | - fixed issue with creating URIs with paths that are not prefixed with '/' 269 | 270 | # Addressable 1.0.3 271 | - implemented a hash method 272 | 273 | # Addressable 1.0.2 274 | - fixed minor bug with the extract_mapping method 275 | 276 | # Addressable 1.0.1 277 | - fixed minor bug with the extract_mapping method 278 | 279 | # Addressable 1.0.0 280 | - heuristic parse method added 281 | - parsing is slightly more strict 282 | - replaced to_h with to_hash 283 | - fixed routing methods 284 | - improved specifications 285 | - improved heckle rake task 286 | - no surviving heckle mutations 287 | 288 | # Addressable 0.1.2 289 | - improved normalization 290 | - fixed bug in joining algorithm 291 | - updated specifications 292 | 293 | # Addressable 0.1.1 294 | - updated documentation 295 | - added URI Template variable extraction 296 | 297 | # Addressable 0.1.0 298 | - initial release 299 | - implementation based on RFC 3986, 3987 300 | - support for IRIs via libidn 301 | - support for the URI Template draft spec 302 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :test do 8 | gem 'bigdecimal' if RUBY_VERSION > '2.4' 9 | gem 'rspec', '~> 3.8' 10 | gem 'rspec-its', '~> 1.3' 11 | end 12 | 13 | group :coverage do 14 | gem "coveralls", "> 0.7", require: false, platforms: :mri 15 | gem "simplecov", require: false 16 | end 17 | 18 | group :development do 19 | gem 'launchy', '~> 2.4', '>= 2.4.3' 20 | gem 'redcarpet', :platform => :mri_19 21 | gem 'yard' 22 | end 23 | 24 | group :test, :development do 25 | gem 'memory_profiler' 26 | gem "rake", ">= 12.3.3" 27 | end 28 | 29 | unless ENV["IDNA_MODE"] == "pure" 30 | gem "idn-ruby", platform: :mri 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Addressable 2 | 3 |
4 |
Homepage
github.com/sporkmonger/addressable
5 |
Author
Bob Aman
6 |
Copyright
Copyright © Bob Aman
7 |
License
Apache 2.0
8 |
9 | 10 | [![Gem Version](https://img.shields.io/gem/dt/addressable.svg)][gem] 11 | [![Build Status](https://github.com/sporkmonger/addressable/workflows/CI/badge.svg)][actions] 12 | [![Test Coverage Status](https://img.shields.io/coveralls/sporkmonger/addressable.svg)][coveralls] 13 | [![Documentation Coverage Status](https://inch-ci.org/github/sporkmonger/addressable.svg?branch=master)][inch] 14 | 15 | [gem]: https://rubygems.org/gems/addressable 16 | [actions]: https://github.com/sporkmonger/addressable/actions 17 | [coveralls]: https://coveralls.io/r/sporkmonger/addressable 18 | [inch]: https://inch-ci.org/github/sporkmonger/addressable 19 | 20 | # Description 21 | 22 | Addressable is an alternative implementation to the URI implementation 23 | that is part of Ruby's standard library. It is flexible, offers heuristic 24 | parsing, and additionally provides extensive support for IRIs and URI templates. 25 | 26 | Addressable closely conforms to RFC 3986, RFC 3987, and RFC 6570 (level 4). 27 | 28 | # Reference 29 | 30 | - {Addressable::URI} 31 | - {Addressable::Template} 32 | 33 | # Example usage 34 | 35 | ```ruby 36 | require "addressable/uri" 37 | 38 | uri = Addressable::URI.parse("http://example.com/path/to/resource/") 39 | uri.scheme 40 | #=> "http" 41 | uri.host 42 | #=> "example.com" 43 | uri.path 44 | #=> "/path/to/resource/" 45 | 46 | uri = Addressable::URI.parse("http://www.詹姆斯.com/") 47 | uri.normalize 48 | #=> # 49 | ``` 50 | 51 | 52 | # URI Templates 53 | 54 | For more details, see [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570.txt). 55 | 56 | 57 | ```ruby 58 | 59 | require "addressable/template" 60 | 61 | template = Addressable::Template.new("http://example.com/{?query*}") 62 | template.expand({ 63 | "query" => { 64 | 'foo' => 'bar', 65 | 'color' => 'red' 66 | } 67 | }) 68 | #=> # 69 | 70 | template = Addressable::Template.new("http://example.com/{?one,two,three}") 71 | template.partial_expand({"one" => "1", "three" => 3}).pattern 72 | #=> "http://example.com/?one=1{&two}&three=3" 73 | 74 | template = Addressable::Template.new( 75 | "http://{host}{/segments*}/{?one,two,bogus}{#fragment}" 76 | ) 77 | uri = Addressable::URI.parse( 78 | "http://example.com/a/b/c/?one=1&two=2#foo" 79 | ) 80 | template.extract(uri) 81 | #=> 82 | # { 83 | # "host" => "example.com", 84 | # "segments" => ["a", "b", "c"], 85 | # "one" => "1", 86 | # "two" => "2", 87 | # "fragment" => "foo" 88 | # } 89 | ``` 90 | 91 | # Install 92 | 93 | ```console 94 | $ gem install addressable 95 | ``` 96 | 97 | You may optionally turn on native IDN support by installing libidn and the 98 | idn gem: 99 | 100 | ```console 101 | $ sudo apt-get install libidn11-dev # Debian/Ubuntu 102 | $ brew install libidn # OS X 103 | $ gem install idn-ruby 104 | ``` 105 | 106 | # Semantic Versioning 107 | 108 | This project uses [Semantic Versioning](https://semver.org/). You can (and should) specify your 109 | dependency using a pessimistic version constraint covering the major and minor 110 | values: 111 | 112 | ```ruby 113 | spec.add_dependency 'addressable', '~> 2.7' 114 | ``` 115 | 116 | If you need a specific bug fix, you can also specify minimum tiny versions 117 | without preventing updates to the latest minor release: 118 | 119 | ```ruby 120 | spec.add_dependency 'addressable', '~> 2.3', '>= 2.3.7' 121 | ``` 122 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Addressable 2 | 3 | 1. Update `CHANGELOG.md` 4 | 1. Update `lib/addressable/version.rb` with the new version 5 | 1. Run `rake gem:gemspec` to update gemspec 6 | 1. Run `rake gem:install` to sanity check your work 7 | 1. Create pull request with all that 8 | 1. Merge the pull request when CI is green 9 | 1. Ensure you have latest changes locally 10 | 1. Run`VERSION=x.y.z rake git:tag:create` to create tag in git 11 | 1. Push tag to upstream: `git push --tags upstream` 12 | 1. Watch GitHub Actions build and push the gem to RubyGems.org 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'rake' 5 | 6 | require File.join(File.dirname(__FILE__), 'lib', 'addressable', 'version') 7 | 8 | PKG_DISPLAY_NAME = 'Addressable' 9 | PKG_NAME = PKG_DISPLAY_NAME.downcase 10 | PKG_VERSION = Addressable::VERSION::STRING 11 | PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" 12 | 13 | RELEASE_NAME = "REL #{PKG_VERSION}" 14 | 15 | PKG_SUMMARY = "URI Implementation" 16 | PKG_DESCRIPTION = <<-TEXT 17 | Addressable is an alternative implementation to the URI implementation that is 18 | part of Ruby's standard library. It is flexible, offers heuristic parsing, and 19 | additionally provides extensive support for IRIs and URI templates. 20 | TEXT 21 | 22 | PKG_FILES = FileList[ 23 | "data/**/*", 24 | "lib/**/*.rb", 25 | "spec/**/*.rb", 26 | "tasks/**/*.rake", 27 | "addressable.gemspec", 28 | "CHANGELOG.md", 29 | "Gemfile", 30 | "LICENSE.txt", 31 | "README.md", 32 | "Rakefile", 33 | ] 34 | 35 | task :default => "spec" 36 | 37 | Dir['tasks/**/*.rake'].each { |rake| load rake } 38 | -------------------------------------------------------------------------------- /addressable.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: addressable 2.8.7 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "addressable".freeze 6 | s.version = "2.8.7".freeze 7 | 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 9 | s.metadata = { "changelog_uri" => "https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md#v2.8.7" } if s.respond_to? :metadata= 10 | s.require_paths = ["lib".freeze] 11 | s.authors = ["Bob Aman".freeze] 12 | s.date = "2024-06-21" 13 | 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 14 | s.email = "bob@sporkmonger.com".freeze 15 | s.extra_rdoc_files = ["README.md".freeze] 16 | s.files = ["CHANGELOG.md".freeze, "Gemfile".freeze, "LICENSE.txt".freeze, "README.md".freeze, "Rakefile".freeze, "addressable.gemspec".freeze, "data/unicode.data".freeze, "lib/addressable.rb".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/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] 17 | s.homepage = "https://github.com/sporkmonger/addressable".freeze 18 | s.licenses = ["Apache-2.0".freeze] 19 | s.rdoc_options = ["--main".freeze, "README.md".freeze] 20 | s.required_ruby_version = Gem::Requirement.new(">= 2.2".freeze) 21 | s.rubygems_version = "3.5.11".freeze 22 | s.summary = "URI Implementation".freeze 23 | 24 | s.specification_version = 4 25 | 26 | s.add_runtime_dependency(%q.freeze, [">= 2.0.2".freeze, "< 7.0".freeze]) 27 | s.add_development_dependency(%q.freeze, [">= 1.0".freeze, "< 3.0".freeze]) 28 | end 29 | -------------------------------------------------------------------------------- /benchmark/simple.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark' 4 | 5 | $: << '../lib' << 'lib' 6 | require 'addressable/uri' 7 | 8 | n = 30000 9 | Benchmark.bm do |x| 10 | x.report do 11 | n.times do 12 | u = Addressable::URI.parse('http://google.com/stuff/../?with_lots=of¶ms=asdff#!stuff') 13 | u.normalize 14 | end 15 | end 16 | end 17 | 18 | # Total: 1026 samples 19 | # 203 19.8% 19.8% 328 32.0% Addressable::URI#normalized_path 20 | # 177 17.3% 37.0% 177 17.3% garbage_collector 21 | # 155 15.1% 52.1% 221 21.5% Addressable::URI#initialize 22 | # 52 5.1% 57.2% 52 5.1% Addressable::URI.encode_component 23 | # 46 4.5% 61.7% 142 13.8% Addressable::URI.normalize_component 24 | # 39 3.8% 65.5% 41 4.0% Addressable::URI.normalize_path 25 | # 38 3.7% 69.2% 128 12.5% Addressable::URI.parse 26 | # 36 3.5% 72.7% 58 5.7% Addressable::URI#normalized_scheme 27 | # 35 3.4% 76.1% 53 5.2% Addressable::URI#normalized_fragment 28 | # 34 3.3% 79.4% 34 3.3% Addressable::IDNA.unicode_normalize_kc 29 | # 34 3.3% 82.7% 56 5.5% Addressable::URI#normalized_query 30 | # 27 2.6% 85.4% 689 67.2% Addressable::URI#normalize 31 | -------------------------------------------------------------------------------- /benchmark/time.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporkmonger/addressable/3450895887d0a1770660d8831d1b6fcfed9bd9d6/benchmark/time.gif -------------------------------------------------------------------------------- /benchmark/unicode_normalize.rb: -------------------------------------------------------------------------------- 1 | # /usr/bin/env ruby 2 | # frozen_string_literal: true. 3 | 4 | require "benchmark" 5 | require_relative "../lib/addressable/idna/pure.rb" 6 | require "idn" 7 | 8 | value = "fiᆵリ宠퐱卄.com" 9 | expected = "fiᆵリ宠퐱卄.com" 10 | N = 100_000 11 | 12 | fail "ruby does not match" unless expected == value.unicode_normalize(:nfkc) 13 | fail "libidn does not match" unless expected == IDN::Stringprep.nfkc_normalize(value) 14 | fail "addressable does not match" unless expected == Addressable::IDNA.unicode_normalize_kc(value) 15 | 16 | Benchmark.bmbm do |x| 17 | x.report("pure") { N.times { Addressable::IDNA.unicode_normalize_kc(value) } } 18 | x.report("libidn") { N.times { IDN::Stringprep.nfkc_normalize(value) } } 19 | x.report("ruby") { N.times { value.unicode_normalize(:nfkc) } } 20 | end 21 | 22 | # February 14th 2023, before replacing the legacy pure normalize code: 23 | 24 | # > ruby benchmark/unicode_normalize.rb 25 | # Rehearsal ------------------------------------------ 26 | # pure 1.335230 0.000315 1.335545 ( 1.335657) 27 | # libidn 0.058568 0.000000 0.058568 ( 0.058570) 28 | # ruby 0.326008 0.000014 0.326022 ( 0.326026) 29 | # --------------------------------- total: 1.720135sec 30 | 31 | # user system total real 32 | # pure 1.325948 0.000000 1.325948 ( 1.326054) 33 | # libidn 0.058067 0.000000 0.058067 ( 0.058069) 34 | # ruby 0.325062 0.000000 0.325062 ( 0.325115) 35 | -------------------------------------------------------------------------------- /data/unicode.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporkmonger/addressable/3450895887d0a1770660d8831d1b6fcfed9bd9d6/data/unicode.data -------------------------------------------------------------------------------- /gemfiles/public_suffix_2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Assumes this gemfile is used from the project root 4 | eval_gemfile "../Gemfile" 5 | 6 | gem "public_suffix", ">= 2.0.2", "~> 2.0" 7 | -------------------------------------------------------------------------------- /gemfiles/public_suffix_3.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Assumes this gemfile is used from the project root 4 | eval_gemfile "../Gemfile" 5 | 6 | gem "public_suffix", "~> 3.0" 7 | -------------------------------------------------------------------------------- /gemfiles/public_suffix_4.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Assumes this gemfile is used from the project root 4 | eval_gemfile "../Gemfile" 5 | 6 | gem "public_suffix", "~> 4.0" 7 | -------------------------------------------------------------------------------- /lib/addressable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'addressable/uri' 4 | require 'addressable/template' 5 | -------------------------------------------------------------------------------- /lib/addressable/idna.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #-- 4 | # Copyright (C) Bob Aman 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | #++ 18 | 19 | 20 | begin 21 | require "addressable/idna/native" 22 | rescue LoadError 23 | # libidn or the idn gem was not available, fall back on a pure-Ruby 24 | # implementation... 25 | require "addressable/idna/pure" 26 | end 27 | -------------------------------------------------------------------------------- /lib/addressable/idna/native.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #-- 4 | # Copyright (C) Bob Aman 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | #++ 18 | 19 | 20 | require "idn" 21 | 22 | module Addressable 23 | module IDNA 24 | def self.punycode_encode(value) 25 | IDN::Punycode.encode(value.to_s) 26 | end 27 | 28 | def self.punycode_decode(value) 29 | IDN::Punycode.decode(value.to_s) 30 | end 31 | 32 | class << self 33 | # @deprecated Use {String#unicode_normalize(:nfkc)} instead 34 | def unicode_normalize_kc(value) 35 | value.to_s.unicode_normalize(:nfkc) 36 | end 37 | 38 | extend Gem::Deprecate 39 | deprecate :unicode_normalize_kc, "String#unicode_normalize(:nfkc)", 2023, 4 40 | end 41 | 42 | def self.to_ascii(value) 43 | value.to_s.split('.', -1).map do |segment| 44 | if segment.size > 0 && segment.size < 64 45 | IDN::Idna.toASCII(segment, IDN::Idna::ALLOW_UNASSIGNED) 46 | elsif segment.size >= 64 47 | segment 48 | else 49 | '' 50 | end 51 | end.join('.') 52 | end 53 | 54 | def self.to_unicode(value) 55 | value.to_s.split('.', -1).map do |segment| 56 | if segment.size > 0 && segment.size < 64 57 | IDN::Idna.toUnicode(segment, IDN::Idna::ALLOW_UNASSIGNED) 58 | elsif segment.size >= 64 59 | segment 60 | else 61 | '' 62 | end 63 | end.join('.') 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/addressable/idna/pure.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #-- 4 | # Copyright (C) Bob Aman 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | #++ 18 | 19 | 20 | module Addressable 21 | module IDNA 22 | # This module is loosely based on idn_actionmailer by Mick Staugaard, 23 | # the unicode library by Yoshida Masato, and the punycode implementation 24 | # by Kazuhiro Nishiyama. Most of the code was copied verbatim, but 25 | # some reformatting was done, and some translation from C was done. 26 | # 27 | # Without their code to work from as a base, we'd all still be relying 28 | # on the presence of libidn. Which nobody ever seems to have installed. 29 | # 30 | # Original sources: 31 | # http://github.com/staugaard/idn_actionmailer 32 | # http://www.yoshidam.net/Ruby.html#unicode 33 | # http://rubyforge.org/frs/?group_id=2550 34 | 35 | 36 | UNICODE_TABLE = File.expand_path( 37 | File.join(File.dirname(__FILE__), '../../..', 'data/unicode.data') 38 | ) 39 | 40 | ACE_PREFIX = "xn--" 41 | 42 | UTF8_REGEX = /\A(?: 43 | [\x09\x0A\x0D\x20-\x7E] # ASCII 44 | | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte 45 | | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs 46 | | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte 47 | | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates 48 | | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 49 | | [\xF1-\xF3][\x80-\xBF]{3} # planes 4nil5 50 | | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 51 | )*\z/mnx 52 | 53 | UTF8_REGEX_MULTIBYTE = /(?: 54 | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte 55 | | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs 56 | | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte 57 | | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates 58 | | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 59 | | [\xF1-\xF3][\x80-\xBF]{3} # planes 4nil5 60 | | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 61 | )/mnx 62 | 63 | # :startdoc: 64 | 65 | # Converts from a Unicode internationalized domain name to an ASCII 66 | # domain name as described in RFC 3490. 67 | def self.to_ascii(input) 68 | input = input.to_s unless input.is_a?(String) 69 | input = input.dup.force_encoding(Encoding::UTF_8).unicode_normalize(:nfkc) 70 | if input.respond_to?(:force_encoding) 71 | input.force_encoding(Encoding::ASCII_8BIT) 72 | end 73 | if input =~ UTF8_REGEX && input =~ UTF8_REGEX_MULTIBYTE 74 | parts = unicode_downcase(input).split('.') 75 | parts.map! do |part| 76 | if part.respond_to?(:force_encoding) 77 | part.force_encoding(Encoding::ASCII_8BIT) 78 | end 79 | if part =~ UTF8_REGEX && part =~ UTF8_REGEX_MULTIBYTE 80 | ACE_PREFIX + punycode_encode(part) 81 | else 82 | part 83 | end 84 | end 85 | parts.join('.') 86 | else 87 | input 88 | end 89 | end 90 | 91 | # Converts from an ASCII domain name to a Unicode internationalized 92 | # domain name as described in RFC 3490. 93 | def self.to_unicode(input) 94 | input = input.to_s unless input.is_a?(String) 95 | parts = input.split('.') 96 | parts.map! do |part| 97 | if part =~ /^#{ACE_PREFIX}(.+)/ 98 | begin 99 | punycode_decode(part[/^#{ACE_PREFIX}(.+)/, 1]) 100 | rescue Addressable::IDNA::PunycodeBadInput 101 | # toUnicode is explicitly defined as never-fails by the spec 102 | part 103 | end 104 | else 105 | part 106 | end 107 | end 108 | output = parts.join('.') 109 | if output.respond_to?(:force_encoding) 110 | output.force_encoding(Encoding::UTF_8) 111 | end 112 | output 113 | end 114 | 115 | class << self 116 | # @deprecated Use {String#unicode_normalize(:nfkc)} instead 117 | def unicode_normalize_kc(value) 118 | value.to_s.unicode_normalize(:nfkc) 119 | end 120 | 121 | extend Gem::Deprecate 122 | deprecate :unicode_normalize_kc, "String#unicode_normalize(:nfkc)", 2023, 4 123 | end 124 | 125 | ## 126 | # Unicode aware downcase method. 127 | # 128 | # @api private 129 | # @param [String] input 130 | # The input string. 131 | # @return [String] The downcased result. 132 | def self.unicode_downcase(input) 133 | input = input.to_s unless input.is_a?(String) 134 | unpacked = input.unpack("U*") 135 | unpacked.map! { |codepoint| lookup_unicode_lowercase(codepoint) } 136 | return unpacked.pack("U*") 137 | end 138 | private_class_method :unicode_downcase 139 | 140 | def self.lookup_unicode_lowercase(codepoint) 141 | codepoint_data = UNICODE_DATA[codepoint] 142 | (codepoint_data ? 143 | (codepoint_data[UNICODE_DATA_LOWERCASE] || codepoint) : 144 | codepoint) 145 | end 146 | private_class_method :lookup_unicode_lowercase 147 | 148 | UNICODE_DATA_COMBINING_CLASS = 0 149 | UNICODE_DATA_EXCLUSION = 1 150 | UNICODE_DATA_CANONICAL = 2 151 | UNICODE_DATA_COMPATIBILITY = 3 152 | UNICODE_DATA_UPPERCASE = 4 153 | UNICODE_DATA_LOWERCASE = 5 154 | UNICODE_DATA_TITLECASE = 6 155 | 156 | begin 157 | if defined?(FakeFS) 158 | fakefs_state = FakeFS.activated? 159 | FakeFS.deactivate! 160 | end 161 | # This is a sparse Unicode table. Codepoints without entries are 162 | # assumed to have the value: [0, 0, nil, nil, nil, nil, nil] 163 | UNICODE_DATA = File.open(UNICODE_TABLE, "rb") do |file| 164 | Marshal.load(file.read) 165 | end 166 | ensure 167 | if defined?(FakeFS) 168 | FakeFS.activate! if fakefs_state 169 | end 170 | end 171 | 172 | COMPOSITION_TABLE = {} 173 | UNICODE_DATA.each do |codepoint, data| 174 | canonical = data[UNICODE_DATA_CANONICAL] 175 | exclusion = data[UNICODE_DATA_EXCLUSION] 176 | 177 | if canonical && exclusion == 0 178 | COMPOSITION_TABLE[canonical.unpack("C*")] = codepoint 179 | end 180 | end 181 | 182 | UNICODE_MAX_LENGTH = 256 183 | ACE_MAX_LENGTH = 256 184 | 185 | PUNYCODE_BASE = 36 186 | PUNYCODE_TMIN = 1 187 | PUNYCODE_TMAX = 26 188 | PUNYCODE_SKEW = 38 189 | PUNYCODE_DAMP = 700 190 | PUNYCODE_INITIAL_BIAS = 72 191 | PUNYCODE_INITIAL_N = 0x80 192 | PUNYCODE_DELIMITER = 0x2D 193 | 194 | PUNYCODE_MAXINT = 1 << 64 195 | 196 | PUNYCODE_PRINT_ASCII = 197 | "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + 198 | "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" + 199 | " !\"\#$%&'()*+,-./" + 200 | "0123456789:;<=>?" + 201 | "@ABCDEFGHIJKLMNO" + 202 | "PQRSTUVWXYZ[\\]^_" + 203 | "`abcdefghijklmno" + 204 | "pqrstuvwxyz{|}~\n" 205 | 206 | # Input is invalid. 207 | class PunycodeBadInput < StandardError; end 208 | # Output would exceed the space provided. 209 | class PunycodeBigOutput < StandardError; end 210 | # Input needs wider integers to process. 211 | class PunycodeOverflow < StandardError; end 212 | 213 | def self.punycode_encode(unicode) 214 | unicode = unicode.to_s unless unicode.is_a?(String) 215 | input = unicode.unpack("U*") 216 | output = [0] * (ACE_MAX_LENGTH + 1) 217 | input_length = input.size 218 | output_length = [ACE_MAX_LENGTH] 219 | 220 | # Initialize the state 221 | n = PUNYCODE_INITIAL_N 222 | delta = out = 0 223 | max_out = output_length[0] 224 | bias = PUNYCODE_INITIAL_BIAS 225 | 226 | # Handle the basic code points: 227 | input_length.times do |j| 228 | if punycode_basic?(input[j]) 229 | if max_out - out < 2 230 | raise PunycodeBigOutput, 231 | "Output would exceed the space provided." 232 | end 233 | output[out] = input[j] 234 | out += 1 235 | end 236 | end 237 | 238 | h = b = out 239 | 240 | # h is the number of code points that have been handled, b is the 241 | # number of basic code points, and out is the number of characters 242 | # that have been output. 243 | 244 | if b > 0 245 | output[out] = PUNYCODE_DELIMITER 246 | out += 1 247 | end 248 | 249 | # Main encoding loop: 250 | 251 | while h < input_length 252 | # All non-basic code points < n have been 253 | # handled already. Find the next larger one: 254 | 255 | m = PUNYCODE_MAXINT 256 | input_length.times do |j| 257 | m = input[j] if (n...m) === input[j] 258 | end 259 | 260 | # Increase delta enough to advance the decoder's 261 | # state to , but guard against overflow: 262 | 263 | if m - n > (PUNYCODE_MAXINT - delta) / (h + 1) 264 | raise PunycodeOverflow, "Input needs wider integers to process." 265 | end 266 | delta += (m - n) * (h + 1) 267 | n = m 268 | 269 | input_length.times do |j| 270 | # Punycode does not need to check whether input[j] is basic: 271 | if input[j] < n 272 | delta += 1 273 | if delta == 0 274 | raise PunycodeOverflow, 275 | "Input needs wider integers to process." 276 | end 277 | end 278 | 279 | if input[j] == n 280 | # Represent delta as a generalized variable-length integer: 281 | 282 | q = delta; k = PUNYCODE_BASE 283 | while true 284 | if out >= max_out 285 | raise PunycodeBigOutput, 286 | "Output would exceed the space provided." 287 | end 288 | t = ( 289 | if k <= bias 290 | PUNYCODE_TMIN 291 | elsif k >= bias + PUNYCODE_TMAX 292 | PUNYCODE_TMAX 293 | else 294 | k - bias 295 | end 296 | ) 297 | break if q < t 298 | output[out] = 299 | punycode_encode_digit(t + (q - t) % (PUNYCODE_BASE - t)) 300 | out += 1 301 | q = (q - t) / (PUNYCODE_BASE - t) 302 | k += PUNYCODE_BASE 303 | end 304 | 305 | output[out] = punycode_encode_digit(q) 306 | out += 1 307 | bias = punycode_adapt(delta, h + 1, h == b) 308 | delta = 0 309 | h += 1 310 | end 311 | end 312 | 313 | delta += 1 314 | n += 1 315 | end 316 | 317 | output_length[0] = out 318 | 319 | outlen = out 320 | outlen.times do |j| 321 | c = output[j] 322 | unless c >= 0 && c <= 127 323 | raise StandardError, "Invalid output char." 324 | end 325 | unless PUNYCODE_PRINT_ASCII[c] 326 | raise PunycodeBadInput, "Input is invalid." 327 | end 328 | end 329 | 330 | output[0..outlen].map { |x| x.chr }.join("").sub(/\0+\z/, "") 331 | end 332 | private_class_method :punycode_encode 333 | 334 | def self.punycode_decode(punycode) 335 | input = [] 336 | output = [] 337 | 338 | if ACE_MAX_LENGTH * 2 < punycode.size 339 | raise PunycodeBigOutput, "Output would exceed the space provided." 340 | end 341 | punycode.each_byte do |c| 342 | unless c >= 0 && c <= 127 343 | raise PunycodeBadInput, "Input is invalid." 344 | end 345 | input.push(c) 346 | end 347 | 348 | input_length = input.length 349 | output_length = [UNICODE_MAX_LENGTH] 350 | 351 | # Initialize the state 352 | n = PUNYCODE_INITIAL_N 353 | 354 | out = i = 0 355 | max_out = output_length[0] 356 | bias = PUNYCODE_INITIAL_BIAS 357 | 358 | # Handle the basic code points: Let b be the number of input code 359 | # points before the last delimiter, or 0 if there is none, then 360 | # copy the first b code points to the output. 361 | 362 | b = 0 363 | input_length.times do |j| 364 | b = j if punycode_delimiter?(input[j]) 365 | end 366 | if b > max_out 367 | raise PunycodeBigOutput, "Output would exceed the space provided." 368 | end 369 | 370 | b.times do |j| 371 | unless punycode_basic?(input[j]) 372 | raise PunycodeBadInput, "Input is invalid." 373 | end 374 | output[out] = input[j] 375 | out+=1 376 | end 377 | 378 | # Main decoding loop: Start just after the last delimiter if any 379 | # basic code points were copied; start at the beginning otherwise. 380 | 381 | in_ = b > 0 ? b + 1 : 0 382 | while in_ < input_length 383 | 384 | # in_ is the index of the next character to be consumed, and 385 | # out is the number of code points in the output array. 386 | 387 | # Decode a generalized variable-length integer into delta, 388 | # which gets added to i. The overflow checking is easier 389 | # if we increase i as we go, then subtract off its starting 390 | # value at the end to obtain delta. 391 | 392 | oldi = i; w = 1; k = PUNYCODE_BASE 393 | while true 394 | if in_ >= input_length 395 | raise PunycodeBadInput, "Input is invalid." 396 | end 397 | digit = punycode_decode_digit(input[in_]) 398 | in_+=1 399 | if digit >= PUNYCODE_BASE 400 | raise PunycodeBadInput, "Input is invalid." 401 | end 402 | if digit > (PUNYCODE_MAXINT - i) / w 403 | raise PunycodeOverflow, "Input needs wider integers to process." 404 | end 405 | i += digit * w 406 | t = ( 407 | if k <= bias 408 | PUNYCODE_TMIN 409 | elsif k >= bias + PUNYCODE_TMAX 410 | PUNYCODE_TMAX 411 | else 412 | k - bias 413 | end 414 | ) 415 | break if digit < t 416 | if w > PUNYCODE_MAXINT / (PUNYCODE_BASE - t) 417 | raise PunycodeOverflow, "Input needs wider integers to process." 418 | end 419 | w *= PUNYCODE_BASE - t 420 | k += PUNYCODE_BASE 421 | end 422 | 423 | bias = punycode_adapt(i - oldi, out + 1, oldi == 0) 424 | 425 | # I was supposed to wrap around from out + 1 to 0, 426 | # incrementing n each time, so we'll fix that now: 427 | 428 | if i / (out + 1) > PUNYCODE_MAXINT - n 429 | raise PunycodeOverflow, "Input needs wider integers to process." 430 | end 431 | n += i / (out + 1) 432 | i %= out + 1 433 | 434 | # Insert n at position i of the output: 435 | 436 | # not needed for Punycode: 437 | # raise PUNYCODE_INVALID_INPUT if decode_digit(n) <= base 438 | if out >= max_out 439 | raise PunycodeBigOutput, "Output would exceed the space provided." 440 | end 441 | 442 | #memmove(output + i + 1, output + i, (out - i) * sizeof *output) 443 | output[i + 1, out - i] = output[i, out - i] 444 | output[i] = n 445 | i += 1 446 | 447 | out += 1 448 | end 449 | 450 | output_length[0] = out 451 | 452 | output.pack("U*") 453 | end 454 | private_class_method :punycode_decode 455 | 456 | def self.punycode_basic?(codepoint) 457 | codepoint < 0x80 458 | end 459 | private_class_method :punycode_basic? 460 | 461 | def self.punycode_delimiter?(codepoint) 462 | codepoint == PUNYCODE_DELIMITER 463 | end 464 | private_class_method :punycode_delimiter? 465 | 466 | def self.punycode_encode_digit(d) 467 | d + 22 + 75 * ((d < 26) ? 1 : 0) 468 | end 469 | private_class_method :punycode_encode_digit 470 | 471 | # Returns the numeric value of a basic codepoint 472 | # (for use in representing integers) in the range 0 to 473 | # base - 1, or PUNYCODE_BASE if codepoint does not represent a value. 474 | def self.punycode_decode_digit(codepoint) 475 | if codepoint - 48 < 10 476 | codepoint - 22 477 | elsif codepoint - 65 < 26 478 | codepoint - 65 479 | elsif codepoint - 97 < 26 480 | codepoint - 97 481 | else 482 | PUNYCODE_BASE 483 | end 484 | end 485 | private_class_method :punycode_decode_digit 486 | 487 | # Bias adaptation method 488 | def self.punycode_adapt(delta, numpoints, firsttime) 489 | delta = firsttime ? delta / PUNYCODE_DAMP : delta >> 1 490 | # delta >> 1 is a faster way of doing delta / 2 491 | delta += delta / numpoints 492 | difference = PUNYCODE_BASE - PUNYCODE_TMIN 493 | 494 | k = 0 495 | while delta > (difference * PUNYCODE_TMAX) / 2 496 | delta /= difference 497 | k += PUNYCODE_BASE 498 | end 499 | 500 | k + (difference + 1) * delta / (delta + PUNYCODE_SKEW) 501 | end 502 | private_class_method :punycode_adapt 503 | end 504 | # :startdoc: 505 | end 506 | -------------------------------------------------------------------------------- /lib/addressable/template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #-- 4 | # Copyright (C) Bob Aman 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | #++ 18 | 19 | 20 | require "addressable/version" 21 | require "addressable/uri" 22 | 23 | module Addressable 24 | ## 25 | # This is an implementation of a URI template based on 26 | # RFC 6570 (http://tools.ietf.org/html/rfc6570). 27 | class Template 28 | # Constants used throughout the template code. 29 | anything = 30 | Addressable::URI::CharacterClasses::RESERVED + 31 | Addressable::URI::CharacterClasses::UNRESERVED 32 | 33 | 34 | variable_char_class = 35 | Addressable::URI::CharacterClasses::ALPHA + 36 | Addressable::URI::CharacterClasses::DIGIT + '_' 37 | 38 | var_char = 39 | "(?>(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)" 40 | RESERVED = 41 | "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])" 42 | UNRESERVED = 43 | "(?:[#{ 44 | Addressable::URI::CharacterClasses::UNRESERVED 45 | }]|%[a-fA-F0-9][a-fA-F0-9])" 46 | variable = 47 | "(?:#{var_char}(?:\\.?#{var_char})*)" 48 | varspec = 49 | "(?:(#{variable})(\\*|:\\d+)?)" 50 | VARNAME = 51 | /^#{variable}$/ 52 | VARSPEC = 53 | /^#{varspec}$/ 54 | VARIABLE_LIST = 55 | /^#{varspec}(?:,#{varspec})*$/ 56 | operator = 57 | "+#./;?&=,!@|" 58 | EXPRESSION = 59 | /\{([#{operator}])?(#{varspec}(?:,#{varspec})*)\}/ 60 | 61 | 62 | LEADERS = { 63 | '?' => '?', 64 | '/' => '/', 65 | '#' => '#', 66 | '.' => '.', 67 | ';' => ';', 68 | '&' => '&' 69 | } 70 | JOINERS = { 71 | '?' => '&', 72 | '.' => '.', 73 | ';' => ';', 74 | '&' => '&', 75 | '/' => '/' 76 | } 77 | 78 | ## 79 | # Raised if an invalid template value is supplied. 80 | class InvalidTemplateValueError < StandardError 81 | end 82 | 83 | ## 84 | # Raised if an invalid template operator is used in a pattern. 85 | class InvalidTemplateOperatorError < StandardError 86 | end 87 | 88 | ## 89 | # Raised if an invalid template operator is used in a pattern. 90 | class TemplateOperatorAbortedError < StandardError 91 | end 92 | 93 | ## 94 | # This class represents the data that is extracted when a Template 95 | # is matched against a URI. 96 | class MatchData 97 | ## 98 | # Creates a new MatchData object. 99 | # MatchData objects should never be instantiated directly. 100 | # 101 | # @param [Addressable::URI] uri 102 | # The URI that the template was matched against. 103 | def initialize(uri, template, mapping) 104 | @uri = uri.dup.freeze 105 | @template = template 106 | @mapping = mapping.dup.freeze 107 | end 108 | 109 | ## 110 | # @return [Addressable::URI] 111 | # The URI that the Template was matched against. 112 | attr_reader :uri 113 | 114 | ## 115 | # @return [Addressable::Template] 116 | # The Template used for the match. 117 | attr_reader :template 118 | 119 | ## 120 | # @return [Hash] 121 | # The mapping that resulted from the match. 122 | # Note that this mapping does not include keys or values for 123 | # variables that appear in the Template, but are not present 124 | # in the URI. 125 | attr_reader :mapping 126 | 127 | ## 128 | # @return [Array] 129 | # The list of variables that were present in the Template. 130 | # Note that this list will include variables which do not appear 131 | # in the mapping because they were not present in URI. 132 | def variables 133 | self.template.variables 134 | end 135 | alias_method :keys, :variables 136 | alias_method :names, :variables 137 | 138 | ## 139 | # @return [Array] 140 | # The list of values that were captured by the Template. 141 | # Note that this list will include nils for any variables which 142 | # were in the Template, but did not appear in the URI. 143 | def values 144 | @values ||= self.variables.inject([]) do |accu, key| 145 | accu << self.mapping[key] 146 | accu 147 | end 148 | end 149 | alias_method :captures, :values 150 | 151 | ## 152 | # Accesses captured values by name or by index. 153 | # 154 | # @param [String, Symbol, Fixnum] key 155 | # Capture index or name. Note that when accessing by with index 156 | # of 0, the full URI will be returned. The intention is to mimic 157 | # the ::MatchData#[] behavior. 158 | # 159 | # @param [#to_int, nil] len 160 | # If provided, an array of values will be returned with the given 161 | # parameter used as length. 162 | # 163 | # @return [Array, String, nil] 164 | # The captured value corresponding to the index or name. If the 165 | # value was not provided or the key is unknown, nil will be 166 | # returned. 167 | # 168 | # If the second parameter is provided, an array of that length will 169 | # be returned instead. 170 | def [](key, len = nil) 171 | if len 172 | to_a[key, len] 173 | elsif String === key or Symbol === key 174 | mapping[key.to_s] 175 | else 176 | to_a[key] 177 | end 178 | end 179 | 180 | ## 181 | # @return [Array] 182 | # Array with the matched URI as first element followed by the captured 183 | # values. 184 | def to_a 185 | [to_s, *values] 186 | end 187 | 188 | ## 189 | # @return [String] 190 | # The matched URI as String. 191 | def to_s 192 | uri.to_s 193 | end 194 | alias_method :string, :to_s 195 | 196 | # Returns multiple captured values at once. 197 | # 198 | # @param [String, Symbol, Fixnum] *indexes 199 | # Indices of the captures to be returned 200 | # 201 | # @return [Array] 202 | # Values corresponding to given indices. 203 | # 204 | # @see Addressable::Template::MatchData#[] 205 | def values_at(*indexes) 206 | indexes.map { |i| self[i] } 207 | end 208 | 209 | ## 210 | # Returns a String representation of the MatchData's state. 211 | # 212 | # @return [String] The MatchData's state, as a String. 213 | def inspect 214 | sprintf("#<%s:%#0x RESULT:%s>", 215 | self.class.to_s, self.object_id, self.mapping.inspect) 216 | end 217 | 218 | ## 219 | # Dummy method for code expecting a ::MatchData instance 220 | # 221 | # @return [String] An empty string. 222 | def pre_match 223 | "" 224 | end 225 | alias_method :post_match, :pre_match 226 | end 227 | 228 | ## 229 | # Creates a new Addressable::Template object. 230 | # 231 | # @param [#to_str] pattern The URI Template pattern. 232 | # 233 | # @return [Addressable::Template] The initialized Template object. 234 | def initialize(pattern) 235 | if !pattern.respond_to?(:to_str) 236 | raise TypeError, "Can't convert #{pattern.class} into String." 237 | end 238 | @pattern = pattern.to_str.dup.freeze 239 | end 240 | 241 | ## 242 | # Freeze URI, initializing instance variables. 243 | # 244 | # @return [Addressable::URI] The frozen URI object. 245 | def freeze 246 | self.variables 247 | self.variable_defaults 248 | self.named_captures 249 | super 250 | end 251 | 252 | ## 253 | # @return [String] The Template object's pattern. 254 | attr_reader :pattern 255 | 256 | ## 257 | # Returns a String representation of the Template object's state. 258 | # 259 | # @return [String] The Template object's state, as a String. 260 | def inspect 261 | sprintf("#<%s:%#0x PATTERN:%s>", 262 | self.class.to_s, self.object_id, self.pattern) 263 | end 264 | 265 | ## 266 | # Returns true if the Template objects are equal. This method 267 | # does NOT normalize either Template before doing the comparison. 268 | # 269 | # @param [Object] template The Template to compare. 270 | # 271 | # @return [TrueClass, FalseClass] 272 | # true if the Templates are equivalent, false 273 | # otherwise. 274 | def ==(template) 275 | return false unless template.kind_of?(Template) 276 | return self.pattern == template.pattern 277 | end 278 | 279 | ## 280 | # Addressable::Template makes no distinction between `==` and `eql?`. 281 | # 282 | # @see #== 283 | alias_method :eql?, :== 284 | 285 | ## 286 | # Extracts a mapping from the URI using a URI Template pattern. 287 | # 288 | # @param [Addressable::URI, #to_str] uri 289 | # The URI to extract from. 290 | # 291 | # @param [#restore, #match] processor 292 | # A template processor object may optionally be supplied. 293 | # 294 | # The object should respond to either the restore or 295 | # match messages or both. The restore method should 296 | # take two parameters: `[String] name` and `[String] value`. 297 | # The restore method should reverse any transformations that 298 | # have been performed on the value to ensure a valid URI. 299 | # The match method should take a single 300 | # parameter: `[String] name`. The match method should return 301 | # a String containing a regular expression capture group for 302 | # matching on that particular variable. The default value is `".*?"`. 303 | # The match method has no effect on multivariate operator 304 | # expansions. 305 | # 306 | # @return [Hash, NilClass] 307 | # The Hash mapping that was extracted from the URI, or 308 | # nil if the URI didn't match the template. 309 | # 310 | # @example 311 | # class ExampleProcessor 312 | # def self.restore(name, value) 313 | # return value.gsub(/\+/, " ") if name == "query" 314 | # return value 315 | # end 316 | # 317 | # def self.match(name) 318 | # return ".*?" if name == "first" 319 | # return ".*" 320 | # end 321 | # end 322 | # 323 | # uri = Addressable::URI.parse( 324 | # "http://example.com/search/an+example+search+query/" 325 | # ) 326 | # Addressable::Template.new( 327 | # "http://example.com/search/{query}/" 328 | # ).extract(uri, ExampleProcessor) 329 | # #=> {"query" => "an example search query"} 330 | # 331 | # uri = Addressable::URI.parse("http://example.com/a/b/c/") 332 | # Addressable::Template.new( 333 | # "http://example.com/{first}/{second}/" 334 | # ).extract(uri, ExampleProcessor) 335 | # #=> {"first" => "a", "second" => "b/c"} 336 | # 337 | # uri = Addressable::URI.parse("http://example.com/a/b/c/") 338 | # Addressable::Template.new( 339 | # "http://example.com/{first}/{-list|/|second}/" 340 | # ).extract(uri) 341 | # #=> {"first" => "a", "second" => ["b", "c"]} 342 | def extract(uri, processor=nil) 343 | match_data = self.match(uri, processor) 344 | return (match_data ? match_data.mapping : nil) 345 | end 346 | 347 | ## 348 | # Extracts match data from the URI using a URI Template pattern. 349 | # 350 | # @param [Addressable::URI, #to_str] uri 351 | # The URI to extract from. 352 | # 353 | # @param [#restore, #match] processor 354 | # A template processor object may optionally be supplied. 355 | # 356 | # The object should respond to either the restore or 357 | # match messages or both. The restore method should 358 | # take two parameters: `[String] name` and `[String] value`. 359 | # The restore method should reverse any transformations that 360 | # have been performed on the value to ensure a valid URI. 361 | # The match method should take a single 362 | # parameter: `[String] name`. The match method should return 363 | # a String containing a regular expression capture group for 364 | # matching on that particular variable. The default value is `".*?"`. 365 | # The match method has no effect on multivariate operator 366 | # expansions. 367 | # 368 | # @return [Hash, NilClass] 369 | # The Hash mapping that was extracted from the URI, or 370 | # nil if the URI didn't match the template. 371 | # 372 | # @example 373 | # class ExampleProcessor 374 | # def self.restore(name, value) 375 | # return value.gsub(/\+/, " ") if name == "query" 376 | # return value 377 | # end 378 | # 379 | # def self.match(name) 380 | # return ".*?" if name == "first" 381 | # return ".*" 382 | # end 383 | # end 384 | # 385 | # uri = Addressable::URI.parse( 386 | # "http://example.com/search/an+example+search+query/" 387 | # ) 388 | # match = Addressable::Template.new( 389 | # "http://example.com/search/{query}/" 390 | # ).match(uri, ExampleProcessor) 391 | # match.variables 392 | # #=> ["query"] 393 | # match.captures 394 | # #=> ["an example search query"] 395 | # 396 | # uri = Addressable::URI.parse("http://example.com/a/b/c/") 397 | # match = Addressable::Template.new( 398 | # "http://example.com/{first}/{+second}/" 399 | # ).match(uri, ExampleProcessor) 400 | # match.variables 401 | # #=> ["first", "second"] 402 | # match.captures 403 | # #=> ["a", "b/c"] 404 | # 405 | # uri = Addressable::URI.parse("http://example.com/a/b/c/") 406 | # match = Addressable::Template.new( 407 | # "http://example.com/{first}{/second*}/" 408 | # ).match(uri) 409 | # match.variables 410 | # #=> ["first", "second"] 411 | # match.captures 412 | # #=> ["a", ["b", "c"]] 413 | def match(uri, processor=nil) 414 | uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI) 415 | mapping = {} 416 | 417 | # First, we need to process the pattern, and extract the values. 418 | expansions, expansion_regexp = 419 | parse_template_pattern(pattern, processor) 420 | 421 | return nil unless uri.to_str.match(expansion_regexp) 422 | unparsed_values = uri.to_str.scan(expansion_regexp).flatten 423 | 424 | if uri.to_str == pattern 425 | return Addressable::Template::MatchData.new(uri, self, mapping) 426 | elsif expansions.size > 0 427 | index = 0 428 | expansions.each do |expansion| 429 | _, operator, varlist = *expansion.match(EXPRESSION) 430 | varlist.split(',').each do |varspec| 431 | _, name, modifier = *varspec.match(VARSPEC) 432 | mapping[name] ||= nil 433 | case operator 434 | when nil, '+', '#', '/', '.' 435 | unparsed_value = unparsed_values[index] 436 | name = varspec[VARSPEC, 1] 437 | value = unparsed_value 438 | value = value.split(JOINERS[operator]) if value && modifier == '*' 439 | when ';', '?', '&' 440 | if modifier == '*' 441 | if unparsed_values[index] 442 | value = unparsed_values[index].split(JOINERS[operator]) 443 | value = value.inject({}) do |acc, v| 444 | key, val = v.split('=') 445 | val = "" if val.nil? 446 | acc[key] = val 447 | acc 448 | end 449 | end 450 | else 451 | if (unparsed_values[index]) 452 | name, value = unparsed_values[index].split('=') 453 | value = "" if value.nil? 454 | end 455 | end 456 | end 457 | if processor != nil && processor.respond_to?(:restore) 458 | value = processor.restore(name, value) 459 | end 460 | if processor == nil 461 | if value.is_a?(Hash) 462 | value = value.inject({}){|acc, (k, v)| 463 | acc[Addressable::URI.unencode_component(k)] = 464 | Addressable::URI.unencode_component(v) 465 | acc 466 | } 467 | elsif value.is_a?(Array) 468 | value = value.map{|v| Addressable::URI.unencode_component(v) } 469 | else 470 | value = Addressable::URI.unencode_component(value) 471 | end 472 | end 473 | if !mapping.has_key?(name) || mapping[name].nil? 474 | # Doesn't exist, set to value (even if value is nil) 475 | mapping[name] = value 476 | end 477 | index = index + 1 478 | end 479 | end 480 | return Addressable::Template::MatchData.new(uri, self, mapping) 481 | else 482 | return nil 483 | end 484 | end 485 | 486 | ## 487 | # Expands a URI template into another URI template. 488 | # 489 | # @param [Hash] mapping The mapping that corresponds to the pattern. 490 | # @param [#validate, #transform] processor 491 | # An optional processor object may be supplied. 492 | # @param [Boolean] normalize_values 493 | # Optional flag to enable/disable unicode normalization. Default: true 494 | # 495 | # The object should respond to either the validate or 496 | # transform messages or both. Both the validate and 497 | # transform methods should take two parameters: name and 498 | # value. The validate method should return true 499 | # or false; true if the value of the variable is valid, 500 | # false otherwise. An InvalidTemplateValueError 501 | # exception will be raised if the value is invalid. The transform 502 | # method should return the transformed variable value as a String. 503 | # If a transform method is used, the value will not be percent 504 | # encoded automatically. Unicode normalization will be performed both 505 | # before and after sending the value to the transform method. 506 | # 507 | # @return [Addressable::Template] The partially expanded URI template. 508 | # 509 | # @example 510 | # Addressable::Template.new( 511 | # "http://example.com/{one}/{two}/" 512 | # ).partial_expand({"one" => "1"}).pattern 513 | # #=> "http://example.com/1/{two}/" 514 | # 515 | # Addressable::Template.new( 516 | # "http://example.com/{?one,two}/" 517 | # ).partial_expand({"one" => "1"}).pattern 518 | # #=> "http://example.com/?one=1{&two}/" 519 | # 520 | # Addressable::Template.new( 521 | # "http://example.com/{?one,two,three}/" 522 | # ).partial_expand({"one" => "1", "three" => 3}).pattern 523 | # #=> "http://example.com/?one=1{&two}&three=3" 524 | def partial_expand(mapping, processor=nil, normalize_values=true) 525 | result = self.pattern.dup 526 | mapping = normalize_keys(mapping) 527 | result.gsub!( EXPRESSION ) do |capture| 528 | transform_partial_capture(mapping, capture, processor, normalize_values) 529 | end 530 | return Addressable::Template.new(result) 531 | end 532 | 533 | ## 534 | # Expands a URI template into a full URI. 535 | # 536 | # @param [Hash] mapping The mapping that corresponds to the pattern. 537 | # @param [#validate, #transform] processor 538 | # An optional processor object may be supplied. 539 | # @param [Boolean] normalize_values 540 | # Optional flag to enable/disable unicode normalization. Default: true 541 | # 542 | # The object should respond to either the validate or 543 | # transform messages or both. Both the validate and 544 | # transform methods should take two parameters: name and 545 | # value. The validate method should return true 546 | # or false; true if the value of the variable is valid, 547 | # false otherwise. An InvalidTemplateValueError 548 | # exception will be raised if the value is invalid. The transform 549 | # method should return the transformed variable value as a String. 550 | # If a transform method is used, the value will not be percent 551 | # encoded automatically. Unicode normalization will be performed both 552 | # before and after sending the value to the transform method. 553 | # 554 | # @return [Addressable::URI] The expanded URI template. 555 | # 556 | # @example 557 | # class ExampleProcessor 558 | # def self.validate(name, value) 559 | # return !!(value =~ /^[\w ]+$/) if name == "query" 560 | # return true 561 | # end 562 | # 563 | # def self.transform(name, value) 564 | # return value.gsub(/ /, "+") if name == "query" 565 | # return value 566 | # end 567 | # end 568 | # 569 | # Addressable::Template.new( 570 | # "http://example.com/search/{query}/" 571 | # ).expand( 572 | # {"query" => "an example search query"}, 573 | # ExampleProcessor 574 | # ).to_str 575 | # #=> "http://example.com/search/an+example+search+query/" 576 | # 577 | # Addressable::Template.new( 578 | # "http://example.com/search/{query}/" 579 | # ).expand( 580 | # {"query" => "an example search query"} 581 | # ).to_str 582 | # #=> "http://example.com/search/an%20example%20search%20query/" 583 | # 584 | # Addressable::Template.new( 585 | # "http://example.com/search/{query}/" 586 | # ).expand( 587 | # {"query" => "bogus!"}, 588 | # ExampleProcessor 589 | # ).to_str 590 | # #=> Addressable::Template::InvalidTemplateValueError 591 | def expand(mapping, processor=nil, normalize_values=true) 592 | result = self.pattern.dup 593 | mapping = normalize_keys(mapping) 594 | result.gsub!( EXPRESSION ) do |capture| 595 | transform_capture(mapping, capture, processor, normalize_values) 596 | end 597 | return Addressable::URI.parse(result) 598 | end 599 | 600 | ## 601 | # Returns an Array of variables used within the template pattern. 602 | # The variables are listed in the Array in the order they appear within 603 | # the pattern. Multiple occurrences of a variable within a pattern are 604 | # not represented in this Array. 605 | # 606 | # @return [Array] The variables present in the template's pattern. 607 | def variables 608 | @variables ||= ordered_variable_defaults.map { |var, val| var }.uniq 609 | end 610 | alias_method :keys, :variables 611 | alias_method :names, :variables 612 | 613 | ## 614 | # Returns a mapping of variables to their default values specified 615 | # in the template. Variables without defaults are not returned. 616 | # 617 | # @return [Hash] Mapping of template variables to their defaults 618 | def variable_defaults 619 | @variable_defaults ||= 620 | Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten] 621 | end 622 | 623 | ## 624 | # Coerces a template into a `Regexp` object. This regular expression will 625 | # behave very similarly to the actual template, and should match the same 626 | # URI values, but it cannot fully handle, for example, values that would 627 | # extract to an `Array`. 628 | # 629 | # @return [Regexp] A regular expression which should match the template. 630 | def to_regexp 631 | _, source = parse_template_pattern(pattern) 632 | Regexp.new(source) 633 | end 634 | 635 | ## 636 | # Returns the source of the coerced `Regexp`. 637 | # 638 | # @return [String] The source of the `Regexp` given by {#to_regexp}. 639 | # 640 | # @api private 641 | def source 642 | self.to_regexp.source 643 | end 644 | 645 | ## 646 | # Returns the named captures of the coerced `Regexp`. 647 | # 648 | # @return [Hash] The named captures of the `Regexp` given by {#to_regexp}. 649 | # 650 | # @api private 651 | def named_captures 652 | self.to_regexp.named_captures 653 | end 654 | 655 | private 656 | def ordered_variable_defaults 657 | @ordered_variable_defaults ||= begin 658 | expansions, _ = parse_template_pattern(pattern) 659 | expansions.flat_map do |capture| 660 | _, _, varlist = *capture.match(EXPRESSION) 661 | varlist.split(',').map do |varspec| 662 | varspec[VARSPEC, 1] 663 | end 664 | end 665 | end 666 | end 667 | 668 | 669 | ## 670 | # Loops through each capture and expands any values available in mapping 671 | # 672 | # @param [Hash] mapping 673 | # Set of keys to expand 674 | # @param [String] capture 675 | # The expression to expand 676 | # @param [#validate, #transform] processor 677 | # An optional processor object may be supplied. 678 | # @param [Boolean] normalize_values 679 | # Optional flag to enable/disable unicode normalization. Default: true 680 | # 681 | # The object should respond to either the validate or 682 | # transform messages or both. Both the validate and 683 | # transform methods should take two parameters: name and 684 | # value. The validate method should return true 685 | # or false; true if the value of the variable is valid, 686 | # false otherwise. An InvalidTemplateValueError exception 687 | # will be raised if the value is invalid. The transform method 688 | # should return the transformed variable value as a String. If a 689 | # transform method is used, the value will not be percent encoded 690 | # automatically. Unicode normalization will be performed both before and 691 | # after sending the value to the transform method. 692 | # 693 | # @return [String] The expanded expression 694 | def transform_partial_capture(mapping, capture, processor = nil, 695 | normalize_values = true) 696 | _, operator, varlist = *capture.match(EXPRESSION) 697 | 698 | vars = varlist.split(",") 699 | 700 | if operator == "?" 701 | # partial expansion of form style query variables sometimes requires a 702 | # slight reordering of the variables to produce a valid url. 703 | first_to_expand = vars.find { |varspec| 704 | _, name, _ = *varspec.match(VARSPEC) 705 | mapping.key?(name) && !mapping[name].nil? 706 | } 707 | 708 | vars = [first_to_expand] + vars.reject {|varspec| varspec == first_to_expand} if first_to_expand 709 | end 710 | 711 | vars. 712 | inject("".dup) do |acc, varspec| 713 | _, name, _ = *varspec.match(VARSPEC) 714 | next_val = if mapping.key? name 715 | transform_capture(mapping, "{#{operator}#{varspec}}", 716 | processor, normalize_values) 717 | else 718 | "{#{operator}#{varspec}}" 719 | end 720 | # If we've already expanded at least one '?' operator with non-empty 721 | # value, change to '&' 722 | operator = "&" if (operator == "?") && (next_val != "") 723 | acc << next_val 724 | end 725 | end 726 | 727 | ## 728 | # Transforms a mapped value so that values can be substituted into the 729 | # template. 730 | # 731 | # @param [Hash] mapping The mapping to replace captures 732 | # @param [String] capture 733 | # The expression to replace 734 | # @param [#validate, #transform] processor 735 | # An optional processor object may be supplied. 736 | # @param [Boolean] normalize_values 737 | # Optional flag to enable/disable unicode normalization. Default: true 738 | # 739 | # 740 | # The object should respond to either the validate or 741 | # transform messages or both. Both the validate and 742 | # transform methods should take two parameters: name and 743 | # value. The validate method should return true 744 | # or false; true if the value of the variable is valid, 745 | # false otherwise. An InvalidTemplateValueError exception 746 | # will be raised if the value is invalid. The transform method 747 | # should return the transformed variable value as a String. If a 748 | # transform method is used, the value will not be percent encoded 749 | # automatically. Unicode normalization will be performed both before and 750 | # after sending the value to the transform method. 751 | # 752 | # @return [String] The expanded expression 753 | def transform_capture(mapping, capture, processor=nil, 754 | normalize_values=true) 755 | _, operator, varlist = *capture.match(EXPRESSION) 756 | return_value = varlist.split(',').inject([]) do |acc, varspec| 757 | _, name, modifier = *varspec.match(VARSPEC) 758 | value = mapping[name] 759 | unless value == nil || value == {} 760 | allow_reserved = %w(+ #).include?(operator) 761 | # Common primitives where the .to_s output is well-defined 762 | if Numeric === value || Symbol === value || 763 | value == true || value == false 764 | value = value.to_s 765 | end 766 | length = modifier.gsub(':', '').to_i if modifier =~ /^:\d+/ 767 | 768 | unless (Hash === value) || 769 | value.respond_to?(:to_ary) || value.respond_to?(:to_str) 770 | raise TypeError, 771 | "Can't convert #{value.class} into String or Array." 772 | end 773 | 774 | value = normalize_value(value) if normalize_values 775 | 776 | if processor == nil || !processor.respond_to?(:transform) 777 | # Handle percent escaping 778 | if allow_reserved 779 | encode_map = 780 | Addressable::URI::CharacterClasses::RESERVED + 781 | Addressable::URI::CharacterClasses::UNRESERVED 782 | else 783 | encode_map = Addressable::URI::CharacterClasses::UNRESERVED 784 | end 785 | if value.kind_of?(Array) 786 | transformed_value = value.map do |val| 787 | if length 788 | Addressable::URI.encode_component(val[0...length], encode_map) 789 | else 790 | Addressable::URI.encode_component(val, encode_map) 791 | end 792 | end 793 | unless modifier == "*" 794 | transformed_value = transformed_value.join(',') 795 | end 796 | elsif value.kind_of?(Hash) 797 | transformed_value = value.map do |key, val| 798 | if modifier == "*" 799 | "#{ 800 | Addressable::URI.encode_component( key, encode_map) 801 | }=#{ 802 | Addressable::URI.encode_component( val, encode_map) 803 | }" 804 | else 805 | "#{ 806 | Addressable::URI.encode_component( key, encode_map) 807 | },#{ 808 | Addressable::URI.encode_component( val, encode_map) 809 | }" 810 | end 811 | end 812 | unless modifier == "*" 813 | transformed_value = transformed_value.join(',') 814 | end 815 | else 816 | if length 817 | transformed_value = Addressable::URI.encode_component( 818 | value[0...length], encode_map) 819 | else 820 | transformed_value = Addressable::URI.encode_component( 821 | value, encode_map) 822 | end 823 | end 824 | end 825 | 826 | # Process, if we've got a processor 827 | if processor != nil 828 | if processor.respond_to?(:validate) 829 | if !processor.validate(name, value) 830 | display_value = value.kind_of?(Array) ? value.inspect : value 831 | raise InvalidTemplateValueError, 832 | "#{name}=#{display_value} is an invalid template value." 833 | end 834 | end 835 | if processor.respond_to?(:transform) 836 | transformed_value = processor.transform(name, value) 837 | if normalize_values 838 | transformed_value = normalize_value(transformed_value) 839 | end 840 | end 841 | end 842 | acc << [name, transformed_value] 843 | end 844 | acc 845 | end 846 | return "" if return_value.empty? 847 | join_values(operator, return_value) 848 | end 849 | 850 | ## 851 | # Takes a set of values, and joins them together based on the 852 | # operator. 853 | # 854 | # @param [String, Nil] operator One of the operators from the set 855 | # (?,&,+,#,;,/,.), or nil if there wasn't one. 856 | # @param [Array] return_value 857 | # The set of return values (as [variable_name, value] tuples) that will 858 | # be joined together. 859 | # 860 | # @return [String] The transformed mapped value 861 | def join_values(operator, return_value) 862 | leader = LEADERS.fetch(operator, '') 863 | joiner = JOINERS.fetch(operator, ',') 864 | case operator 865 | when '&', '?' 866 | leader + return_value.map{|k,v| 867 | if v.is_a?(Array) && v.first =~ /=/ 868 | v.join(joiner) 869 | elsif v.is_a?(Array) 870 | v.map{|inner_value| "#{k}=#{inner_value}"}.join(joiner) 871 | else 872 | "#{k}=#{v}" 873 | end 874 | }.join(joiner) 875 | when ';' 876 | return_value.map{|k,v| 877 | if v.is_a?(Array) && v.first =~ /=/ 878 | ';' + v.join(";") 879 | elsif v.is_a?(Array) 880 | ';' + v.map{|inner_value| "#{k}=#{inner_value}"}.join(";") 881 | else 882 | v && v != '' ? ";#{k}=#{v}" : ";#{k}" 883 | end 884 | }.join 885 | else 886 | leader + return_value.map{|k,v| v}.join(joiner) 887 | end 888 | end 889 | 890 | ## 891 | # Takes a set of values, and joins them together based on the 892 | # operator. 893 | # 894 | # @param [Hash, Array, String] value 895 | # Normalizes unicode keys and values with String#unicode_normalize (NFC) 896 | # 897 | # @return [Hash, Array, String] The normalized values 898 | def normalize_value(value) 899 | # Handle unicode normalization 900 | if value.respond_to?(:to_ary) 901 | value.to_ary.map! { |val| normalize_value(val) } 902 | elsif value.kind_of?(Hash) 903 | value = value.inject({}) { |acc, (k, v)| 904 | acc[normalize_value(k)] = normalize_value(v) 905 | acc 906 | } 907 | else 908 | value = value.to_s if !value.kind_of?(String) 909 | if value.encoding != Encoding::UTF_8 910 | value = value.dup.force_encoding(Encoding::UTF_8) 911 | end 912 | value = value.unicode_normalize(:nfc) 913 | end 914 | value 915 | end 916 | 917 | ## 918 | # Generates a hash with string keys 919 | # 920 | # @param [Hash] mapping A mapping hash to normalize 921 | # 922 | # @return [Hash] 923 | # A hash with stringified keys 924 | def normalize_keys(mapping) 925 | return mapping.inject({}) do |accu, pair| 926 | name, value = pair 927 | if Symbol === name 928 | name = name.to_s 929 | elsif name.respond_to?(:to_str) 930 | name = name.to_str 931 | else 932 | raise TypeError, 933 | "Can't convert #{name.class} into String." 934 | end 935 | accu[name] = value 936 | accu 937 | end 938 | end 939 | 940 | ## 941 | # Generates the Regexp that parses a template pattern. Memoizes the 942 | # value if template processor not set (processors may not be deterministic) 943 | # 944 | # @param [String] pattern The URI template pattern. 945 | # @param [#match] processor The template processor to use. 946 | # 947 | # @return [Array, Regexp] 948 | # An array of expansion variables nad a regular expression which may be 949 | # used to parse a template pattern 950 | def parse_template_pattern(pattern, processor = nil) 951 | if processor.nil? && pattern == @pattern 952 | @cached_template_parse ||= 953 | parse_new_template_pattern(pattern, processor) 954 | else 955 | parse_new_template_pattern(pattern, processor) 956 | end 957 | end 958 | 959 | ## 960 | # Generates the Regexp that parses a template pattern. 961 | # 962 | # @param [String] pattern The URI template pattern. 963 | # @param [#match] processor The template processor to use. 964 | # 965 | # @return [Array, Regexp] 966 | # An array of expansion variables nad a regular expression which may be 967 | # used to parse a template pattern 968 | def parse_new_template_pattern(pattern, processor = nil) 969 | # Escape the pattern. The two gsubs restore the escaped curly braces 970 | # back to their original form. Basically, escape everything that isn't 971 | # within an expansion. 972 | escaped_pattern = Regexp.escape( 973 | pattern 974 | ).gsub(/\\\{(.*?)\\\}/) do |escaped| 975 | escaped.gsub(/\\(.)/, "\\1") 976 | end 977 | 978 | expansions = [] 979 | 980 | # Create a regular expression that captures the values of the 981 | # variables in the URI. 982 | regexp_string = escaped_pattern.gsub( EXPRESSION ) do |expansion| 983 | 984 | expansions << expansion 985 | _, operator, varlist = *expansion.match(EXPRESSION) 986 | leader = Regexp.escape(LEADERS.fetch(operator, '')) 987 | joiner = Regexp.escape(JOINERS.fetch(operator, ',')) 988 | combined = varlist.split(',').map do |varspec| 989 | _, name, modifier = *varspec.match(VARSPEC) 990 | 991 | result = processor && processor.respond_to?(:match) ? processor.match(name) : nil 992 | if result 993 | "(?<#{name}>#{ result })" 994 | else 995 | group = case operator 996 | when '+' 997 | "#{ RESERVED }*?" 998 | when '#' 999 | "#{ RESERVED }*?" 1000 | when '/' 1001 | "#{ UNRESERVED }*?" 1002 | when '.' 1003 | "#{ UNRESERVED.gsub('\.', '') }*?" 1004 | when ';' 1005 | "#{ UNRESERVED }*=?#{ UNRESERVED }*?" 1006 | when '?' 1007 | "#{ UNRESERVED }*=#{ UNRESERVED }*?" 1008 | when '&' 1009 | "#{ UNRESERVED }*=#{ UNRESERVED }*?" 1010 | else 1011 | "#{ UNRESERVED }*?" 1012 | end 1013 | if modifier == '*' 1014 | "(?<#{name}>#{group}(?:#{joiner}?#{group})*)?" 1015 | else 1016 | "(?<#{name}>#{group})?" 1017 | end 1018 | end 1019 | end.join("#{joiner}?") 1020 | "(?:|#{leader}#{combined})" 1021 | end 1022 | 1023 | # Ensure that the regular expression matches the whole URI. 1024 | regexp_string = "\\A#{regexp_string}\\z" 1025 | return expansions, Regexp.new(regexp_string) 1026 | end 1027 | 1028 | end 1029 | end 1030 | -------------------------------------------------------------------------------- /lib/addressable/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | #-- 4 | # Copyright (C) Bob Aman 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | #++ 18 | 19 | 20 | # Used to prevent the class/module from being loaded more than once 21 | if !defined?(Addressable::VERSION) 22 | module Addressable 23 | module VERSION 24 | MAJOR = 2 25 | MINOR = 8 26 | TINY = 7 27 | 28 | STRING = [MAJOR, MINOR, TINY].join('.') 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/addressable/idna_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright (C) Bob Aman 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | require "spec_helper" 19 | 20 | # Have to use RubyGems to load the idn gem. 21 | require "rubygems" 22 | 23 | require "addressable/idna" 24 | 25 | shared_examples_for "converting from unicode to ASCII" do 26 | it "should convert 'www.google.com' correctly" do 27 | expect(Addressable::IDNA.to_ascii("www.google.com")).to eq("www.google.com") 28 | end 29 | 30 | long = 'AcinusFallumTrompetumNullunCreditumVisumEstAtCuadLongumEtCefallum.com' 31 | it "should convert '#{long}' correctly" do 32 | expect(Addressable::IDNA.to_ascii(long)).to eq(long) 33 | end 34 | 35 | it "should convert 'www.詹姆斯.com' correctly" do 36 | expect(Addressable::IDNA.to_ascii( 37 | "www.詹姆斯.com" 38 | )).to eq("www.xn--8ws00zhy3a.com") 39 | end 40 | 41 | it "also accepts unicode strings encoded as ascii-8bit" do 42 | expect(Addressable::IDNA.to_ascii( 43 | "www.詹姆斯.com".b 44 | )).to eq("www.xn--8ws00zhy3a.com") 45 | end 46 | 47 | it "should convert 'www.Iñtërnâtiônàlizætiøn.com' correctly" do 48 | "www.Iñtërnâtiônàlizætiøn.com" 49 | expect(Addressable::IDNA.to_ascii( 50 | "www.I\xC3\xB1t\xC3\xABrn\xC3\xA2ti\xC3\xB4" + 51 | "n\xC3\xA0liz\xC3\xA6ti\xC3\xB8n.com" 52 | )).to eq("www.xn--itrntinliztin-vdb0a5exd8ewcye.com") 53 | end 54 | 55 | it "should convert 'www.Iñtërnâtiônàlizætiøn.com' correctly" do 56 | expect(Addressable::IDNA.to_ascii( 57 | "www.In\xCC\x83te\xCC\x88rna\xCC\x82tio\xCC\x82n" + 58 | "a\xCC\x80liz\xC3\xA6ti\xC3\xB8n.com" 59 | )).to eq("www.xn--itrntinliztin-vdb0a5exd8ewcye.com") 60 | end 61 | 62 | it "should convert " + 63 | "'www.ほんとうにながいわけのわからないどめいんめいのらべるまだながくしないとたりない.w3.mag.keio.ac.jp' " + 64 | "correctly" do 65 | expect(Addressable::IDNA.to_ascii( 66 | "www.\343\201\273\343\202\223\343\201\250\343\201\206\343\201\253\343" + 67 | "\201\252\343\201\214\343\201\204\343\202\217\343\201\221\343\201\256" + 68 | "\343\202\217\343\201\213\343\202\211\343\201\252\343\201\204\343\201" + 69 | "\251\343\202\201\343\201\204\343\202\223\343\202\201\343\201\204\343" + 70 | "\201\256\343\202\211\343\201\271\343\202\213\343\201\276\343\201\240" + 71 | "\343\201\252\343\201\214\343\201\217\343\201\227\343\201\252\343\201" + 72 | "\204\343\201\250\343\201\237\343\202\212\343\201\252\343\201\204." + 73 | "w3.mag.keio.ac.jp" 74 | )).to eq( 75 | "www.xn--n8jaaaaai5bhf7as8fsfk3jnknefdde3" + 76 | "fg11amb5gzdb4wi9bya3kc6lra.w3.mag.keio.ac.jp" 77 | ) 78 | end 79 | 80 | it "should convert " + 81 | "'www.ほんとうにながいわけのわからないどめいんめいのらべるまだながくしないとたりない.w3.mag.keio.ac.jp' " + 82 | "correctly" do 83 | expect(Addressable::IDNA.to_ascii( 84 | "www.\343\201\273\343\202\223\343\201\250\343\201\206\343\201\253\343" + 85 | "\201\252\343\201\213\343\202\231\343\201\204\343\202\217\343\201\221" + 86 | "\343\201\256\343\202\217\343\201\213\343\202\211\343\201\252\343\201" + 87 | "\204\343\201\250\343\202\231\343\202\201\343\201\204\343\202\223\343" + 88 | "\202\201\343\201\204\343\201\256\343\202\211\343\201\270\343\202\231" + 89 | "\343\202\213\343\201\276\343\201\237\343\202\231\343\201\252\343\201" + 90 | "\213\343\202\231\343\201\217\343\201\227\343\201\252\343\201\204\343" + 91 | "\201\250\343\201\237\343\202\212\343\201\252\343\201\204." + 92 | "w3.mag.keio.ac.jp" 93 | )).to eq( 94 | "www.xn--n8jaaaaai5bhf7as8fsfk3jnknefdde3" + 95 | "fg11amb5gzdb4wi9bya3kc6lra.w3.mag.keio.ac.jp" 96 | ) 97 | end 98 | 99 | it "should convert '点心和烤鸭.w3.mag.keio.ac.jp' correctly" do 100 | expect(Addressable::IDNA.to_ascii( 101 | "点心和烤鸭.w3.mag.keio.ac.jp" 102 | )).to eq("xn--0trv4xfvn8el34t.w3.mag.keio.ac.jp") 103 | end 104 | 105 | it "should convert '가각갂갃간갅갆갇갈갉힢힣.com' correctly" do 106 | expect(Addressable::IDNA.to_ascii( 107 | "가각갂갃간갅갆갇갈갉힢힣.com" 108 | )).to eq("xn--o39acdefghijk5883jma.com") 109 | end 110 | 111 | it "should convert " + 112 | "'\347\242\274\346\250\231\346\272\226\350" + 113 | "\220\254\345\234\213\347\242\274.com' correctly" do 114 | expect(Addressable::IDNA.to_ascii( 115 | "\347\242\274\346\250\231\346\272\226\350" + 116 | "\220\254\345\234\213\347\242\274.com" 117 | )).to eq("xn--9cs565brid46mda086o.com") 118 | end 119 | 120 | it "should convert 'リ宠퐱〹.com' correctly" do 121 | expect(Addressable::IDNA.to_ascii( 122 | "\357\276\230\345\256\240\355\220\261\343\200\271.com" 123 | )).to eq("xn--eek174hoxfpr4k.com") 124 | end 125 | 126 | it "should convert 'リ宠퐱卄.com' correctly" do 127 | expect(Addressable::IDNA.to_ascii( 128 | "\343\203\252\345\256\240\355\220\261\345\215\204.com" 129 | )).to eq("xn--eek174hoxfpr4k.com") 130 | end 131 | 132 | it "should convert 'ᆵ' correctly" do 133 | expect(Addressable::IDNA.to_ascii( 134 | "\341\206\265" 135 | )).to eq("xn--4ud") 136 | end 137 | 138 | it "should convert 'ᆵ' correctly" do 139 | expect(Addressable::IDNA.to_ascii( 140 | "\357\276\257" 141 | )).to eq("xn--4ud") 142 | end 143 | 144 | it "should convert '🌹🌹🌹.ws' correctly" do 145 | expect(Addressable::IDNA.to_ascii( 146 | "\360\237\214\271\360\237\214\271\360\237\214\271.ws" 147 | )).to eq("xn--2h8haa.ws") 148 | end 149 | 150 | it "should handle two adjacent '.'s correctly" do 151 | expect(Addressable::IDNA.to_ascii( 152 | "example..host" 153 | )).to eq("example..host") 154 | end 155 | end 156 | 157 | shared_examples_for "converting from ASCII to unicode" do 158 | long = 'AcinusFallumTrompetumNullunCreditumVisumEstAtCuadLongumEtCefallum.com' 159 | it "should convert '#{long}' correctly" do 160 | expect(Addressable::IDNA.to_unicode(long)).to eq(long) 161 | end 162 | 163 | it "should return the identity conversion when punycode decode fails" do 164 | expect(Addressable::IDNA.to_unicode("xn--zckp1cyg1.sblo.jp")).to eq( 165 | "xn--zckp1cyg1.sblo.jp") 166 | end 167 | 168 | it "should return the identity conversion when the ACE prefix has no suffix" do 169 | expect(Addressable::IDNA.to_unicode("xn--...-")).to eq("xn--...-") 170 | end 171 | 172 | it "should convert 'www.google.com' correctly" do 173 | expect(Addressable::IDNA.to_unicode("www.google.com")).to eq( 174 | "www.google.com") 175 | end 176 | 177 | it "should convert 'www.詹姆斯.com' correctly" do 178 | expect(Addressable::IDNA.to_unicode( 179 | "www.xn--8ws00zhy3a.com" 180 | )).to eq("www.詹姆斯.com") 181 | end 182 | 183 | it "should convert '詹姆斯.com' correctly" do 184 | expect(Addressable::IDNA.to_unicode( 185 | "xn--8ws00zhy3a.com" 186 | )).to eq("詹姆斯.com") 187 | end 188 | 189 | it "should convert 'www.iñtërnâtiônàlizætiøn.com' correctly" do 190 | expect(Addressable::IDNA.to_unicode( 191 | "www.xn--itrntinliztin-vdb0a5exd8ewcye.com" 192 | )).to eq("www.iñtërnâtiônàlizætiøn.com") 193 | end 194 | 195 | it "should convert 'iñtërnâtiônàlizætiøn.com' correctly" do 196 | expect(Addressable::IDNA.to_unicode( 197 | "xn--itrntinliztin-vdb0a5exd8ewcye.com" 198 | )).to eq("iñtërnâtiônàlizætiøn.com") 199 | end 200 | 201 | it "should convert " + 202 | "'www.ほんとうにながいわけのわからないどめいんめいのらべるまだながくしないとたりない.w3.mag.keio.ac.jp' " + 203 | "correctly" do 204 | expect(Addressable::IDNA.to_unicode( 205 | "www.xn--n8jaaaaai5bhf7as8fsfk3jnknefdde3" + 206 | "fg11amb5gzdb4wi9bya3kc6lra.w3.mag.keio.ac.jp" 207 | )).to eq( 208 | "www.ほんとうにながいわけのわからないどめいんめいのらべるまだながくしないとたりない.w3.mag.keio.ac.jp" 209 | ) 210 | end 211 | 212 | it "should convert '点心和烤鸭.w3.mag.keio.ac.jp' correctly" do 213 | expect(Addressable::IDNA.to_unicode( 214 | "xn--0trv4xfvn8el34t.w3.mag.keio.ac.jp" 215 | )).to eq("点心和烤鸭.w3.mag.keio.ac.jp") 216 | end 217 | 218 | it "should convert '가각갂갃간갅갆갇갈갉힢힣.com' correctly" do 219 | expect(Addressable::IDNA.to_unicode( 220 | "xn--o39acdefghijk5883jma.com" 221 | )).to eq("가각갂갃간갅갆갇갈갉힢힣.com") 222 | end 223 | 224 | it "should convert " + 225 | "'\347\242\274\346\250\231\346\272\226\350" + 226 | "\220\254\345\234\213\347\242\274.com' correctly" do 227 | expect(Addressable::IDNA.to_unicode( 228 | "xn--9cs565brid46mda086o.com" 229 | )).to eq( 230 | "\347\242\274\346\250\231\346\272\226\350" + 231 | "\220\254\345\234\213\347\242\274.com" 232 | ) 233 | end 234 | 235 | it "should convert 'リ宠퐱卄.com' correctly" do 236 | expect(Addressable::IDNA.to_unicode( 237 | "xn--eek174hoxfpr4k.com" 238 | )).to eq("\343\203\252\345\256\240\355\220\261\345\215\204.com") 239 | end 240 | 241 | it "should convert 'ᆵ' correctly" do 242 | expect(Addressable::IDNA.to_unicode( 243 | "xn--4ud" 244 | )).to eq("\341\206\265") 245 | end 246 | 247 | it "should convert '🌹🌹🌹.ws' correctly" do 248 | expect(Addressable::IDNA.to_unicode( 249 | "xn--2h8haa.ws" 250 | )).to eq("\360\237\214\271\360\237\214\271\360\237\214\271.ws") 251 | end 252 | 253 | it "should handle two adjacent '.'s correctly" do 254 | expect(Addressable::IDNA.to_unicode( 255 | "example..host" 256 | )).to eq("example..host") 257 | end 258 | end 259 | 260 | describe Addressable::IDNA, "when using the pure-Ruby implementation" do 261 | before do 262 | Addressable.send(:remove_const, :IDNA) 263 | load "addressable/idna/pure.rb" 264 | end 265 | 266 | it_should_behave_like "converting from unicode to ASCII" 267 | it_should_behave_like "converting from ASCII to unicode" 268 | 269 | begin 270 | require "fiber" 271 | 272 | it "should not blow up inside fibers" do 273 | f = Fiber.new do 274 | Addressable.send(:remove_const, :IDNA) 275 | load "addressable/idna/pure.rb" 276 | end 277 | f.resume 278 | end 279 | rescue LoadError 280 | # Fibers aren't supported in this version of Ruby, skip this test. 281 | warn('Fibers unsupported.') 282 | end 283 | end 284 | 285 | begin 286 | require "idn" 287 | 288 | describe Addressable::IDNA, "when using the native-code implementation" do 289 | before do 290 | Addressable.send(:remove_const, :IDNA) 291 | load "addressable/idna/native.rb" 292 | end 293 | 294 | it_should_behave_like "converting from unicode to ASCII" 295 | it_should_behave_like "converting from ASCII to unicode" 296 | end 297 | rescue LoadError => error 298 | raise error if ENV["CI"] && TestHelper.native_supported? 299 | 300 | # Cannot test the native implementation without libidn support. 301 | warn('Could not load native IDN implementation.') 302 | end 303 | -------------------------------------------------------------------------------- /spec/addressable/net_http_compat_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright (C) Bob Aman 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | require "spec_helper" 19 | 20 | require "addressable/uri" 21 | require "net/http" 22 | 23 | describe Net::HTTP do 24 | it "should be compatible with Addressable" do 25 | response_body = 26 | Net::HTTP.get(Addressable::URI.parse('http://www.google.com/')) 27 | expect(response_body).not_to be_nil 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/addressable/security_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright (C) Bob Aman 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | require "spec_helper" 19 | 20 | require "addressable/uri" 21 | 22 | describe Addressable::URI, "when created with a URI known to cause crashes " + 23 | "in certain browsers" do 24 | it "should parse correctly" do 25 | uri = Addressable::URI.parse('%%30%30') 26 | expect(uri.path).to eq('%%30%30') 27 | expect(uri.normalize.path).to eq('%2500') 28 | end 29 | 30 | it "should parse correctly as a full URI" do 31 | uri = Addressable::URI.parse('http://www.example.com/%%30%30') 32 | expect(uri.path).to eq('/%%30%30') 33 | expect(uri.normalize.path).to eq('/%2500') 34 | end 35 | end 36 | 37 | describe Addressable::URI, "when created with a URI known to cause crashes " + 38 | "in certain browsers" do 39 | it "should parse correctly" do 40 | uri = Addressable::URI.parse('لُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ 冗') 41 | expect(uri.path).to eq('لُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ 冗') 42 | expect(uri.normalize.path).to eq( 43 | '%D9%84%D9%8F%D8%B5%D9%91%D8%A8%D9%8F%D9%84%D9%8F%D9%84%D8%B5%D9%91' + 44 | '%D8%A8%D9%8F%D8%B1%D8%B1%D9%8B%20%E0%A5%A3%20%E0%A5%A3h%20%E0%A5' + 45 | '%A3%20%E0%A5%A3%20%E5%86%97' 46 | ) 47 | end 48 | 49 | it "should parse correctly as a full URI" do 50 | uri = Addressable::URI.parse('http://www.example.com/لُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ 冗') 51 | expect(uri.path).to eq('/لُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ 冗') 52 | expect(uri.normalize.path).to eq( 53 | '/%D9%84%D9%8F%D8%B5%D9%91%D8%A8%D9%8F%D9%84%D9%8F%D9%84%D8%B5%D9%91' + 54 | '%D8%A8%D9%8F%D8%B1%D8%B1%D9%8B%20%E0%A5%A3%20%E0%A5%A3h%20%E0%A5' + 55 | '%A3%20%E0%A5%A3%20%E5%86%97' 56 | ) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/addressable/template_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright (C) Bob Aman 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | require "spec_helper" 19 | 20 | require "bigdecimal" 21 | require "timeout" 22 | require "addressable/template" 23 | 24 | shared_examples_for 'expands' do |tests| 25 | tests.each do |template, expansion| 26 | exp = expansion.is_a?(Array) ? expansion.first : expansion 27 | it "#{template} to #{exp}" do 28 | tmpl = Addressable::Template.new(template).expand(subject) 29 | expect(tmpl.to_str).to eq(expansion) 30 | end 31 | end 32 | end 33 | 34 | describe "eql?" do 35 | let(:template) { Addressable::Template.new('https://www.example.com/{foo}') } 36 | it 'is equal when the pattern matches' do 37 | other_template = Addressable::Template.new('https://www.example.com/{foo}') 38 | expect(template).to be_eql(other_template) 39 | expect(other_template).to be_eql(template) 40 | end 41 | it 'is not equal when the pattern differs' do 42 | other_template = Addressable::Template.new('https://www.example.com/{bar}') 43 | expect(template).to_not be_eql(other_template) 44 | expect(other_template).to_not be_eql(template) 45 | end 46 | it 'is not equal to non-templates' do 47 | uri = 'https://www.example.com/foo/bar' 48 | addressable_template = Addressable::Template.new uri 49 | addressable_uri = Addressable::URI.parse uri 50 | expect(addressable_template).to_not be_eql(addressable_uri) 51 | expect(addressable_uri).to_not be_eql(addressable_template) 52 | end 53 | end 54 | 55 | describe "==" do 56 | let(:template) { Addressable::Template.new('https://www.example.com/{foo}') } 57 | it 'is equal when the pattern matches' do 58 | other_template = Addressable::Template.new('https://www.example.com/{foo}') 59 | expect(template).to eq other_template 60 | expect(other_template).to eq template 61 | end 62 | it 'is not equal when the pattern differs' do 63 | other_template = Addressable::Template.new('https://www.example.com/{bar}') 64 | expect(template).not_to eq other_template 65 | expect(other_template).not_to eq template 66 | end 67 | it 'is not equal to non-templates' do 68 | uri = 'https://www.example.com/foo/bar' 69 | addressable_template = Addressable::Template.new uri 70 | addressable_uri = Addressable::URI.parse uri 71 | expect(addressable_template).not_to eq addressable_uri 72 | expect(addressable_uri).not_to eq addressable_template 73 | end 74 | end 75 | 76 | describe "#to_regexp" do 77 | it "does not match the first line of multiline strings" do 78 | uri = "https://www.example.com/bar" 79 | template = Addressable::Template.new(uri) 80 | expect(template.match(uri)).not_to be_nil 81 | expect(template.match("#{uri}\ngarbage")).to be_nil 82 | end 83 | end 84 | 85 | describe "Type conversion" do 86 | subject { 87 | { 88 | :var => true, 89 | :hello => 1234, 90 | :nothing => nil, 91 | :sym => :symbolic, 92 | :decimal => BigDecimal('1') 93 | } 94 | } 95 | 96 | it_behaves_like 'expands', { 97 | '{var}' => 'true', 98 | '{hello}' => '1234', 99 | '{nothing}' => '', 100 | '{sym}' => 'symbolic', 101 | '{decimal}' => RUBY_VERSION < '2.4.0' ? '0.1E1' : '0.1e1' 102 | } 103 | end 104 | 105 | describe "Level 1:" do 106 | subject { 107 | {:var => "value", :hello => "Hello World!"} 108 | } 109 | it_behaves_like 'expands', { 110 | '{var}' => 'value', 111 | '{hello}' => 'Hello%20World%21' 112 | } 113 | end 114 | 115 | describe "Level 2" do 116 | subject { 117 | { 118 | :var => "value", 119 | :hello => "Hello World!", 120 | :path => "/foo/bar" 121 | } 122 | } 123 | context "Operator +:" do 124 | it_behaves_like 'expands', { 125 | '{+var}' => 'value', 126 | '{+hello}' => 'Hello%20World!', 127 | '{+path}/here' => '/foo/bar/here', 128 | 'here?ref={+path}' => 'here?ref=/foo/bar' 129 | } 130 | end 131 | context "Operator #:" do 132 | it_behaves_like 'expands', { 133 | 'X{#var}' => 'X#value', 134 | 'X{#hello}' => 'X#Hello%20World!' 135 | } 136 | end 137 | end 138 | 139 | describe "Level 3" do 140 | subject { 141 | { 142 | :var => "value", 143 | :hello => "Hello World!", 144 | :empty => "", 145 | :path => "/foo/bar", 146 | :x => "1024", 147 | :y => "768" 148 | } 149 | } 150 | context "Operator nil (multiple vars):" do 151 | it_behaves_like 'expands', { 152 | 'map?{x,y}' => 'map?1024,768', 153 | '{x,hello,y}' => '1024,Hello%20World%21,768' 154 | } 155 | end 156 | context "Operator + (multiple vars):" do 157 | it_behaves_like 'expands', { 158 | '{+x,hello,y}' => '1024,Hello%20World!,768', 159 | '{+path,x}/here' => '/foo/bar,1024/here' 160 | } 161 | end 162 | context "Operator # (multiple vars):" do 163 | it_behaves_like 'expands', { 164 | '{#x,hello,y}' => '#1024,Hello%20World!,768', 165 | '{#path,x}/here' => '#/foo/bar,1024/here' 166 | } 167 | end 168 | context "Operator ." do 169 | it_behaves_like 'expands', { 170 | 'X{.var}' => 'X.value', 171 | 'X{.x,y}' => 'X.1024.768' 172 | } 173 | end 174 | context "Operator /" do 175 | it_behaves_like 'expands', { 176 | '{/var}' => '/value', 177 | '{/var,x}/here' => '/value/1024/here' 178 | } 179 | end 180 | context "Operator ;" do 181 | it_behaves_like 'expands', { 182 | '{;x,y}' => ';x=1024;y=768', 183 | '{;x,y,empty}' => ';x=1024;y=768;empty' 184 | } 185 | end 186 | context "Operator ?" do 187 | it_behaves_like 'expands', { 188 | '{?x,y}' => '?x=1024&y=768', 189 | '{?x,y,empty}' => '?x=1024&y=768&empty=' 190 | } 191 | end 192 | context "Operator &" do 193 | it_behaves_like 'expands', { 194 | '?fixed=yes{&x}' => '?fixed=yes&x=1024', 195 | '{&x,y,empty}' => '&x=1024&y=768&empty=' 196 | } 197 | end 198 | end 199 | 200 | describe "Level 4" do 201 | subject { 202 | { 203 | :var => "value", 204 | :hello => "Hello World!", 205 | :path => "/foo/bar", 206 | :semi => ";", 207 | :list => %w(red green blue), 208 | :keys => {"semi" => ';', "dot" => '.', :comma => ','} 209 | } 210 | } 211 | context "Expansion with value modifiers" do 212 | it_behaves_like 'expands', { 213 | '{var:3}' => 'val', 214 | '{var:30}' => 'value', 215 | '{list}' => 'red,green,blue', 216 | '{list*}' => 'red,green,blue', 217 | '{keys}' => 'semi,%3B,dot,.,comma,%2C', 218 | '{keys*}' => 'semi=%3B,dot=.,comma=%2C', 219 | } 220 | end 221 | context "Operator + with value modifiers" do 222 | it_behaves_like 'expands', { 223 | '{+path:6}/here' => '/foo/b/here', 224 | '{+list}' => 'red,green,blue', 225 | '{+list*}' => 'red,green,blue', 226 | '{+keys}' => 'semi,;,dot,.,comma,,', 227 | '{+keys*}' => 'semi=;,dot=.,comma=,', 228 | } 229 | end 230 | context "Operator # with value modifiers" do 231 | it_behaves_like 'expands', { 232 | '{#path:6}/here' => '#/foo/b/here', 233 | '{#list}' => '#red,green,blue', 234 | '{#list*}' => '#red,green,blue', 235 | '{#keys}' => '#semi,;,dot,.,comma,,', 236 | '{#keys*}' => '#semi=;,dot=.,comma=,', 237 | } 238 | end 239 | context "Operator . with value modifiers" do 240 | it_behaves_like 'expands', { 241 | 'X{.var:3}' => 'X.val', 242 | 'X{.list}' => 'X.red,green,blue', 243 | 'X{.list*}' => 'X.red.green.blue', 244 | 'X{.keys}' => 'X.semi,%3B,dot,.,comma,%2C', 245 | 'X{.keys*}' => 'X.semi=%3B.dot=..comma=%2C', 246 | } 247 | end 248 | context "Operator / with value modifiers" do 249 | it_behaves_like 'expands', { 250 | '{/var:1,var}' => '/v/value', 251 | '{/list}' => '/red,green,blue', 252 | '{/list*}' => '/red/green/blue', 253 | '{/list*,path:4}' => '/red/green/blue/%2Ffoo', 254 | '{/keys}' => '/semi,%3B,dot,.,comma,%2C', 255 | '{/keys*}' => '/semi=%3B/dot=./comma=%2C', 256 | } 257 | end 258 | context "Operator ; with value modifiers" do 259 | it_behaves_like 'expands', { 260 | '{;hello:5}' => ';hello=Hello', 261 | '{;list}' => ';list=red,green,blue', 262 | '{;list*}' => ';list=red;list=green;list=blue', 263 | '{;keys}' => ';keys=semi,%3B,dot,.,comma,%2C', 264 | '{;keys*}' => ';semi=%3B;dot=.;comma=%2C', 265 | } 266 | end 267 | context "Operator ? with value modifiers" do 268 | it_behaves_like 'expands', { 269 | '{?var:3}' => '?var=val', 270 | '{?list}' => '?list=red,green,blue', 271 | '{?list*}' => '?list=red&list=green&list=blue', 272 | '{?keys}' => '?keys=semi,%3B,dot,.,comma,%2C', 273 | '{?keys*}' => '?semi=%3B&dot=.&comma=%2C', 274 | } 275 | end 276 | context "Operator & with value modifiers" do 277 | it_behaves_like 'expands', { 278 | '{&var:3}' => '&var=val', 279 | '{&list}' => '&list=red,green,blue', 280 | '{&list*}' => '&list=red&list=green&list=blue', 281 | '{&keys}' => '&keys=semi,%3B,dot,.,comma,%2C', 282 | '{&keys*}' => '&semi=%3B&dot=.&comma=%2C', 283 | } 284 | end 285 | end 286 | describe "Modifiers" do 287 | subject { 288 | { 289 | :var => "value", 290 | :semi => ";", 291 | :year => [1965, 2000, 2012], 292 | :dom => %w(example com) 293 | } 294 | } 295 | context "length" do 296 | it_behaves_like 'expands', { 297 | '{var:3}' => 'val', 298 | '{var:30}' => 'value', 299 | '{var}' => 'value', 300 | '{semi}' => '%3B', 301 | '{semi:2}' => '%3B' 302 | } 303 | end 304 | context "explode" do 305 | it_behaves_like 'expands', { 306 | 'find{?year*}' => 'find?year=1965&year=2000&year=2012', 307 | 'www{.dom*}' => 'www.example.com', 308 | } 309 | end 310 | end 311 | describe "Expansion" do 312 | subject { 313 | { 314 | :count => ["one", "two", "three"], 315 | :dom => ["example", "com"], 316 | :dub => "me/too", 317 | :hello => "Hello World!", 318 | :half => "50%", 319 | :var => "value", 320 | :who => "fred", 321 | :base => "http://example.com/home/", 322 | :path => "/foo/bar", 323 | :list => ["red", "green", "blue"], 324 | :keys => {"semi" => ";","dot" => ".",:comma => ","}, 325 | :v => "6", 326 | :x => "1024", 327 | :y => "768", 328 | :empty => "", 329 | :empty_keys => {}, 330 | :undef => nil 331 | } 332 | } 333 | context "concatenation" do 334 | it_behaves_like 'expands', { 335 | '{count}' => 'one,two,three', 336 | '{count*}' => 'one,two,three', 337 | '{/count}' => '/one,two,three', 338 | '{/count*}' => '/one/two/three', 339 | '{;count}' => ';count=one,two,three', 340 | '{;count*}' => ';count=one;count=two;count=three', 341 | '{?count}' => '?count=one,two,three', 342 | '{?count*}' => '?count=one&count=two&count=three', 343 | '{&count*}' => '&count=one&count=two&count=three' 344 | } 345 | end 346 | context "simple expansion" do 347 | it_behaves_like 'expands', { 348 | '{var}' => 'value', 349 | '{hello}' => 'Hello%20World%21', 350 | '{half}' => '50%25', 351 | 'O{empty}X' => 'OX', 352 | 'O{undef}X' => 'OX', 353 | '{x,y}' => '1024,768', 354 | '{x,hello,y}' => '1024,Hello%20World%21,768', 355 | '?{x,empty}' => '?1024,', 356 | '?{x,undef}' => '?1024', 357 | '?{undef,y}' => '?768', 358 | '{var:3}' => 'val', 359 | '{var:30}' => 'value', 360 | '{list}' => 'red,green,blue', 361 | '{list*}' => 'red,green,blue', 362 | '{keys}' => 'semi,%3B,dot,.,comma,%2C', 363 | '{keys*}' => 'semi=%3B,dot=.,comma=%2C', 364 | } 365 | end 366 | context "reserved expansion (+)" do 367 | it_behaves_like 'expands', { 368 | '{+var}' => 'value', 369 | '{+hello}' => 'Hello%20World!', 370 | '{+half}' => '50%25', 371 | '{base}index' => 'http%3A%2F%2Fexample.com%2Fhome%2Findex', 372 | '{+base}index' => 'http://example.com/home/index', 373 | 'O{+empty}X' => 'OX', 374 | 'O{+undef}X' => 'OX', 375 | '{+path}/here' => '/foo/bar/here', 376 | 'here?ref={+path}' => 'here?ref=/foo/bar', 377 | 'up{+path}{var}/here' => 'up/foo/barvalue/here', 378 | '{+x,hello,y}' => '1024,Hello%20World!,768', 379 | '{+path,x}/here' => '/foo/bar,1024/here', 380 | '{+path:6}/here' => '/foo/b/here', 381 | '{+list}' => 'red,green,blue', 382 | '{+list*}' => 'red,green,blue', 383 | '{+keys}' => 'semi,;,dot,.,comma,,', 384 | '{+keys*}' => 'semi=;,dot=.,comma=,', 385 | } 386 | end 387 | context "fragment expansion (#)" do 388 | it_behaves_like 'expands', { 389 | '{#var}' => '#value', 390 | '{#hello}' => '#Hello%20World!', 391 | '{#half}' => '#50%25', 392 | 'foo{#empty}' => 'foo#', 393 | 'foo{#undef}' => 'foo', 394 | '{#x,hello,y}' => '#1024,Hello%20World!,768', 395 | '{#path,x}/here' => '#/foo/bar,1024/here', 396 | '{#path:6}/here' => '#/foo/b/here', 397 | '{#list}' => '#red,green,blue', 398 | '{#list*}' => '#red,green,blue', 399 | '{#keys}' => '#semi,;,dot,.,comma,,', 400 | '{#keys*}' => '#semi=;,dot=.,comma=,', 401 | } 402 | end 403 | context "label expansion (.)" do 404 | it_behaves_like 'expands', { 405 | '{.who}' => '.fred', 406 | '{.who,who}' => '.fred.fred', 407 | '{.half,who}' => '.50%25.fred', 408 | 'www{.dom*}' => 'www.example.com', 409 | 'X{.var}' => 'X.value', 410 | 'X{.empty}' => 'X.', 411 | 'X{.undef}' => 'X', 412 | 'X{.var:3}' => 'X.val', 413 | 'X{.list}' => 'X.red,green,blue', 414 | 'X{.list*}' => 'X.red.green.blue', 415 | 'X{.keys}' => 'X.semi,%3B,dot,.,comma,%2C', 416 | 'X{.keys*}' => 'X.semi=%3B.dot=..comma=%2C', 417 | 'X{.empty_keys}' => 'X', 418 | 'X{.empty_keys*}' => 'X' 419 | } 420 | end 421 | context "path expansion (/)" do 422 | it_behaves_like 'expands', { 423 | '{/who}' => '/fred', 424 | '{/who,who}' => '/fred/fred', 425 | '{/half,who}' => '/50%25/fred', 426 | '{/who,dub}' => '/fred/me%2Ftoo', 427 | '{/var}' => '/value', 428 | '{/var,empty}' => '/value/', 429 | '{/var,undef}' => '/value', 430 | '{/var,x}/here' => '/value/1024/here', 431 | '{/var:1,var}' => '/v/value', 432 | '{/list}' => '/red,green,blue', 433 | '{/list*}' => '/red/green/blue', 434 | '{/list*,path:4}' => '/red/green/blue/%2Ffoo', 435 | '{/keys}' => '/semi,%3B,dot,.,comma,%2C', 436 | '{/keys*}' => '/semi=%3B/dot=./comma=%2C', 437 | } 438 | end 439 | context "path-style expansion (;)" do 440 | it_behaves_like 'expands', { 441 | '{;who}' => ';who=fred', 442 | '{;half}' => ';half=50%25', 443 | '{;empty}' => ';empty', 444 | '{;v,empty,who}' => ';v=6;empty;who=fred', 445 | '{;v,bar,who}' => ';v=6;who=fred', 446 | '{;x,y}' => ';x=1024;y=768', 447 | '{;x,y,empty}' => ';x=1024;y=768;empty', 448 | '{;x,y,undef}' => ';x=1024;y=768', 449 | '{;hello:5}' => ';hello=Hello', 450 | '{;list}' => ';list=red,green,blue', 451 | '{;list*}' => ';list=red;list=green;list=blue', 452 | '{;keys}' => ';keys=semi,%3B,dot,.,comma,%2C', 453 | '{;keys*}' => ';semi=%3B;dot=.;comma=%2C', 454 | } 455 | end 456 | context "form query expansion (?)" do 457 | it_behaves_like 'expands', { 458 | '{?who}' => '?who=fred', 459 | '{?half}' => '?half=50%25', 460 | '{?x,y}' => '?x=1024&y=768', 461 | '{?x,y,empty}' => '?x=1024&y=768&empty=', 462 | '{?x,y,undef}' => '?x=1024&y=768', 463 | '{?var:3}' => '?var=val', 464 | '{?list}' => '?list=red,green,blue', 465 | '{?list*}' => '?list=red&list=green&list=blue', 466 | '{?keys}' => '?keys=semi,%3B,dot,.,comma,%2C', 467 | '{?keys*}' => '?semi=%3B&dot=.&comma=%2C', 468 | } 469 | end 470 | context "form query expansion (&)" do 471 | it_behaves_like 'expands', { 472 | '{&who}' => '&who=fred', 473 | '{&half}' => '&half=50%25', 474 | '?fixed=yes{&x}' => '?fixed=yes&x=1024', 475 | '{&x,y,empty}' => '&x=1024&y=768&empty=', 476 | '{&x,y,undef}' => '&x=1024&y=768', 477 | '{&var:3}' => '&var=val', 478 | '{&list}' => '&list=red,green,blue', 479 | '{&list*}' => '&list=red&list=green&list=blue', 480 | '{&keys}' => '&keys=semi,%3B,dot,.,comma,%2C', 481 | '{&keys*}' => '&semi=%3B&dot=.&comma=%2C', 482 | } 483 | end 484 | context "non-string key in match data" do 485 | subject {Addressable::Template.new("http://example.com/{one}")} 486 | 487 | it "raises TypeError" do 488 | expect { subject.expand(Object.new => "1") }.to raise_error TypeError 489 | end 490 | end 491 | end 492 | 493 | class ExampleTwoProcessor 494 | def self.restore(name, value) 495 | return value.gsub(/-/, " ") if name == "query" 496 | return value 497 | end 498 | 499 | def self.match(name) 500 | return ".*?" if name == "first" 501 | return ".*" 502 | end 503 | def self.validate(name, value) 504 | return !!(value =~ /^[\w ]+$/) if name == "query" 505 | return true 506 | end 507 | 508 | def self.transform(name, value) 509 | return value.gsub(/ /, "+") if name == "query" 510 | return value 511 | end 512 | end 513 | 514 | class DumbProcessor 515 | def self.match(name) 516 | return ".*?" if name == "first" 517 | end 518 | end 519 | 520 | describe Addressable::Template do 521 | describe 'initialize' do 522 | context 'with a non-string' do 523 | it 'raises a TypeError' do 524 | expect { Addressable::Template.new(nil) }.to raise_error(TypeError) 525 | end 526 | end 527 | end 528 | 529 | describe 'freeze' do 530 | subject { Addressable::Template.new("http://example.com/{first}/{+second}/") } 531 | it 'freezes the template' do 532 | expect(subject.freeze).to be_frozen 533 | end 534 | end 535 | 536 | describe "Matching" do 537 | let(:uri){ 538 | Addressable::URI.parse( 539 | "http://example.com/search/an-example-search-query/" 540 | ) 541 | } 542 | let(:uri2){ 543 | Addressable::URI.parse("http://example.com/a/b/c/") 544 | } 545 | let(:uri3){ 546 | Addressable::URI.parse("http://example.com/;a=1;b=2;c=3;first=foo") 547 | } 548 | let(:uri4){ 549 | Addressable::URI.parse("http://example.com/?a=1&b=2&c=3&first=foo") 550 | } 551 | let(:uri5){ 552 | "http://example.com/foo" 553 | } 554 | context "first uri with ExampleTwoProcessor" do 555 | subject { 556 | Addressable::Template.new( 557 | "http://example.com/search/{query}/" 558 | ).match(uri, ExampleTwoProcessor) 559 | } 560 | its(:variables){ should == ["query"] } 561 | its(:captures){ should == ["an example search query"] } 562 | end 563 | 564 | context "second uri with ExampleTwoProcessor" do 565 | subject { 566 | Addressable::Template.new( 567 | "http://example.com/{first}/{+second}/" 568 | ).match(uri2, ExampleTwoProcessor) 569 | } 570 | its(:variables){ should == ["first", "second"] } 571 | its(:captures){ should == ["a", "b/c"] } 572 | end 573 | 574 | context "second uri with DumbProcessor" do 575 | subject { 576 | Addressable::Template.new( 577 | "http://example.com/{first}/{+second}/" 578 | ).match(uri2, DumbProcessor) 579 | } 580 | its(:variables){ should == ["first", "second"] } 581 | its(:captures){ should == ["a", "b/c"] } 582 | end 583 | 584 | context "second uri" do 585 | subject { 586 | Addressable::Template.new( 587 | "http://example.com/{first}{/second*}/" 588 | ).match(uri2) 589 | } 590 | its(:variables){ should == ["first", "second"] } 591 | its(:captures){ should == ["a", ["b","c"]] } 592 | end 593 | context "third uri" do 594 | subject { 595 | Addressable::Template.new( 596 | "http://example.com/{;hash*,first}" 597 | ).match(uri3) 598 | } 599 | its(:variables){ should == ["hash", "first"] } 600 | its(:captures){ should == [ 601 | {"a" => "1", "b" => "2", "c" => "3", "first" => "foo"}, nil] } 602 | end 603 | # Note that this expansion is impossible to revert deterministically - the 604 | # * operator means first could have been a key of hash or a separate key. 605 | # Semantically, a separate key is more likely, but both are possible. 606 | context "fourth uri" do 607 | subject { 608 | Addressable::Template.new( 609 | "http://example.com/{?hash*,first}" 610 | ).match(uri4) 611 | } 612 | its(:variables){ should == ["hash", "first"] } 613 | its(:captures){ should == [ 614 | {"a" => "1", "b" => "2", "c" => "3", "first"=> "foo"}, nil] } 615 | end 616 | context "fifth uri" do 617 | subject { 618 | Addressable::Template.new( 619 | "http://example.com/{path}{?hash*,first}" 620 | ).match(uri5) 621 | } 622 | its(:variables){ should == ["path", "hash", "first"] } 623 | its(:captures){ should == ["foo", nil, nil] } 624 | end 625 | end 626 | 627 | describe 'match' do 628 | subject { Addressable::Template.new('http://example.com/first/second/') } 629 | context 'when the URI is the same as the template' do 630 | it 'returns the match data itself with an empty mapping' do 631 | uri = Addressable::URI.parse('http://example.com/first/second/') 632 | match_data = subject.match(uri) 633 | expect(match_data).to be_an Addressable::Template::MatchData 634 | expect(match_data.uri).to eq(uri) 635 | expect(match_data.template).to eq(subject) 636 | expect(match_data.mapping).to be_empty 637 | expect(match_data.inspect).to be_an String 638 | end 639 | end 640 | end 641 | 642 | describe "extract" do 643 | let(:template) { 644 | Addressable::Template.new( 645 | "http://{host}{/segments*}/{?one,two,bogus}{#fragment}" 646 | ) 647 | } 648 | let(:uri){ "http://example.com/a/b/c/?one=1&two=2#foo" } 649 | let(:uri2){ "http://example.com/a/b/c/#foo" } 650 | it "should be able to extract with queries" do 651 | expect(template.extract(uri)).to eq({ 652 | "host" => "example.com", 653 | "segments" => %w(a b c), 654 | "one" => "1", 655 | "bogus" => nil, 656 | "two" => "2", 657 | "fragment" => "foo" 658 | }) 659 | end 660 | it "should be able to extract without queries" do 661 | expect(template.extract(uri2)).to eq({ 662 | "host" => "example.com", 663 | "segments" => %w(a b c), 664 | "one" => nil, 665 | "bogus" => nil, 666 | "two" => nil, 667 | "fragment" => "foo" 668 | }) 669 | end 670 | 671 | context "issue #137" do 672 | subject { Addressable::Template.new('/path{?page,per_page}') } 673 | 674 | it "can match empty" do 675 | data = subject.extract("/path") 676 | expect(data["page"]).to eq(nil) 677 | expect(data["per_page"]).to eq(nil) 678 | expect(data.keys.sort).to eq(['page', 'per_page']) 679 | end 680 | 681 | it "can match first var" do 682 | data = subject.extract("/path?page=1") 683 | expect(data["page"]).to eq("1") 684 | expect(data["per_page"]).to eq(nil) 685 | expect(data.keys.sort).to eq(['page', 'per_page']) 686 | end 687 | 688 | it "can match second var" do 689 | data = subject.extract("/path?per_page=1") 690 | expect(data["page"]).to eq(nil) 691 | expect(data["per_page"]).to eq("1") 692 | expect(data.keys.sort).to eq(['page', 'per_page']) 693 | end 694 | 695 | it "can match both vars" do 696 | data = subject.extract("/path?page=2&per_page=1") 697 | expect(data["page"]).to eq("2") 698 | expect(data["per_page"]).to eq("1") 699 | expect(data.keys.sort).to eq(['page', 'per_page']) 700 | end 701 | end 702 | end 703 | 704 | describe "Partial expand with symbols" do 705 | context "partial_expand with two simple values" do 706 | subject { 707 | Addressable::Template.new("http://example.com/{one}/{two}/") 708 | } 709 | it "builds a new pattern" do 710 | expect(subject.partial_expand(:one => "1").pattern).to eq( 711 | "http://example.com/1/{two}/" 712 | ) 713 | end 714 | end 715 | context "partial_expand query with missing param in middle" do 716 | subject { 717 | Addressable::Template.new("http://example.com/{?one,two,three}/") 718 | } 719 | it "builds a new pattern" do 720 | expect(subject.partial_expand(:one => "1", :three => "3").pattern).to eq( 721 | "http://example.com/?one=1{&two}&three=3/" 722 | ) 723 | end 724 | end 725 | context "partial_expand form style query with missing param at beginning" do 726 | subject { 727 | Addressable::Template.new("http://example.com/{?one,two}/") 728 | } 729 | it "builds a new pattern" do 730 | expect(subject.partial_expand(:two => "2").pattern).to eq( 731 | "http://example.com/?two=2{&one}/" 732 | ) 733 | end 734 | end 735 | context "issue #307 - partial_expand form query with nil params" do 736 | subject do 737 | Addressable::Template.new("http://example.com/{?one,two,three}/") 738 | end 739 | it "builds a new pattern with two=nil" do 740 | expect(subject.partial_expand(two: nil).pattern).to eq( 741 | "http://example.com/{?one}{&three}/" 742 | ) 743 | end 744 | it "builds a new pattern with one=nil and two=nil" do 745 | expect(subject.partial_expand(one: nil, two: nil).pattern).to eq( 746 | "http://example.com/{?three}/" 747 | ) 748 | end 749 | it "builds a new pattern with one=1 and two=nil" do 750 | expect(subject.partial_expand(one: 1, two: nil).pattern).to eq( 751 | "http://example.com/?one=1{&three}/" 752 | ) 753 | end 754 | it "builds a new pattern with one=nil and two=2" do 755 | expect(subject.partial_expand(one: nil, two: 2).pattern).to eq( 756 | "http://example.com/?two=2{&three}/" 757 | ) 758 | end 759 | it "builds a new pattern with one=nil" do 760 | expect(subject.partial_expand(one: nil).pattern).to eq( 761 | "http://example.com/{?two}{&three}/" 762 | ) 763 | end 764 | end 765 | context "partial_expand with query string" do 766 | subject { 767 | Addressable::Template.new("http://example.com/{?two,one}/") 768 | } 769 | it "builds a new pattern" do 770 | expect(subject.partial_expand(:one => "1").pattern).to eq( 771 | "http://example.com/?one=1{&two}/" 772 | ) 773 | end 774 | end 775 | context "partial_expand with path operator" do 776 | subject { 777 | Addressable::Template.new("http://example.com{/one,two}/") 778 | } 779 | it "builds a new pattern" do 780 | expect(subject.partial_expand(:one => "1").pattern).to eq( 781 | "http://example.com/1{/two}/" 782 | ) 783 | end 784 | end 785 | context "partial expand with unicode values" do 786 | subject do 787 | Addressable::Template.new("http://example.com/{resource}/{query}/") 788 | end 789 | it "normalizes unicode by default" do 790 | template = subject.partial_expand("query" => "Cafe\u0301") 791 | expect(template.pattern).to eq( 792 | "http://example.com/{resource}/Caf%C3%A9/" 793 | ) 794 | end 795 | 796 | it "normalizes as unicode even with wrong encoding specified" do 797 | template = subject.partial_expand("query" => "Cafe\u0301".b) 798 | expect(template.pattern).to eq( 799 | "http://example.com/{resource}/Caf%C3%A9/" 800 | ) 801 | end 802 | 803 | it "raises on invalid unicode input" do 804 | expect { 805 | subject.partial_expand("query" => "M\xE9thode".b) 806 | }.to raise_error(ArgumentError, "invalid byte sequence in UTF-8") 807 | end 808 | 809 | it "does not normalize unicode when byte semantics requested" do 810 | template = subject.partial_expand({"query" => "Cafe\u0301"}, nil, false) 811 | expect(template.pattern).to eq( 812 | "http://example.com/{resource}/Cafe%CC%81/" 813 | ) 814 | end 815 | end 816 | end 817 | describe "Partial expand with strings" do 818 | context "partial_expand with two simple values" do 819 | subject { 820 | Addressable::Template.new("http://example.com/{one}/{two}/") 821 | } 822 | it "builds a new pattern" do 823 | expect(subject.partial_expand("one" => "1").pattern).to eq( 824 | "http://example.com/1/{two}/" 825 | ) 826 | end 827 | end 828 | context "partial_expand query with missing param in middle" do 829 | subject { 830 | Addressable::Template.new("http://example.com/{?one,two,three}/") 831 | } 832 | it "builds a new pattern" do 833 | expect(subject.partial_expand("one" => "1", "three" => "3").pattern).to eq( 834 | "http://example.com/?one=1{&two}&three=3/" 835 | ) 836 | end 837 | end 838 | context "partial_expand with query string" do 839 | subject { 840 | Addressable::Template.new("http://example.com/{?two,one}/") 841 | } 842 | it "builds a new pattern" do 843 | expect(subject.partial_expand("one" => "1").pattern).to eq( 844 | "http://example.com/?one=1{&two}/" 845 | ) 846 | end 847 | end 848 | context "partial_expand with path operator" do 849 | subject { 850 | Addressable::Template.new("http://example.com{/one,two}/") 851 | } 852 | it "builds a new pattern" do 853 | expect(subject.partial_expand("one" => "1").pattern).to eq( 854 | "http://example.com/1{/two}/" 855 | ) 856 | end 857 | end 858 | end 859 | describe "Expand" do 860 | context "expand with unicode values" do 861 | subject do 862 | Addressable::Template.new("http://example.com/search/{query}/") 863 | end 864 | it "normalizes unicode by default" do 865 | uri = subject.expand("query" => "Cafe\u0301").to_str 866 | expect(uri).to eq("http://example.com/search/Caf%C3%A9/") 867 | end 868 | 869 | it "normalizes as unicode even with wrong encoding specified" do 870 | uri = subject.expand("query" => "Cafe\u0301".b).to_str 871 | expect(uri).to eq("http://example.com/search/Caf%C3%A9/") 872 | end 873 | 874 | it "raises on invalid unicode input" do 875 | expect { 876 | subject.expand("query" => "M\xE9thode".b).to_str 877 | }.to raise_error(ArgumentError, "invalid byte sequence in UTF-8") 878 | end 879 | 880 | it "does not normalize unicode when byte semantics requested" do 881 | uri = subject.expand({ "query" => "Cafe\u0301" }, nil, false).to_str 882 | expect(uri).to eq("http://example.com/search/Cafe%CC%81/") 883 | end 884 | end 885 | context "expand with a processor" do 886 | subject { 887 | Addressable::Template.new("http://example.com/search/{query}/") 888 | } 889 | it "processes spaces" do 890 | expect(subject.expand({"query" => "an example search query"}, 891 | ExampleTwoProcessor).to_str).to eq( 892 | "http://example.com/search/an+example+search+query/" 893 | ) 894 | end 895 | it "validates" do 896 | expect{ 897 | subject.expand({"query" => "Bogus!"}, 898 | ExampleTwoProcessor).to_str 899 | }.to raise_error(Addressable::Template::InvalidTemplateValueError) 900 | end 901 | end 902 | context "partial_expand query with missing param in middle" do 903 | subject { 904 | Addressable::Template.new("http://example.com/{?one,two,three}/") 905 | } 906 | it "builds a new pattern" do 907 | expect(subject.partial_expand("one" => "1", "three" => "3").pattern).to eq( 908 | "http://example.com/?one=1{&two}&three=3/" 909 | ) 910 | end 911 | end 912 | context "partial_expand with query string" do 913 | subject { 914 | Addressable::Template.new("http://example.com/{?two,one}/") 915 | } 916 | it "builds a new pattern" do 917 | expect(subject.partial_expand("one" => "1").pattern).to eq( 918 | "http://example.com/?one=1{&two}/" 919 | ) 920 | end 921 | end 922 | context "partial_expand with path operator" do 923 | subject { 924 | Addressable::Template.new("http://example.com{/one,two}/") 925 | } 926 | it "builds a new pattern" do 927 | expect(subject.partial_expand("one" => "1").pattern).to eq( 928 | "http://example.com/1{/two}/" 929 | ) 930 | end 931 | end 932 | end 933 | context "Matching with operators" do 934 | describe "Level 1:" do 935 | subject { Addressable::Template.new("foo{foo}/{bar}baz") } 936 | it "can match" do 937 | data = subject.match("foofoo/bananabaz") 938 | expect(data.mapping["foo"]).to eq("foo") 939 | expect(data.mapping["bar"]).to eq("banana") 940 | end 941 | it "can fail" do 942 | expect(subject.match("bar/foo")).to be_nil 943 | expect(subject.match("foobaz")).to be_nil 944 | end 945 | it "can match empty" do 946 | data = subject.match("foo/baz") 947 | expect(data.mapping["foo"]).to eq(nil) 948 | expect(data.mapping["bar"]).to eq(nil) 949 | end 950 | it "lists vars" do 951 | expect(subject.variables).to eq(["foo", "bar"]) 952 | end 953 | end 954 | 955 | describe "Level 2:" do 956 | subject { Addressable::Template.new("foo{+foo}{#bar}baz") } 957 | it "can match" do 958 | data = subject.match("foo/test/banana#bazbaz") 959 | expect(data.mapping["foo"]).to eq("/test/banana") 960 | expect(data.mapping["bar"]).to eq("baz") 961 | end 962 | it "can match empty level 2 #" do 963 | data = subject.match("foo/test/bananabaz") 964 | expect(data.mapping["foo"]).to eq("/test/banana") 965 | expect(data.mapping["bar"]).to eq(nil) 966 | data = subject.match("foo/test/banana#baz") 967 | expect(data.mapping["foo"]).to eq("/test/banana") 968 | expect(data.mapping["bar"]).to eq("") 969 | end 970 | it "can match empty level 2 +" do 971 | data = subject.match("foobaz") 972 | expect(data.mapping["foo"]).to eq(nil) 973 | expect(data.mapping["bar"]).to eq(nil) 974 | data = subject.match("foo#barbaz") 975 | expect(data.mapping["foo"]).to eq(nil) 976 | expect(data.mapping["bar"]).to eq("bar") 977 | end 978 | it "lists vars" do 979 | expect(subject.variables).to eq(["foo", "bar"]) 980 | end 981 | end 982 | 983 | describe "Level 3:" do 984 | context "no operator" do 985 | subject { Addressable::Template.new("foo{foo,bar}baz") } 986 | it "can match" do 987 | data = subject.match("foofoo,barbaz") 988 | expect(data.mapping["foo"]).to eq("foo") 989 | expect(data.mapping["bar"]).to eq("bar") 990 | end 991 | it "lists vars" do 992 | expect(subject.variables).to eq(["foo", "bar"]) 993 | end 994 | end 995 | context "+ operator" do 996 | subject { Addressable::Template.new("foo{+foo,bar}baz") } 997 | it "can match" do 998 | data = subject.match("foofoo/bar,barbaz") 999 | expect(data.mapping["bar"]).to eq("foo/bar,bar") 1000 | expect(data.mapping["foo"]).to eq("") 1001 | end 1002 | it "lists vars" do 1003 | expect(subject.variables).to eq(["foo", "bar"]) 1004 | end 1005 | end 1006 | context ". operator" do 1007 | subject { Addressable::Template.new("foo{.foo,bar}baz") } 1008 | it "can match" do 1009 | data = subject.match("foo.foo.barbaz") 1010 | expect(data.mapping["foo"]).to eq("foo") 1011 | expect(data.mapping["bar"]).to eq("bar") 1012 | end 1013 | it "lists vars" do 1014 | expect(subject.variables).to eq(["foo", "bar"]) 1015 | end 1016 | end 1017 | context "/ operator" do 1018 | subject { Addressable::Template.new("foo{/foo,bar}baz") } 1019 | it "can match" do 1020 | data = subject.match("foo/foo/barbaz") 1021 | expect(data.mapping["foo"]).to eq("foo") 1022 | expect(data.mapping["bar"]).to eq("bar") 1023 | end 1024 | it "lists vars" do 1025 | expect(subject.variables).to eq(["foo", "bar"]) 1026 | end 1027 | end 1028 | context "; operator" do 1029 | subject { Addressable::Template.new("foo{;foo,bar,baz}baz") } 1030 | it "can match" do 1031 | data = subject.match("foo;foo=bar%20baz;bar=foo;bazbaz") 1032 | expect(data.mapping["foo"]).to eq("bar baz") 1033 | expect(data.mapping["bar"]).to eq("foo") 1034 | expect(data.mapping["baz"]).to eq("") 1035 | end 1036 | it "lists vars" do 1037 | expect(subject.variables).to eq(%w(foo bar baz)) 1038 | end 1039 | end 1040 | context "? operator" do 1041 | context "test" do 1042 | subject { Addressable::Template.new("foo{?foo,bar}baz") } 1043 | it "can match" do 1044 | data = subject.match("foo?foo=bar%20baz&bar=foobaz") 1045 | expect(data.mapping["foo"]).to eq("bar baz") 1046 | expect(data.mapping["bar"]).to eq("foo") 1047 | end 1048 | it "lists vars" do 1049 | expect(subject.variables).to eq(%w(foo bar)) 1050 | end 1051 | end 1052 | 1053 | context "issue #137" do 1054 | subject { Addressable::Template.new('/path{?page,per_page}') } 1055 | 1056 | it "can match empty" do 1057 | data = subject.match("/path") 1058 | expect(data.mapping["page"]).to eq(nil) 1059 | expect(data.mapping["per_page"]).to eq(nil) 1060 | expect(data.mapping.keys.sort).to eq(['page', 'per_page']) 1061 | end 1062 | 1063 | it "can match first var" do 1064 | data = subject.match("/path?page=1") 1065 | expect(data.mapping["page"]).to eq("1") 1066 | expect(data.mapping["per_page"]).to eq(nil) 1067 | expect(data.mapping.keys.sort).to eq(['page', 'per_page']) 1068 | end 1069 | 1070 | it "can match second var" do 1071 | data = subject.match("/path?per_page=1") 1072 | expect(data.mapping["page"]).to eq(nil) 1073 | expect(data.mapping["per_page"]).to eq("1") 1074 | expect(data.mapping.keys.sort).to eq(['page', 'per_page']) 1075 | end 1076 | 1077 | it "can match both vars" do 1078 | data = subject.match("/path?page=2&per_page=1") 1079 | expect(data.mapping["page"]).to eq("2") 1080 | expect(data.mapping["per_page"]).to eq("1") 1081 | expect(data.mapping.keys.sort).to eq(['page', 'per_page']) 1082 | end 1083 | end 1084 | 1085 | context "issue #71" do 1086 | subject { Addressable::Template.new("http://cyberscore.dev/api/users{?username}") } 1087 | it "can match" do 1088 | data = subject.match("http://cyberscore.dev/api/users?username=foobaz") 1089 | expect(data.mapping["username"]).to eq("foobaz") 1090 | end 1091 | it "lists vars" do 1092 | expect(subject.variables).to eq(%w(username)) 1093 | expect(subject.keys).to eq(%w(username)) 1094 | end 1095 | end 1096 | end 1097 | context "& operator" do 1098 | subject { Addressable::Template.new("foo{&foo,bar}baz") } 1099 | it "can match" do 1100 | data = subject.match("foo&foo=bar%20baz&bar=foobaz") 1101 | expect(data.mapping["foo"]).to eq("bar baz") 1102 | expect(data.mapping["bar"]).to eq("foo") 1103 | end 1104 | it "lists vars" do 1105 | expect(subject.variables).to eq(%w(foo bar)) 1106 | end 1107 | end 1108 | end 1109 | end 1110 | 1111 | context "support regexes:" do 1112 | context "EXPRESSION" do 1113 | subject { Addressable::Template::EXPRESSION } 1114 | it "should be able to match an expression" do 1115 | expect(subject).to match("{foo}") 1116 | expect(subject).to match("{foo,9}") 1117 | expect(subject).to match("{foo.bar,baz}") 1118 | expect(subject).to match("{+foo.bar,baz}") 1119 | expect(subject).to match("{foo,foo%20bar}") 1120 | expect(subject).to match("{#foo:20,baz*}") 1121 | expect(subject).to match("stuff{#foo:20,baz*}things") 1122 | end 1123 | it "should fail on non vars" do 1124 | expect(subject).not_to match("!{foo") 1125 | expect(subject).not_to match("{foo.bar.}") 1126 | expect(subject).not_to match("!{}") 1127 | end 1128 | end 1129 | context "VARNAME" do 1130 | subject { Addressable::Template::VARNAME } 1131 | it "should be able to match a variable" do 1132 | expect(subject).to match("foo") 1133 | expect(subject).to match("9") 1134 | expect(subject).to match("foo.bar") 1135 | expect(subject).to match("foo_bar") 1136 | expect(subject).to match("foo_bar.baz") 1137 | expect(subject).to match("foo%20bar") 1138 | expect(subject).to match("foo%20bar.baz") 1139 | end 1140 | it "should fail on non vars" do 1141 | expect(subject).not_to match("!foo") 1142 | expect(subject).not_to match("foo.bar.") 1143 | expect(subject).not_to match("foo%2%00bar") 1144 | expect(subject).not_to match("foo_ba%r") 1145 | expect(subject).not_to match("foo_bar*") 1146 | expect(subject).not_to match("foo_bar:20") 1147 | end 1148 | 1149 | it 'should parse in a reasonable time' do 1150 | expect do 1151 | Timeout.timeout(0.1) do 1152 | expect(subject).not_to match("0"*25 + "!") 1153 | end 1154 | end.not_to raise_error 1155 | end 1156 | end 1157 | context "VARIABLE_LIST" do 1158 | subject { Addressable::Template::VARIABLE_LIST } 1159 | it "should be able to match a variable list" do 1160 | expect(subject).to match("foo,bar") 1161 | expect(subject).to match("foo") 1162 | expect(subject).to match("foo,bar*,baz") 1163 | expect(subject).to match("foo.bar,bar_baz*,baz:12") 1164 | end 1165 | it "should fail on non vars" do 1166 | expect(subject).not_to match(",foo,bar*,baz") 1167 | expect(subject).not_to match("foo,*bar,baz") 1168 | expect(subject).not_to match("foo,,bar*,baz") 1169 | end 1170 | end 1171 | context "VARSPEC" do 1172 | subject { Addressable::Template::VARSPEC } 1173 | it "should be able to match a variable with modifier" do 1174 | expect(subject).to match("9:8") 1175 | expect(subject).to match("foo.bar*") 1176 | expect(subject).to match("foo_bar:12") 1177 | expect(subject).to match("foo_bar.baz*") 1178 | expect(subject).to match("foo%20bar:12") 1179 | expect(subject).to match("foo%20bar.baz*") 1180 | end 1181 | it "should fail on non vars" do 1182 | expect(subject).not_to match("!foo") 1183 | expect(subject).not_to match("*foo") 1184 | expect(subject).not_to match("fo*o") 1185 | expect(subject).not_to match("fo:o") 1186 | expect(subject).not_to match("foo:") 1187 | end 1188 | end 1189 | end 1190 | end 1191 | 1192 | describe Addressable::Template::MatchData do 1193 | let(:template) { Addressable::Template.new('{foo}/{bar}') } 1194 | subject(:its) { template.match('ab/cd') } 1195 | its(:uri) { should == Addressable::URI.parse('ab/cd') } 1196 | its(:template) { should == template } 1197 | its(:mapping) { should == { 'foo' => 'ab', 'bar' => 'cd' } } 1198 | its(:variables) { should == ['foo', 'bar'] } 1199 | its(:keys) { should == ['foo', 'bar'] } 1200 | its(:names) { should == ['foo', 'bar'] } 1201 | its(:values) { should == ['ab', 'cd'] } 1202 | its(:captures) { should == ['ab', 'cd'] } 1203 | its(:to_a) { should == ['ab/cd', 'ab', 'cd'] } 1204 | its(:to_s) { should == 'ab/cd' } 1205 | its(:string) { should == its.to_s } 1206 | its(:pre_match) { should == "" } 1207 | its(:post_match) { should == "" } 1208 | 1209 | describe 'values_at' do 1210 | it 'returns an array with the values' do 1211 | expect(its.values_at(0, 2)).to eq(['ab/cd', 'cd']) 1212 | end 1213 | it 'allows mixing integer an string keys' do 1214 | expect(its.values_at('foo', 1)).to eq(['ab', 'ab']) 1215 | end 1216 | it 'accepts unknown keys' do 1217 | expect(its.values_at('baz', 'foo')).to eq([nil, 'ab']) 1218 | end 1219 | end 1220 | 1221 | describe '[]' do 1222 | context 'string key' do 1223 | it 'returns the corresponding capture' do 1224 | expect(its['foo']).to eq('ab') 1225 | expect(its['bar']).to eq('cd') 1226 | end 1227 | it 'returns nil for unknown keys' do 1228 | expect(its['baz']).to be_nil 1229 | end 1230 | end 1231 | context 'symbol key' do 1232 | it 'returns the corresponding capture' do 1233 | expect(its[:foo]).to eq('ab') 1234 | expect(its[:bar]).to eq('cd') 1235 | end 1236 | it 'returns nil for unknown keys' do 1237 | expect(its[:baz]).to be_nil 1238 | end 1239 | end 1240 | context 'integer key' do 1241 | it 'returns the full URI for index 0' do 1242 | expect(its[0]).to eq('ab/cd') 1243 | end 1244 | it 'returns the corresponding capture' do 1245 | expect(its[1]).to eq('ab') 1246 | expect(its[2]).to eq('cd') 1247 | end 1248 | it 'returns nil for unknown keys' do 1249 | expect(its[3]).to be_nil 1250 | end 1251 | end 1252 | context 'other key' do 1253 | it 'raises an exception' do 1254 | expect { its[Object.new] }.to raise_error(TypeError) 1255 | end 1256 | end 1257 | context 'with length' do 1258 | it 'returns an array starting at index with given length' do 1259 | expect(its[0, 2]).to eq(['ab/cd', 'ab']) 1260 | expect(its[2, 1]).to eq(['cd']) 1261 | end 1262 | end 1263 | end 1264 | end 1265 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'rspec/its' 5 | 6 | begin 7 | require 'coveralls' 8 | Coveralls.wear! do 9 | add_filter "spec/" 10 | add_filter "vendor/" 11 | end 12 | rescue LoadError 13 | warn "warning: coveralls gem not found; skipping Coveralls" 14 | require 'simplecov' 15 | SimpleCov.start do 16 | add_filter "spec/" 17 | add_filter "vendor/" 18 | end 19 | end if Gem.loaded_specs.key?("simplecov") 20 | 21 | class TestHelper 22 | def self.native_supported? 23 | mri = RUBY_ENGINE == "ruby" 24 | windows = RUBY_PLATFORM.include?("mingw") 25 | 26 | mri && !windows 27 | end 28 | end 29 | 30 | RSpec.configure do |config| 31 | config.warnings = true 32 | config.filter_run_when_matching :focus 33 | end 34 | -------------------------------------------------------------------------------- /tasks/clobber.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc "Remove all build products" 4 | task "clobber" 5 | -------------------------------------------------------------------------------- /tasks/gem.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems/package_task" 4 | 5 | namespace :gem do 6 | GEM_SPEC = Gem::Specification.new do |s| 7 | s.name = PKG_NAME 8 | s.version = PKG_VERSION 9 | s.summary = PKG_SUMMARY 10 | s.description = PKG_DESCRIPTION 11 | 12 | s.files = PKG_FILES.to_a 13 | 14 | s.extra_rdoc_files = %w( README.md ) 15 | s.rdoc_options.concat ["--main", "README.md"] 16 | 17 | if !s.respond_to?(:add_development_dependency) 18 | puts "Cannot build Gem with this version of RubyGems." 19 | exit(1) 20 | end 21 | 22 | s.required_ruby_version = ">= 2.2" 23 | 24 | s.add_runtime_dependency "public_suffix", ">= 2.0.2", "< 7.0" 25 | s.add_development_dependency "bundler", ">= 1.0", "< 3.0" 26 | 27 | s.require_path = "lib" 28 | 29 | s.author = "Bob Aman" 30 | s.email = "bob@sporkmonger.com" 31 | s.homepage = "https://github.com/sporkmonger/addressable" 32 | s.license = "Apache-2.0" 33 | s.metadata = { 34 | "changelog_uri" => "https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md#v#{PKG_VERSION}" 35 | } 36 | end 37 | 38 | Gem::PackageTask.new(GEM_SPEC) do |p| 39 | p.gem_spec = GEM_SPEC 40 | p.need_tar = true 41 | p.need_zip = true 42 | end 43 | 44 | desc "Generates .gemspec file" 45 | task :gemspec do 46 | spec_string = GEM_SPEC.to_ruby 47 | File.open("#{GEM_SPEC.name}.gemspec", "w") do |file| 48 | file.write spec_string 49 | end 50 | end 51 | 52 | desc "Show information about the gem" 53 | task :debug do 54 | puts GEM_SPEC.to_ruby 55 | end 56 | 57 | desc "Install the gem" 58 | task :install => ["clobber", "gem:package"] do 59 | sh "gem install --local ./pkg/#{GEM_SPEC.full_name}.gem" 60 | end 61 | 62 | desc "Uninstall the gem" 63 | task :uninstall do 64 | installed_list = Gem.source_index.find_name(PKG_NAME) 65 | if installed_list && 66 | (installed_list.collect { |s| s.version.to_s}.include?(PKG_VERSION)) 67 | sh( 68 | "gem uninstall --version '#{PKG_VERSION}' " + 69 | "--ignore-dependencies --executables #{PKG_NAME}" 70 | ) 71 | end 72 | end 73 | 74 | desc "Reinstall the gem" 75 | task :reinstall => [:uninstall, :install] 76 | 77 | desc "Package for release" 78 | task :release => ["gem:package", "gem:gemspec"] do |t| 79 | v = ENV["VERSION"] or abort "Must supply VERSION=x.y.z" 80 | abort "Versions don't match #{v} vs #{PROJ.version}" if v != PKG_VERSION 81 | pkg = "pkg/#{GEM_SPEC.full_name}" 82 | 83 | changelog = File.open("CHANGELOG.md") { |file| file.read } 84 | 85 | puts "Releasing #{PKG_NAME} v. #{PKG_VERSION}" 86 | Rake::Task["git:tag:create"].invoke 87 | end 88 | end 89 | 90 | desc "Alias to gem:package" 91 | task "gem" => "gem:package" 92 | 93 | task "gem:release" => "gem:gemspec" 94 | 95 | task "clobber" => ["gem:clobber_package"] 96 | -------------------------------------------------------------------------------- /tasks/git.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :git do 4 | namespace :tag do 5 | desc "List tags from the Git repository" 6 | task :list do 7 | tags = `git tag -l` 8 | tags.gsub!("\r", "") 9 | tags = tags.split("\n").sort {|a, b| b <=> a } 10 | puts tags.join("\n") 11 | end 12 | 13 | desc "Create a new tag in the Git repository" 14 | task :create do 15 | changelog = File.open("CHANGELOG.md", "r") { |file| file.read } 16 | puts "-" * 80 17 | puts changelog 18 | puts "-" * 80 19 | puts 20 | 21 | v = ENV["VERSION"] or abort "Must supply VERSION=x.y.z" 22 | abort "Versions don't match #{v} vs #{PKG_VERSION}" if v != PKG_VERSION 23 | 24 | git_status = `git status` 25 | if git_status !~ /^nothing to commit/ 26 | abort "Working directory isn't clean." 27 | end 28 | 29 | tag = "#{PKG_NAME}-#{PKG_VERSION}" 30 | msg = "Release #{PKG_NAME}-#{PKG_VERSION}" 31 | 32 | existing_tags = `git tag -l #{PKG_NAME}-*`.split('\n') 33 | if existing_tags.include?(tag) 34 | warn("Tag already exists, deleting...") 35 | unless system "git tag -d #{tag}" 36 | abort "Tag deletion failed." 37 | end 38 | end 39 | puts "Creating git tag '#{tag}'..." 40 | unless system "git tag -a -m \"#{msg}\" #{tag}" 41 | abort "Tag creation failed." 42 | end 43 | end 44 | end 45 | end 46 | 47 | task "gem:release" => "git:tag:create" 48 | -------------------------------------------------------------------------------- /tasks/metrics.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :metrics do 4 | task :lines do 5 | lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 6 | for file_name in FileList["lib/**/*.rb"] 7 | f = File.open(file_name) 8 | while line = f.gets 9 | lines += 1 10 | next if line =~ /^\s*$/ 11 | next if line =~ /^\s*#/ 12 | codelines += 1 13 | end 14 | puts "L: #{sprintf("%4d", lines)}, " + 15 | "LOC #{sprintf("%4d", codelines)} | #{file_name}" 16 | total_lines += lines 17 | total_codelines += codelines 18 | 19 | lines, codelines = 0, 0 20 | end 21 | 22 | puts "Total: Lines #{total_lines}, LOC #{total_codelines}" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /tasks/profile.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :profile do 4 | desc "Profile Template match memory allocations" 5 | task :template_match_memory do 6 | require "memory_profiler" 7 | require "addressable/template" 8 | 9 | start_at = Time.now.to_f 10 | template = Addressable::Template.new("http://example.com/{?one,two,three}") 11 | report = MemoryProfiler.report do 12 | 30_000.times do 13 | template.match( 14 | "http://example.com/?one=one&two=floo&three=me" 15 | ) 16 | end 17 | end 18 | end_at = Time.now.to_f 19 | print_options = { scale_bytes: true, normalize_paths: true } 20 | puts "\n\n" 21 | 22 | if ENV["CI"] 23 | report.pretty_print(print_options) 24 | else 25 | t_allocated = report.scale_bytes(report.total_allocated_memsize) 26 | t_retained = report.scale_bytes(report.total_retained_memsize) 27 | 28 | puts "Total allocated: #{t_allocated} (#{report.total_allocated} objects)" 29 | puts "Total retained: #{t_retained} (#{report.total_retained} objects)" 30 | puts "Took #{end_at - start_at} seconds" 31 | 32 | FileUtils.mkdir_p("tmp") 33 | report.pretty_print(to_file: "tmp/memprof.txt", **print_options) 34 | end 35 | end 36 | 37 | desc "Profile URI parse memory allocations" 38 | task :memory do 39 | require "memory_profiler" 40 | require "addressable/uri" 41 | if ENV["IDNA_MODE"] == "pure" 42 | Addressable.send(:remove_const, :IDNA) 43 | load "addressable/idna/pure.rb" 44 | end 45 | 46 | start_at = Time.now.to_f 47 | report = MemoryProfiler.report do 48 | 30_000.times do 49 | Addressable::URI.parse( 50 | "http://google.com/stuff/../?with_lots=of¶ms=asdff#!stuff" 51 | ).normalize 52 | end 53 | end 54 | end_at = Time.now.to_f 55 | print_options = { scale_bytes: true, normalize_paths: true } 56 | puts "\n\n" 57 | 58 | if ENV["CI"] 59 | report.pretty_print(**print_options) 60 | else 61 | t_allocated = report.scale_bytes(report.total_allocated_memsize) 62 | t_retained = report.scale_bytes(report.total_retained_memsize) 63 | 64 | puts "Total allocated: #{t_allocated} (#{report.total_allocated} objects)" 65 | puts "Total retained: #{t_retained} (#{report.total_retained} objects)" 66 | puts "Took #{end_at - start_at} seconds" 67 | 68 | FileUtils.mkdir_p("tmp") 69 | report.pretty_print(to_file: "tmp/memprof.txt", **print_options) 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec/core/rake_task" 4 | 5 | namespace :spec do 6 | RSpec::Core::RakeTask.new(:simplecov) do |t| 7 | t.pattern = FileList['spec/**/*_spec.rb'] 8 | t.rspec_opts = %w[--color --format documentation] unless ENV["CI"] 9 | end 10 | 11 | namespace :simplecov do 12 | desc "Browse the code coverage report." 13 | task :browse => "spec:simplecov" do 14 | require "launchy" 15 | Launchy.open("coverage/index.html") 16 | end 17 | end 18 | end 19 | 20 | desc "Alias to spec:simplecov" 21 | task "spec" => "spec:simplecov" 22 | 23 | task "clobber" => ["spec:clobber_simplecov"] 24 | -------------------------------------------------------------------------------- /tasks/yard.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rake" 4 | 5 | begin 6 | require "yard" 7 | require "yard/rake/yardoc_task" 8 | 9 | namespace :doc do 10 | desc "Generate Yardoc documentation" 11 | YARD::Rake::YardocTask.new do |yardoc| 12 | yardoc.name = "yard" 13 | yardoc.options = ["--verbose", "--markup", "markdown"] 14 | yardoc.files = FileList[ 15 | "lib/**/*.rb", "ext/**/*.c", 16 | "README.md", "CHANGELOG.md", "LICENSE.txt" 17 | ].exclude(/idna/) 18 | end 19 | end 20 | 21 | task "clobber" => ["doc:clobber_yard"] 22 | 23 | desc "Alias to doc:yard" 24 | task "doc" => "doc:yard" 25 | rescue LoadError 26 | # If yard isn't available, it's not the end of the world 27 | desc "Alias to doc:rdoc" 28 | task "doc" => "doc:rdoc" 29 | end 30 | --------------------------------------------------------------------------------