├── .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]
11 | [][actions]
12 | [][coveralls]
13 | [][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 |
--------------------------------------------------------------------------------