├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── BSDL ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── cgi.gemspec ├── ext ├── cgi │ └── escape │ │ ├── depend │ │ ├── escape.c │ │ └── extconf.rb └── java │ └── org │ └── jruby │ └── ext │ └── cgi │ └── escape │ ├── CGIEscape.java │ └── lib │ └── cgi │ └── escape.rb ├── lib ├── cgi.rb └── cgi │ ├── cookie.rb │ ├── core.rb │ ├── escape.rb │ ├── html.rb │ ├── session.rb │ ├── session │ └── pstore.rb │ └── util.rb ├── rakelib ├── changelogs.rake ├── check.rake ├── epoch.rake └── version.rake └── test ├── cgi ├── test_cgi_cookie.rb ├── test_cgi_core.rb ├── test_cgi_escape.rb ├── test_cgi_header.rb ├── test_cgi_modruby.rb ├── test_cgi_multipart.rb ├── test_cgi_session.rb ├── test_cgi_tag_helper.rb ├── test_cgi_util.rb ├── testdata │ ├── file1.html │ ├── large.png │ └── small.png └── update_env.rb └── lib └── helper.rb /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This is a file used by GitHub to ignore the following commits on `git blame`. 2 | # 3 | # You can also do the same thing in your local repository with: 4 | # $ git config --local blame.ignoreRevsFile .git-blame-ignore-revs 5 | 6 | # Expand tabs 7 | 4d987cf78f7f3eaeb67ef0463d3c0d07aaa3de09 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/cgi' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/cgi 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | strategy: 25 | matrix: 26 | ruby: ["ruby", "jruby"] 27 | 28 | steps: 29 | - name: Harden Runner 30 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 31 | with: 32 | egress-policy: audit 33 | 34 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | 36 | - name: Set up Ruby 37 | uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 38 | with: 39 | ruby-version: ${{ matrix.ruby }} 40 | 41 | # https://github.com/rubygems/rubygems/issues/5882 42 | - name: Install dependencies and build for JRuby 43 | run: | 44 | sudo apt install default-jdk maven 45 | gem update --system 46 | gem install ruby-maven rake-compiler --no-document 47 | rake compile 48 | if: matrix.ruby == 'jruby' 49 | 50 | - name: Install dependencies 51 | run: bundle install --jobs 4 --retry 3 52 | 53 | - name: Publish to RubyGems 54 | uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 55 | 56 | - name: Create GitHub release 57 | run: | 58 | tag_name="$(git describe --tags --abbrev=0)" 59 | gh release create "${tag_name}" --verify-tag --generate-notes 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | if: matrix.ruby == 'ruby' 63 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | min_version: 2.5 10 | test: 11 | needs: ruby-versions 12 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 17 | os: [ ubuntu-latest, macos-latest, windows-latest ] 18 | exclude: 19 | - { os: macos-latest, ruby: 2.5 } 20 | - { os: windows-latest, ruby: head } 21 | - { os: macos-latest, ruby: jruby } 22 | - { os: windows-latest, ruby: jruby } 23 | - { os: macos-latest, ruby: jruby-head } 24 | - { os: windows-latest, ruby: jruby-head } 25 | - { os: windows-latest, ruby: truffleruby } 26 | - { os: windows-latest, ruby: truffleruby-head } 27 | include: 28 | - { os: windows-latest, ruby: mingw } 29 | - { os: windows-latest, ruby: mswin } 30 | runs-on: ${{ matrix.os }} 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Set up Ruby 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: ${{ matrix.ruby }} 37 | - name: Install dependencies 38 | run: bundle install 39 | - name: Build 40 | run: rake compile 41 | - name: Run test 42 | run: bundle exec rake test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /logs/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /ChangeLog 11 | /Gemfile.lock 12 | *.jar 13 | *.bundle 14 | *.so 15 | *.dll 16 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development do 4 | gem "bundler" 5 | gem "rake" 6 | gem "rake-compiler" 7 | gem "test-unit" 8 | gem "test-unit-ruby-core" 9 | end 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | CGI is a large class, providing several categories of methods, many of which 4 | are mixed in from other modules. Some of the documentation is in this class, 5 | some in the modules CGI::QueryExtension and CGI::HtmlExtension. See 6 | CGI::Cookie for specific information on handling cookies, and cgi/session.rb 7 | (CGI::Session) for information on sessions. 8 | 9 | For queries, CGI provides methods to get at environmental variables, 10 | parameters, cookies, and multipart request data. For responses, CGI provides 11 | methods for writing output and generating HTML. 12 | 13 | Read on for more details. Examples are provided at the bottom. 14 | 15 | ## Installation 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | ```ruby 20 | gem 'cgi' 21 | ``` 22 | 23 | And then execute: 24 | 25 | $ bundle 26 | 27 | Or install it yourself as: 28 | 29 | $ gem install cgi 30 | 31 | ## Usage 32 | 33 | ### Get form values 34 | 35 | Given a form with the content `field_name=123`: 36 | 37 | ```ruby 38 | require "cgi" 39 | cgi = CGI.new 40 | value = cgi['field_name'] # => "123" 41 | cgi['flowerpot'] # => "" 42 | fields = cgi.keys # => [ "field_name" ] 43 | 44 | cgi.has_key?('field_name') # => true 45 | cgi.include?('field_name') # => true 46 | cgi.include?('flowerpot') # => false 47 | ``` 48 | 49 | ### Get form values as hash 50 | 51 | ```ruby 52 | require "cgi" 53 | cgi = CGI.new 54 | params = cgi.params 55 | ``` 56 | 57 | cgi.params is a hash. 58 | 59 | ```ruby 60 | cgi.params['new_field_name'] = ["value"] # add new param 61 | cgi.params['field_name'] = ["new_value"] # change value 62 | cgi.params.delete('field_name') # delete param 63 | cgi.params.clear # delete all params 64 | ``` 65 | 66 | ### Save form values to file 67 | 68 | ```ruby 69 | require "pstore" 70 | db = PStore.new("query.db") 71 | db.transaction do 72 | db["params"] = cgi.params 73 | end 74 | ``` 75 | 76 | 77 | ### Restore form values from file 78 | 79 | ```ruby 80 | require "pstore" 81 | db = PStore.new("query.db") 82 | db.transaction do 83 | cgi.params = db["params"] 84 | end 85 | ``` 86 | 87 | ## Development 88 | 89 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 90 | 91 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 92 | 93 | ## Contributing 94 | 95 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/cgi. 96 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | require 'rake/javaextensiontask' 5 | Rake::JavaExtensionTask.new("escape") do |ext| 6 | ext.source_version = '1.8' 7 | ext.target_version = '1.8' 8 | ext.ext_dir = 'ext/java' 9 | ext.lib_dir = 'lib/cgi' 10 | 11 | task :build => :compile 12 | end 13 | 14 | if RUBY_ENGINE == 'ruby' 15 | require 'rake/extensiontask' 16 | extask = Rake::ExtensionTask.new("cgi/escape") do |x| 17 | x.lib_dir.sub!(%r[(?=/|\z)], "/#{RUBY_VERSION}/#{x.platform}") 18 | end 19 | end 20 | 21 | Rake::TestTask.new(:test) do |t| 22 | t.libs << "test/lib" 23 | if RUBY_ENGINE == 'jruby' 24 | t.libs << "ext/java/org/jruby/ext/cgi/escape/lib" 25 | elsif RUBY_ENGINE == 'ruby' 26 | t.libs << "lib/#{RUBY_VERSION}/#{extask.platform}" 27 | end 28 | t.ruby_opts << "-rhelper" 29 | t.test_files = FileList['test/**/test_*.rb'] 30 | end 31 | 32 | task :default => :test 33 | task :test => :compile 34 | -------------------------------------------------------------------------------- /cgi.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{Support for the Common Gateway Interface protocol.} 17 | spec.description = %q{Support for the Common Gateway Interface protocol.} 18 | spec.homepage = "https://github.com/ruby/cgi" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | spec.required_ruby_version = ">= 2.5.0" 21 | 22 | spec.metadata["homepage_uri"] = spec.homepage 23 | spec.metadata["source_code_uri"] = spec.homepage 24 | 25 | spec.executables = [] 26 | 27 | spec.files = [ 28 | "COPYING", 29 | "BSDL", 30 | "README.md", 31 | *Dir["lib{.rb,/**/*.rb}", "bin/*"] ] 32 | 33 | spec.require_paths = ["lib"] 34 | 35 | if Gem::Platform === spec.platform and spec.platform =~ 'java' or RUBY_ENGINE == 'jruby' 36 | spec.platform = 'java' 37 | spec.require_paths << "ext/java/org/jruby/ext/cgi/escape/lib" 38 | spec.files += Dir["ext/java/**/*.{rb}", "lib/cgi/escape.jar"] 39 | else 40 | spec.files += Dir["ext/cgi/**/*.{rb,c,h,sh}", "ext/cgi/escape/depend", "lib/cgi/escape.so"] 41 | spec.extensions = ["ext/cgi/escape/extconf.rb"] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /ext/cgi/escape/depend: -------------------------------------------------------------------------------- 1 | escape.o: $(RUBY_EXTCONF_H) 2 | escape.o: escape.c 3 | -------------------------------------------------------------------------------- /ext/cgi/escape/escape.c: -------------------------------------------------------------------------------- 1 | #include "ruby.h" 2 | #include "ruby/encoding.h" 3 | 4 | RUBY_EXTERN unsigned long ruby_scan_digits(const char *str, ssize_t len, int base, size_t *retlen, int *overflow); 5 | RUBY_EXTERN const char ruby_hexdigits[]; 6 | RUBY_EXTERN const signed char ruby_digit36_to_number_table[]; 7 | #define lower_hexdigits (ruby_hexdigits+0) 8 | #define upper_hexdigits (ruby_hexdigits+16) 9 | #define char_to_number(c) ruby_digit36_to_number_table[(unsigned char)(c)] 10 | 11 | static VALUE rb_cCGI, rb_mEscape, rb_mEscapeExt; 12 | static ID id_accept_charset; 13 | 14 | #define HTML_ESCAPE_MAX_LEN 6 15 | 16 | static const struct { 17 | uint8_t len; 18 | char str[HTML_ESCAPE_MAX_LEN+1]; 19 | } html_escape_table[UCHAR_MAX+1] = { 20 | #define HTML_ESCAPE(c, str) [c] = {rb_strlen_lit(str), str} 21 | HTML_ESCAPE('\'', "'"), 22 | HTML_ESCAPE('&', "&"), 23 | HTML_ESCAPE('"', """), 24 | HTML_ESCAPE('<', "<"), 25 | HTML_ESCAPE('>', ">"), 26 | #undef HTML_ESCAPE 27 | }; 28 | 29 | static inline void 30 | preserve_original_state(VALUE orig, VALUE dest) 31 | { 32 | rb_enc_associate(dest, rb_enc_get(orig)); 33 | } 34 | 35 | static inline long 36 | escaped_length(VALUE str) 37 | { 38 | const long len = RSTRING_LEN(str); 39 | if (len >= LONG_MAX / HTML_ESCAPE_MAX_LEN) { 40 | ruby_malloc_size_overflow(len, HTML_ESCAPE_MAX_LEN); 41 | } 42 | return len * HTML_ESCAPE_MAX_LEN; 43 | } 44 | 45 | static VALUE 46 | optimized_escape_html(VALUE str) 47 | { 48 | VALUE vbuf; 49 | char *buf = ALLOCV_N(char, vbuf, escaped_length(str)); 50 | const char *cstr = RSTRING_PTR(str); 51 | const char *end = cstr + RSTRING_LEN(str); 52 | 53 | char *dest = buf; 54 | while (cstr < end) { 55 | const unsigned char c = *cstr++; 56 | uint8_t len = html_escape_table[c].len; 57 | if (len) { 58 | memcpy(dest, html_escape_table[c].str, len); 59 | dest += len; 60 | } 61 | else { 62 | *dest++ = c; 63 | } 64 | } 65 | 66 | VALUE escaped; 67 | if (RSTRING_LEN(str) < (dest - buf)) { 68 | escaped = rb_str_new(buf, dest - buf); 69 | preserve_original_state(str, escaped); 70 | } 71 | else { 72 | escaped = rb_str_dup(str); 73 | } 74 | ALLOCV_END(vbuf); 75 | return escaped; 76 | } 77 | 78 | static VALUE 79 | optimized_unescape_html(VALUE str) 80 | { 81 | enum {UNICODE_MAX = 0x10ffff}; 82 | rb_encoding *enc = rb_enc_get(str); 83 | unsigned long charlimit = (strcasecmp(rb_enc_name(enc), "UTF-8") == 0 ? UNICODE_MAX : 84 | strcasecmp(rb_enc_name(enc), "ISO-8859-1") == 0 ? 256 : 85 | 128); 86 | long i, j, len, beg = 0; 87 | size_t clen, plen; 88 | int overflow; 89 | const char *cstr; 90 | char buf[6]; 91 | VALUE dest = 0; 92 | 93 | len = RSTRING_LEN(str); 94 | cstr = RSTRING_PTR(str); 95 | 96 | for (i = 0; i < len; i++) { 97 | unsigned long cc; 98 | char c = cstr[i]; 99 | if (c != '&') continue; 100 | plen = i - beg; 101 | if (++i >= len) break; 102 | c = (unsigned char)cstr[i]; 103 | j = i; 104 | #define MATCH(s) (len - i >= (int)rb_strlen_lit(s) && \ 105 | memcmp(&cstr[i], s, rb_strlen_lit(s)) == 0 && \ 106 | (i += rb_strlen_lit(s) - 1, 1)) 107 | switch (c) { 108 | case 'a': 109 | ++i; 110 | if (MATCH("pos;")) { 111 | c = '\''; 112 | } 113 | else if (MATCH("mp;")) { 114 | c = '&'; 115 | } 116 | else { 117 | i = j; 118 | continue; 119 | } 120 | break; 121 | case 'q': 122 | ++i; 123 | if (MATCH("uot;")) { 124 | c = '"'; 125 | } 126 | else { 127 | i = j; 128 | continue; 129 | } 130 | break; 131 | case 'g': 132 | ++i; 133 | if (MATCH("t;")) { 134 | c = '>'; 135 | } 136 | else { 137 | i = j; 138 | continue; 139 | } 140 | break; 141 | case 'l': 142 | ++i; 143 | if (MATCH("t;")) { 144 | c = '<'; 145 | } 146 | else { 147 | i = j; 148 | continue; 149 | } 150 | break; 151 | case '#': 152 | if (len - ++i >= 2 && ISDIGIT(cstr[i])) { 153 | cc = ruby_scan_digits(&cstr[i], len-i, 10, &clen, &overflow); 154 | } 155 | else if ((cstr[i] == 'x' || cstr[i] == 'X') && len - ++i >= 2 && ISXDIGIT(cstr[i])) { 156 | cc = ruby_scan_digits(&cstr[i], len-i, 16, &clen, &overflow); 157 | } 158 | else { 159 | i = j; 160 | continue; 161 | } 162 | i += clen; 163 | if (overflow || cc >= charlimit || cstr[i] != ';') { 164 | i = j; 165 | continue; 166 | } 167 | if (!dest) { 168 | dest = rb_str_buf_new(len); 169 | } 170 | rb_str_cat(dest, cstr + beg, plen); 171 | if (charlimit > 256) { 172 | rb_str_cat(dest, buf, rb_enc_mbcput((OnigCodePoint)cc, buf, enc)); 173 | } 174 | else { 175 | c = (unsigned char)cc; 176 | rb_str_cat(dest, &c, 1); 177 | } 178 | beg = i + 1; 179 | continue; 180 | default: 181 | --i; 182 | continue; 183 | } 184 | if (!dest) { 185 | dest = rb_str_buf_new(len); 186 | } 187 | rb_str_cat(dest, cstr + beg, plen); 188 | rb_str_cat(dest, &c, 1); 189 | beg = i + 1; 190 | } 191 | 192 | if (dest) { 193 | rb_str_cat(dest, cstr + beg, len - beg); 194 | preserve_original_state(str, dest); 195 | return dest; 196 | } 197 | else { 198 | return rb_str_dup(str); 199 | } 200 | } 201 | 202 | static unsigned char 203 | url_unreserved_char(unsigned char c) 204 | { 205 | switch (c) { 206 | case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': 207 | case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': 208 | case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': 209 | case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': 210 | case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': 211 | case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': 212 | case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': 213 | case '-': case '.': case '_': case '~': 214 | return 1; 215 | default: 216 | break; 217 | } 218 | return 0; 219 | } 220 | 221 | static VALUE 222 | optimized_escape(VALUE str, int plus_escape) 223 | { 224 | long i, len, beg = 0; 225 | VALUE dest = 0; 226 | const char *cstr; 227 | char buf[4] = {'%'}; 228 | 229 | len = RSTRING_LEN(str); 230 | cstr = RSTRING_PTR(str); 231 | 232 | for (i = 0; i < len; ++i) { 233 | const unsigned char c = (unsigned char)cstr[i]; 234 | if (!url_unreserved_char(c)) { 235 | if (!dest) { 236 | dest = rb_str_buf_new(len); 237 | } 238 | 239 | rb_str_cat(dest, cstr + beg, i - beg); 240 | beg = i + 1; 241 | 242 | if (plus_escape && c == ' ') { 243 | rb_str_cat_cstr(dest, "+"); 244 | } 245 | else { 246 | buf[1] = upper_hexdigits[(c >> 4) & 0xf]; 247 | buf[2] = upper_hexdigits[c & 0xf]; 248 | rb_str_cat(dest, buf, 3); 249 | } 250 | } 251 | } 252 | 253 | if (dest) { 254 | rb_str_cat(dest, cstr + beg, len - beg); 255 | preserve_original_state(str, dest); 256 | return dest; 257 | } 258 | else { 259 | return rb_str_dup(str); 260 | } 261 | } 262 | 263 | static VALUE 264 | optimized_unescape(VALUE str, VALUE encoding, int unescape_plus) 265 | { 266 | long i, len, beg = 0; 267 | VALUE dest = 0; 268 | const char *cstr; 269 | rb_encoding *enc = rb_to_encoding(encoding); 270 | int cr, origenc, encidx = rb_enc_to_index(enc); 271 | 272 | len = RSTRING_LEN(str); 273 | cstr = RSTRING_PTR(str); 274 | 275 | for (i = 0; i < len; ++i) { 276 | char buf[1]; 277 | const char c = cstr[i]; 278 | int clen = 0; 279 | if (c == '%') { 280 | if (i + 3 > len) break; 281 | if (!ISXDIGIT(cstr[i+1])) continue; 282 | if (!ISXDIGIT(cstr[i+2])) continue; 283 | buf[0] = ((char_to_number(cstr[i+1]) << 4) 284 | | char_to_number(cstr[i+2])); 285 | clen = 2; 286 | } 287 | else if (unescape_plus && c == '+') { 288 | buf[0] = ' '; 289 | } 290 | else { 291 | continue; 292 | } 293 | 294 | if (!dest) { 295 | dest = rb_str_buf_new(len); 296 | } 297 | 298 | rb_str_cat(dest, cstr + beg, i - beg); 299 | i += clen; 300 | beg = i + 1; 301 | 302 | rb_str_cat(dest, buf, 1); 303 | } 304 | 305 | if (dest) { 306 | rb_str_cat(dest, cstr + beg, len - beg); 307 | preserve_original_state(str, dest); 308 | cr = ENC_CODERANGE_UNKNOWN; 309 | } 310 | else { 311 | dest = rb_str_dup(str); 312 | cr = ENC_CODERANGE(str); 313 | } 314 | origenc = rb_enc_get_index(str); 315 | if (origenc != encidx) { 316 | rb_enc_associate_index(dest, encidx); 317 | if (!ENC_CODERANGE_CLEAN_P(rb_enc_str_coderange(dest))) { 318 | rb_enc_associate_index(dest, origenc); 319 | if (cr != ENC_CODERANGE_UNKNOWN) 320 | ENC_CODERANGE_SET(dest, cr); 321 | } 322 | } 323 | return dest; 324 | } 325 | 326 | /* 327 | * call-seq: 328 | * CGI.escapeHTML(string) -> string 329 | * 330 | * Returns HTML-escaped string. 331 | * 332 | */ 333 | static VALUE 334 | cgiesc_escape_html(VALUE self, VALUE str) 335 | { 336 | StringValue(str); 337 | 338 | if (rb_enc_str_asciicompat_p(str)) { 339 | return optimized_escape_html(str); 340 | } 341 | else { 342 | return rb_call_super(1, &str); 343 | } 344 | } 345 | 346 | /* 347 | * call-seq: 348 | * CGI.unescapeHTML(string) -> string 349 | * 350 | * Returns HTML-unescaped string. 351 | * 352 | */ 353 | static VALUE 354 | cgiesc_unescape_html(VALUE self, VALUE str) 355 | { 356 | StringValue(str); 357 | 358 | if (rb_enc_str_asciicompat_p(str)) { 359 | return optimized_unescape_html(str); 360 | } 361 | else { 362 | return rb_call_super(1, &str); 363 | } 364 | } 365 | 366 | /* 367 | * call-seq: 368 | * CGI.escape(string) -> string 369 | * 370 | * Returns URL-escaped string (+application/x-www-form-urlencoded+). 371 | * 372 | */ 373 | static VALUE 374 | cgiesc_escape(VALUE self, VALUE str) 375 | { 376 | StringValue(str); 377 | 378 | if (rb_enc_str_asciicompat_p(str)) { 379 | return optimized_escape(str, 1); 380 | } 381 | else { 382 | return rb_call_super(1, &str); 383 | } 384 | } 385 | 386 | static VALUE 387 | accept_charset(int argc, VALUE *argv, VALUE self) 388 | { 389 | if (argc > 0) 390 | return argv[0]; 391 | return rb_cvar_get(CLASS_OF(self), id_accept_charset); 392 | } 393 | 394 | /* 395 | * call-seq: 396 | * CGI.unescape(string, encoding=@@accept_charset) -> string 397 | * 398 | * Returns URL-unescaped string (+application/x-www-form-urlencoded+). 399 | * 400 | */ 401 | static VALUE 402 | cgiesc_unescape(int argc, VALUE *argv, VALUE self) 403 | { 404 | VALUE str = (rb_check_arity(argc, 1, 2), argv[0]); 405 | 406 | StringValue(str); 407 | 408 | if (rb_enc_str_asciicompat_p(str)) { 409 | VALUE enc = accept_charset(argc-1, argv+1, self); 410 | return optimized_unescape(str, enc, 1); 411 | } 412 | else { 413 | return rb_call_super(argc, argv); 414 | } 415 | } 416 | 417 | /* 418 | * call-seq: 419 | * CGI.escapeURIComponent(string) -> string 420 | * 421 | * Returns URL-escaped string following RFC 3986. 422 | * 423 | */ 424 | static VALUE 425 | cgiesc_escape_uri_component(VALUE self, VALUE str) 426 | { 427 | StringValue(str); 428 | 429 | if (rb_enc_str_asciicompat_p(str)) { 430 | return optimized_escape(str, 0); 431 | } 432 | else { 433 | return rb_call_super(1, &str); 434 | } 435 | } 436 | 437 | /* 438 | * call-seq: 439 | * CGI.unescapeURIComponent(string, encoding=@@accept_charset) -> string 440 | * 441 | * Returns URL-unescaped string following RFC 3986. 442 | * 443 | */ 444 | static VALUE 445 | cgiesc_unescape_uri_component(int argc, VALUE *argv, VALUE self) 446 | { 447 | VALUE str = (rb_check_arity(argc, 1, 2), argv[0]); 448 | 449 | StringValue(str); 450 | 451 | if (rb_enc_str_asciicompat_p(str)) { 452 | VALUE enc = accept_charset(argc-1, argv+1, self); 453 | return optimized_unescape(str, enc, 0); 454 | } 455 | else { 456 | return rb_call_super(argc, argv); 457 | } 458 | } 459 | 460 | void 461 | Init_escape(void) 462 | { 463 | #ifdef HAVE_RB_EXT_RACTOR_SAFE 464 | rb_ext_ractor_safe(true); 465 | #endif 466 | 467 | id_accept_charset = rb_intern_const("@@accept_charset"); 468 | InitVM(escape); 469 | } 470 | 471 | void 472 | InitVM_escape(void) 473 | { 474 | rb_cCGI = rb_define_class("CGI", rb_cObject); 475 | rb_mEscapeExt = rb_define_module_under(rb_cCGI, "EscapeExt"); 476 | rb_mEscape = rb_define_module_under(rb_cCGI, "Escape"); 477 | rb_define_method(rb_mEscapeExt, "escapeHTML", cgiesc_escape_html, 1); 478 | rb_define_method(rb_mEscapeExt, "unescapeHTML", cgiesc_unescape_html, 1); 479 | rb_define_method(rb_mEscapeExt, "escapeURIComponent", cgiesc_escape_uri_component, 1); 480 | rb_define_alias(rb_mEscapeExt, "escape_uri_component", "escapeURIComponent"); 481 | rb_define_method(rb_mEscapeExt, "unescapeURIComponent", cgiesc_unescape_uri_component, -1); 482 | rb_define_alias(rb_mEscapeExt, "unescape_uri_component", "unescapeURIComponent"); 483 | rb_define_method(rb_mEscapeExt, "escape", cgiesc_escape, 1); 484 | rb_define_method(rb_mEscapeExt, "unescape", cgiesc_unescape, -1); 485 | rb_prepend_module(rb_mEscape, rb_mEscapeExt); 486 | rb_extend_object(rb_cCGI, rb_mEscapeExt); 487 | } 488 | -------------------------------------------------------------------------------- /ext/cgi/escape/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | if RUBY_ENGINE == 'truffleruby' 4 | File.write("Makefile", dummy_makefile($srcdir).join("")) 5 | else 6 | create_makefile 'cgi/escape' 7 | end 8 | -------------------------------------------------------------------------------- /ext/java/org/jruby/ext/cgi/escape/CGIEscape.java: -------------------------------------------------------------------------------- 1 | /* 2 | **** BEGIN LICENSE BLOCK ***** 3 | * BSD 2-Clause License 4 | * 5 | * Copyright (c) 2016, Charles Oliver Nutter 6 | * All rights reserved. 7 | * 8 | * Redistribution and use in source and binary forms, with or without 9 | * modification, are permitted provided that the following conditions are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright notice, this 12 | * list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright notice, 15 | * this list of conditions and the following disclaimer in the documentation 16 | * and/or other materials provided with the distribution. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | ***** END LICENSE BLOCK *****/ 29 | 30 | package org.jruby.ext.cgi.escape; 31 | 32 | import org.jcodings.Encoding; 33 | import org.jcodings.specific.ISO8859_1Encoding; 34 | import org.jcodings.specific.UTF8Encoding; 35 | import org.jruby.Ruby; 36 | import org.jruby.RubyClass; 37 | import org.jruby.RubyEncoding; 38 | import org.jruby.RubyModule; 39 | import org.jruby.RubyString; 40 | import org.jruby.anno.JRubyMethod; 41 | import org.jruby.runtime.Block; 42 | import org.jruby.runtime.Helpers; 43 | import org.jruby.runtime.ThreadContext; 44 | import org.jruby.runtime.builtin.IRubyObject; 45 | import org.jruby.runtime.load.Library; 46 | import org.jruby.util.ByteList; 47 | import org.jruby.util.StringSupport; 48 | import org.jruby.util.io.EncodingUtils; 49 | 50 | public class CGIEscape implements Library { 51 | public static final String ACCEPT_CHARSET = "@@accept_charset"; 52 | public static final byte[] NO39 = "'".getBytes(RubyEncoding.UTF8); 53 | public static final byte[] AMP = "&".getBytes(RubyEncoding.UTF8); 54 | public static final byte[] QUOT = """.getBytes(RubyEncoding.UTF8); 55 | public static final byte[] LT = "<".getBytes(RubyEncoding.UTF8); 56 | public static final byte[] GT = ">".getBytes(RubyEncoding.UTF8); 57 | public static final int UNICODE_MAX = 0x10ffff; 58 | public static final byte[] TSEMI = "t;".getBytes(RubyEncoding.UTF8); 59 | public static final byte[] UOTSEMI = "uot;".getBytes(RubyEncoding.UTF8); 60 | public static final byte[] MPSEMI = "mp;".getBytes(RubyEncoding.UTF8); 61 | public static final byte[] POSSEMI = "pos;".getBytes(RubyEncoding.UTF8); 62 | 63 | static void html_escaped_cat(RubyString str, byte c) { 64 | switch (c) { 65 | case '\'': 66 | str.cat(NO39); 67 | break; 68 | case '&': 69 | str.cat(AMP); 70 | break; 71 | case '"': 72 | str.cat(QUOT); 73 | break; 74 | case '<': 75 | str.cat(LT); 76 | break; 77 | case '>': 78 | str.cat(GT); 79 | break; 80 | } 81 | } 82 | 83 | static void preserve_original_state(RubyString orig, RubyString dest) { 84 | dest.setEncoding(orig.getEncoding()); 85 | 86 | dest.infectBy(orig); 87 | } 88 | 89 | static IRubyObject 90 | optimized_escape_html(Ruby runtime, RubyString str) { 91 | int i, len, beg = 0; 92 | RubyString dest = null; 93 | byte[] cstrBytes; 94 | 95 | len = str.size(); 96 | ByteList byteList = str.getByteList(); 97 | cstrBytes = byteList.unsafeBytes(); 98 | int cstr = byteList.begin(); 99 | 100 | for (i = 0; i < len; i++) { 101 | switch (cstrBytes[cstr + i]) { 102 | case '\'': 103 | case '&': 104 | case '"': 105 | case '<': 106 | case '>': 107 | if (dest == null) { 108 | dest = RubyString.newStringLight(runtime, len); 109 | } 110 | 111 | dest.cat(cstrBytes, cstr + beg, i - beg); 112 | beg = i + 1; 113 | 114 | html_escaped_cat(dest, cstrBytes[cstr + i]); 115 | break; 116 | } 117 | } 118 | 119 | if (dest != null) { 120 | dest.cat(cstrBytes, cstr + beg, len - beg); 121 | preserve_original_state(str, dest); 122 | return dest; 123 | } else { 124 | return str.strDup(runtime); 125 | } 126 | } 127 | 128 | // Set of i has to happen outside this; see MATCH macro in MRI ext/cgi/escape/escape.c 129 | static boolean MATCH(byte[] s, int len, int i, byte[] cstrBytes, int cstr) { 130 | if (len - i >= s.length && ByteList.memcmp(cstrBytes, cstr + i, s, 0, s.length) == 0) { 131 | return true; 132 | } else { 133 | return false; 134 | } 135 | } 136 | 137 | static IRubyObject 138 | optimized_unescape_html(Ruby runtime, RubyString str) { 139 | Encoding enc = str.getEncoding(); 140 | int charlimit = (enc instanceof UTF8Encoding) ? UNICODE_MAX : 141 | (enc instanceof ISO8859_1Encoding) ? 256 : 142 | 128; 143 | int i, j, len, beg = 0; 144 | int clen = 0, plen; 145 | boolean overflow = false; 146 | byte[] cstrBytes; 147 | int cstr; 148 | byte[] buf = new byte[6]; 149 | RubyString dest = null; 150 | 151 | len = str.size(); 152 | ByteList byteList = str.getByteList(); 153 | cstrBytes = byteList.getUnsafeBytes(); 154 | cstr = byteList.begin(); 155 | 156 | for (i = 0; i < len; i++) { 157 | int cc; 158 | int c = cstrBytes[cstr + i]; 159 | if (c != '&') continue; 160 | plen = i - beg; 161 | if (++i >= len) break; 162 | c = cstrBytes[cstr + i] & 0xFF; 163 | j = i; 164 | switch (c) { 165 | case 'a': 166 | ++i; 167 | if (MATCH(POSSEMI, len, i, cstrBytes, cstr)) { 168 | i += POSSEMI.length - 1; 169 | c = '\''; 170 | } else if (MATCH(MPSEMI, len, i, cstrBytes, cstr)) { 171 | i += MPSEMI.length - 1; 172 | c = '&'; 173 | } else { 174 | i = j; 175 | continue; 176 | } 177 | break; 178 | case 'q': 179 | ++i; 180 | if (MATCH(UOTSEMI, len, i, cstrBytes, cstr)) { 181 | i += UOTSEMI.length - 1; 182 | c = '"'; 183 | } else { 184 | i = j; 185 | continue; 186 | } 187 | break; 188 | case 'g': 189 | ++i; 190 | if (MATCH(TSEMI, len, i, cstrBytes, cstr)) { 191 | i += TSEMI.length - 1; 192 | c = '>'; 193 | } else { 194 | i = j; 195 | continue; 196 | } 197 | break; 198 | case 'l': 199 | ++i; 200 | if (MATCH(TSEMI, len, i, cstrBytes, cstr)) { 201 | i += TSEMI.length - 1; 202 | c = '<'; 203 | } else { 204 | i = j; 205 | continue; 206 | } 207 | break; 208 | case '#': 209 | if (len - ++i >= 2 && Character.isDigit(cstrBytes[cstr + i])) { 210 | int[] clenOverflow = {clen, overflow ? 1 : 0}; 211 | cc = ruby_scan_digits(cstrBytes, cstr + i, len - i, 10, clenOverflow); 212 | clen = clenOverflow[0]; 213 | overflow = clenOverflow[1] == 1; 214 | } else if (i < len && (cstrBytes[cstr + i] == 'x' || cstrBytes[cstr + i] == 'X') && len - ++i >= 2 && ISXDIGIT(cstrBytes, cstr + i)) { 215 | int[] clenOverflow = {clen, overflow ? 1 : 0}; 216 | cc = ruby_scan_digits(cstrBytes, cstr + i, len - i, 16, clenOverflow); 217 | clen = clenOverflow[0]; 218 | overflow = clenOverflow[1] == 1; 219 | } else { 220 | i = j; 221 | continue; 222 | } 223 | i += clen; 224 | if (overflow || cc >= charlimit || i >= len || cstrBytes[cstr + i] != ';') { 225 | i = j; 226 | continue; 227 | } 228 | if (dest == null) { 229 | dest = RubyString.newStringLight(runtime, len); 230 | } 231 | dest.cat(cstrBytes, cstr + beg, plen); 232 | if (charlimit > 256) { 233 | dest.cat(buf, 0, enc.codeToMbc(cc, buf, 0)); 234 | } else { 235 | c = cc; 236 | dest.cat(c); 237 | } 238 | beg = i + 1; 239 | continue; 240 | default: 241 | --i; 242 | continue; 243 | } 244 | if (dest == null) { 245 | dest = RubyString.newStringLight(runtime, len); 246 | } 247 | dest.cat(cstrBytes, cstr + beg, plen); 248 | dest.cat(c); 249 | beg = i + 1; 250 | } 251 | 252 | if (dest != null) { 253 | dest.cat(cstrBytes, cstr + beg, len - beg); 254 | preserve_original_state(str, dest); 255 | return dest; 256 | } else { 257 | return str.strDup(runtime); 258 | } 259 | } 260 | 261 | static boolean ISXDIGIT(byte[] cstrBytes, int i) { 262 | byte cstrByte = cstrBytes[i]; 263 | return (cstrByte >= '0' && cstrByte <= '9') || (cstrByte >= 'a' && cstrByte <= 'f') || (cstrByte >= 'A' && cstrByte <= 'F'); 264 | } 265 | 266 | static boolean url_unreserved_char(int c) { 267 | switch (c) { 268 | case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': 269 | case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': 270 | case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': 271 | case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': 272 | case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': 273 | case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': 274 | case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': 275 | case '-': case '.': case '_': case '~': 276 | return true; 277 | default: 278 | break; 279 | } 280 | return false; 281 | } 282 | 283 | static final byte[] upper_hexdigits = "0123456789ABCDEF".getBytes(RubyEncoding.UTF8); 284 | 285 | static IRubyObject optimized_escape(Ruby runtime, RubyString str, boolean escapePlus) { 286 | int i, len, beg = 0; 287 | RubyString dest = null; 288 | byte[] cstrBytes; 289 | int cstr; 290 | byte[] buf = {'%', 0, 0}; 291 | 292 | len = str.size(); 293 | ByteList byteList = str.getByteList(); 294 | cstrBytes = byteList.unsafeBytes(); 295 | cstr = byteList.begin(); 296 | 297 | for (i = 0; i < len; ++i) { 298 | int c = cstrBytes[cstr + i] & 0xFF; 299 | if (!url_unreserved_char(c)) { 300 | if (dest == null) { 301 | dest = RubyString.newStringLight(runtime, len); 302 | } 303 | 304 | dest.cat(cstrBytes, cstr + beg, i - beg); 305 | beg = i + 1; 306 | 307 | if (escapePlus && c == ' ') { 308 | dest.cat('+'); 309 | } else { 310 | buf[1] = upper_hexdigits[(c >> 4) & 0xf]; 311 | buf[2] = upper_hexdigits[c & 0xf]; 312 | dest.cat(buf, 0, 3); 313 | } 314 | } 315 | } 316 | 317 | if (dest != null) { 318 | dest.cat(cstrBytes, cstr + beg, len - beg); 319 | preserve_original_state(str, dest); 320 | return dest; 321 | } else { 322 | return str.strDup(runtime); 323 | } 324 | } 325 | 326 | static IRubyObject 327 | optimized_unescape(ThreadContext context, RubyString str, IRubyObject encoding, boolean unescapePlus) { 328 | int i, len, beg = 0; 329 | RubyString dest = null; 330 | byte[] cstrBytes; 331 | int cstr; 332 | int cr; 333 | Encoding origenc, encidx = EncodingUtils.rbToEncoding(context, encoding); 334 | 335 | len = str.size(); 336 | ByteList byteList = str.getByteList(); 337 | cstrBytes = byteList.unsafeBytes(); 338 | cstr = byteList.begin(); 339 | 340 | int buf = 0; 341 | Ruby runtime = context.runtime; 342 | 343 | for (i = 0; i < len; ++i) { 344 | int c = cstrBytes[cstr + i] & 0xFF; 345 | int clen = 0; 346 | if (c == '%') { 347 | if (i + 3 > len) break; 348 | if (!ISXDIGIT(cstrBytes, cstr + i + 1)) continue; 349 | if (!ISXDIGIT(cstrBytes, cstr + i + 2)) continue; 350 | buf = ((char_to_number(cstrBytes[cstr + i + 1]) << 4) 351 | | char_to_number(cstrBytes[cstr + i + 2])); 352 | clen = 2; 353 | } else if (unescapePlus && c == '+') { 354 | buf = ' '; 355 | } else { 356 | continue; 357 | } 358 | 359 | if (dest == null) { 360 | dest = RubyString.newStringLight(runtime, len); 361 | } 362 | 363 | dest.cat(cstrBytes, cstr + beg, i - beg); 364 | i += clen; 365 | beg = i + 1; 366 | 367 | dest.cat(buf); 368 | } 369 | 370 | if (dest != null) { 371 | dest.cat(cstrBytes, cstr + beg, len - beg); 372 | preserve_original_state(str, dest); 373 | cr = StringSupport.CR_UNKNOWN; 374 | } else { 375 | dest = str.strDup(runtime); 376 | cr = str.getCodeRange(); 377 | } 378 | origenc = str.getEncoding(); 379 | if (origenc != encidx) { 380 | dest.setEncoding(encidx); 381 | if (StringSupport.encCoderangeClean(dest.getCodeRange()) == 0) { 382 | dest.setEncoding(origenc); 383 | if (cr != StringSupport.CR_UNKNOWN) 384 | dest.setCodeRange(cr); 385 | } 386 | } 387 | return dest; 388 | } 389 | 390 | /* 391 | * call-seq: 392 | * CGI.escapeHTML(string) -> string 393 | * 394 | * Returns HTML-escaped string. 395 | * 396 | */ 397 | @JRubyMethod(name = "escapeHTML", module = true, frame = true) 398 | public static IRubyObject cgiesc_escape_html(ThreadContext context, IRubyObject self, IRubyObject _str) { 399 | RubyString str = _str.convertToString(); 400 | 401 | if (str.getEncoding().isAsciiCompatible()) { 402 | return optimized_escape_html(context.runtime, str); 403 | } else { 404 | return Helpers.invokeSuper(context, self, _str, Block.NULL_BLOCK); 405 | } 406 | } 407 | 408 | /* 409 | * call-seq: 410 | * CGI.unescapeHTML(string) -> string 411 | * 412 | * Returns HTML-unescaped string. 413 | * 414 | */ 415 | @JRubyMethod(name = "unescapeHTML", module = true, frame = true) 416 | public static IRubyObject cgiesc_unescape_html(ThreadContext context, IRubyObject self, IRubyObject _str) { 417 | RubyString str = _str.convertToString(); 418 | 419 | if (str.getEncoding().isAsciiCompatible()) { 420 | return optimized_unescape_html(context.runtime, str); 421 | } else { 422 | return Helpers.invokeSuper(context, self, _str, Block.NULL_BLOCK); 423 | } 424 | } 425 | 426 | /* 427 | * call-seq: 428 | * CGI.escape(string) -> string 429 | * 430 | * Returns URL-escaped string. 431 | * 432 | */ 433 | @JRubyMethod(name = "escape", module = true, frame = true) 434 | public static IRubyObject cgiesc_escape(ThreadContext context, IRubyObject self, IRubyObject _str) { 435 | RubyString str = _str.convertToString(); 436 | 437 | if (str.getEncoding().isAsciiCompatible()) { 438 | return optimized_escape(context.runtime, str, true); 439 | } else { 440 | return Helpers.invokeSuper(context, self, _str, Block.NULL_BLOCK); 441 | } 442 | } 443 | 444 | /* 445 | * call-seq: 446 | * CGI.escapeURIComponent(string) -> string 447 | * 448 | * Returns URL-escaped string following RFC 3986. 449 | * 450 | */ 451 | @JRubyMethod(name = "escapeURIComponent", alias = { "escape_uri_component" }, module = true, frame = true) 452 | public static IRubyObject cgiesc_escape_uri_component(ThreadContext context, IRubyObject self, IRubyObject _str) { 453 | RubyString str = _str.convertToString(); 454 | 455 | if (str.getEncoding().isAsciiCompatible()) { 456 | return optimized_escape(context.runtime, str, false); 457 | } else { 458 | return Helpers.invokeSuper(context, self, _str, Block.NULL_BLOCK); 459 | } 460 | } 461 | 462 | static IRubyObject accept_charset(IRubyObject[] args, int argc, int argv, IRubyObject self) { 463 | if (argc > 0) 464 | return args[argv]; 465 | return self.getMetaClass().getClassVar(ACCEPT_CHARSET); 466 | } 467 | 468 | /* 469 | * call-seq: 470 | * CGI.unescape(string, encoding=@@accept_charset) -> string 471 | * 472 | * Returns URL-unescaped string. 473 | * 474 | */ 475 | @JRubyMethod(name = "unescape", required = 1, optional = 1, module = true, frame = true) 476 | public static IRubyObject cgiesc_unescape(ThreadContext context, IRubyObject self, IRubyObject[] argv) { 477 | IRubyObject _str = argv[0]; 478 | 479 | RubyString str = _str.convertToString(); 480 | 481 | if (str.getEncoding().isAsciiCompatible()) { 482 | IRubyObject enc = accept_charset(argv, argv.length - 1, 1, self); 483 | return optimized_unescape(context, str, enc, true); 484 | } else { 485 | return Helpers.invokeSuper(context, self, argv, Block.NULL_BLOCK); 486 | } 487 | } 488 | 489 | /* 490 | * call-seq: 491 | * CGI.unescapeURIComponent(string, encoding=@@accept_charset) -> string 492 | * 493 | * Returns URL-unescaped string following RFC 3986. 494 | * 495 | */ 496 | @JRubyMethod(name = "unescapeURIComponent", alias = { "unescape_uri_component" }, required = 1, optional = 1, module = true, frame = true) 497 | public static IRubyObject cgiesc_unescape_uri_component(ThreadContext context, IRubyObject self, IRubyObject[] argv) { 498 | IRubyObject _str = argv[0]; 499 | 500 | RubyString str = _str.convertToString(); 501 | 502 | if (str.getEncoding().isAsciiCompatible()) { 503 | IRubyObject enc = accept_charset(argv, argv.length - 1, 1, self); 504 | return optimized_unescape(context, str, enc, false); 505 | } else { 506 | return Helpers.invokeSuper(context, self, argv, Block.NULL_BLOCK); 507 | } 508 | } 509 | 510 | public void load(Ruby runtime, boolean wrap) { 511 | RubyClass rb_cCGI = runtime.defineClass("CGI", runtime.getObject(), runtime.getObject().getAllocator()); 512 | RubyModule rb_mEscape = rb_cCGI.defineModuleUnder("Escape"); 513 | RubyModule rb_mUtil = rb_cCGI.defineModuleUnder("Util"); 514 | rb_mEscape.defineAnnotatedMethods(CGIEscape.class); 515 | rb_mUtil.prependModule(rb_mEscape); 516 | rb_mEscape.extend_object(rb_cCGI); 517 | } 518 | 519 | // PORTED FROM OTHER FILES IN MRI 520 | 521 | static int ruby_scan_digits(byte[] strBytes, int str, int len, int base, int[] retlenOverflow) { 522 | int start = str; 523 | int ret = 0, x; 524 | int mul_overflow = Integer.MAX_VALUE / base; 525 | 526 | retlenOverflow[1] = 0; 527 | 528 | if (len == 0) { 529 | retlenOverflow[0] = 0; 530 | return 0; 531 | } 532 | 533 | do { 534 | int d = ruby_digit36_to_number_table[strBytes[str++]]; 535 | if (d == -1 || base <= d) { 536 | --str; 537 | break; 538 | } 539 | if (mul_overflow < ret) 540 | retlenOverflow[1] = 1; 541 | ret *= base; 542 | x = ret; 543 | ret += d; 544 | if (ret < x) 545 | retlenOverflow[1] = 1; 546 | } while (len < 0 || (--len != 0)); 547 | retlenOverflow[0] = str - start; 548 | return ret; 549 | } 550 | 551 | static int char_to_number(int c) { 552 | return ruby_digit36_to_number_table[c]; 553 | } 554 | 555 | private static final int ruby_digit36_to_number_table[] = { 556 | /* 0 1 2 3 4 5 6 7 8 9 a b c d e f */ 557 | /*0*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 558 | /*1*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 559 | /*2*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 560 | /*3*/ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, 561 | /*4*/ -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 562 | /*5*/ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, 563 | /*6*/ -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 564 | /*7*/ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, 565 | /*8*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 566 | /*9*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 567 | /*a*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 568 | /*b*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 569 | /*c*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 570 | /*d*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 571 | /*e*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 572 | /*f*/ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 573 | }; 574 | } 575 | -------------------------------------------------------------------------------- /ext/java/org/jruby/ext/cgi/escape/lib/cgi/escape.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load built-in cgi/escape library 4 | require 'cgi/escape.jar' 5 | JRuby::Util.load_ext("org.jruby.ext.cgi.escape.CGIEscape") 6 | -------------------------------------------------------------------------------- /lib/cgi.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # cgi.rb - cgi support library 4 | # 5 | # Copyright (C) 2000 Network Applied Communication Laboratory, Inc. 6 | # 7 | # Copyright (C) 2000 Information-technology Promotion Agency, Japan 8 | # 9 | # Author: Wakou Aoyama 10 | # 11 | # Documentation: Wakou Aoyama (RDoc'd and embellished by William Webber) 12 | # 13 | 14 | # == Overview 15 | # 16 | # The Common Gateway Interface (CGI) is a simple protocol for passing an HTTP 17 | # request from a web server to a standalone program, and returning the output 18 | # to the web browser. Basically, a CGI program is called with the parameters 19 | # of the request passed in either in the environment (GET) or via $stdin 20 | # (POST), and everything it prints to $stdout is returned to the client. 21 | # 22 | # This file holds the CGI class. This class provides functionality for 23 | # retrieving HTTP request parameters, managing cookies, and generating HTML 24 | # output. 25 | # 26 | # The file CGI::Session provides session management functionality; see that 27 | # class for more details. 28 | # 29 | # See http://www.w3.org/CGI/ for more information on the CGI protocol. 30 | # 31 | # == Introduction 32 | # 33 | # CGI is a large class, providing several categories of methods, many of which 34 | # are mixed in from other modules. Some of the documentation is in this class, 35 | # some in the modules CGI::QueryExtension and CGI::HtmlExtension. See 36 | # CGI::Cookie for specific information on handling cookies, and cgi/session.rb 37 | # (CGI::Session) for information on sessions. 38 | # 39 | # For queries, CGI provides methods to get at environmental variables, 40 | # parameters, cookies, and multipart request data. For responses, CGI provides 41 | # methods for writing output and generating HTML. 42 | # 43 | # Read on for more details. Examples are provided at the bottom. 44 | # 45 | # == Queries 46 | # 47 | # The CGI class dynamically mixes in parameter and cookie-parsing 48 | # functionality, environmental variable access, and support for 49 | # parsing multipart requests (including uploaded files) from the 50 | # CGI::QueryExtension module. 51 | # 52 | # === Environmental Variables 53 | # 54 | # The standard CGI environmental variables are available as read-only 55 | # attributes of a CGI object. The following is a list of these variables: 56 | # 57 | # 58 | # AUTH_TYPE HTTP_HOST REMOTE_IDENT 59 | # CONTENT_LENGTH HTTP_NEGOTIATE REMOTE_USER 60 | # CONTENT_TYPE HTTP_PRAGMA REQUEST_METHOD 61 | # GATEWAY_INTERFACE HTTP_REFERER SCRIPT_NAME 62 | # HTTP_ACCEPT HTTP_USER_AGENT SERVER_NAME 63 | # HTTP_ACCEPT_CHARSET PATH_INFO SERVER_PORT 64 | # HTTP_ACCEPT_ENCODING PATH_TRANSLATED SERVER_PROTOCOL 65 | # HTTP_ACCEPT_LANGUAGE QUERY_STRING SERVER_SOFTWARE 66 | # HTTP_CACHE_CONTROL REMOTE_ADDR 67 | # HTTP_FROM REMOTE_HOST 68 | # 69 | # 70 | # For each of these variables, there is a corresponding attribute with the 71 | # same name, except all lower case and without a preceding HTTP_. 72 | # +content_length+ and +server_port+ are integers; the rest are strings. 73 | # 74 | # === Parameters 75 | # 76 | # The method #params() returns a hash of all parameters in the request as 77 | # name/value-list pairs, where the value-list is an Array of one or more 78 | # values. The CGI object itself also behaves as a hash of parameter names 79 | # to values, but only returns a single value (as a String) for each 80 | # parameter name. 81 | # 82 | # For instance, suppose the request contains the parameter 83 | # "favourite_colours" with the multiple values "blue" and "green". The 84 | # following behavior would occur: 85 | # 86 | # cgi.params["favourite_colours"] # => ["blue", "green"] 87 | # cgi["favourite_colours"] # => "blue" 88 | # 89 | # If a parameter does not exist, the former method will return an empty 90 | # array, the latter an empty string. The simplest way to test for existence 91 | # of a parameter is by the #has_key? method. 92 | # 93 | # === Cookies 94 | # 95 | # HTTP Cookies are automatically parsed from the request. They are available 96 | # from the #cookies() accessor, which returns a hash from cookie name to 97 | # CGI::Cookie object. 98 | # 99 | # === Multipart requests 100 | # 101 | # If a request's method is POST and its content type is multipart/form-data, 102 | # then it may contain uploaded files. These are stored by the QueryExtension 103 | # module in the parameters of the request. The parameter name is the name 104 | # attribute of the file input field, as usual. However, the value is not 105 | # a string, but an IO object, either an IOString for small files, or a 106 | # Tempfile for larger ones. This object also has the additional singleton 107 | # methods: 108 | # 109 | # #local_path():: the path of the uploaded file on the local filesystem 110 | # #original_filename():: the name of the file on the client computer 111 | # #content_type():: the content type of the file 112 | # 113 | # == Responses 114 | # 115 | # The CGI class provides methods for sending header and content output to 116 | # the HTTP client, and mixes in methods for programmatic HTML generation 117 | # from CGI::HtmlExtension and CGI::TagMaker modules. The precise version of HTML 118 | # to use for HTML generation is specified at object creation time. 119 | # 120 | # === Writing output 121 | # 122 | # The simplest way to send output to the HTTP client is using the #out() method. 123 | # This takes the HTTP headers as a hash parameter, and the body content 124 | # via a block. The headers can be generated as a string using the #http_header() 125 | # method. The output stream can be written directly to using the #print() 126 | # method. 127 | # 128 | # === Generating HTML 129 | # 130 | # Each HTML element has a corresponding method for generating that 131 | # element as a String. The name of this method is the same as that 132 | # of the element, all lowercase. The attributes of the element are 133 | # passed in as a hash, and the body as a no-argument block that evaluates 134 | # to a String. The HTML generation module knows which elements are 135 | # always empty, and silently drops any passed-in body. It also knows 136 | # which elements require matching closing tags and which don't. However, 137 | # it does not know what attributes are legal for which elements. 138 | # 139 | # There are also some additional HTML generation methods mixed in from 140 | # the CGI::HtmlExtension module. These include individual methods for the 141 | # different types of form inputs, and methods for elements that commonly 142 | # take particular attributes where the attributes can be directly specified 143 | # as arguments, rather than via a hash. 144 | # 145 | # === Utility HTML escape and other methods like a function. 146 | # 147 | # There are some utility tools defined in cgi/util.rb and cgi/escape.rb. 148 | # Escape and unescape methods are defined in cgi/escape.rb. 149 | # And when include, you can use utility methods like a function. 150 | # 151 | # == Examples of use 152 | # 153 | # === Get form values 154 | # 155 | # require "cgi" 156 | # cgi = CGI.new 157 | # value = cgi['field_name'] # <== value string for 'field_name' 158 | # # if not 'field_name' included, then return "". 159 | # fields = cgi.keys # <== array of field names 160 | # 161 | # # returns true if form has 'field_name' 162 | # cgi.has_key?('field_name') 163 | # cgi.has_key?('field_name') 164 | # cgi.include?('field_name') 165 | # 166 | # CAUTION! cgi['field_name'] returned an Array with the old 167 | # cgi.rb(included in Ruby 1.6) 168 | # 169 | # === Get form values as hash 170 | # 171 | # require "cgi" 172 | # cgi = CGI.new 173 | # params = cgi.params 174 | # 175 | # cgi.params is a hash. 176 | # 177 | # cgi.params['new_field_name'] = ["value"] # add new param 178 | # cgi.params['field_name'] = ["new_value"] # change value 179 | # cgi.params.delete('field_name') # delete param 180 | # cgi.params.clear # delete all params 181 | # 182 | # 183 | # === Save form values to file 184 | # 185 | # require "pstore" 186 | # db = PStore.new("query.db") 187 | # db.transaction do 188 | # db["params"] = cgi.params 189 | # end 190 | # 191 | # 192 | # === Restore form values from file 193 | # 194 | # require "pstore" 195 | # db = PStore.new("query.db") 196 | # db.transaction do 197 | # cgi.params = db["params"] 198 | # end 199 | # 200 | # 201 | # === Get multipart form values 202 | # 203 | # require "cgi" 204 | # cgi = CGI.new 205 | # value = cgi['field_name'] # <== value string for 'field_name' 206 | # value.read # <== body of value 207 | # value.local_path # <== path to local file of value 208 | # value.original_filename # <== original filename of value 209 | # value.content_type # <== content_type of value 210 | # 211 | # and value has StringIO or Tempfile class methods. 212 | # 213 | # === Get cookie values 214 | # 215 | # require "cgi" 216 | # cgi = CGI.new 217 | # values = cgi.cookies['name'] # <== array of 'name' 218 | # # if not 'name' included, then return []. 219 | # names = cgi.cookies.keys # <== array of cookie names 220 | # 221 | # and cgi.cookies is a hash. 222 | # 223 | # === Get cookie objects 224 | # 225 | # require "cgi" 226 | # cgi = CGI.new 227 | # for name, cookie in cgi.cookies 228 | # cookie.expires = Time.now + 30 229 | # end 230 | # cgi.out("cookie" => cgi.cookies) {"string"} 231 | # 232 | # cgi.cookies # { "name1" => cookie1, "name2" => cookie2, ... } 233 | # 234 | # require "cgi" 235 | # cgi = CGI.new 236 | # cgi.cookies['name'].expires = Time.now + 30 237 | # cgi.out("cookie" => cgi.cookies['name']) {"string"} 238 | # 239 | # === Print http header and html string to $DEFAULT_OUTPUT ($>) 240 | # 241 | # require "cgi" 242 | # cgi = CGI.new("html4") # add HTML generation methods 243 | # cgi.out do 244 | # cgi.html do 245 | # cgi.head do 246 | # cgi.title { "TITLE" } 247 | # end + 248 | # cgi.body do 249 | # cgi.form("ACTION" => "uri") do 250 | # cgi.p do 251 | # cgi.textarea("get_text") + 252 | # cgi.br + 253 | # cgi.submit 254 | # end 255 | # end + 256 | # cgi.pre do 257 | # CGI.escapeHTML( 258 | # "params: #{cgi.params.inspect}\n" + 259 | # "cookies: #{cgi.cookies.inspect}\n" + 260 | # ENV.collect do |key, value| 261 | # "#{key} --> #{value}\n" 262 | # end.join("") 263 | # ) 264 | # end 265 | # end 266 | # end 267 | # end 268 | # 269 | # # add HTML generation methods 270 | # CGI.new("html3") # html3.2 271 | # CGI.new("html4") # html4.01 (Strict) 272 | # CGI.new("html4Tr") # html4.01 Transitional 273 | # CGI.new("html4Fr") # html4.01 Frameset 274 | # CGI.new("html5") # html5 275 | # 276 | # === Some utility methods 277 | # 278 | # require 'cgi/escape' 279 | # CGI.escapeHTML('Usage: foo "bar" ') 280 | # 281 | # 282 | # === Some utility methods like a function 283 | # 284 | # require 'cgi/escape' 285 | # include CGI::Escape 286 | # escapeHTML('Usage: foo "bar" ') 287 | # h('Usage: foo "bar" ') # alias 288 | # 289 | # 290 | 291 | class CGI 292 | VERSION = "0.5.0" 293 | end 294 | 295 | require 'cgi/util' 296 | require 'cgi/escape' unless defined?(CGI::EscapeExt) 297 | require 'cgi/core' 298 | require 'cgi/cookie' 299 | CGI.autoload(:HtmlExtension, 'cgi/html') 300 | -------------------------------------------------------------------------------- /lib/cgi/cookie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative 'util' 3 | class CGI 4 | # Class representing an HTTP cookie. 5 | # 6 | # In addition to its specific fields and methods, a Cookie instance 7 | # is a delegator to the array of its values. 8 | # 9 | # See RFC 2965. 10 | # 11 | # == Examples of use 12 | # cookie1 = CGI::Cookie.new("name", "value1", "value2", ...) 13 | # cookie1 = CGI::Cookie.new("name" => "name", "value" => "value") 14 | # cookie1 = CGI::Cookie.new('name' => 'name', 15 | # 'value' => ['value1', 'value2', ...], 16 | # 'path' => 'path', # optional 17 | # 'domain' => 'domain', # optional 18 | # 'expires' => Time.now, # optional 19 | # 'secure' => true, # optional 20 | # 'httponly' => true # optional 21 | # ) 22 | # 23 | # cgi.out("cookie" => [cookie1, cookie2]) { "string" } 24 | # 25 | # name = cookie1.name 26 | # values = cookie1.value 27 | # path = cookie1.path 28 | # domain = cookie1.domain 29 | # expires = cookie1.expires 30 | # secure = cookie1.secure 31 | # httponly = cookie1.httponly 32 | # 33 | # cookie1.name = 'name' 34 | # cookie1.value = ['value1', 'value2', ...] 35 | # cookie1.path = 'path' 36 | # cookie1.domain = 'domain' 37 | # cookie1.expires = Time.now + 30 38 | # cookie1.secure = true 39 | # cookie1.httponly = true 40 | class Cookie < Array 41 | @@accept_charset="UTF-8" unless defined?(@@accept_charset) 42 | 43 | TOKEN_RE = %r"\A[[!-~]&&[^()<>@,;:\\\"/?=\[\]{}]]+\z" 44 | PATH_VALUE_RE = %r"\A[[ -~]&&[^;]]*\z" 45 | DOMAIN_VALUE_RE = %r"\A\.?(?