summaryrefslogtreecommitdiff
path: root/tool
diff options
context:
space:
mode:
Diffstat (limited to 'tool')
-rw-r--r--tool/asm_parse.rb53
-rwxr-xr-xtool/bisect.sh65
-rwxr-xr-xtool/build-transcode16
-rw-r--r--tool/bundler/dev_gems.rb19
-rw-r--r--tool/bundler/dev_gems.rb.lock57
-rw-r--r--tool/bundler/rubocop_gems.rb12
-rw-r--r--tool/bundler/rubocop_gems.rb.lock70
-rw-r--r--tool/bundler/standard_gems.rb12
-rw-r--r--tool/bundler/standard_gems.rb.lock76
-rw-r--r--tool/bundler/test_gems.rb14
-rw-r--r--tool/bundler/test_gems.rb.lock47
-rwxr-xr-xtool/checksum.rb72
-rw-r--r--tool/ci_functions.sh29
-rw-r--r--tool/colors3
-rwxr-xr-xtool/darwin-cc6
-rwxr-xr-xtool/disable_ipv6.sh9
-rw-r--r--tool/downloader.rb415
-rw-r--r--tool/enc-emoji-citrus-gen.rb131
-rw-r--r--tool/enc-emoji4unicode.rb133
-rwxr-xr-xtool/enc-unicode.rb577
-rw-r--r--tool/eval.rb158
-rwxr-xr-xtool/expand-config.rb33
-rwxr-xr-xtool/extlibs.rb263
-rw-r--r--tool/fake.rb61
-rwxr-xr-xtool/fetch-bundled_gems.rb27
-rwxr-xr-xtool/file2lastrev.rb124
-rwxr-xr-xtool/format-release262
-rwxr-xr-xtool/gen-mailmap.rb47
-rwxr-xr-xtool/gen_dummy_probes.rb32
-rwxr-xr-xtool/gen_ruby_tapset.rb105
-rw-r--r--tool/generic_erb.rb61
-rwxr-xr-xtool/git-refresh46
-rw-r--r--tool/gperf.sed22
-rwxr-xr-xtool/id2token.rb26
-rwxr-xr-xtool/ifchange119
-rwxr-xr-xtool/insns2vm.rb15
-rw-r--r--tool/install-sh17
-rwxr-xr-xtool/intern_ids.rb35
-rwxr-xr-xtool/leaked-globals65
-rw-r--r--tool/lib/-test-/integer.rb14
-rw-r--r--tool/lib/bundled_gem.rb68
-rw-r--r--tool/lib/colorize.rb55
-rw-r--r--tool/lib/core_assertions.rb809
-rw-r--r--tool/lib/envutil.rb367
-rw-r--r--tool/lib/find_executable.rb22
-rw-r--r--tool/lib/gc_checker.rb36
-rw-r--r--tool/lib/iseq_loader_checker.rb81
-rw-r--r--tool/lib/jisx0208.rb86
-rw-r--r--tool/lib/leakchecker.rb314
-rw-r--r--tool/lib/memory_status.rb151
-rw-r--r--tool/lib/profile_test_all.rb91
-rw-r--r--tool/lib/test/unit.rb1762
-rw-r--r--tool/lib/test/unit/assertions.rb839
-rw-r--r--tool/lib/test/unit/parallel.rb212
-rw-r--r--tool/lib/test/unit/testcase.rb296
-rw-r--r--tool/lib/tracepointchecker.rb126
-rw-r--r--tool/lib/vcs.rb733
-rw-r--r--tool/lib/vpath.rb87
-rw-r--r--tool/lib/webrick.rb232
-rw-r--r--tool/lib/webrick/.document6
-rw-r--r--tool/lib/webrick/accesslog.rb157
-rw-r--r--tool/lib/webrick/cgi.rb313
-rw-r--r--tool/lib/webrick/compat.rb36
-rw-r--r--tool/lib/webrick/config.rb158
-rw-r--r--tool/lib/webrick/cookie.rb172
-rw-r--r--tool/lib/webrick/htmlutils.rb30
-rw-r--r--tool/lib/webrick/httpauth.rb96
-rw-r--r--tool/lib/webrick/httpauth/authenticator.rb117
-rw-r--r--tool/lib/webrick/httpauth/basicauth.rb116
-rw-r--r--tool/lib/webrick/httpauth/digestauth.rb395
-rw-r--r--tool/lib/webrick/httpauth/htdigest.rb132
-rw-r--r--tool/lib/webrick/httpauth/htgroup.rb97
-rw-r--r--tool/lib/webrick/httpauth/htpasswd.rb158
-rw-r--r--tool/lib/webrick/httpauth/userdb.rb53
-rw-r--r--tool/lib/webrick/httpproxy.rb354
-rw-r--r--tool/lib/webrick/httprequest.rb636
-rw-r--r--tool/lib/webrick/httpresponse.rb564
-rw-r--r--tool/lib/webrick/https.rb152
-rw-r--r--tool/lib/webrick/httpserver.rb294
-rw-r--r--tool/lib/webrick/httpservlet.rb23
-rw-r--r--tool/lib/webrick/httpservlet/abstract.rb152
-rw-r--r--tool/lib/webrick/httpservlet/cgi_runner.rb47
-rw-r--r--tool/lib/webrick/httpservlet/cgihandler.rb126
-rw-r--r--tool/lib/webrick/httpservlet/erbhandler.rb88
-rw-r--r--tool/lib/webrick/httpservlet/filehandler.rb552
-rw-r--r--tool/lib/webrick/httpservlet/prochandler.rb47
-rw-r--r--tool/lib/webrick/httpstatus.rb194
-rw-r--r--tool/lib/webrick/httputils.rb512
-rw-r--r--tool/lib/webrick/httpversion.rb76
-rw-r--r--tool/lib/webrick/log.rb156
-rw-r--r--tool/lib/webrick/server.rb381
-rw-r--r--tool/lib/webrick/ssl.rb215
-rw-r--r--tool/lib/webrick/utils.rb265
-rw-r--r--tool/lib/webrick/version.rb18
-rw-r--r--tool/lib/zombie_hunter.rb10
-rw-r--r--tool/ln_sr.rb131
-rw-r--r--tool/m4/_colorize_result_prepare.m434
-rw-r--r--tool/m4/ac_msg_result.m45
-rw-r--r--tool/m4/colorize_result.m49
-rw-r--r--tool/m4/ruby_append_option.m45
-rw-r--r--tool/m4/ruby_append_options.m47
-rw-r--r--tool/m4/ruby_check_builtin_func.m410
-rw-r--r--tool/m4/ruby_check_builtin_setjmp.m427
-rw-r--r--tool/m4/ruby_check_printf_prefix.m429
-rw-r--r--tool/m4/ruby_check_setjmp.m417
-rw-r--r--tool/m4/ruby_check_signedness.m45
-rw-r--r--tool/m4/ruby_check_sizeof.m4108
-rw-r--r--tool/m4/ruby_check_sysconf.m413
-rw-r--r--tool/m4/ruby_cppoutfile.m418
-rw-r--r--tool/m4/ruby_decl_attribute.m445
-rw-r--r--tool/m4/ruby_default_arch.m411
-rw-r--r--tool/m4/ruby_define_if.m46
-rw-r--r--tool/m4/ruby_defint.m440
-rw-r--r--tool/m4/ruby_dtrace_available.m420
-rw-r--r--tool/m4/ruby_dtrace_postprocess.m430
-rw-r--r--tool/m4/ruby_func_attribute.m47
-rw-r--r--tool/m4/ruby_mingw32.m424
-rw-r--r--tool/m4/ruby_prepend_option.m45
-rw-r--r--tool/m4/ruby_prog_gnu_ld.m410
-rw-r--r--tool/m4/ruby_replace_funcs.m413
-rw-r--r--tool/m4/ruby_replace_type.m458
-rw-r--r--tool/m4/ruby_rm_recursive.m418
-rw-r--r--tool/m4/ruby_setjmp_type.m452
-rw-r--r--tool/m4/ruby_stack_grow_direction.m430
-rw-r--r--tool/m4/ruby_thread.m433
-rw-r--r--tool/m4/ruby_try_cflags.m412
-rw-r--r--tool/m4/ruby_try_cxxflags.m417
-rw-r--r--tool/m4/ruby_try_ldflags.m415
-rw-r--r--tool/m4/ruby_type_attribute.m48
-rw-r--r--tool/m4/ruby_universal_arch.m4122
-rw-r--r--tool/m4/ruby_werror_flag.m418
-rwxr-xr-xtool/make-snapshot654
-rw-r--r--tool/make_hgraph.rb95
-rwxr-xr-xtool/mdoc2man.rb505
-rwxr-xr-xtool/merger.rb314
-rw-r--r--tool/mjit_archflag.sh40
-rw-r--r--tool/mjit_tabs.rb67
-rw-r--r--tool/mk_builtin_loader.rb370
-rwxr-xr-xtool/mkconfig.rb392
-rwxr-xr-xtool/mkrunnable.rb149
-rwxr-xr-xtool/node_name.rb8
-rw-r--r--tool/parse.rb16
-rw-r--r--tool/prereq.status44
-rw-r--r--tool/probes_to_wiki.rb16
-rwxr-xr-xtool/pure_parser.rb24
-rwxr-xr-xtool/rbinstall.rb1142
-rwxr-xr-xtool/rbuninstall.rb73
-rwxr-xr-xtool/redmine-backporter.rb507
-rwxr-xr-xtool/release.sh19
-rwxr-xr-xtool/releng/gen-mail.rb50
-rwxr-xr-xtool/releng/gen-release-note.rb36
-rwxr-xr-xtool/releng/update-www-meta.rb213
-rwxr-xr-xtool/rmdirs14
-rw-r--r--tool/ruby_vm/controllers/application_controller.rb25
-rw-r--r--tool/ruby_vm/helpers/c_escape.rb128
-rw-r--r--tool/ruby_vm/helpers/dumper.rb113
-rw-r--r--tool/ruby_vm/helpers/scanner.rb53
-rw-r--r--tool/ruby_vm/loaders/insns_def.rb100
-rw-r--r--tool/ruby_vm/loaders/opt_insn_unif_def.rb34
-rw-r--r--tool/ruby_vm/loaders/opt_operand_def.rb56
-rw-r--r--tool/ruby_vm/loaders/vm_opts_h.rb37
-rw-r--r--tool/ruby_vm/models/attribute.rb59
-rwxr-xr-xtool/ruby_vm/models/bare_instructions.rb240
-rw-r--r--tool/ruby_vm/models/c_expr.rb41
-rw-r--r--tool/ruby_vm/models/instructions.rb22
-rw-r--r--tool/ruby_vm/models/instructions_unifications.rb43
-rw-r--r--tool/ruby_vm/models/operands_unifications.rb142
-rw-r--r--tool/ruby_vm/models/trace_instructions.rb71
-rw-r--r--tool/ruby_vm/models/typemap.rb62
-rw-r--r--tool/ruby_vm/scripts/converter.rb29
-rw-r--r--tool/ruby_vm/scripts/insns2vm.rb93
-rw-r--r--tool/ruby_vm/tests/.gitkeep0
-rw-r--r--tool/ruby_vm/views/_attributes.erb35
-rw-r--r--tool/ruby_vm/views/_c_expr.erb17
-rw-r--r--tool/ruby_vm/views/_comptime_insn_stack_increase.erb62
-rw-r--r--tool/ruby_vm/views/_copyright.erb31
-rw-r--r--tool/ruby_vm/views/_insn_entry.erb76
-rw-r--r--tool/ruby_vm/views/_insn_len_info.erb28
-rw-r--r--tool/ruby_vm/views/_insn_name_info.erb44
-rw-r--r--tool/ruby_vm/views/_insn_operand_info.erb53
-rw-r--r--tool/ruby_vm/views/_insn_sp_pc_dependency.erb27
-rw-r--r--tool/ruby_vm/views/_insn_type_chars.erb13
-rw-r--r--tool/ruby_vm/views/_leaf_helpers.erb54
-rw-r--r--tool/ruby_vm/views/_mjit_compile_getinlinecache.erb31
-rw-r--r--tool/ruby_vm/views/_mjit_compile_insn.erb92
-rw-r--r--tool/ruby_vm/views/_mjit_compile_insn_body.erb129
-rw-r--r--tool/ruby_vm/views/_mjit_compile_invokebuiltin.erb29
-rw-r--r--tool/ruby_vm/views/_mjit_compile_ivar.erb101
-rw-r--r--tool/ruby_vm/views/_mjit_compile_pc_and_sp.erb38
-rw-r--r--tool/ruby_vm/views/_mjit_compile_send.erb119
-rw-r--r--tool/ruby_vm/views/_notice.erb22
-rw-r--r--tool/ruby_vm/views/_sp_inc_helpers.erb37
-rw-r--r--tool/ruby_vm/views/_trace_instruction.erb21
-rw-r--r--tool/ruby_vm/views/insns.inc.erb26
-rw-r--r--tool/ruby_vm/views/insns_info.inc.erb22
-rw-r--r--tool/ruby_vm/views/mjit_compile.inc.erb110
-rw-r--r--tool/ruby_vm/views/opt_sc.inc.erb40
-rw-r--r--tool/ruby_vm/views/optinsn.inc.erb71
-rw-r--r--tool/ruby_vm/views/optunifs.inc.erb21
-rw-r--r--tool/ruby_vm/views/vm.inc.erb30
-rw-r--r--tool/ruby_vm/views/vmtc.inc.erb21
-rw-r--r--tool/run-gcov.rb54
-rw-r--r--tool/run-lcov.rb164
-rwxr-xr-xtool/runruby.rb178
-rw-r--r--tool/search-cgvars.rb55
-rwxr-xr-xtool/strip-rdoc.rb14
-rwxr-xr-xtool/sync_default_gems.rb638
-rw-r--r--tool/test-bundled-gems.rb116
-rw-r--r--tool/test-coverage.rb118
-rw-r--r--tool/test/runner.rb23
-rw-r--r--tool/test/test_jisx0208.rb40
-rw-r--r--tool/test/testunit/metametameta.rb70
-rw-r--r--tool/test/testunit/test4test_hideskip.rb10
-rw-r--r--tool/test/testunit/test4test_redefinition.rb14
-rw-r--r--tool/test/testunit/test4test_sorting.rb18
-rw-r--r--tool/test/testunit/test_assertion.rb29
-rw-r--r--tool/test/testunit/test_hideskip.rb21
-rw-r--r--tool/test/testunit/test_minitest_unit.rb1474
-rw-r--r--tool/test/testunit/test_parallel.rb219
-rw-r--r--tool/test/testunit/test_redefinition.rb11
-rw-r--r--tool/test/testunit/test_sorting.rb75
-rw-r--r--tool/test/testunit/tests_for_parallel/ptest_first.rb8
-rw-r--r--tool/test/testunit/tests_for_parallel/ptest_forth.rb30
-rw-r--r--tool/test/testunit/tests_for_parallel/ptest_second.rb12
-rw-r--r--tool/test/testunit/tests_for_parallel/ptest_third.rb11
-rw-r--r--tool/test/testunit/tests_for_parallel/runner.rb14
-rw-r--r--tool/test/testunit/tests_for_parallel/test4test_hungup.rb15
-rw-r--r--tool/test/webrick/.htaccess1
-rw-r--r--tool/test/webrick/test_cgi.rb170
-rw-r--r--tool/test/webrick/test_config.rb17
-rw-r--r--tool/test/webrick/test_cookie.rb141
-rw-r--r--tool/test/webrick/test_do_not_reverse_lookup.rb71
-rw-r--r--tool/test/webrick/test_filehandler.rb403
-rw-r--r--tool/test/webrick/test_htgroup.rb19
-rw-r--r--tool/test/webrick/test_htmlutils.rb21
-rw-r--r--tool/test/webrick/test_httpauth.rb366
-rw-r--r--tool/test/webrick/test_httpproxy.rb467
-rw-r--r--tool/test/webrick/test_httprequest.rb488
-rw-r--r--tool/test/webrick/test_httpresponse.rb282
-rw-r--r--tool/test/webrick/test_https.rb112
-rw-r--r--tool/test/webrick/test_httpserver.rb543
-rw-r--r--tool/test/webrick/test_httpstatus.rb35
-rw-r--r--tool/test/webrick/test_httputils.rb101
-rw-r--r--tool/test/webrick/test_httpversion.rb41
-rw-r--r--tool/test/webrick/test_server.rb191
-rw-r--r--tool/test/webrick/test_ssl_server.rb67
-rw-r--r--tool/test/webrick/test_utils.rb110
-rw-r--r--tool/test/webrick/utils.rb84
-rw-r--r--tool/test/webrick/webrick.cgi38
-rw-r--r--tool/test/webrick/webrick.rhtml4
-rw-r--r--tool/test/webrick/webrick_long_filename.cgi36
-rw-r--r--tool/transcode-tblgen.rb1118
-rw-r--r--tool/transform_mjit_header.rb326
-rwxr-xr-xtool/travis_retry.sh13
-rwxr-xr-xtool/travis_wait.sh18
-rwxr-xr-xtool/update-bundled_gems.rb20
-rwxr-xr-xtool/update-deps650
-rw-r--r--tool/vtlh.rb17
-rwxr-xr-xtool/ytab.sed80
259 files changed, 34602 insertions, 0 deletions
diff --git a/tool/asm_parse.rb b/tool/asm_parse.rb
new file mode 100644
index 0000000000..32882be3ad
--- /dev/null
+++ b/tool/asm_parse.rb
@@ -0,0 +1,53 @@
+# YARV tool to parse assembly output.
+
+stat = {}
+
+while line = ARGF.gets
+ if /\[start\] (\w+)/ =~ line
+ name = $1
+ puts '--------------------------------------------------------------'
+ puts line
+ size = 0
+ len = 0
+
+ while line = ARGF.gets
+ if /\[start\] (\w+)/ =~ line
+ puts "\t; # length: #{len}, size: #{size}"
+ puts "\t; # !!"
+ stat[name] = [len, size]
+ #
+ name = $1
+ puts '--------------------------------------------------------------'
+ puts line
+ size = 0
+ len = 0
+ next
+ end
+
+ unless /(\ALM)|(\ALB)|(\A\.)|(\A\/)/ =~ line
+ puts line
+ if /\[length = (\d+)\]/ =~ line
+ len += $1.to_i
+ size += 1
+ end
+ end
+
+
+ if /__NEXT_INSN__/ !~ line && /\[end \] (\w+)/ =~ line
+ ename = $1
+ if name != ename
+ puts "!! start with #{name}, but end with #{ename}"
+ end
+ stat[ename] = [len, size]
+ puts "\t; # length: #{len}, size: #{size}"
+ break
+ end
+ end
+ end
+end
+
+stat.sort_by{|a, b| -b[0] * 1000 - a[0]}.each{|a, b|
+ puts "#{a}\t#{b.join("\t")}"
+}
+puts "total length :\t#{stat.inject(0){|r, e| r+e[1][0]}}"
+puts "total size :\t#{stat.inject(0){|r, e| r+e[1][1]}}"
diff --git a/tool/bisect.sh b/tool/bisect.sh
new file mode 100755
index 0000000000..dfc3a64041
--- /dev/null
+++ b/tool/bisect.sh
@@ -0,0 +1,65 @@
+#!/bin/sh
+# usage:
+# edit $(srcdir)/test.rb
+# git bisect start <bad> <good>
+# cd <builddir>
+# make bisect (or bisect-ruby for full ruby)
+
+if [ "x" = "x$MAKE" ]; then
+ MAKE=make
+fi
+
+case $1 in
+ miniruby | ruby ) # (miniruby|ruby) <srcdir>
+ srcdir="$2"
+ builddir=`pwd` # assume pwd is builddir
+ path="$builddir/_bisect.sh"
+ echo "path: $path"
+ cp "$0" "$path"
+ cd "$srcdir"
+ set -x
+ exec git bisect run "$path" "run-$1"
+ ;;
+ run-miniruby )
+ prep=mini
+ run=run
+ ;;
+ run-ruby )
+ prep=program
+ run=runruby
+ ;;
+ "" )
+ echo missing command 1>&2
+ exit 1
+ ;;
+ * )
+ echo unknown command "'$1'" 1>&2
+ exit 1
+ ;;
+esac
+
+# Apply $(srcdir)/bisect.patch to build if exists
+# e.g., needs 5c2508060b~2..5c2508060b to use Bison 3.5.91.
+if [ -f bisect.patch ]; then
+ if ! patch -p1 -N < bisect.patch || git diff --no-patch --exit-code; then
+ exit 125
+ fi
+ git status
+ exec=
+else
+ exec=exec
+fi
+
+case "$0" in
+*/*)
+ # assume a copy of this script is in builddir
+ cd `echo "$0" | sed 's:\(.*\)/.*:\1:'` || exit 125
+ ;;
+esac
+for target in srcs Makefile $prep; do
+ $MAKE $target || exit 125
+done
+$exec $MAKE $run
+status=$?
+git checkout -f HEAD
+exit $status
diff --git a/tool/build-transcode b/tool/build-transcode
new file mode 100755
index 0000000000..fa71155530
--- /dev/null
+++ b/tool/build-transcode
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+[ "$1" -a -d "$1" ] && { cd "$1" || exit $?; } && shift
+[ "$#" = 0 ] && set enc/trans/*.trans
+for src; do
+ case "$src" in
+ *.trans)
+ c="`dirname $src`/`basename $src .trans`.c"
+ ${BASERUBY-ruby} tool/transcode-tblgen.rb -vo "$c" "$src"
+ ;;
+ *)
+ echo "$0: don't know how to deal with $src"
+ continue
+ ;;
+ esac
+done
diff --git a/tool/bundler/dev_gems.rb b/tool/bundler/dev_gems.rb
new file mode 100644
index 0000000000..5b3ab24f5b
--- /dev/null
+++ b/tool/bundler/dev_gems.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "rdoc", "6.2.0" # 6.2.1 is required > Ruby 2.3
+gem "test-unit", "~> 3.0"
+gem "rake", "~> 13.0"
+
+gem "webrick", "~> 1.6"
+gem "parallel_tests", "~> 2.29"
+gem "parallel", "1.19.2" # 1.20+ is required > Ruby 2.3
+gem "rspec-core", "~> 3.8"
+gem "rspec-expectations", "~> 3.8"
+gem "rspec-mocks", "~> 3.8"
+gem "uri", "~> 0.10.1"
+
+group :doc do
+ gem "ronn", "~> 0.7.3", :platform => :ruby
+end
diff --git a/tool/bundler/dev_gems.rb.lock b/tool/bundler/dev_gems.rb.lock
new file mode 100644
index 0000000000..9787994e95
--- /dev/null
+++ b/tool/bundler/dev_gems.rb.lock
@@ -0,0 +1,57 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ diff-lcs (1.5.0)
+ hpricot (0.8.6)
+ hpricot (0.8.6-java)
+ mustache (1.1.1)
+ parallel (1.19.2)
+ parallel_tests (2.32.0)
+ parallel
+ power_assert (2.0.3)
+ rake (13.1.0)
+ rdiscount (2.2.7.1)
+ rdoc (6.2.0)
+ ronn (0.7.3)
+ hpricot (>= 0.8.2)
+ mustache (>= 0.7.0)
+ rdiscount (>= 1.5.8)
+ rspec-core (3.12.2)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.6)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-support (3.12.1)
+ test-unit (3.6.1)
+ power_assert
+ uri (0.10.3)
+ webrick (1.8.1)
+
+PLATFORMS
+ java
+ ruby
+ universal-java-11
+ universal-java-18
+ x64-mingw-ucrt
+ x64-mingw32
+ x86_64-darwin-20
+ x86_64-linux
+
+DEPENDENCIES
+ parallel (= 1.19.2)
+ parallel_tests (~> 2.29)
+ rake (~> 13.0)
+ rdoc (= 6.2.0)
+ ronn (~> 0.7.3)
+ rspec-core (~> 3.8)
+ rspec-expectations (~> 3.8)
+ rspec-mocks (~> 3.8)
+ test-unit (~> 3.0)
+ uri (~> 0.10.1)
+ webrick (~> 1.6)
+
+BUNDLED WITH
+ 2.3.27
diff --git a/tool/bundler/rubocop_gems.rb b/tool/bundler/rubocop_gems.rb
new file mode 100644
index 0000000000..25408b70f3
--- /dev/null
+++ b/tool/bundler/rubocop_gems.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "rubocop", "1.31.0"
+gem "parser", "3.2.2.2"
+
+gem "minitest"
+gem "rake"
+gem "rake-compiler"
+gem "rspec"
+gem "test-unit"
diff --git a/tool/bundler/rubocop_gems.rb.lock b/tool/bundler/rubocop_gems.rb.lock
new file mode 100644
index 0000000000..fd7a290019
--- /dev/null
+++ b/tool/bundler/rubocop_gems.rb.lock
@@ -0,0 +1,70 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ ast (2.4.2)
+ diff-lcs (1.5.0)
+ minitest (5.20.0)
+ parallel (1.23.0)
+ parser (3.2.2.2)
+ ast (~> 2.4.1)
+ power_assert (2.0.3)
+ rainbow (3.1.1)
+ rake (13.1.0)
+ rake-compiler (1.2.5)
+ rake
+ regexp_parser (2.8.2)
+ rexml (3.2.6)
+ rspec (3.12.0)
+ rspec-core (~> 3.12.0)
+ rspec-expectations (~> 3.12.0)
+ rspec-mocks (~> 3.12.0)
+ rspec-core (3.12.2)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.6)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-support (3.12.1)
+ rubocop (1.31.0)
+ parallel (~> 1.10)
+ parser (>= 3.1.0.0)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.18.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 1.4.0, < 3.0)
+ rubocop-ast (1.30.0)
+ parser (>= 3.2.1.0)
+ ruby-progressbar (1.13.0)
+ test-unit (3.6.1)
+ power_assert
+ unicode-display_width (2.5.0)
+
+PLATFORMS
+ aarch64-linux
+ arm64-darwin-20
+ arm64-darwin-21
+ arm64-darwin-22
+ arm64-darwin-23
+ universal-java-11
+ universal-java-18
+ x64-mingw-ucrt
+ x86_64-darwin-19
+ x86_64-darwin-20
+ x86_64-darwin-21
+ x86_64-linux
+
+DEPENDENCIES
+ minitest
+ parser (= 3.2.2.2)
+ rake
+ rake-compiler
+ rspec
+ rubocop (= 1.31.0)
+ test-unit
+
+BUNDLED WITH
+ 2.3.27
diff --git a/tool/bundler/standard_gems.rb b/tool/bundler/standard_gems.rb
new file mode 100644
index 0000000000..34cdfa2a61
--- /dev/null
+++ b/tool/bundler/standard_gems.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "standard", "1.12.1"
+gem "parser", "3.2.2.2"
+
+gem "minitest"
+gem "rake"
+gem "rake-compiler"
+gem "rspec"
+gem "test-unit"
diff --git a/tool/bundler/standard_gems.rb.lock b/tool/bundler/standard_gems.rb.lock
new file mode 100644
index 0000000000..b3861729e6
--- /dev/null
+++ b/tool/bundler/standard_gems.rb.lock
@@ -0,0 +1,76 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ ast (2.4.2)
+ diff-lcs (1.5.0)
+ minitest (5.20.0)
+ parallel (1.23.0)
+ parser (3.2.2.2)
+ ast (~> 2.4.1)
+ power_assert (2.0.3)
+ rainbow (3.1.1)
+ rake (13.1.0)
+ rake-compiler (1.2.5)
+ rake
+ regexp_parser (2.8.2)
+ rexml (3.2.6)
+ rspec (3.12.0)
+ rspec-core (~> 3.12.0)
+ rspec-expectations (~> 3.12.0)
+ rspec-mocks (~> 3.12.0)
+ rspec-core (3.12.2)
+ rspec-support (~> 3.12.0)
+ rspec-expectations (3.12.3)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-mocks (3.12.6)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.12.0)
+ rspec-support (3.12.1)
+ rubocop (1.29.1)
+ parallel (~> 1.10)
+ parser (>= 3.1.0.0)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.17.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 1.4.0, < 3.0)
+ rubocop-ast (1.30.0)
+ parser (>= 3.2.1.0)
+ rubocop-performance (1.13.3)
+ rubocop (>= 1.7.0, < 2.0)
+ rubocop-ast (>= 0.4.0)
+ ruby-progressbar (1.13.0)
+ standard (1.12.1)
+ rubocop (= 1.29.1)
+ rubocop-performance (= 1.13.3)
+ test-unit (3.6.1)
+ power_assert
+ unicode-display_width (2.5.0)
+
+PLATFORMS
+ aarch64-linux
+ arm64-darwin-20
+ arm64-darwin-21
+ arm64-darwin-22
+ arm64-darwin-23
+ universal-java-11
+ universal-java-18
+ x64-mingw-ucrt
+ x86_64-darwin-19
+ x86_64-darwin-20
+ x86_64-darwin-21
+ x86_64-linux
+
+DEPENDENCIES
+ minitest
+ parser (= 3.2.2.2)
+ rake
+ rake-compiler
+ rspec
+ standard (= 1.12.1)
+ test-unit
+
+BUNDLED WITH
+ 2.3.27
diff --git a/tool/bundler/test_gems.rb b/tool/bundler/test_gems.rb
new file mode 100644
index 0000000000..c848ade9c7
--- /dev/null
+++ b/tool/bundler/test_gems.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "rack", "~> 2.0"
+gem "webrick", "1.7.0"
+gem "rack-test", "~> 1.1"
+gem "artifice", "~> 0.6.0"
+gem "compact_index", "~> 0.13.0"
+gem "sinatra", "~> 2.0"
+# for Ruby 2.6. bundler/spec/install/gemfile/sources_spec.rb is failed with sinatra-2.0.8.1 and tilt-2.2.0.
+gem "tilt", "~> 2.0.11"
+gem "rake", "13.0.1"
+gem "builder", "~> 3.2"
diff --git a/tool/bundler/test_gems.rb.lock b/tool/bundler/test_gems.rb.lock
new file mode 100644
index 0000000000..ad8fed8044
--- /dev/null
+++ b/tool/bundler/test_gems.rb.lock
@@ -0,0 +1,47 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ artifice (0.6)
+ rack-test
+ builder (3.2.4)
+ compact_index (0.13.0)
+ mustermann (2.0.2)
+ ruby2_keywords (~> 0.0.1)
+ rack (2.2.8)
+ rack-protection (2.2.4)
+ rack
+ rack-test (1.1.0)
+ rack (>= 1.0, < 3)
+ rake (13.0.1)
+ ruby2_keywords (0.0.5)
+ sinatra (2.2.4)
+ mustermann (~> 2.0)
+ rack (~> 2.2)
+ rack-protection (= 2.2.4)
+ tilt (~> 2.0)
+ tilt (2.0.11)
+ webrick (1.7.0)
+
+PLATFORMS
+ java
+ ruby
+ universal-java-11
+ universal-java-18
+ x64-mingw-ucrt
+ x64-mingw32
+ x86_64-darwin-20
+ x86_64-linux
+
+DEPENDENCIES
+ artifice (~> 0.6.0)
+ builder (~> 3.2)
+ compact_index (~> 0.13.0)
+ rack (~> 2.0)
+ rack-test (~> 1.1)
+ rake (= 13.0.1)
+ sinatra (~> 2.0)
+ tilt (~> 2.0.11)
+ webrick (= 1.7.0)
+
+BUNDLED WITH
+ 2.3.27
diff --git a/tool/checksum.rb b/tool/checksum.rb
new file mode 100755
index 0000000000..bcc60ee14a
--- /dev/null
+++ b/tool/checksum.rb
@@ -0,0 +1,72 @@
+#!ruby
+
+require_relative 'lib/vpath'
+
+class Checksum
+ def initialize(vpath)
+ @vpath = vpath
+ end
+
+ attr_reader :source, :target
+
+ def source=(source)
+ @source = source
+ @checksum = File.basename(source, ".*") + ".chksum"
+ end
+
+ def target=(target)
+ @target = target
+ end
+
+ def update?
+ src = @vpath.read(@source)
+ @len = src.length
+ @sum = src.sum
+ return false unless @vpath.search(File.method(:exist?), @target)
+ begin
+ data = @vpath.read(@checksum)
+ rescue
+ return false
+ else
+ return false unless data[/src="([0-9a-z_.-]+)",/, 1] == @source
+ return false unless @len == data[/\blen=(\d+)/, 1].to_i
+ return false unless @sum == data[/\bchecksum=(\d+)/, 1].to_i
+ return true
+ end
+ end
+
+ def update!
+ open(@checksum, "wb") {|f|
+ f.puts("src=\"#{@source}\", len=#{@len}, checksum=#{@sum}")
+ }
+ end
+
+ def update
+ return true if update?
+ update! if ret = yield(self)
+ ret
+ end
+
+ def copy(name)
+ @vpath.open(name, "rb") {|f|
+ IO.copy_stream(f, name)
+ }
+ true
+ end
+
+ def make(*args)
+ system(@make, *args)
+ end
+
+ def def_options(opt = (require 'optparse'; OptionParser.new))
+ @vpath.def_options(opt)
+ opt.on("--make=PATH") {|v| @make = v}
+ opt
+ end
+
+ def self.update(argv)
+ k = new(VPath.new)
+ k.source, k.target, *argv = k.def_options.parse(*argv)
+ k.update {|_| yield(_, *argv)}
+ end
+end
diff --git a/tool/ci_functions.sh b/tool/ci_functions.sh
new file mode 100644
index 0000000000..7066bbe4ec
--- /dev/null
+++ b/tool/ci_functions.sh
@@ -0,0 +1,29 @@
+# -*- BASH -*-
+# Manage functions used on a CI.
+# Run `. tool/ci_functions.sh` to use it.
+
+# Create options with patterns `-n !/name1/ -n !/name2/ ..` to exclude the test
+# method names by the method names `name1 name2 ..`.
+# See `ruby tool/test/runner.rb --help` `-n` option.
+function ci_to_excluded_test_opts {
+ local tests_str="${1}"
+ # Use the backward matching `!/name$/`, as the perfect matching doesn't work.
+ # https://bugs.ruby-lang.org/issues/16936
+ ruby <<EOF
+ opts = "${tests_str}".split.map { |test| "-n \!/#{test}\$$/" }
+ puts opts.join(' ')
+EOF
+ return 0
+}
+
+# Create options with patterns `-n name1 -n name2 ..` to include the test
+# method names by the method names `name1 name2 ..`.
+# See `ruby tool/test/runner.rb --help` `-n` option.
+function ci_to_included_test_opts {
+ local tests_str="${1}"
+ ruby <<EOF
+ opts = "${tests_str}".split.map { |test| "-n #{test}" }
+ puts opts.join(' ')
+EOF
+ return 0
+}
diff --git a/tool/colors b/tool/colors
new file mode 100644
index 0000000000..a65c326ade
--- /dev/null
+++ b/tool/colors
@@ -0,0 +1,3 @@
+pass=36;7
+fail=31;1;7
+skip=33;1
diff --git a/tool/darwin-cc b/tool/darwin-cc
new file mode 100755
index 0000000000..6eee96e435
--- /dev/null
+++ b/tool/darwin-cc
@@ -0,0 +1,6 @@
+#!/bin/bash
+exec 2> >(exec grep -v \
+ -e '^ld: warning: The [a-z0-9_][a-z0-9_]* architecture is deprecated for macOS' \
+ -e '^ld: warning: text-based stub file /System/Library/Frameworks/' \
+ >&2)
+exec "$@"
diff --git a/tool/disable_ipv6.sh b/tool/disable_ipv6.sh
new file mode 100755
index 0000000000..ce1cc0da68
--- /dev/null
+++ b/tool/disable_ipv6.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -ex
+sysctl -w net.ipv6.conf.all.disable_ipv6=1
+sysctl -w net.ipv6.conf.default.disable_ipv6=1
+sysctl -w net.ipv6.conf.lo.disable_ipv6=1
+
+cat /etc/hosts
+ruby -e "hosts = File.read('/etc/hosts').sub(/^::1\s*localhost.*$/, ''); File.write('/etc/hosts', hosts)"
+cat /etc/hosts
diff --git a/tool/downloader.rb b/tool/downloader.rb
new file mode 100644
index 0000000000..d3a9f75637
--- /dev/null
+++ b/tool/downloader.rb
@@ -0,0 +1,415 @@
+# Used by configure and make to download or update mirrored Ruby and GCC
+# files. This will use HTTPS if possible, falling back to HTTP.
+
+require 'fileutils'
+require 'open-uri'
+require 'pathname'
+begin
+ require 'net/https'
+rescue LoadError
+ https = 'http'
+else
+ https = 'https'
+
+ # open-uri of ruby 2.2.0 accepts an array of PEMs as ssl_ca_cert, but old
+ # versions do not. so, patching OpenSSL::X509::Store#add_file instead.
+ class OpenSSL::X509::Store
+ alias orig_add_file add_file
+ def add_file(pems)
+ Array(pems).each do |pem|
+ if File.directory?(pem)
+ add_path pem
+ else
+ orig_add_file pem
+ end
+ end
+ end
+ end
+ # since open-uri internally checks ssl_ca_cert using File.directory?,
+ # allow to accept an array.
+ class <<File
+ alias orig_directory? directory?
+ def File.directory? files
+ files.is_a?(Array) ? false : orig_directory?(files)
+ end
+ end
+end
+
+class Downloader
+ def self.https=(https)
+ @@https = https
+ end
+
+ def self.https?
+ @@https == 'https'
+ end
+
+ def self.https
+ @@https
+ end
+
+ class GNU < self
+ def self.download(name, *rest)
+ if https?
+ begin
+ super("https://cdn.jsdelivr.net/gh/gcc-mirror/gcc@master/#{name}", name, *rest)
+ rescue => e
+ STDERR.puts "Download failed (#{e.message}), try another URL"
+ super("https://raw.githubusercontent.com/gcc-mirror/gcc/master/#{name}", name, *rest)
+ end
+ else
+ super("https://repo.or.cz/official-gcc.git/blob_plain/HEAD:/#{name}", name, *rest)
+ end
+ end
+ end
+
+ class RubyGems < self
+ def self.download(name, dir = nil, since = true, options = {})
+ require 'rubygems'
+ options = options.dup
+ options[:ssl_ca_cert] = Dir.glob(File.expand_path("../lib/rubygems/ssl_certs/**/*.pem", File.dirname(__FILE__)))
+ super("https://rubygems.org/downloads/#{name}", name, dir, since, options)
+ end
+ end
+
+ Gems = RubyGems
+
+ class Unicode < self
+ INDEX = {} # cache index file information across files in the same directory
+ UNICODE_PUBLIC = "https://www.unicode.org/Public/"
+
+ def self.download(name, dir = nil, since = true, options = {})
+ options = options.dup
+ unicode_beta = options.delete(:unicode_beta)
+ name_dir_part = name.sub(/[^\/]+$/, '')
+ if unicode_beta == 'YES'
+ if INDEX.size == 0
+ index_options = options.dup
+ index_options[:cache_save] = false # TODO: make sure caching really doesn't work for index file
+ index_data = File.read(under(dir, "index.html")) rescue nil
+ index_file = super(UNICODE_PUBLIC+name_dir_part, "#{name_dir_part}index.html", dir, true, index_options)
+ INDEX[:index] = File.read(index_file)
+ since = true unless INDEX[:index] == index_data
+ end
+ file_base = File.basename(name, '.txt')
+ return if file_base == '.' # Use pre-generated headers and tables
+ beta_name = INDEX[:index][/#{Regexp.quote(file_base)}(-[0-9.]+d\d+)?\.txt/]
+ # make sure we always check for new versions of files,
+ # because they can easily change in the beta period
+ super(UNICODE_PUBLIC+name_dir_part+beta_name, name, dir, since, options)
+ else
+ index_file = Pathname.new(under(dir, name_dir_part+'index.html'))
+ if index_file.exist? and name_dir_part !~ /^(12\.1\.0|emoji\/12\.0)/
+ raise "Although Unicode is not in beta, file #{index_file} exists. " +
+ "Remove all files in this directory and in .downloaded-cache/ " +
+ "because they may be leftovers from the beta period."
+ end
+ super(UNICODE_PUBLIC+name, name, dir, since, options)
+ end
+ end
+ end
+
+ def self.mode_for(data)
+ /\A#!/ =~ data ? 0755 : 0644
+ end
+
+ def self.http_options(file, since)
+ options = {}
+ if since
+ case since
+ when true
+ since = (File.mtime(file).httpdate rescue nil)
+ when Time
+ since = since.httpdate
+ end
+ if since
+ options['If-Modified-Since'] = since
+ end
+ end
+ options['Accept-Encoding'] = 'identity' # to disable Net::HTTP::GenericRequest#decode_content
+ options
+ end
+
+ def self.httpdate(date)
+ Time.httpdate(date)
+ rescue ArgumentError => e
+ # Some hosts (e.g., zlib.net) return similar to RFC 850 but 4
+ # digit year, sometimes.
+ /\A\s*
+ (?:Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day,\x20
+ (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{4})\x20
+ (\d\d):(\d\d):(\d\d)\x20
+ GMT
+ \s*\z/ix =~ date or raise
+ warn e.message
+ Time.utc($3, $2, $1, $4, $5, $6)
+ end
+
+ # Downloader.download(url, name, [dir, [since]])
+ #
+ # Update a file from url if newer version is available.
+ # Creates the file if the file doesn't yet exist; however, the
+ # directory where the file is being created has to exist already.
+ # The +since+ parameter can take the following values, with associated meanings:
+ # true ::
+ # Take the last-modified time of the current file on disk, and only download
+ # if the server has a file that was modified later. Download unconditionally
+ # if we don't have the file yet. Default.
+ # +some time value+ ::
+ # Use this time value instead of the time of modification of the file on disk.
+ # nil ::
+ # Only download the file if it doesn't exist yet.
+ # false ::
+ # always download url regardless of whether we already have a file,
+ # and regardless of modification times. (This is essentially just a waste of
+ # network resources, except in the case that the file we have is somehow damaged.
+ # Please note that using this recurringly might create or be seen as a
+ # denial of service attack.)
+ #
+ # Example usage:
+ # download 'http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt',
+ # 'UnicodeData.txt', 'enc/unicode/data'
+ def self.download(url, name, dir = nil, since = true, options = {})
+ options = options.dup
+ url = URI(url)
+ dryrun = options.delete(:dryrun)
+ options.delete(:unicode_beta) # just to be on the safe side for gems and gcc
+
+ if name
+ file = Pathname.new(under(dir, name))
+ else
+ name = File.basename(url.path)
+ end
+ cache_save = options.delete(:cache_save) {
+ ENV["CACHE_SAVE"] != "no"
+ }
+ cache = cache_file(url, name, options.delete(:cache_dir))
+ file ||= cache
+ if since.nil? and file.exist?
+ if $VERBOSE
+ $stdout.puts "#{file} already exists"
+ $stdout.flush
+ end
+ if cache_save
+ save_cache(cache, file, name)
+ end
+ return file.to_path
+ end
+ if dryrun
+ puts "Download #{url} into #{file}"
+ return
+ end
+ if link_cache(cache, file, name, $VERBOSE)
+ return file.to_path
+ end
+ if !https? and URI::HTTPS === url
+ warn "*** using http instead of https ***"
+ url.scheme = 'http'
+ url = URI(url.to_s)
+ end
+ if $VERBOSE
+ $stdout.print "downloading #{name} ... "
+ $stdout.flush
+ end
+ mtime = nil
+ options = options.merge(http_options(file, since.nil? ? true : since))
+ begin
+ data = with_retry(10) do
+ data = url.read(options)
+ if mtime = data.meta["last-modified"]
+ mtime = Time.httpdate(mtime)
+ end
+ data
+ end
+ rescue OpenURI::HTTPError => http_error
+ if http_error.message =~ /^304 / # 304 Not Modified
+ if $VERBOSE
+ $stdout.puts "#{name} not modified"
+ $stdout.flush
+ end
+ return file.to_path
+ end
+ raise
+ rescue Timeout::Error
+ if since.nil? and file.exist?
+ puts "Request for #{url} timed out, using old version."
+ return file.to_path
+ end
+ raise
+ rescue SocketError
+ if since.nil? and file.exist?
+ puts "No network connection, unable to download #{url}, using old version."
+ return file.to_path
+ end
+ raise
+ end
+ dest = (cache_save && cache && !cache.exist? ? cache : file)
+ dest.parent.mkpath
+ dest.open("wb", 0600) do |f|
+ f.write(data)
+ f.chmod(mode_for(data))
+ end
+ if mtime
+ dest.utime(mtime, mtime)
+ end
+ if $VERBOSE
+ $stdout.puts "done"
+ $stdout.flush
+ end
+ if dest.eql?(cache)
+ link_cache(cache, file, name)
+ elsif cache_save
+ save_cache(cache, file, name)
+ end
+ return file.to_path
+ rescue => e
+ raise "failed to download #{name}\n#{e.class}: #{e.message}: #{url}"
+ end
+
+ def self.under(dir, name)
+ dir ? File.join(dir, File.basename(name)) : name
+ end
+
+ def self.cache_file(url, name, cache_dir = nil)
+ case cache_dir
+ when false
+ return nil
+ when nil
+ cache_dir = ENV['CACHE_DIR']
+ if !cache_dir or cache_dir.empty?
+ cache_dir = ".downloaded-cache"
+ end
+ end
+ Pathname.new(cache_dir) + (name || File.basename(URI(url).path))
+ end
+
+ def self.link_cache(cache, file, name, verbose = false)
+ return false unless cache and cache.exist?
+ return true if cache.eql?(file)
+ if /cygwin/ !~ RUBY_PLATFORM or /winsymlink:nativestrict/ =~ ENV['CYGWIN']
+ begin
+ file.make_symlink(cache.relative_path_from(file.parent))
+ rescue SystemCallError
+ else
+ if verbose
+ $stdout.puts "made symlink #{name} to #{cache}"
+ $stdout.flush
+ end
+ return true
+ end
+ end
+ begin
+ file.make_link(cache)
+ rescue SystemCallError
+ else
+ if verbose
+ $stdout.puts "made link #{name} to #{cache}"
+ $stdout.flush
+ end
+ return true
+ end
+ end
+
+ def self.save_cache(cache, file, name)
+ return unless cache or cache.eql?(file)
+ begin
+ st = cache.stat
+ rescue
+ begin
+ file.rename(cache)
+ rescue
+ return
+ end
+ else
+ return unless st.mtime > file.lstat.mtime
+ file.unlink
+ end
+ link_cache(cache, file, name)
+ end
+
+ def self.with_retry(max_times, &block)
+ times = 0
+ begin
+ block.call
+ rescue Errno::ETIMEDOUT, SocketError, OpenURI::HTTPError, Net::ReadTimeout, Net::OpenTimeout, ArgumentError => e
+ raise if e.is_a?(OpenURI::HTTPError) && e.message !~ /^50[023] / # retry only 500, 502, 503 for http error
+ times += 1
+ if times <= max_times
+ $stderr.puts "retrying #{e.class} (#{e.message}) after #{times ** 2} seconds..."
+ sleep(times ** 2)
+ retry
+ else
+ raise
+ end
+ end
+ end
+ private_class_method :with_retry
+end
+
+Downloader.https = https.freeze
+
+if $0 == __FILE__
+ since = true
+ options = {}
+ until ARGV.empty?
+ case ARGV[0]
+ when '-d'
+ destdir = ARGV[1]
+ ARGV.shift
+ when '-p'
+ # strip directory names from the name to download, and add the
+ # prefix instead.
+ prefix = ARGV[1]
+ ARGV.shift
+ when '-e'
+ since = nil
+ when '-a'
+ since = false
+ when '-n', '--dryrun'
+ options[:dryrun] = true
+ when '--cache-dir'
+ options[:cache_dir] = ARGV[1]
+ ARGV.shift
+ when '--unicode-beta'
+ options[:unicode_beta] = ARGV[1]
+ ARGV.shift
+ when /\A--cache-dir=(.*)/m
+ options[:cache_dir] = $1
+ when /\A-/
+ abort "#{$0}: unknown option #{ARGV[0]}"
+ else
+ break
+ end
+ ARGV.shift
+ end
+ dl = Downloader.constants.find do |name|
+ ARGV[0].casecmp(name.to_s) == 0
+ end unless ARGV.empty?
+ $VERBOSE = true
+ if dl
+ dl = Downloader.const_get(dl)
+ ARGV.shift
+ ARGV.each do |name|
+ dir = destdir
+ if prefix
+ name = name.sub(/\A\.\//, '')
+ destdir2 = destdir.sub(/\A\.\//, '')
+ if name.start_with?(destdir2+"/")
+ name = name[(destdir2.size+1)..-1]
+ if (dir = File.dirname(name)) == '.'
+ dir = destdir
+ else
+ dir = File.join(destdir, dir)
+ end
+ else
+ name = File.basename(name)
+ end
+ name = "#{prefix}/#{name}"
+ end
+ dl.download(name, dir, since, options)
+ end
+ else
+ abort "usage: #{$0} url name" unless ARGV.size == 2
+ Downloader.download(ARGV[0], ARGV[1], destdir, since, options)
+ end
+end
diff --git a/tool/enc-emoji-citrus-gen.rb b/tool/enc-emoji-citrus-gen.rb
new file mode 100644
index 0000000000..da9c8a6b62
--- /dev/null
+++ b/tool/enc-emoji-citrus-gen.rb
@@ -0,0 +1,131 @@
+require File.expand_path('../lib/jisx0208', __FILE__)
+
+ENCODES = [
+ {
+ :name => "SHIFT_JIS-DOCOMO",
+ :src_zone => [0xF8..0xFC, 0x40..0xFC, 8],
+ :dst_ilseq => 0xFFFE,
+ :map => [
+ [0xE63E..0xE757, JISX0208::Char.from_sjis(0xF89F)],
+ ],
+ },
+ {
+ :name => "ISO-2022-JP-KDDI",
+ :src_zone => [0x21..0x7E, 0x21..0x7E, 8],
+ :dst_ilseq => 0xFFFE,
+ :map => [
+ [0xE468..0xE5B4, JISX0208::Char.new(0x7521)],
+ [0xE5B5..0xE5CC, JISX0208::Char.new(0x7867)],
+ [0xE5CD..0xE5DF, JISX0208::Char.new(0x7921)],
+ [0xEA80..0xEAFA, JISX0208::Char.new(0x7934)],
+ [0xEAFB..0xEB0D, JISX0208::Char.new(0x7854)],
+ [0xEB0E..0xEB8E, JISX0208::Char.new(0x7A51)],
+ ],
+ },
+ {
+ :name => "SHIFT_JIS-KDDI",
+ :src_zone => [0xF3..0xFC, 0x40..0xFC, 8],
+ :dst_ilseq => 0xFFFE,
+ :map => [
+ [0xE468..0xE5B4, JISX0208::Char.from_sjis(0xF640)],
+ [0xE5B5..0xE5CC, JISX0208::Char.from_sjis(0xF7E5)],
+ [0xE5CD..0xE5DF, JISX0208::Char.from_sjis(0xF340)],
+ [0xEA80..0xEAFA, JISX0208::Char.from_sjis(0xF353)],
+ [0xEAFB..0xEB0D, JISX0208::Char.from_sjis(0xF7D2)],
+ [0xEB0E..0xEB8E, JISX0208::Char.from_sjis(0xF3CF)],
+ ],
+ },
+ {
+ :name => "SHIFT_JIS-SOFTBANK",
+ :src_zone => [0xF3..0xFC, 0x40..0xFC, 8],
+ :dst_ilseq => 0xFFFE,
+ :map => [
+ [0xE001..0xE05A, JISX0208::Char.from_sjis(0xF941)],
+ [0xE101..0xE15A, JISX0208::Char.from_sjis(0xF741)],
+ [0xE201..0xE25A, JISX0208::Char.from_sjis(0xF7A1)],
+ [0xE301..0xE34D, JISX0208::Char.from_sjis(0xF9A1)],
+ [0xE401..0xE44C, JISX0208::Char.from_sjis(0xFB41)],
+ [0xE501..0xE53E, JISX0208::Char.from_sjis(0xFBA1)],
+ ],
+ },
+]
+
+def zone(*args)
+ bits = args.pop
+ [*args.map{|range| "0x%02X-0x%02X" % [range.begin, range.end] }, bits].join(' / ')
+end
+
+def header(params)
+ (<<END_HEADER_TEMPLATE % [params[:name], zone(*params[:src_zone]), params[:dst_ilseq]])
+# DO NOT EDIT THIS FILE DIRECTLY
+
+TYPE ROWCOL
+NAME %s
+SRC_ZONE %s
+OOB_MODE ILSEQ
+DST_ILSEQ 0x%04X
+DST_UNIT_BITS 16
+END_HEADER_TEMPLATE
+end
+
+def generate_to_ucs(params, pairs)
+ pairs.sort_by! {|u, c| c }
+ name = "EMOJI_#{params[:name]}%UCS"
+ open("#{name}.src", "w") do |io|
+ io.print header(params.merge(name: name.tr('%', '/')))
+ io.puts
+ io.puts "BEGIN_MAP"
+ io.print pairs.inject("") {|acc, uc| acc += "0x%04X = 0x%04X\n" % uc.reverse }
+ io.puts "END_MAP"
+ end
+end
+
+def generate_from_ucs(params, pairs)
+ pairs.sort_by! {|u, c| u }
+ name = "UCS%EMOJI_#{params[:name]}"
+ open("#{name}.src", "w") do |io|
+ io.print header(params.merge(name: name.tr('%', '/')))
+ io.puts
+ io.puts "BEGIN_MAP"
+ io.print pairs.inject("") {|acc, uc| acc += "0x%04X = 0x%04X\n" % uc }
+ io.puts "END_MAP"
+ end
+end
+
+def make_pairs(code_map)
+ code_map.inject([]) {|acc, (range, ch)|
+ acc += range.map{|uni| pair = [uni, Integer(ch)]; ch = ch.succ; next pair }
+ }
+end
+
+ENCODES.each do |params|
+ pairs = make_pairs(params[:map], &params[:conv])
+ generate_to_ucs(params, pairs)
+ generate_from_ucs(params, pairs)
+end
+
+# generate KDDI-UNDOC for Shift_JIS-KDDI
+kddi_sjis_map = ENCODES.select{|enc| enc[:name] == "SHIFT_JIS-KDDI"}.first[:map]
+pairs = kddi_sjis_map.inject([]) {|acc, (range, ch)|
+ acc += range.map{|uni| pair = [ch.to_sjis - 0x700, Integer(ch)]; ch = ch.succ; next pair }
+}
+params = {
+ :name => "SHIFT_JIS-KDDI-UNDOC",
+ :src_zone => [0xF3..0xFC, 0x40..0xFC, 8],
+ :dst_ilseq => 0xFFFE,
+}
+generate_from_ucs(params, pairs)
+generate_to_ucs(params, pairs)
+
+# generate KDDI-UNDOC for ISO-2022-JP-KDDI
+kddi_2022_map = ENCODES.select{|enc| enc[:name] == "ISO-2022-JP-KDDI"}.first[:map]
+pairs = kddi_2022_map.each_with_index.inject([]) {|acc, ((range, ch), i)|
+ sjis = kddi_sjis_map[i][1]
+ acc += range.map{|uni| pair = [sjis.to_sjis - 0x700, Integer(ch)]; ch = ch.succ; sjis = sjis.succ; next pair }
+}
+params = {
+ :name => "ISO-2022-JP-KDDI-UNDOC",
+ :src_zone => [0x21..0x7E, 0x21..0x7E, 8],
+ :dst_ilseq => 0xFFFE,
+}
+generate_from_ucs(params, pairs)
diff --git a/tool/enc-emoji4unicode.rb b/tool/enc-emoji4unicode.rb
new file mode 100644
index 0000000000..1e7d45901f
--- /dev/null
+++ b/tool/enc-emoji4unicode.rb
@@ -0,0 +1,133 @@
+#!/usr/bin/env ruby
+
+# example:
+# ./enc-emoji4unicode.rb emoji4unicode.xml > ../enc/trans/emoji-exchange-tbl.rb
+
+require 'rexml/document'
+require File.expand_path("../transcode-tblgen", __FILE__)
+
+class EmojiTable
+ VERBOSE_MODE = false
+
+ def initialize(xml_path)
+ @doc = REXML::Document.new File.open(xml_path)
+ @kddi_undoc = make_kddi_undoc_map()
+ end
+
+ def conversion(from_carrier, to_carrier, &block)
+ REXML::XPath.each(@doc.root, '//e') do |e|
+ from = e.attribute(from_carrier.downcase).to_s
+ to = e.attribute(to_carrier.downcase).to_s
+ text_fallback = e.attribute('text_fallback').to_s
+ name = e.attribute('name').to_s
+ if from =~ /^(?:\*|\+)(.+)$/ # proposed or unified
+ from = $1
+ end
+ if from.empty? || from !~ /^[0-9A-F]+$/
+ # do nothing
+ else
+ from_utf8 = [from.hex].pack("U").unpack("H*").first
+ if to =~ /^(?:&gt;|\*)?([0-9A-F\+]+)$/
+ str_to = $1
+ if str_to =~ /^\+/ # unicode "proposed" begins at "+"
+ proposal = true
+ str_to.sub!(/^\+/, '')
+ else
+ proposal = false
+ end
+ tos = str_to.split('+')
+ to_utf8 = tos.map(&:hex).pack("U*").unpack("H*").first
+ comment = "[%s] U+%X -> %s" % [name, from.hex, tos.map{|c| "U+%X"%c.hex}.join(' ')]
+ block.call(:from => from_utf8,
+ :to => to_utf8,
+ :comment => comment,
+ :fallback => false,
+ :proposal => proposal)
+ elsif to.empty?
+ if text_fallback.empty?
+ comment = "[%s] U+%X -> U+3013 (GETA)" % [name, from.hex]
+ block.call(:from => from_utf8,
+ :to => "\u{3013}".unpack("H*").first,
+ :comment => comment, # geta
+ :fallback => true,
+ :proposal => false)
+ else
+ to_utf8 = text_fallback.unpack("H*").first
+ comment = %([%s] U+%X -> "%s") % [name, from.hex, text_fallback]
+ block.call(:from => from_utf8,
+ :to => to_utf8,
+ :comment => comment,
+ :fallback => true,
+ :proposal => false)
+ end
+ else
+ raise "something wrong: %s -> %s" % [from, to]
+ end
+ end
+ end
+ end
+
+ def generate(io, from_carrier, to_carrier)
+ from_encoding = (from_carrier == "Unicode") ? "UTF-8" : "UTF8-"+from_carrier
+ to_encoding = (to_carrier == "Unicode" ) ? "UTF-8" : "UTF8-"+to_carrier
+ io.puts "EMOJI_EXCHANGE_TBL['#{from_encoding}']['#{to_encoding}'] = ["
+ io.puts " # for documented codepoints" if from_carrier == "KDDI"
+ self.conversion(from_carrier, to_carrier) do |params|
+ from, to = params[:from], %Q{"#{params[:to]}"}
+ to = ":undef" if params[:fallback] || params[:proposal]
+ io.puts %{ ["#{from}", #{to}], # #{params[:comment]}}
+ end
+ if from_carrier == "KDDI"
+ io.puts " # for undocumented codepoints"
+ self.conversion(from_carrier, to_carrier) do |params|
+ from, to = params[:from], %Q{"#{params[:to]}"}
+ to = ":undef" if params[:fallback] || params[:proposal]
+ unicode = utf8_to_ucs(from)
+ undoc = ucs_to_utf8(@kddi_undoc[unicode])
+ io.puts %{ ["#{undoc}", #{to}], # #{params[:comment]}}
+ end
+ end
+ io.puts "]"
+ io.puts
+ end
+
+ private
+
+ def utf8_to_ucs(cp)
+ return [cp].pack("H*").unpack("U*").first
+ end
+
+ def ucs_to_utf8(cp)
+ return [cp].pack("U*").unpack("H*").first
+ end
+
+ def make_kddi_undoc_map()
+ pub_to_sjis = citrus_decode_mapsrc(
+ "mskanji", 2, "UCS/EMOJI_SHIFT_JIS-KDDI").sort_by{|u, s| s}
+ sjis_to_undoc = citrus_decode_mapsrc(
+ "mskanji", 2, "EMOJI_SHIFT_JIS-KDDI-UNDOC/UCS").sort_by{|s, u| s}
+ return pub_to_sjis.zip(sjis_to_undoc).inject({}) {|h, rec|
+ raise "no match sjis codepoint" if rec[0][1] != rec[1][0]
+ h[rec[0][0]] = rec[1][1]
+ next h
+ }
+ end
+end
+
+if ARGV.empty?
+ puts "usage: #$0 [emoji4unicode.xml]"
+ exit 1
+end
+$srcdir = File.expand_path("../../enc/trans", __FILE__)
+emoji_table = EmojiTable.new(ARGV[0])
+
+companies = %w(DoCoMo KDDI SoftBank Unicode)
+
+io = STDOUT
+io.puts "EMOJI_EXCHANGE_TBL = Hash.new{|h,k| h[k] = {}}"
+companies.each do |from_company|
+ companies.each do |to_company|
+ next if from_company == to_company
+ emoji_table.generate(io, from_company, to_company)
+ end
+end
diff --git a/tool/enc-unicode.rb b/tool/enc-unicode.rb
new file mode 100755
index 0000000000..93f6e869f8
--- /dev/null
+++ b/tool/enc-unicode.rb
@@ -0,0 +1,577 @@
+#!/usr/bin/env ruby
+
+# Creates the data structures needed by Oniguruma to map Unicode codepoints to
+# property names and POSIX character classes
+#
+# To use this, get UnicodeData.txt, Scripts.txt, PropList.txt,
+# PropertyAliases.txt, PropertyValueAliases.txt, DerivedCoreProperties.txt,
+# DerivedAge.txt and Blocks.txt from unicode.org.
+# (http://unicode.org/Public/UNIDATA/) And run following command.
+# ruby1.9 tool/enc-unicode.rb data_dir > enc/unicode/name2ctype.kwd
+# You can get source file for gperf. After this, simply make ruby.
+
+if ARGV[0] == "--header"
+ header = true
+ ARGV.shift
+end
+unless ARGV.size == 2
+ abort "Usage: #{$0} data_directory emoji_data_directory"
+end
+
+pat = /(?:\A|\/)([.\d]+)\z/
+$versions = {
+ :Unicode => ARGV[0][pat, 1],
+ :Emoji => ARGV[1][pat, 1],
+}
+
+POSIX_NAMES = %w[NEWLINE Alpha Blank Cntrl Digit Graph Lower Print XPosixPunct Space Upper XDigit Word Alnum ASCII Punct]
+
+def pair_codepoints(codepoints)
+
+ # We have a sorted Array of codepoints that we wish to partition into
+ # ranges such that the start- and endpoints form an inclusive set of
+ # codepoints with property _property_. Note: It is intended that some ranges
+ # will begin with the value with which they end, e.g. 0x0020 -> 0x0020
+
+ codepoints.sort!
+ last_cp = codepoints.first
+ pairs = [[last_cp, nil]]
+ codepoints[1..-1].each do |codepoint|
+ next if last_cp == codepoint
+
+ # If the current codepoint does not follow directly on from the last
+ # codepoint, the last codepoint represents the end of the current range,
+ # and the current codepoint represents the start of the next range.
+ if last_cp.next != codepoint
+ pairs[-1][-1] = last_cp
+ pairs << [codepoint, nil]
+ end
+ last_cp = codepoint
+ end
+
+ # The final pair has as its endpoint the last codepoint for this property
+ pairs[-1][-1] = codepoints.last
+ pairs
+end
+
+def parse_unicode_data(file)
+ last_cp = 0
+ data = {'Any' => (0x0000..0x10ffff).to_a, 'Assigned' => [],
+ 'ASCII' => (0..0x007F).to_a, 'NEWLINE' => [0x0a], 'Cn' => []}
+ beg_cp = nil
+ IO.foreach(file) do |line|
+ fields = line.split(';')
+ cp = fields[0].to_i(16)
+
+ case fields[1]
+ when /\A<(.*),\s*First>\z/
+ beg_cp = cp
+ next
+ when /\A<(.*),\s*Last>\z/
+ cps = (beg_cp..cp).to_a
+ else
+ beg_cp = cp
+ cps = [cp]
+ end
+
+ # The Cn category represents unassigned characters. These are not listed in
+ # UnicodeData.txt so we must derive them by looking for 'holes' in the range
+ # of listed codepoints. We increment the last codepoint seen and compare it
+ # with the current codepoint. If the current codepoint is less than
+ # last_cp.next we have found a hole, so we add the missing codepoint to the
+ # Cn category.
+ data['Cn'].concat((last_cp.next...beg_cp).to_a)
+
+ # Assigned - Defined in unicode.c; interpreted as every character in the
+ # Unicode range minus the unassigned characters
+ data['Assigned'].concat(cps)
+
+ # The third field denotes the 'General' category, e.g. Lu
+ (data[fields[2]] ||= []).concat(cps)
+
+ # The 'Major' category is the first letter of the 'General' category, e.g.
+ # 'Lu' -> 'L'
+ (data[fields[2][0,1]] ||= []).concat(cps)
+ last_cp = cp
+ end
+
+ # The last Cn codepoint should be 0x10ffff. If it's not, append the missing
+ # codepoints to Cn and C
+ cn_remainder = (last_cp.next..0x10ffff).to_a
+ data['Cn'] += cn_remainder
+ data['C'] += data['Cn']
+
+ # Special case for LC (Cased_Letter). LC = Ll + Lt + Lu
+ data['LC'] = data['Ll'] + data['Lt'] + data['Lu']
+
+ # Define General Category properties
+ gcps = data.keys.sort - POSIX_NAMES
+
+ # Returns General Category Property names and the data
+ [gcps, data]
+end
+
+def define_posix_props(data)
+ # We now derive the character classes (POSIX brackets), e.g. [[:alpha:]]
+ #
+
+ data['Alpha'] = data['Alphabetic']
+ data['Upper'] = data['Uppercase']
+ data['Lower'] = data['Lowercase']
+ data['Punct'] = data['Punctuation']
+ data['XPosixPunct'] = data['Punctuation'] + [0x24, 0x2b, 0x3c, 0x3d, 0x3e, 0x5e, 0x60, 0x7c, 0x7e]
+ data['Digit'] = data['Decimal_Number']
+ data['XDigit'] = (0x0030..0x0039).to_a + (0x0041..0x0046).to_a +
+ (0x0061..0x0066).to_a
+ data['Alnum'] = data['Alpha'] + data['Digit']
+ data['Space'] = data['White_Space']
+ data['Blank'] = data['Space_Separator'] + [0x0009]
+ data['Cntrl'] = data['Cc']
+ data['Word'] = data['Alpha'] + data['Mark'] + data['Digit'] + data['Connector_Punctuation']
+ data['Graph'] = data['Any'] - data['Space'] - data['Cntrl'] -
+ data['Surrogate'] - data['Unassigned']
+ data['Print'] = data['Graph'] + data['Space_Separator']
+end
+
+def parse_scripts(data, categories)
+ files = [
+ {:fn => 'DerivedCoreProperties.txt', :title => 'Derived Property'},
+ {:fn => 'Scripts.txt', :title => 'Script'},
+ {:fn => 'PropList.txt', :title => 'Binary Property'},
+ {:fn => 'emoji/emoji-data.txt', :title => 'Emoji'}
+ ]
+ current = nil
+ cps = []
+ names = {}
+ files.each do |file|
+ data_foreach(file[:fn]) do |line|
+ if /^# Total (?:code points|elements): / =~ line
+ data[current] = cps
+ categories[current] = file[:title]
+ (names[file[:title]] ||= []) << current
+ cps = []
+ elsif /^([0-9a-fA-F]+)(?:\.\.([0-9a-fA-F]+))?\s*;\s*(\w+)/ =~ line
+ current = $3
+ $2 ? cps.concat(($1.to_i(16)..$2.to_i(16)).to_a) : cps.push($1.to_i(16))
+ end
+ end
+ end
+ # All code points not explicitly listed for Script
+ # have the value Unknown (Zzzz).
+ data['Unknown'] = (0..0x10ffff).to_a - data.values_at(*names['Script']).flatten
+ categories['Unknown'] = 'Script'
+ names.values.flatten << 'Unknown'
+end
+
+def parse_aliases(data)
+ kv = {}
+ data_foreach('PropertyAliases.txt') do |line|
+ next unless /^(\w+)\s*; (\w+)/ =~ line
+ data[$1] = data[$2]
+ kv[normalize_propname($1)] = normalize_propname($2)
+ end
+ data_foreach('PropertyValueAliases.txt') do |line|
+ next unless /^(sc|gc)\s*; (\w+)\s*; (\w+)(?:\s*; (\w+))?/ =~ line
+ if $1 == 'gc'
+ data[$3] = data[$2]
+ data[$4] = data[$2]
+ kv[normalize_propname($3)] = normalize_propname($2)
+ kv[normalize_propname($4)] = normalize_propname($2) if $4
+ else
+ data[$2] = data[$3]
+ data[$4] = data[$3]
+ kv[normalize_propname($2)] = normalize_propname($3)
+ kv[normalize_propname($4)] = normalize_propname($3) if $4
+ end
+ end
+ kv
+end
+
+# According to Unicode6.0.0/ch03.pdf, Section 3.1, "An update version
+# never involves any additions to the character repertoire." Versions
+# in DerivedAge.txt should always be /\d+\.\d+/
+def parse_age(data)
+ current = nil
+ last_constname = nil
+ cps = []
+ ages = []
+ data_foreach('DerivedAge.txt') do |line|
+ if /^# Total code points: / =~ line
+ constname = constantize_agename(current)
+ # each version matches all previous versions
+ cps.concat(data[last_constname]) if last_constname
+ data[constname] = cps
+ make_const(constname, cps, "Derived Age #{current}")
+ ages << current
+ last_constname = constname
+ cps = []
+ elsif /^([0-9a-fA-F]+)(?:\.\.([0-9a-fA-F]+))?\s*;\s*(\d+\.\d+)/ =~ line
+ current = $3
+ $2 ? cps.concat(($1.to_i(16)..$2.to_i(16)).to_a) : cps.push($1.to_i(16))
+ end
+ end
+ ages
+end
+
+def parse_GraphemeBreakProperty(data)
+ current = nil
+ cps = []
+ ages = []
+ data_foreach('auxiliary/GraphemeBreakProperty.txt') do |line|
+ if /^# Total code points: / =~ line
+ constname = constantize_Grapheme_Cluster_Break(current)
+ data[constname] = cps
+ make_const(constname, cps, "Grapheme_Cluster_Break=#{current}")
+ ages << current
+ cps = []
+ elsif /^([0-9a-fA-F]+)(?:\.\.([0-9a-fA-F]+))?\s*;\s*(\w+)/ =~ line
+ current = $3
+ $2 ? cps.concat(($1.to_i(16)..$2.to_i(16)).to_a) : cps.push($1.to_i(16))
+ end
+ end
+ ages
+end
+
+def parse_block(data)
+ cps = []
+ blocks = []
+ data_foreach('Blocks.txt') do |line|
+ if /^([0-9a-fA-F]+)\.\.([0-9a-fA-F]+);\s*(.*)/ =~ line
+ cps = ($1.to_i(16)..$2.to_i(16)).to_a
+ constname = constantize_blockname($3)
+ data[constname] = cps
+ make_const(constname, cps, "Block")
+ blocks << constname
+ end
+ end
+
+ # All code points not belonging to any of the named blocks
+ # have the value No_Block.
+ no_block = (0..0x10ffff).to_a - data.values_at(*blocks).flatten
+ constname = constantize_blockname("No_Block")
+ make_const(constname, no_block, "Block")
+ blocks << constname
+end
+
+# shim for Ruby 1.8
+unless {}.respond_to?(:key)
+ class Hash
+ alias key index
+ end
+end
+
+$const_cache = {}
+# make_const(property, pairs, name): Prints a 'static const' structure for a
+# given property, group of paired codepoints, and a human-friendly name for
+# the group
+def make_const(prop, data, name)
+ if name.empty?
+ puts "\n/* '#{prop}' */"
+ else
+ puts "\n/* '#{prop}': #{name} */"
+ end
+ if origprop = $const_cache.key(data)
+ puts "#define CR_#{prop} CR_#{origprop}"
+ else
+ $const_cache[prop] = data
+ pairs = pair_codepoints(data)
+ puts "static const OnigCodePoint CR_#{prop}[] = {"
+ # The first element of the constant is the number of pairs of codepoints
+ puts "\t#{pairs.size},"
+ pairs.each do |pair|
+ pair.map! { |c| c == 0 ? '0x0000' : sprintf("%0#6x", c) }
+ puts "\t#{pair.first}, #{pair.last},"
+ end
+ puts "}; /* CR_#{prop} */"
+ end
+end
+
+def normalize_propname(name)
+ name = name.downcase
+ name.delete!('- _')
+ name
+end
+
+def constantize_agename(name)
+ "Age_#{name.sub(/\./, '_')}"
+end
+
+def constantize_Grapheme_Cluster_Break(name)
+ "Grapheme_Cluster_Break_#{name}"
+end
+
+def constantize_blockname(name)
+ "In_#{name.gsub(/\W/, '_')}"
+end
+
+def get_file(name)
+ File.join(ARGV[name.start_with?("emoji-[stz]") ? 1 : 0], name)
+end
+
+def data_foreach(name, &block)
+ fn = get_file(name)
+ warn "Reading #{name}"
+ if /^emoji/ =~ name
+ sep = ""
+ pat = /^# #{Regexp.quote(File.basename(name))}.*^# Version: ([\d.]+)/m
+ type = :Emoji
+ else
+ sep = "\n"
+ pat = /^# #{File.basename(name).sub(/\./, '-([\\d.]+)\\.')}/
+ type = :Unicode
+ end
+ File.open(fn, 'rb') do |f|
+ line = f.gets(sep)
+ unless version = line[pat, 1]
+ raise ArgumentError, <<-ERROR
+#{name}: no #{type} version
+#{line.gsub(/^/, '> ')}
+ ERROR
+ end
+ if !(v = $versions[type])
+ $versions[type] = version
+ elsif v != version
+ raise ArgumentError, <<-ERROR
+#{name}: #{type} version mismatch: #{version} to #{v}
+#{line.gsub(/^/, '> ')}
+ ERROR
+ end
+ f.each(&block)
+ end
+end
+
+# Write Data
+class Unifdef
+ attr_accessor :output, :top, :stack, :stdout, :kwdonly
+ def initialize(out)
+ @top = @output = []
+ @stack = []
+ $stdout, @stdout = self, out
+ end
+ def restore
+ $stdout = @stdout
+ end
+ def ifdef(sym)
+ if @kwdonly
+ @stdout.puts "#ifdef #{sym}"
+ else
+ @stack << @top
+ @top << tmp = [sym]
+ @top = tmp
+ end
+ if block_given?
+ begin
+ return yield
+ ensure
+ endif(sym)
+ end
+ end
+ end
+ def endif(sym)
+ if @kwdonly
+ @stdout.puts "#endif /* #{sym} */"
+ else
+ unless sym == @top[0]
+ restore
+ raise ArgumentError, "#{sym} unmatch to #{@top[0]}"
+ end
+ @top = @stack.pop
+ end
+ end
+ def show(dest, *syms)
+ _show(dest, @output, syms)
+ end
+ def _show(dest, ary, syms)
+ if Symbol === (sym = ary[0])
+ unless syms.include?(sym)
+ return
+ end
+ end
+ ary.each do |e|
+ case e
+ when Array
+ _show(dest, e, syms)
+ when String
+ dest.print e
+ end
+ end
+ end
+ def write(str)
+ if @kwdonly
+ @stdout.write(str)
+ else
+ @top << str
+ end
+ self
+ end
+ alias << write
+end
+
+output = Unifdef.new($stdout)
+output.kwdonly = !header
+
+puts '%{'
+props, data = parse_unicode_data(get_file('UnicodeData.txt'))
+categories = {}
+props.concat parse_scripts(data, categories)
+aliases = parse_aliases(data)
+ages = blocks = graphemeBreaks = nil
+define_posix_props(data)
+POSIX_NAMES.each do |name|
+ if name == 'XPosixPunct'
+ make_const(name, data[name], "[[:Punct:]]")
+ elsif name == 'Punct'
+ make_const(name, data[name], "")
+ else
+ make_const(name, data[name], "[[:#{name}:]]")
+ end
+end
+output.ifdef :USE_UNICODE_PROPERTIES do
+ props.each do |name|
+ category = categories[name] ||
+ case name.size
+ when 1 then 'Major Category'
+ when 2 then 'General Category'
+ else '-'
+ end
+ make_const(name, data[name], category)
+ end
+ output.ifdef :USE_UNICODE_AGE_PROPERTIES do
+ ages = parse_age(data)
+ end
+ graphemeBreaks = parse_GraphemeBreakProperty(data)
+ blocks = parse_block(data)
+end
+puts(<<'__HEREDOC')
+
+static const OnigCodePoint* const CodeRanges[] = {
+__HEREDOC
+POSIX_NAMES.each{|name|puts" CR_#{name},"}
+output.ifdef :USE_UNICODE_PROPERTIES do
+ props.each{|name| puts" CR_#{name},"}
+ output.ifdef :USE_UNICODE_AGE_PROPERTIES do
+ ages.each{|name| puts" CR_#{constantize_agename(name)},"}
+ end
+ graphemeBreaks.each{|name| puts" CR_#{constantize_Grapheme_Cluster_Break(name)},"}
+ blocks.each{|name|puts" CR_#{name},"}
+end
+
+puts(<<'__HEREDOC')
+};
+struct uniname2ctype_struct {
+ short name;
+ unsigned short ctype;
+};
+#define uniname2ctype_offset(str) offsetof(struct uniname2ctype_pool_t, uniname2ctype_pool_##str)
+
+static const struct uniname2ctype_struct *uniname2ctype_p(
+#if !(/*ANSI*/+0) /* if ANSI, old style not to conflict with generated prototype */
+ const char *, unsigned int
+#endif
+);
+%}
+struct uniname2ctype_struct;
+%%
+__HEREDOC
+
+i = -1
+name_to_index = {}
+POSIX_NAMES.each do |name|
+ i += 1
+ next if name == 'NEWLINE'
+ name = normalize_propname(name)
+ name_to_index[name] = i
+ puts"%-40s %3d" % [name + ',', i]
+end
+output.ifdef :USE_UNICODE_PROPERTIES do
+ props.each do |name|
+ i += 1
+ name = normalize_propname(name)
+ name_to_index[name] = i
+ puts "%-40s %3d" % [name + ',', i]
+ end
+ aliases.each_pair do |k, v|
+ next if name_to_index[k]
+ next unless v = name_to_index[v]
+ puts "%-40s %3d" % [k + ',', v]
+ end
+ output.ifdef :USE_UNICODE_AGE_PROPERTIES do
+ ages.each do |name|
+ i += 1
+ name = "age=#{name}"
+ name_to_index[name] = i
+ puts "%-40s %3d" % [name + ',', i]
+ end
+ end
+ graphemeBreaks.each do |name|
+ i += 1
+ name = "graphemeclusterbreak=#{name.delete('_').downcase}"
+ name_to_index[name] = i
+ puts "%-40s %3d" % [name + ',', i]
+ end
+ blocks.each do |name|
+ i += 1
+ name = normalize_propname(name)
+ name_to_index[name] = i
+ puts "%-40s %3d" % [name + ',', i]
+ end
+end
+puts(<<'__HEREDOC')
+%%
+static int
+uniname2ctype(const UChar *name, unsigned int len)
+{
+ const struct uniname2ctype_struct *p = uniname2ctype_p((const char *)name, len);
+ if (p) return p->ctype;
+ return -1;
+}
+__HEREDOC
+$versions.each do |type, ver|
+ name = type == :Unicode ? "ONIG_UNICODE_VERSION" : "ONIG_UNICODE_EMOJI_VERSION"
+ versions = ver.scan(/\d+/)
+ print("#if defined #{name}_STRING && !( \\\n")
+ versions.zip(%w[MAJOR MINOR TEENY]) do |v, n|
+ print(" #{name}_#{n} == #{v} && \\\n")
+ end
+ print(" 1)\n")
+ print("# error #{name}_STRING mismatch\n")
+ print("#endif\n")
+ print("#define #{name}_STRING #{ver.dump}\n")
+ versions.zip(%w[MAJOR MINOR TEENY]) do |v, n|
+ print("#define #{name}_#{n} #{v}\n")
+ end
+end
+
+output.restore
+
+if header
+ require 'tempfile'
+
+ NAME2CTYPE = %w[gperf -7 -c -j1 -i1 -t -C -P -T -H uniname2ctype_hash -Q uniname2ctype_pool -N uniname2ctype_p]
+
+ fds = []
+ syms = %i[USE_UNICODE_PROPERTIES USE_UNICODE_AGE_PROPERTIES]
+ begin
+ fds << (tmp = Tempfile.new(%w"name2ctype .h"))
+ IO.popen([*NAME2CTYPE, out: tmp], "w") {|f| output.show(f, *syms)}
+ end while syms.pop
+ fds.each(&:close)
+ ff = nil
+ IO.popen(%W[diff -DUSE_UNICODE_AGE_PROPERTIES #{fds[1].path} #{fds[0].path}], "r") {|age|
+ IO.popen(%W[diff -DUSE_UNICODE_PROPERTIES #{fds[2].path} -], "r", in: age) {|f|
+ ansi = false
+ f.each {|line|
+ if /ANSI-C code produced by gperf/ =~ line
+ ansi = true
+ end
+ line.sub!(/\/\*ANSI\*\//, '1') if ansi
+ line.gsub!(/\(int\)\((?:long|size_t)\)&\(\(struct uniname2ctype_pool_t \*\)0\)->uniname2ctype_pool_(str\d+),\s+/,
+ 'uniname2ctype_offset(\1), ')
+ if ff = (!ff ? /^(uniname2ctype_hash) /=~line : /^\}/!~line) # no line can match both, exclusive flip-flop
+ line.sub!(/^( *(?:register\s+)?(.*\S)\s+hval\s*=\s*)(?=len;)/, '\1(\2)')
+ end
+ puts line
+ }
+ }
+ }
+end
diff --git a/tool/eval.rb b/tool/eval.rb
new file mode 100644
index 0000000000..9153573e6e
--- /dev/null
+++ b/tool/eval.rb
@@ -0,0 +1,158 @@
+# VM checking and benchmarking code
+
+require './rbconfig'
+require 'fileutils'
+require 'pp'
+
+Ruby = ENV['RUBY'] || RbConfig.ruby
+#
+
+OPTIONS = %w{
+ opt-direct-threaded-code
+ opt-basic-operations
+ opt-operands-unification
+ opt-instructions-unification
+ opt-inline-method-cache
+ opt-stack-caching
+}.map{|opt|
+ '--disable-' + opt
+}
+
+opts = OPTIONS.dup
+Configs = OPTIONS.map{|opt|
+ o = opts.dup
+ opts.delete(opt)
+ o
+} + [[]]
+
+pp Configs if $DEBUG
+
+
+def exec_cmd(cmd)
+ puts cmd
+ unless system(cmd)
+ p cmd
+ raise "error"
+ end
+end
+
+def dirname idx
+ "ev-#{idx}"
+end
+
+def build
+ Configs.each_with_index{|config, idx|
+ dir = dirname(idx)
+ FileUtils.rm_rf(dir) if FileTest.exist?(dir)
+ Dir.mkdir(dir)
+ FileUtils.cd(dir){
+ exec_cmd("#{Ruby} ../extconf.rb " + config.join(" "))
+ exec_cmd("make clean test-all")
+ }
+ }
+end
+
+def check
+ Configs.each_with_index{|c, idx|
+ puts "= #{idx}"
+ system("#{Ruby} -r ev-#{idx}/yarvcore -e 'puts YARVCore::OPTS'")
+ }
+end
+
+def bench_each idx
+ puts "= #{idx}"
+ 5.times{|count|
+ print count
+ FileUtils.cd(dirname(idx)){
+ exec_cmd("make benchmark OPT=-y ITEMS=#{ENV['ITEMS']} > ../b#{idx}-#{count}")
+ }
+ }
+ puts
+end
+
+def bench
+ # return bench_each(6)
+ Configs.each_with_index{|c, idx|
+ bench_each idx
+ }
+end
+
+def parse_result data
+ flag = false
+ stat = []
+ data.each{|line|
+ if flag
+ if /(\w+)\t([\d\.]+)/ =~ line
+ stat << [$1, $2.to_f]
+ else
+ raise "not a data"
+ end
+
+ end
+ if /benchmark summary/ =~ line
+ flag = true
+ end
+ }
+ stat
+end
+
+def calc_each data
+ data.sort!
+ data.pop # remove max
+ data.shift # remove min
+
+ data.inject(0.0){|res, e|
+ res += e
+ } / data.size
+end
+
+def calc_stat stats
+ stats[0].each_with_index{|e, idx|
+ bm = e[0]
+ vals = stats.map{|st|
+ st[idx][1]
+ }
+ [bm, calc_each(vals)]
+ }
+end
+
+def stat
+ total = []
+ Configs.each_with_index{|c, idx|
+ stats = []
+ 5.times{|count|
+ file = "b#{idx}-#{count}"
+ # p file
+ open(file){|f|
+ stats << parse_result(f.read)
+ }
+ }
+ # merge stats
+ total << calc_stat(stats)
+ total
+ }
+ # pp total
+ total[0].each_with_index{|e, idx|
+ # print "#{e[0]}\t"
+ total.each{|st|
+ print st[idx][1], "\t"
+ }
+ puts
+ }
+end
+
+ARGV.each{|cmd|
+ case cmd
+ when 'build'
+ build
+ when 'check'
+ check
+ when 'bench'
+ bench
+ when 'stat'
+ stat
+ else
+ raise
+ end
+}
+
diff --git a/tool/expand-config.rb b/tool/expand-config.rb
new file mode 100755
index 0000000000..81ffa6cb98
--- /dev/null
+++ b/tool/expand-config.rb
@@ -0,0 +1,33 @@
+#!./miniruby -s
+
+# Used to expand Ruby config entries for Win32 Makefiles.
+
+config = File.read(conffile = $config)
+config.sub!(/^(\s*)RUBY_VERSION\b.*(\sor\s*)$/, '\1true\2')
+rbconfig = Module.new {module_eval(config, conffile)}::RbConfig
+config = $expand ? rbconfig::CONFIG : rbconfig::MAKEFILE_CONFIG
+config["RUBY_RELEASE_DATE"] ||=
+ File.read(File.expand_path("../../version.h", __FILE__))[/^\s*#\s*define\s+RUBY_RELEASE_DATE\s+"(.*)"/, 1]
+
+while /\A(\w+)=(.*)/ =~ ARGV[0]
+ config[$1] = $2
+ config[$1].tr!(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
+ ARGV.shift
+end
+
+if $output
+ output = open($output, "wb", $mode &&= $mode.oct)
+ output.chmod($mode) if $mode
+else
+ output = STDOUT
+ output.binmode
+end
+
+ARGF.each do |line|
+ line.gsub!(/@([a-z_]\w*)@/i) {
+ s = config.fetch($1, $expand ? $& : "")
+ s = s.gsub(/\$\((.+?)\)/, %Q[${\\1}]) unless $expand
+ s
+ }
+ output.puts line
+end
diff --git a/tool/extlibs.rb b/tool/extlibs.rb
new file mode 100755
index 0000000000..cd8e5239b3
--- /dev/null
+++ b/tool/extlibs.rb
@@ -0,0 +1,263 @@
+#!/usr/bin/ruby
+
+# Used to download, extract and patch extension libraries (extlibs)
+# for Ruby. See common.mk for Ruby's usage.
+
+require 'digest'
+require_relative 'downloader'
+require_relative 'lib/colorize'
+
+class Vars < Hash
+ def pattern
+ /\$\((#{Regexp.union(keys)})\)/
+ end
+
+ def expand(str)
+ if empty?
+ str
+ else
+ str.gsub(pattern) {self[$1]}
+ end
+ end
+end
+
+class ExtLibs
+ def initialize
+ @colorize = Colorize.new
+ end
+
+ def cache_file(url, cache_dir)
+ Downloader.cache_file(url, nil, cache_dir).to_path
+ end
+
+ def do_download(url, cache_dir)
+ Downloader.download(url, nil, nil, nil, :cache_dir => cache_dir)
+ end
+
+ def do_checksum(cache, chksums)
+ chksums.each do |sum|
+ name, sum = sum.split(/:/)
+ if $VERBOSE
+ $stdout.print "checking #{name} of #{cache} ..."
+ $stdout.flush
+ end
+ hd = Digest(name.upcase).file(cache).hexdigest
+ if $VERBOSE
+ $stdout.print " "
+ $stdout.puts hd == sum ? @colorize.pass("OK") : @colorize.fail("NG")
+ $stdout.flush
+ end
+ unless hd == sum
+ raise "checksum mismatch: #{cache}, #{name}:#{hd}, expected #{sum}"
+ end
+ end
+ end
+
+ def do_extract(cache, dir)
+ if $VERBOSE
+ $stdout.puts "extracting #{cache} into #{dir}"
+ $stdout.flush
+ end
+ ext = File.extname(cache)
+ case ext
+ when '.gz', '.tgz'
+ f = IO.popen(["gzip", "-dc", cache])
+ cache = cache.chomp('.gz')
+ when '.bz2', '.tbz'
+ f = IO.popen(["bzip2", "-dc", cache])
+ cache = cache.chomp('.bz2')
+ when '.xz', '.txz'
+ f = IO.popen(["xz", "-dc", cache])
+ cache = cache.chomp('.xz')
+ else
+ inp = cache
+ end
+ inp ||= f.binmode
+ ext = File.extname(cache)
+ case ext
+ when '.tar', /\A\.t[gbx]z\z/
+ pid = Process.spawn("tar", "xpf", "-", in: inp, chdir: dir)
+ when '.zip'
+ pid = Process.spawn("unzip", inp, "-d", dir)
+ end
+ f.close if f
+ Process.wait(pid)
+ $?.success? or raise "failed to extract #{cache}"
+ end
+
+ def do_patch(dest, patch, args)
+ if $VERBOSE
+ $stdout.puts "applying #{patch} under #{dest}"
+ $stdout.flush
+ end
+ Process.wait(Process.spawn(ENV.fetch("PATCH", "patch"), "-d", dest, "-i", patch, *args))
+ $?.success? or raise "failed to patch #{patch}"
+ end
+
+ def do_link(file, src, dest)
+ file = File.join(dest, file)
+ if (target = src).start_with?("/")
+ target = File.join([".."] * file.count("/"), src)
+ end
+ return unless File.exist?(File.expand_path(target, File.dirname(file)))
+ File.unlink(file) rescue nil
+ begin
+ File.symlink(target, file)
+ rescue
+ else
+ if $VERBOSE
+ $stdout.puts "linked #{target} to #{file}"
+ $stdout.flush
+ end
+ return
+ end
+ begin
+ src = src.sub(/\A\//, '')
+ File.copy_stream(src, file)
+ rescue
+ if $VERBOSE
+ $stdout.puts "failed to link #{src} to #{file}: #{$!.message}"
+ end
+ else
+ if $VERBOSE
+ $stdout.puts "copied #{src} to #{file}"
+ end
+ end
+ end
+
+ def do_exec(command, dir, dest)
+ dir = dir ? File.join(dest, dir) : dest
+ if $VERBOSE
+ $stdout.puts "running #{command.dump} under #{dir}"
+ $stdout.flush
+ end
+ system(command, chdir: dir) or raise "failed #{command.dump}"
+ end
+
+ def do_command(mode, dest, url, cache_dir, chksums)
+ extracted = false
+ base = /.*(?=\.tar(?:\.\w+)?\z)/
+
+ case mode
+ when :download
+ cache = do_download(url, cache_dir)
+ do_checksum(cache, chksums)
+ when :extract
+ cache = cache_file(url, cache_dir)
+ target = File.join(dest, File.basename(cache)[base])
+ unless File.directory?(target)
+ do_checksum(cache, chksums)
+ extracted = do_extract(cache, dest)
+ end
+ when :all
+ cache = do_download(url, cache_dir)
+ target = File.join(dest, File.basename(cache)[base])
+ unless File.directory?(target)
+ do_checksum(cache, chksums)
+ extracted = do_extract(cache, dest)
+ end
+ end
+ extracted
+ end
+
+ def run(argv)
+ cache_dir = nil
+ mode = :all
+ until argv.empty?
+ case argv[0]
+ when '--download'
+ mode = :download
+ when '--extract'
+ mode = :extract
+ when '--patch'
+ mode = :patch
+ when '--all'
+ mode = :all
+ when '--cache'
+ argv.shift
+ cache_dir = argv[0]
+ when /\A--cache=/
+ cache_dir = $'
+ when '--'
+ argv.shift
+ break
+ when /\A-/
+ warn "unknown option: #{argv[0]}"
+ return false
+ else
+ break
+ end
+ argv.shift
+ end
+
+ success = true
+ argv.each do |dir|
+ Dir.glob("#{dir}/**/extlibs") do |list|
+ if $VERBOSE
+ $stdout.puts "downloading for #{list}"
+ $stdout.flush
+ end
+ vars = Vars.new
+ extracted = false
+ dest = File.dirname(list)
+ url = chksums = nil
+ IO.foreach(list) do |line|
+ line.sub!(/\s*#.*/, '')
+ if /^(\w+)\s*=\s*(.*)/ =~ line
+ vars[$1] = vars.expand($2)
+ next
+ end
+ if chksums
+ chksums.concat(line.split)
+ elsif /^\t/ =~ line
+ if extracted and (mode == :all or mode == :patch)
+ patch, *args = line.split.map {|s| vars.expand(s)}
+ do_patch(dest, patch, args)
+ end
+ next
+ elsif /^!\s*(?:chdir:\s*([^|\s]+)\|\s*)?(.*)/ =~ line
+ if extracted and (mode == :all or mode == :patch)
+ command = vars.expand($2.strip)
+ chdir = $1 and chdir = vars.expand(chdir)
+ do_exec(command, chdir, dest)
+ end
+ next
+ elsif /->/ =~ line
+ if extracted and (mode == :all or mode == :patch)
+ link, file = $`.strip, $'.strip
+ do_link(vars.expand(link), vars.expand(file), dest)
+ end
+ next
+ else
+ url, *chksums = line.split(' ')
+ end
+ if chksums.last == '\\'
+ chksums.pop
+ next
+ end
+ unless url
+ chksums = nil
+ next
+ end
+ url = vars.expand(url)
+ begin
+ extracted = do_command(mode, dest, url, cache_dir, chksums)
+ rescue => e
+ warn e.full_message
+ success = false
+ end
+ url = chksums = nil
+ end
+ end
+ end
+ success
+ end
+
+ def self.run(argv)
+ self.new.run(argv)
+ end
+end
+
+if $0 == __FILE__
+ exit ExtLibs.run(ARGV)
+end
diff --git a/tool/fake.rb b/tool/fake.rb
new file mode 100644
index 0000000000..91dfb041c4
--- /dev/null
+++ b/tool/fake.rb
@@ -0,0 +1,61 @@
+# Used by Makefile and configure for building Ruby.
+# See common.mk and Makefile.in for details.
+
+class File
+ sep = ("\\" if RUBY_PLATFORM =~ /mswin|bccwin|mingw/)
+ if sep != ALT_SEPARATOR
+ remove_const :ALT_SEPARATOR
+ ALT_SEPARATOR = sep
+ end
+end
+
+static = !!(defined?($static) && $static)
+$:.unshift(builddir)
+posthook = proc do
+ RbConfig.fire_update!("top_srcdir", $top_srcdir)
+ RbConfig.fire_update!("topdir", $topdir)
+ $hdrdir.sub!(/\A#{Regexp.quote($top_srcdir)}(?=\/)/, "$(top_srcdir)")
+ if $extmk
+ $ruby = "$(topdir)/miniruby -I'$(topdir)' -I'$(top_srcdir)/lib' -I'$(extout)/$(arch)' -I'$(extout)/common'"
+ else
+ $ruby = baseruby
+ end
+ $static = static
+ untrace_var(:$ruby, posthook)
+end
+prehook = proc do |extmk|
+=begin
+ pat = %r[(?:\A(?:\w:|//[^/]+)|\G)/[^/]*]
+ dir = builddir.scan(pat)
+ pwd = Dir.pwd.scan(pat)
+ if dir[0] == pwd[0]
+ while dir[0] and dir[0] == pwd[0]
+ dir.shift
+ pwd.shift
+ end
+ builddir = File.join((pwd.empty? ? ["."] : [".."]*pwd.size) + dir)
+ builddir = "." if builddir.empty?
+ end
+=end
+ join = proc {|*args| File.join(*args).sub!(/\A(?:\.\/)*/, '')}
+ $topdir ||= builddir
+ $top_srcdir ||= (File.identical?(top_srcdir, dir = join[$topdir, srcdir]) ?
+ dir : top_srcdir)
+ $extout = '$(topdir)/.ext'
+ $extout_prefix = '$(extout)$(target_prefix)/'
+ config = RbConfig::CONFIG
+ mkconfig = RbConfig::MAKEFILE_CONFIG
+ $builtruby ||= File.join(builddir, config['RUBY_INSTALL_NAME'] + config['EXEEXT'])
+ RbConfig.fire_update!("builddir", builddir)
+ RbConfig.fire_update!("buildlibdir", builddir)
+ RbConfig.fire_update!("libdir", builddir)
+ RbConfig.fire_update!("prefix", $topdir)
+ RbConfig.fire_update!("top_srcdir", $top_srcdir ||= top_srcdir)
+ RbConfig.fire_update!("extout", $extout)
+ RbConfig.fire_update!("rubyhdrdir", "$(top_srcdir)/include")
+ RbConfig.fire_update!("rubyarchhdrdir", "$(extout)/include/$(arch)")
+ RbConfig.fire_update!("libdirname", "buildlibdir")
+ trace_var(:$ruby, posthook)
+ untrace_var(:$extmk, prehook)
+end
+trace_var(:$extmk, prehook)
diff --git a/tool/fetch-bundled_gems.rb b/tool/fetch-bundled_gems.rb
new file mode 100755
index 0000000000..12d6f3d9cd
--- /dev/null
+++ b/tool/fetch-bundled_gems.rb
@@ -0,0 +1,27 @@
+#!ruby -an
+BEGIN {
+ require 'fileutils'
+
+ dir = ARGV.shift
+ ARGF.eof?
+ FileUtils.mkdir_p(dir)
+ Dir.chdir(dir)
+}
+
+n, v, u, r = $F
+
+next if n =~ /^#/
+
+if File.directory?(n)
+ puts "updating #{n} ..."
+ system("git", "fetch", chdir: n) or abort
+else
+ puts "retrieving #{n} ..."
+ system(*%W"git clone #{u} #{n}") or abort
+end
+c = r || "v#{v}"
+checkout = %w"git -c advice.detachedHead=false checkout"
+puts "checking out #{c} (v=#{v}, r=#{r}) ..."
+unless system(*checkout, c, "--", chdir: n)
+ abort if r or !system(*checkout, v, "--", chdir: n)
+end
diff --git a/tool/file2lastrev.rb b/tool/file2lastrev.rb
new file mode 100755
index 0000000000..3d8c69357d
--- /dev/null
+++ b/tool/file2lastrev.rb
@@ -0,0 +1,124 @@
+#!/usr/bin/env ruby
+
+# Gets the most recent revision of a file in a VCS-agnostic way.
+# Used by Doxygen, Makefiles and merger.rb.
+
+require 'optparse'
+
+# this file run with BASERUBY, which may be older than 1.9, so no
+# require_relative
+require File.expand_path('../lib/vcs', __FILE__)
+
+Program = $0
+
+@output = nil
+def self.output=(output)
+ if @output and @output != output
+ raise "you can specify only one of --changed, --revision.h and --doxygen"
+ end
+ @output = output
+end
+@suppress_not_found = false
+@limit = 20
+
+format = '%Y-%m-%dT%H:%M:%S%z'
+vcs = nil
+OptionParser.new {|opts|
+ opts.banner << " paths..."
+ vcs_options = VCS.define_options(opts)
+ new_vcs = proc do |path|
+ begin
+ vcs = VCS.detect(path, vcs_options, opts.new)
+ rescue VCS::NotFoundError => e
+ abort "#{File.basename(Program)}: #{e.message}" unless @suppress_not_found
+ opts.remove
+ end
+ nil
+ end
+ opts.new
+ opts.on("--srcdir=PATH", "use PATH as source directory") do |path|
+ abort "#{File.basename(Program)}: srcdir is already set" if vcs
+ new_vcs[path]
+ end
+ opts.on("--changed", "changed rev") do
+ self.output = :changed
+ end
+ opts.on("--revision.h", "RUBY_REVISION macro") do
+ self.output = :revision_h
+ end
+ opts.on("--doxygen", "Doxygen format") do
+ self.output = :doxygen
+ end
+ opts.on("--modified[=FORMAT]", "modified time") do |fmt|
+ self.output = :modified
+ format = fmt if fmt
+ end
+ opts.on("--limit=NUM", "limit branch name length (#@limit)", Integer) do |n|
+ @limit = n
+ end
+ opts.on("-q", "--suppress_not_found") do
+ @suppress_not_found = true
+ end
+ opts.order! rescue abort "#{File.basename(Program)}: #{$!}\n#{opts}"
+ if vcs
+ vcs.set_options(vcs_options) # options after --srcdir
+ else
+ new_vcs["."]
+ end
+}
+exit unless vcs
+
+@output =
+ case @output
+ when :changed, nil
+ Proc.new {|last, changed|
+ changed
+ }
+ when :revision_h
+ Proc.new {|last, changed, modified, branch, title|
+ short = vcs.short_revision(last)
+ if /[^\x00-\x7f]/ =~ title and title.respond_to?(:force_encoding)
+ title = title.dup.force_encoding("US-ASCII")
+ end
+ [
+ "#define RUBY_REVISION #{short.inspect}",
+ ("#define RUBY_FULL_REVISION #{last.inspect}" unless short == last),
+ if branch
+ e = '..'
+ limit = @limit
+ name = branch.sub(/\A(.{#{limit-e.size}}).{#{e.size+1},}/o) {$1+e}
+ name = name.dump.sub(/\\#/, '#')
+ "#define RUBY_BRANCH_NAME #{name}"
+ end,
+ if title
+ title = title.dump.sub(/\\#/, '#')
+ "#define RUBY_LAST_COMMIT_TITLE #{title}"
+ end,
+ if modified
+ modified.utc.strftime('#define RUBY_RELEASE_DATETIME "%FT%TZ"')
+ end,
+ ].compact
+ }
+ when :doxygen
+ Proc.new {|last, changed|
+ "r#{changed}/r#{last}"
+ }
+ when :modified
+ Proc.new {|last, changed, modified|
+ modified.strftime(format)
+ }
+ else
+ raise "unknown output format `#{@output}'"
+ end
+
+ok = true
+(ARGV.empty? ? [nil] : ARGV).each do |arg|
+ begin
+ puts @output[*vcs.get_revisions(arg)]
+ rescue => e
+ next if @suppress_not_found and VCS::NotFoundError === e
+ warn "#{File.basename(Program)}: #{e.message}"
+ ok = false
+ end
+end
+exit ok
diff --git a/tool/format-release b/tool/format-release
new file mode 100755
index 0000000000..e0de841127
--- /dev/null
+++ b/tool/format-release
@@ -0,0 +1,262 @@
+#!/usr/bin/env ruby
+# https://rubygems.org/gems/diffy
+require "diffy"
+require "open-uri"
+require "yaml"
+
+Diffy::Diff.default_options.merge!(
+ include_diff_info: true,
+ context: 1,
+)
+
+class Tarball
+ attr_reader :version, :size, :sha1, :sha256, :sha512
+
+ def initialize(version, url, size, sha1, sha256, sha512)
+ @url = url
+ @size = size
+ @sha1 = sha1
+ @sha256 = sha256
+ @sha512 = sha512
+ @version = version
+ @xy = version[/\A\d+\.\d+/]
+ end
+
+ def gz?; @url.end_with?('.gz'); end
+ def zip?; @url.end_with?('.zip'); end
+ def bz2?; @url.end_with?('.bz2'); end
+ def xz?; @url.end_with?('.xz'); end
+
+ def ext; @url[/(?:zip|tar\.(?:gz|bz2|xz))\z/]; end
+
+ def to_md
+ <<eom
+* <https://cache.ruby-lang.org/pub/ruby/#{@xy}/ruby-#{@version}.#{ext}>
+
+ SIZE: #{@size} bytes
+ SHA1: #{@sha1}
+ SHA256: #{@sha256}
+ SHA512: #{@sha512}
+eom
+ end
+
+ # * /home/naruse/obj/ruby-trunk/tmp/ruby-2.6.0-preview3.tar.gz
+ # SIZE: 17116009 bytes
+ # SHA1: 21f62c369661a2ab1b521fd2fa8191a4273e12a1
+ # SHA256: 97cea8aa63dfa250ba6902b658a7aa066daf817b22f82b7ee28f44aec7c2e394
+ # SHA512: 1e2042324821bb4e110af7067f52891606dcfc71e640c194ab1c117f0b941550e0b3ac36ad3511214ac80c536b9e5cfaf8789eec74cf56971a832ea8fc4e6d94
+ def self.parse(wwwdir, version, rubydir)
+ unless /\A(\d+)\.(\d+)\.(\d+)(?:-(?:preview|rc)\d+)?\z/ =~ version
+ raise "unexpected version string '#{version}'"
+ end
+ x = $1.to_i
+ y = $2.to_i
+ z = $3.to_i
+ # previous tag for git diff --shortstat
+ # It's only for x.y.0 release
+ if z != 0
+ prev_tag = nil
+ elsif y != 0
+ prev_tag = "v#{x}_#{y-1}_0"
+ prev_ver = "#{x}.#{y-1}.0"
+ elsif x == 3 && y == 0 && z == 0
+ prev_tag = "v2_7_0"
+ prev_ver = "2.7.0"
+ else
+ raise "unexpected version for prev_ver '#{version}'"
+ end
+
+ uri = "https://cache.ruby-lang.org/pub/tmp/ruby-info-#{version}-draft.yml"
+ info = YAML.load(URI(uri).read)
+ if info.size != 1
+ raise "unexpected info.yml '#{uri}'"
+ end
+ tarballs = []
+ info[0]["size"].each_key do |ext|
+ url = info[0]["url"][ext]
+ size = info[0]["size"][ext]
+ sha1 = info[0]["sha1"][ext]
+ sha256 = info[0]["sha256"][ext]
+ sha512 = info[0]["sha512"][ext]
+ tarball = Tarball.new(version, url, size, sha1, sha256, sha512)
+ tarballs << tarball
+ end
+
+ if prev_tag
+ # show diff shortstat
+ tag = "v#{version.gsub(/[.\-]/, '_')}"
+ stat = `git -C #{rubydir} diff --shortstat #{prev_tag}..#{tag}`
+ files_changed, insertions, deletions = stat.scan(/\d+/)
+ end
+
+ xy = version[/\A\d+\.\d+/]
+ #puts "## Download\n\n"
+ #tarballs.each do |tarball|
+ # puts tarball.to_md
+ #end
+ update_branches_yml(version, xy, wwwdir)
+ update_downloads_yml(version, xy, wwwdir)
+ update_releases_yml(version, xy, tarballs, wwwdir, files_changed, insertions, deletions)
+ tarballs
+ end
+
+ def self.update_branches_yml(ver, xy, wwwdir)
+ filename = "_data/branches.yml"
+ orig_data = File.read(File.join(wwwdir, filename))
+ data = orig_data.dup
+ if data.include?("\n- name: #{xy}\n")
+ data.sub!(/\n- name: #{Regexp.escape(xy)}\n(?: .*\n)*/) do |node|
+ unless ver.include?("-")
+ # assume this is X.Y.0 release
+ node.sub!(/^ status: preview\n/, " status: normal maintenance\n")
+ node.sub!(/^ date:\n/, " date: #{Time.now.year}-12-25\n")
+ end
+ node
+ end
+ else
+ if ver.include?("-")
+ status = "preview"
+ year = nil
+ else
+ status = "normal maintenance"
+ year = Time.now.year
+ end
+ entry = <<eom
+- name: #{xy}
+ status: #{status}
+ date:#{ year && " #{year}-12-25" }
+ eol_date:
+
+eom
+ data.sub!(/(?=^- name)/, entry)
+ end
+ if data != orig_data
+ diff = Diffy::Diff.new(orig_data, data)
+ show_diff(filename, diff)
+ end
+ end
+
+ def self.update_downloads_yml(ver, xy, wwwdir)
+ filename = "_data/downloads.yml"
+ orig_data = File.read(File.join(wwwdir, filename))
+ data = orig_data.dup
+
+ if /^preview:\n\n(?: .*\n)* - #{Regexp.escape(xy)}\./ =~ data
+ if ver.include?("-")
+ data.sub!(/^ - #{Regexp.escape(xy)}\..*/, " - #{ver}")
+ else
+ data.sub!(/^ - #{Regexp.escape(xy)}\..*\n/, "")
+ data.sub!(/(?<=^stable:\n\n)/, " - #{ver}\n")
+ end
+ else
+ unless data.sub!(/^ - #{Regexp.escape(xy)}\..*/, " - #{ver}")
+ if ver.include?("-")
+ data.sub!(/(?<=^preview:\n\n)/, " - #{ver}\n")
+ else
+ data.sub!(/(?<=^stable:\n\n)/, " - #{ver}\n")
+ end
+ end
+ end
+ if data != orig_data
+ diff = Diffy::Diff.new(orig_data, data)
+ show_diff(filename, diff)
+ end
+ end
+
+ def self.update_releases_yml(ver, xy, ary, wwwdir, files_changed, insertions, deletions)
+ filename = "_data/releases.yml"
+ orig_data = File.read(File.join(wwwdir, filename))
+ data = orig_data.dup
+
+ date = Time.now.utc # use utc to use previous day in midnight
+ entry = <<eom
+- version: #{ver}
+ date: #{date.strftime("%Y-%m-%d")}
+ post: /en/news/#{date.strftime("%Y/%m/%d")}/ruby-#{ver.tr('.', '-')}-released/
+eom
+
+ if /\.0(?:-\w+)?\z/ =~ ver
+ # preview, rc, or first release
+ entry <<= <<eom
+ tag: ruby_#{ver.tr('.-', '_')}
+ stats:
+ files_changed: #{files_changed}
+ insertions: #{insertions}
+ deletions: #{deletions}
+eom
+ end
+
+ entry <<= <<eom
+ url:
+ gz: https://cache.ruby-lang.org/pub/ruby/#{xy}/ruby-#{ver}.tar.gz
+ zip: https://cache.ruby-lang.org/pub/ruby/#{xy}/ruby-#{ver}.zip
+ bz2: https://cache.ruby-lang.org/pub/ruby/#{xy}/ruby-#{ver}.tar.bz2
+ xz: https://cache.ruby-lang.org/pub/ruby/#{xy}/ruby-#{ver}.tar.xz
+ size:
+ gz: #{ary.find{|x|x.gz? }.size}
+ zip: #{ary.find{|x|x.zip?}.size}
+ bz2: #{ary.find{|x|x.bz2?}&.size}
+ xz: #{ary.find{|x|x.xz? }.size}
+ sha1:
+ gz: #{ary.find{|x|x.gz? }.sha1}
+ zip: #{ary.find{|x|x.zip?}.sha1}
+ bz2: #{ary.find{|x|x.bz2?}&.sha1}
+ xz: #{ary.find{|x|x.xz? }.sha1}
+ sha256:
+ gz: #{ary.find{|x|x.gz? }.sha256}
+ zip: #{ary.find{|x|x.zip?}.sha256}
+ bz2: #{ary.find{|x|x.bz2?}&.sha256}
+ xz: #{ary.find{|x|x.xz? }.sha256}
+ sha512:
+ gz: #{ary.find{|x|x.gz? }.sha512}
+ zip: #{ary.find{|x|x.zip?}.sha512}
+ bz2: #{ary.find{|x|x.bz2?}&.sha512}
+ xz: #{ary.find{|x|x.xz? }.sha512}
+eom
+
+ if ver.start_with?("3.")
+ entry = entry.gsub(/ bz2: .*\n/, "")
+ end
+
+ if data.include?("\n- version: #{ver}\n")
+ # update existing entry
+ data.sub!(/\n- version: #{ver}\n(^ .*\n)*\n/, "\n#{entry}\n")
+ elsif data.sub!(/\n# #{Regexp.escape(xy)} series\n/, "\\&\n#{entry}")
+ else
+ data.sub!(/^$/, "\n# #{xy} series\n\n#{entry}")
+ end
+ if data != orig_data
+ diff = Diffy::Diff.new(orig_data, data)
+ show_diff(filename, diff)
+ end
+ end
+
+ def self.show_diff(filename, diff)
+ diff.each_with_index do |line, index|
+ case index
+ when 0
+ line.sub!(/\A--- (.*)\t(\d+-\d+-\d+ [0-9:.]+ [\-+]\d+)\Z/) do
+ "--- a/#{filename}\t#{$2}"
+ end
+ when 1
+ line.sub!(/\A\+\+\+ (.*)\t(\d+-\d+-\d+ [0-9:.]+ [\-+]\d+)\Z/) do
+ "+++ b/#{filename}\t#{$2}"
+ end
+ end
+ puts line
+ end
+ end
+end
+
+def main
+ wwwdir = ARGV.shift
+ version = ARGV.shift
+ rubydir = ARGV.shift
+ unless rubydir
+ STDERR.puts "usage: format-release <dir-of-w.r-l.o> <version> <ruby-dir>"
+ exit
+ end
+ Tarball.parse(wwwdir, version, rubydir)
+end
+
+main
diff --git a/tool/gen-mailmap.rb b/tool/gen-mailmap.rb
new file mode 100755
index 0000000000..27b7abf8de
--- /dev/null
+++ b/tool/gen-mailmap.rb
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+
+require "open-uri"
+require "yaml"
+
+EMAIL_YML_URL = "https://cdn.jsdelivr.net/gh/ruby/ruby-commit-hook/config/email.yml"
+
+email_yml = URI(EMAIL_YML_URL).read.sub(/\A(?:#.*\n)+/, "").gsub(/^# +(.+)$/) { $1 + ": []" }
+
+email = YAML.load(email_yml)
+YAML.load(DATA.read).each do |name, mails|
+ email[name] ||= []
+ email[name] |= mails
+end
+
+open(File.join(__dir__, "../.mailmap"), "w") do |f|
+ email.each do |name, mails|
+ canonical = "#{ name }@ruby-lang.org"
+ mails.delete(canonical)
+ svn = "#{ name }@b2dd03c8-39d4-4d8f-98ff-823fe69b080e"
+ ((mails | [canonical]) + [svn]).each do |mail|
+ f.puts "#{ name } <#{ canonical }> <#{ mail }>"
+ end
+ end
+end
+
+puts "You'll see canonical names (SVN account names) by the following commands:"
+puts
+puts " git shortlog -ce"
+puts " git log --pretty=format:'%cN <%cE>'"
+puts " git log --use-mailmap --pretty=full"
+
+__END__
+git:
+- svn@b2dd03c8-39d4-4d8f-98ff-823fe69b080e
+- "(no author)@b2dd03c8-39d4-4d8f-98ff-823fe69b080e"
+kazu:
+- znz@users.noreply.github.com
+marcandre:
+- github@marc-andre.ca
+mrkn:
+- mrkn@users.noreply.github.com
+- muraken@b2dd03c8-39d4-4d8f-98ff-823fe69b080e
+naruse:
+- nurse@users.noreply.github.com
+tenderlove:
+- tenderlove@github.com
diff --git a/tool/gen_dummy_probes.rb b/tool/gen_dummy_probes.rb
new file mode 100755
index 0000000000..45222830f3
--- /dev/null
+++ b/tool/gen_dummy_probes.rb
@@ -0,0 +1,32 @@
+#!/usr/bin/ruby
+# -*- coding: us-ascii -*-
+
+# Used to create dummy probes (as for systemtap and DTrace) by Makefiles.
+# See common.mk.
+
+text = ARGF.read
+
+# remove comments
+text.gsub!(%r'(?:^ *)?/\*.*?\*/\n?'m, '')
+
+# remove the pragma declarations and ifdefs
+text.gsub!(/^#(?:pragma|include|if|endif).*\n/, '')
+
+# replace the provider section with the start of the header file
+text.gsub!(/provider ruby \{/, "#ifndef\t_PROBES_H\n#define\t_PROBES_H\n#define DTRACE_PROBES_DISABLED 1\n")
+
+# finish up the #ifndef sandwich
+text.gsub!(/\};/, "\n#endif\t/* _PROBES_H */")
+
+# expand probes to DTRACE macros
+text.gsub!(/^ *probe ([^\(]*)\(([^\)]*)\);/) {
+ name, args = $1, $2
+ name.upcase!
+ name.gsub!(/__/, '_')
+ args.gsub!(/(\A|, *)[^,]*\b(?=\w+(?=,|\z))/, '\1')
+ "#define RUBY_DTRACE_#{name}_ENABLED() 0\n" \
+ "#define RUBY_DTRACE_#{name}(#{args}) do {} while (0)"
+}
+
+puts "/* -*- c -*- */"
+print text
diff --git a/tool/gen_ruby_tapset.rb b/tool/gen_ruby_tapset.rb
new file mode 100755
index 0000000000..ae3c1eccd2
--- /dev/null
+++ b/tool/gen_ruby_tapset.rb
@@ -0,0 +1,105 @@
+#!/usr/bin/ruby
+# -*- coding: us-ascii -*-
+# Create a tapset for systemtap and DTrace
+# usage: ./ruby gen_ruby_tapset.rb --ruby-path=/path/to/ruby probes.d > output
+
+require "optparse"
+
+def set_argument(argname, nth)
+ # remove C style type info
+ argname.gsub!(/.+ (.+)/, '\1') # e.g. char *hoge -> *hoge
+ argname.gsub!(/^\*/, '') # e.g. *filename -> filename
+
+ "#{argname} = $arg#{nth}"
+end
+
+ruby_path = "/usr/local/ruby"
+
+opts = OptionParser.new
+opts.on("--ruby-path=PATH"){|v| ruby_path = v}
+opts.parse!(ARGV)
+
+text = ARGF.read
+
+# remove preprocessor directives
+text.gsub!(/^#.*$/, '')
+
+# remove provider name
+text.gsub!(/^provider ruby \{/, "")
+text.gsub!(/^\};/, "")
+
+# probename()
+text.gsub!(/probe (.+)\( *\);/) {
+ probe_name = $1
+ <<-End
+ probe #{probe_name} = process("ruby").provider("ruby").mark("#{probe_name}")
+ {
+ }
+ End
+}
+
+# probename(arg1)
+text.gsub!(/ *probe (.+)\(([^,)]+)\);/) {
+ probe_name = $1
+ arg1 = $2
+
+ <<-End
+ probe #{probe_name} = process("ruby").provider("ruby").mark("#{probe_name}")
+ {
+ #{set_argument(arg1, 1)}
+ }
+ End
+}
+
+# probename(arg1, arg2)
+text.gsub!(/ *probe (.+)\(([^,)]+),([^,)]+)\);/) {
+ probe_name = $1
+ arg1 = $2
+ arg2 = $3
+
+ <<-End
+ probe #{probe_name} = process("#{ruby_path}").provider("ruby").mark("#{probe_name}")
+ {
+ #{set_argument(arg1, 1)}
+ #{set_argument(arg2, 2)}
+ }
+ End
+}
+
+# probename(arg1, arg2, arg3)
+text.gsub!(/ *probe (.+)\(([^,)]+),([^,)]+),([^,)]+)\);/) {
+ probe_name = $1
+ arg1 = $2
+ arg2 = $3
+ arg3 = $4
+
+ <<-End
+ probe #{probe_name} = process("#{ruby_path}").provider("ruby").mark("#{probe_name}")
+ {
+ #{set_argument(arg1, 1)}
+ #{set_argument(arg2, 2)}
+ #{set_argument(arg3, 3)}
+ }
+ End
+}
+
+# probename(arg1, arg2, arg3, arg4)
+text.gsub!(/ *probe (.+)\(([^,)]+),([^,)]+),([^,)]+),([^,)]+)\);/) {
+ probe_name = $1
+ arg1 = $2
+ arg2 = $3
+ arg3 = $4
+ arg4 = $5
+
+ <<-End
+ probe #{probe_name} = process("#{ruby_path}").provider("ruby").mark("#{probe_name}")
+ {
+ #{set_argument(arg1, 1)}
+ #{set_argument(arg2, 2)}
+ #{set_argument(arg3, 3)}
+ #{set_argument(arg4, 4)}
+ }
+ End
+}
+
+print text
diff --git a/tool/generic_erb.rb b/tool/generic_erb.rb
new file mode 100644
index 0000000000..6af995fc13
--- /dev/null
+++ b/tool/generic_erb.rb
@@ -0,0 +1,61 @@
+# -*- coding: us-ascii -*-
+
+# Used to expand Ruby template files by common.mk, uncommon.mk and
+# some Ruby extension libraries.
+
+require 'erb'
+require 'optparse'
+require_relative 'lib/vpath'
+require_relative 'lib/colorize'
+
+vpath = VPath.new
+timestamp = nil
+output = nil
+ifchange = nil
+source = false
+color = nil
+templates = []
+
+ARGV.options do |o|
+ o.on('-t', '--timestamp[=PATH]') {|v| timestamp = v || true}
+ o.on('-i', '--input=PATH') {|v| template << v}
+ o.on('-o', '--output=PATH') {|v| output = v}
+ o.on('-c', '--[no-]if-change') {|v| ifchange = v}
+ o.on('-x', '--source') {source = true}
+ o.on('--color') {color = true}
+ vpath.def_options(o)
+ o.order!(ARGV)
+ templates << (ARGV.shift or abort o.to_s) if templates.empty?
+end
+color = Colorize.new(color)
+unchanged = color.pass("unchanged")
+updated = color.fail("updated")
+
+result = templates.map do |template|
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
+ erb = ERB.new(File.read(template), trim_mode: '%-')
+ else
+ erb = ERB.new(File.read(template), nil, '%-')
+ end
+ erb.filename = template
+ source ? erb.src : proc{erb.result(binding)}.call
+end
+result = result.size == 1 ? result[0] : result.join("")
+if output
+ if ifchange and (vpath.open(output, "rb") {|f| f.read} rescue nil) == result
+ puts "#{output} #{unchanged}"
+ else
+ open(output, "wb") {|f| f.print result}
+ puts "#{output} #{updated}"
+ end
+ if timestamp
+ if timestamp == true
+ dir, base = File.split(output)
+ timestamp = File.join(dir, ".time." + base)
+ end
+ File.open(timestamp, 'a') {}
+ File.utime(nil, nil, timestamp)
+ end
+else
+ print result
+end
diff --git a/tool/git-refresh b/tool/git-refresh
new file mode 100755
index 0000000000..9ed7d7c76e
--- /dev/null
+++ b/tool/git-refresh
@@ -0,0 +1,46 @@
+#!/bin/sh
+set -e
+
+if (cd -P .) 2>/dev/null; then
+ CHDIR='cd -P'
+else
+ CHDIR='cd'
+fi
+
+quiet=
+branch=
+
+until [ $# = 0 ]; do
+ case "$1" in
+ --) shift; break;;
+ -C|--directory) shift; $CHDIR "$1";;
+ -C*) $CHDIR `expr "$1" : '-C\(.*\)'`;;
+ --directory=*) $CHDIR `expr "$1" : '[^=]*=\(.*\)'`;;
+ -q) quiet=1;;
+ -b|--branch) shift; branch="$1";;
+ -b*) branch=`expr "$1" : '-b\(.*\)'`;;
+ --branch=*) branch=`expr "$1" : '[^=]*=\(.*\)'`;;
+ -*) echo "unknown option: $1" 1>&2; exit 1;;
+ *) break;;
+ esac
+ shift
+done
+
+url="$1"
+dir="$2"
+shift 2
+[ x"$branch" = x ] && unset branch || :
+if [ -d "$dir" ]; then
+ if [ x"$(git -C "$dir" describe --tags)" = x"$branch" ]; then
+ exit 0 # already up-to-date
+ fi
+ echo updating `expr "/$dir/" : '.*/\([^/][^/]*\)/'` ...
+ [ $quiet ] || set -x
+ $CHDIR "$dir"
+ ${branch+git} ${branch+fetch} ${branch+"$@"}
+ exec git ${branch+checkout} "${branch-pull}" "$@"
+else
+ echo retrieving `expr "/$dir/" : '.*/\([^/][^/]*\)/'` ...
+ [ $quiet ] || set -x
+ exec git clone ${branch+--branch} ${branch+"$branch"} "$url" "$dir" "$@"
+fi
diff --git a/tool/gperf.sed b/tool/gperf.sed
new file mode 100644
index 0000000000..6b3e1980be
--- /dev/null
+++ b/tool/gperf.sed
@@ -0,0 +1,22 @@
+/ANSI-C code/{
+ h
+ s/.*/ANSI:offset:/
+ x
+}
+/\/\*!ANSI{\*\//{
+ G
+ s/\/\*!ANSI{\*\/\(.*\)\/\*}!ANSI\*\/\(.*\)\nANSI:.*/\/\*\1\*\/\2/
+}
+s/(int)([a-z_]*)&((struct \([a-zA-Z_0-9][a-zA-Z_0-9]*\)_t *\*)0)->\1_str\([1-9][0-9]*\),/gperf_offsetof(\1, \2),/g
+/^#line/{
+ G
+ x
+ s/:offset:/:/
+ x
+ s/\(.*\)\(\n\).*:offset:.*/#define gperf_offsetof(s, n) (short)offsetof(struct s##_t, s##_str##n)\2\1/
+ s/\n[^#].*//
+}
+/^[a-zA-Z_0-9]*hash/,/^}/{
+ s/ hval = / hval = (unsigned int)/
+ s/ return / return (unsigned int)/
+}
diff --git a/tool/id2token.rb b/tool/id2token.rb
new file mode 100755
index 0000000000..d12ea9c08e
--- /dev/null
+++ b/tool/id2token.rb
@@ -0,0 +1,26 @@
+#! /usr/bin/ruby -p
+# -*- coding: us-ascii -*-
+
+# Used to build the Ruby parsing code in common.mk and Ripper.
+
+BEGIN {
+ require 'optparse'
+ require_relative 'lib/vpath'
+ vpath = VPath.new
+ header = nil
+
+ opt = OptionParser.new do |o|
+ vpath.def_options(o)
+ header = o.order!(ARGV).shift
+ end or abort opt.opt_s
+
+ TOKENS = {}
+ h = vpath.read(header) rescue abort("#{header} not found in #{vpath.inspect}")
+ h.scan(/^#define\s+RUBY_TOKEN_(\w+)\s+(\d+)/) do |token, id|
+ TOKENS[token] = id
+ end
+
+ TOKENS_RE = /\bRUBY_TOKEN\((#{TOKENS.keys.join('|')})\)\s*(?=\s)/
+}
+
+$_.gsub!(TOKENS_RE) {TOKENS[$1]} if /^%token/ =~ $_
diff --git a/tool/ifchange b/tool/ifchange
new file mode 100755
index 0000000000..5af41e0156
--- /dev/null
+++ b/tool/ifchange
@@ -0,0 +1,119 @@
+#!/bin/sh
+# usage: ifchange target temporary
+
+# Used in generating revision.h via Makefiles.
+
+help() {
+ cat <<HELP
+usage: $0 [options] target new-file
+options:
+ --timestamp[=file] touch timestamp file. (default: prefixed with ".time".
+ under the directory of the target)
+ --keep[=suffix] keep old file with suffix. (default: '.old')
+ --empty assume unchanged if the new file is empty.
+ --color[=always|auto|never] colorize output.
+HELP
+}
+
+set -e
+timestamp=
+keepsuffix=
+empty=
+color=auto
+until [ $# -eq 0 ]; do
+ case "$1" in
+ --)
+ shift
+ break;
+ ;;
+ --timestamp)
+ timestamp=.
+ ;;
+ --timestamp=*)
+ timestamp=`expr \( "$1" : '[^=]*=\(.*\)' \)`
+ ;;
+ --keep)
+ keepsuffix=.old
+ ;;
+ --keep=*)
+ keepsuffix=`expr \( "$1" : '[^=]*=\(.*\)' \)`
+ ;;
+ --empty)
+ empty=yes
+ ;;
+ --color)
+ color=always
+ ;;
+ --color=*)
+ color=`expr \( "$1" : '[^=]*=\(.*\)' \)`
+ ;;
+ --debug)
+ set -x
+ ;;
+ --help)
+ help
+ exit
+ ;;
+ --*)
+ echo "$0: unknown option: $1" 1>&2
+ exit 1
+ ;;
+ *)
+ break
+ ;;
+ esac
+ shift
+done
+
+if [ "$#" != 2 ]; then
+ help
+ exit 1
+fi
+
+target="$1"
+temp="$2"
+if [ "$temp" = - ]; then
+ temp="tmpdata$$.tmp~"
+ cat > "$temp"
+ trap 'rm -f "$temp"' 0
+fi
+
+msg_begin= msg_unchanged= msg_updated= msg_reset=
+if [ "$color" = always -o \( "$color" = auto -a -t 1 \) ]; then
+ msg_begin="["
+ case "`tput smso 2>/dev/null`" in
+ "$msg_begin"*m)
+ if [ ${TEST_COLORS:+set} ]; then
+ msg_unchanged=`expr ":$TEST_COLORS:" : ".*:pass=\([^:]*\):"` || :
+ msg_updated=`expr ":$TEST_COLORS:" : ".*:fail=\([^:]*\):"` || :
+ fi
+ msg_unchanged="${msg_begin}${msg_unchanged:-32}m"
+ msg_updated="${msg_begin}${msg_updated:-31;1}m"
+ msg_reset="${msg_begin}m"
+ ;;
+ esac
+ unset msg_begin
+fi
+
+targetdir=
+case "$target" in */*) targetdir=`dirname "$target"`;; esac
+if [ -f "$target" -a ! -${empty:+f}${empty:-s} "$temp" ] || cmp "$target" "$temp" >/dev/null 2>&1; then
+ echo "$target ${msg_unchanged}unchanged${msg_reset}"
+ rm -f "$temp"
+else
+ echo "$target ${msg_updated}updated${msg_reset}"
+ [ x"${targetdir}" = x -o -d "${targetdir}" ] || mkdir -p "${targetdir}"
+ [ x"${keepsuffix}" != x -a -f "$target" ] && mv -f "$target" "${target}${keepsuffix}"
+ mv -f "$temp" "$target"
+fi
+
+if [ -n "${timestamp}" ]; then
+ if [ x"${timestamp}" = x. ]; then
+ if [ x"$targetdir" = x ]; then
+ timestamp=.time."$target"
+ else
+ timestamp="$targetdir"/.time.`basename "$target"`
+ fi
+ fi
+ : > "$timestamp"
+fi
diff --git a/tool/insns2vm.rb b/tool/insns2vm.rb
new file mode 100755
index 0000000000..027dc4e380
--- /dev/null
+++ b/tool/insns2vm.rb
@@ -0,0 +1,15 @@
+#!ruby
+
+# This is used by Makefile.in to generate .inc files.
+# See Makefile.in for details.
+
+require_relative 'ruby_vm/scripts/insns2vm'
+
+if $0 == __FILE__
+ RubyVM::Insns2VM.router(ARGV).each do |(path, generator)|
+ str = generator.generate path
+ path.open 'wb:utf-8' do |fp|
+ fp.write str
+ end
+ end
+end
diff --git a/tool/install-sh b/tool/install-sh
new file mode 100644
index 0000000000..11e502f56d
--- /dev/null
+++ b/tool/install-sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Just only for using AC_PROG_INSTALL in configure.ac.
+# See autoconf.info for more detail.
+
+cat <<EOF >&2
+Ruby uses a BSD-compatible install(1) if possible. If not, Ruby
+provides its own install(1) alternative.
+
+This script is a place holder for AC_PROG_INSTALL in configure.ac.
+
+Please report a bug in Ruby to http://bugs.ruby-lang.org if you see
+this message.
+
+Thank you.
+EOF
+exit 1
diff --git a/tool/intern_ids.rb b/tool/intern_ids.rb
new file mode 100755
index 0000000000..20483195e0
--- /dev/null
+++ b/tool/intern_ids.rb
@@ -0,0 +1,35 @@
+#!/usr/bin/ruby -sp
+# $ ruby -i tool/intern_ids.rb -prefix=_ foo.c
+
+BEGIN {
+ $prefix ||= nil
+
+ defs = File.join(File.dirname(__dir__), "defs/id.def")
+ ids = eval(File.read(defs), binding, defs)
+ table = {}
+ ids[:predefined].each {|v, t| table[t] = "id#{v}"}
+ ids[:token_op].each {|v, t, *| table[t] = "id#{v}"}
+ predefined = table.keys
+}
+
+$_.gsub!(/rb_intern\("([^\"]+)"\)/) do
+ token = $1
+ table[token] ||= "id" + id2varname(token, $prefix)
+end
+
+END {
+ predefined.each {|t| table.delete(t)}
+ unless table.empty?
+ table = table.sort_by {|t, v| v}
+
+ # Append at the last, then edit and move appropriately.
+ puts
+ puts "==== defs"
+ table.each {|t, v| puts "static ID #{v};"}
+ puts ">>>>"
+ puts
+ puts "==== init"
+ table.each {|t, v|puts "#{v} = rb_intern_const(\"#{t}\");"}
+ puts ">>>>"
+ end
+}
diff --git a/tool/leaked-globals b/tool/leaked-globals
new file mode 100755
index 0000000000..d95f3794e8
--- /dev/null
+++ b/tool/leaked-globals
@@ -0,0 +1,65 @@
+#!/usr/bin/ruby
+require_relative 'lib/colorize'
+
+until ARGV.empty?
+ case ARGV[0]
+ when /\ASYMBOL_PREFIX=(.*)/
+ SYMBOL_PREFIX = $1
+ when /\ANM=(.*)/ # may be multiple words
+ NM = $1
+ when /\APLATFORM=(.+)?/
+ platform = $1
+ else
+ break
+ end
+ ARGV.shift
+end
+
+config = ARGV.shift
+count = 0
+col = Colorize.new
+config_code = File.read(config)
+REPLACE = config_code.scan(/\bAC_(?:REPLACE|CHECK)_FUNCS?\((\w+)/).flatten
+# REPLACE << 'memcmp' if /\bAC_FUNC_MEMCMP\b/ =~ config_code
+REPLACE.push('main', 'DllMain')
+if platform and !platform.empty?
+ begin
+ h = File.read(platform)
+ rescue Errno::ENOENT
+ else
+ REPLACE.concat(
+ h .gsub(%r[/\*.*?\*/]m, " ") # delete block comments
+ .gsub(%r[//.*], ' ') # delete oneline comments
+ .gsub(/^\s*#.*(?:\\\n.*)*/, "") # delete preprocessor directives
+ .scan(/\b((?!rb_|DEPRECATED|_)\w+)\s*\(.*\);/)
+ .flatten)
+ end
+end
+missing = File.dirname(config) + "/missing/"
+ARGV.reject! do |n|
+ unless (src = Dir.glob(missing + File.basename(n, ".*") + ".[cS]")).empty?
+ puts "Ignore #{n} because of #{src.map {|s| File.basename(s)}.join(', ')} under missing"
+ true
+ end
+end
+print "Checking leaked global symbols..."
+STDOUT.flush
+IO.foreach("|#{NM} -Pgp #{ARGV.join(' ')}") do |line|
+ n, t, = line.split
+ next unless /[A-TV-Z]/ =~ t
+ next unless n.sub!(/^#{SYMBOL_PREFIX}/o, "")
+ next if n.include?(".")
+ next if /\A(?:Init_|InitVM_|RUBY_|ruby_|rb_|[Oo]nig|dln_|mjit_|coroutine_)/ =~ n
+ next if REPLACE.include?(n)
+ puts col.fail("leaked") if count.zero?
+ count += 1
+ puts " #{n}"
+end
+case count
+when 0
+ puts col.pass("none")
+when 1
+ abort col.fail("1 un-prefixed symbol leaked")
+else
+ abort col.fail("#{count} un-prefixed symbols leaked")
+end
diff --git a/tool/lib/-test-/integer.rb b/tool/lib/-test-/integer.rb
new file mode 100644
index 0000000000..e60abf03a0
--- /dev/null
+++ b/tool/lib/-test-/integer.rb
@@ -0,0 +1,14 @@
+require 'test/unit'
+require '-test-/integer.so'
+
+module Test::Unit::Assertions
+ def assert_fixnum(v, msg=nil)
+ assert_instance_of(Integer, v, msg)
+ assert_send([Bug::Integer, :fixnum?, v], msg)
+ end
+
+ def assert_bignum(v, msg=nil)
+ assert_instance_of(Integer, v, msg)
+ assert_send([Bug::Integer, :bignum?, v], msg)
+ end
+end
diff --git a/tool/lib/bundled_gem.rb b/tool/lib/bundled_gem.rb
new file mode 100644
index 0000000000..895aed4510
--- /dev/null
+++ b/tool/lib/bundled_gem.rb
@@ -0,0 +1,68 @@
+require 'fileutils'
+require 'rubygems'
+require 'rubygems/package'
+
+# This library is used by "make extract-gems" to
+# unpack bundled gem files.
+
+module BundledGem
+ DEFAULT_GEMS_DEPENDENCIES = [
+ "net-protocol", # net-ftp
+ "time", # net-ftp
+ "singleton", # prime
+ "ipaddr", # rinda
+ "forwardable", # prime, rinda
+ "ruby2_keywords", # drb
+ "strscan" # rexml
+ ]
+
+ module_function
+
+ def unpack(file, *rest)
+ pkg = Gem::Package.new(file)
+ prepare_test(pkg.spec, *rest) {|dir| pkg.extract_files(dir)}
+ puts "Unpacked #{file}"
+ end
+
+ def copy(path, *rest)
+ spec = Gem::Specification.load(path)
+ path = File.dirname(path)
+ prepare_test(spec, *rest) do |dir|
+ FileUtils.rm_rf(dir)
+ files = spec.files.reject {|f| f.start_with?(".git")}
+ dirs = files.map {|f| File.dirname(f) if f.include?("/")}.uniq
+ FileUtils.mkdir_p(dirs.map {|d| d ? "#{dir}/#{d}" : dir}.sort_by {|d| d.count("/")})
+ files.each do |f|
+ File.copy_stream(File.join(path, f), File.join(dir, f))
+ end
+ end
+ puts "Copied #{path}"
+ end
+
+ def prepare_test(spec, dir = ".")
+ target = spec.full_name
+ Gem.ensure_gem_subdirectories(dir)
+ gem_dir = File.join(dir, "gems", target)
+ yield gem_dir
+ spec_dir = spec.extensions.empty? ? "specifications" : File.join("gems", target)
+ if spec.extensions.empty?
+ spec.dependencies.reject! {|dep| DEFAULT_GEMS_DEPENDENCIES.include?(dep.name)}
+ end
+ File.binwrite(File.join(dir, spec_dir, "#{target}.gemspec"), spec.to_ruby)
+ unless spec.extensions.empty?
+ spec.dependencies.clear
+ File.binwrite(File.join(dir, spec_dir, ".bundled.#{target}.gemspec"), spec.to_ruby)
+ end
+ if spec.bindir and spec.executables
+ bindir = File.join(dir, "bin")
+ Dir.mkdir(bindir) rescue nil
+ spec.executables.each do |exe|
+ File.open(File.join(bindir, exe), "wb", 0o777) {|f|
+ f.print "#!ruby\n",
+ %[load File.realpath("../gems/#{target}/#{spec.bindir}/#{exe}", __dir__)\n]
+ }
+ end
+ end
+ FileUtils.rm_rf(Dir.glob("#{gem_dir}/.git*"))
+ end
+end
diff --git a/tool/lib/colorize.rb b/tool/lib/colorize.rb
new file mode 100644
index 0000000000..11b878d318
--- /dev/null
+++ b/tool/lib/colorize.rb
@@ -0,0 +1,55 @@
+# frozen-string-literal: true
+
+class Colorize
+ # call-seq:
+ # Colorize.new(colorize = nil)
+ # Colorize.new(color: color, colors_file: colors_file)
+ def initialize(color = nil, opts = ((_, color = color, nil)[0] if Hash === color))
+ @colors = @reset = nil
+ @color = (opts[:color] if opts)
+ if color or (color == nil && STDOUT.tty?)
+ if (%w[smso so].any? {|attr| /\A\e\[.*m\z/ =~ IO.popen("tput #{attr}", "r", :err => IO::NULL, &:read)} rescue nil)
+ @beg = "\e["
+ colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {}
+ if opts and colors_file = opts[:colors_file]
+ begin
+ File.read(colors_file).scan(/(\w+)=([^:\n]*)/) do |n, c|
+ colors[n] ||= c
+ end
+ rescue Errno::ENOENT
+ end
+ end
+ @colors = colors
+ @reset = "#{@beg}m"
+ end
+ end
+ self
+ end
+
+ DEFAULTS = {
+ "pass"=>"32", "fail"=>"31;1", "skip"=>"33;1",
+ "black"=>"30", "red"=>"31", "green"=>"32", "yellow"=>"33",
+ "blue"=>"34", "magenta"=>"35", "cyan"=>"36", "white"=>"37",
+ "bold"=>"1", "underline"=>"4", "reverse"=>"7",
+ }
+
+ # colorize.decorate(str, name = color_name)
+ def decorate(str, name = @color)
+ if @colors and color = (@colors[name] || DEFAULTS[name])
+ "#{@beg}#{color}m#{str}#{@reset}"
+ else
+ str
+ end
+ end
+
+ DEFAULTS.each_key do |name|
+ define_method(name) {|str|
+ decorate(str, name)
+ }
+ end
+end
+
+if $0 == __FILE__
+ colorize = Colorize.new(ARGV.shift)
+ ARGV.each {|str| puts colorize.decorate(str)}
+end
diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb
new file mode 100644
index 0000000000..acfaf00cef
--- /dev/null
+++ b/tool/lib/core_assertions.rb
@@ -0,0 +1,809 @@
+# frozen_string_literal: true
+
+module Test
+ module Unit
+ module Assertions
+ def _assertions= n # :nodoc:
+ @_assertions = n
+ end
+
+ def _assertions # :nodoc:
+ @_assertions ||= 0
+ end
+
+ ##
+ # Returns a proc that will output +msg+ along with the default message.
+
+ def message msg = nil, ending = nil, &default
+ proc {
+ ending ||= (ending_pattern = /(?<!\.)\z/; ".")
+ ending_pattern ||= /(?<!#{Regexp.quote(ending)})\z/
+ msg = msg.call if Proc === msg
+ ary = [msg, (default.call if default)].compact.reject(&:empty?)
+ ary.map! {|str| str.to_s.sub(ending_pattern, ending) }
+ begin
+ ary.join("\n")
+ rescue Encoding::CompatibilityError
+ ary.map(&:b).join("\n")
+ end
+ }
+ end
+ end
+
+ module CoreAssertions
+ require_relative 'envutil'
+ require 'pp'
+ nil.pretty_inspect
+
+ def mu_pp(obj) #:nodoc:
+ obj.pretty_inspect.chomp
+ end
+
+ def assert_file
+ AssertFile
+ end
+
+ FailDesc = proc do |status, message = "", out = ""|
+ now = Time.now
+ proc do
+ EnvUtil.failure_description(status, now, message, out)
+ end
+ end
+
+ def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil,
+ success: nil, **opt)
+ args = Array(args).dup
+ args.insert((Hash === args[0] ? 1 : 0), '--disable=gems')
+ stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt)
+ desc = FailDesc[status, message, stderr]
+ if block_given?
+ raise "test_stdout ignored, use block only or without block" if test_stdout != []
+ raise "test_stderr ignored, use block only or without block" if test_stderr != []
+ yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status)
+ else
+ all_assertions(desc) do |a|
+ [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act|
+ a.for(key) do
+ if exp.is_a?(Regexp)
+ assert_match(exp, act)
+ elsif exp.all? {|e| String === e}
+ assert_equal(exp, act.lines.map {|l| l.chomp })
+ else
+ assert_pattern_list(exp, act)
+ end
+ end
+ end
+ unless success.nil?
+ a.for("success?") do
+ if success
+ assert_predicate(status, :success?)
+ else
+ assert_not_predicate(status, :success?)
+ end
+ end
+ end
+ end
+ status
+ end
+ end
+
+ if defined?(RubyVM::InstructionSequence)
+ def syntax_check(code, fname, line)
+ code = code.dup.force_encoding(Encoding::UTF_8)
+ RubyVM::InstructionSequence.compile(code, fname, fname, line)
+ :ok
+ ensure
+ raise if SyntaxError === $!
+ end
+ else
+ def syntax_check(code, fname, line)
+ code = code.b
+ code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) {
+ "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n"
+ }
+ code = code.force_encoding(Encoding::UTF_8)
+ catch {|tag| eval(code, binding, fname, line - 1)}
+ end
+ end
+
+ def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt)
+ # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail
+ pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled?
+
+ require_relative 'memory_status'
+ raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status)
+
+ token_dump, token_re = new_test_token
+ envs = args.shift if Array === args and Hash === args.first
+ args = [
+ "--disable=gems",
+ "-r", File.expand_path("../memory_status", __FILE__),
+ *args,
+ "-v", "-",
+ ]
+ if defined? Memory::NO_MEMORY_LEAK_ENVS then
+ envs ||= {}
+ newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break }
+ envs = newenvs if newenvs
+ end
+ args.unshift(envs) if envs
+ cmd = [
+ 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}',
+ prepare,
+ 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")',
+ '$initial_size = $initial_status.size',
+ code,
+ 'GC.start',
+ ].join("\n")
+ _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt)
+ before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1)
+ after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1)
+ assert(status.success?, FailDesc[status, message, err])
+ ([:size, (rss && :rss)] & after.members).each do |n|
+ b = before[n]
+ a = after[n]
+ next unless a > 0 and b > 0
+ assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"})
+ end
+ rescue LoadError
+ pend
+ end
+
+ # :call-seq:
+ # assert_nothing_raised( *args, &block )
+ #
+ #If any exceptions are given as arguments, the assertion will
+ #fail if one of those exceptions are raised. Otherwise, the test fails
+ #if any exceptions are raised.
+ #
+ #The final argument may be a failure message.
+ #
+ # assert_nothing_raised RuntimeError do
+ # raise Exception #Assertion passes, Exception is not a RuntimeError
+ # end
+ #
+ # assert_nothing_raised do
+ # raise Exception #Assertion fails
+ # end
+ def assert_nothing_raised(*args)
+ self._assertions += 1
+ if Module === args.last
+ msg = nil
+ else
+ msg = args.pop
+ end
+ begin
+ yield
+ rescue Test::Unit::PendedError, *(Test::Unit::AssertionFailedError if args.empty?)
+ raise
+ rescue *(args.empty? ? Exception : args) => e
+ msg = message(msg) {
+ "Exception raised:\n<#{mu_pp(e)}>\n""Backtrace:\n" <<
+ Test.filter_backtrace(e.backtrace).map{|frame| " #{frame}"}.join("\n")
+ }
+ raise Test::Unit::AssertionFailedError, msg.call, e.backtrace
+ end
+ end
+
+ def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil)
+ fname ||= caller_locations(2, 1)[0]
+ mesg ||= fname.to_s
+ verbose, $VERBOSE = $VERBOSE, verbose
+ case
+ when Array === fname
+ fname, line = *fname
+ when defined?(fname.path) && defined?(fname.lineno)
+ fname, line = fname.path, fname.lineno
+ else
+ line = 1
+ end
+ yield(code, fname, line, message(mesg) {
+ if code.end_with?("\n")
+ "```\n#{code}```\n"
+ else
+ "```\n#{code}\n```\n""no-newline"
+ end
+ })
+ ensure
+ $VERBOSE = verbose
+ end
+
+ def assert_valid_syntax(code, *args, **opt)
+ prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg|
+ yield if defined?(yield)
+ assert_nothing_raised(SyntaxError, mesg) do
+ assert_equal(:ok, syntax_check(src, fname, line), mesg)
+ end
+ end
+ end
+
+ def assert_normal_exit(testsrc, message = '', child_env: nil, **opt)
+ assert_valid_syntax(testsrc, caller_locations(1, 1)[0])
+ if child_env
+ child_env = [child_env]
+ else
+ child_env = []
+ end
+ out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt)
+ assert !status.signaled?, FailDesc[status, message, out]
+ end
+
+ def assert_ruby_status(args, test_stdin="", message=nil, **opt)
+ out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt)
+ desc = FailDesc[status, message, out]
+ assert(!status.signaled?, desc)
+ message ||= "ruby exit status is not success:"
+ assert(status.success?, desc)
+ end
+
+ ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM")
+
+ def separated_runner(token, out = nil)
+ include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) })
+ out = out ? IO.new(out, 'w') : STDOUT
+ at_exit {
+ out.puts "#{token}<error>", [Marshal.dump($!)].pack('m'), "#{token}</error>", "#{token}assertions=#{self._assertions}"
+ }
+ Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner)
+ end
+
+ def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt)
+ unless file and line
+ loc, = caller_locations(1,1)
+ file ||= loc.path
+ line ||= loc.lineno
+ end
+ capture_stdout = true
+ unless /mswin|mingw/ =~ RUBY_PLATFORM
+ capture_stdout = false
+ opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner)
+ res_p, res_c = IO.pipe
+ opt[:ios] = [res_c]
+ end
+ token_dump, token_re = new_test_token
+ src = <<eom
+# -*- coding: #{line += __LINE__; src.encoding}; -*-
+BEGIN {
+ require "test/unit";include Test::Unit::Assertions;require #{__FILE__.dump};include Test::Unit::CoreAssertions
+ separated_runner #{token_dump}, #{res_c&.fileno || 'nil'}
+}
+#{line -= __LINE__; src}
+eom
+ args = args.dup
+ args.insert((Hash === args.first ? 1 : 0), "-w", "--disable=gems", *$:.map {|l| "-I#{l}"})
+ args << "--debug" if RUBY_ENGINE == 'jruby' # warning: tracing (e.g. set_trace_func) will not capture all events without --debug flag
+ stdout, stderr, status = EnvUtil.invoke_ruby(args, src, capture_stdout, true, **opt)
+ ensure
+ if res_c
+ res_c.close
+ res = res_p.read
+ res_p.close
+ else
+ res = stdout
+ end
+ raise if $!
+ abort = status.coredump? || (status.signaled? && ABORT_SIGNALS.include?(status.termsig))
+ assert(!abort, FailDesc[status, nil, stderr])
+ self._assertions += res[/^#{token_re}assertions=(\d+)/, 1].to_i
+ begin
+ res = Marshal.load(res[/^#{token_re}<error>\n\K.*\n(?=#{token_re}<\/error>$)/m].unpack1("m"))
+ rescue => marshal_error
+ ignore_stderr = nil
+ res = nil
+ end
+ if res and !(SystemExit === res)
+ if bt = res.backtrace
+ bt.each do |l|
+ l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"}
+ end
+ bt.concat(caller)
+ else
+ res.set_backtrace(caller)
+ end
+ raise res
+ end
+
+ # really is it succeed?
+ unless ignore_stderr
+ # the body of assert_separately must not output anything to detect error
+ assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr])
+ end
+ assert(status.success?, FailDesc[status, "assert_separately failed", stderr])
+ raise marshal_error if marshal_error
+ end
+
+ # Run Ractor-related test without influencing the main test suite
+ def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt)
+ return unless defined?(Ractor)
+
+ require = "require #{require.inspect}" if require
+ if require_relative
+ dir = File.dirname(caller_locations[0,1][0].absolute_path)
+ full_path = File.expand_path(require_relative, dir)
+ require = "#{require}; require #{full_path.inspect}"
+ end
+
+ assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt)
+ #{require}
+ previous_verbose = $VERBOSE
+ $VERBOSE = nil
+ Ractor.new {} # trigger initial warning
+ $VERBOSE = previous_verbose
+ #{src}
+ RUBY
+ end
+
+ # :call-seq:
+ # assert_throw( tag, failure_message = nil, &block )
+ #
+ #Fails unless the given block throws +tag+, returns the caught
+ #value otherwise.
+ #
+ #An optional failure message may be provided as the final argument.
+ #
+ # tag = Object.new
+ # assert_throw(tag, "#{tag} was not thrown!") do
+ # throw tag
+ # end
+ def assert_throw(tag, msg = nil)
+ ret = catch(tag) do
+ begin
+ yield(tag)
+ rescue UncaughtThrowError => e
+ thrown = e.tag
+ end
+ msg = message(msg) {
+ "Expected #{mu_pp(tag)} to have been thrown"\
+ "#{%Q[, not #{thrown}] if thrown}"
+ }
+ assert(false, msg)
+ end
+ assert(true)
+ ret
+ end
+
+ # :call-seq:
+ # assert_raise( *args, &block )
+ #
+ #Tests if the given block raises an exception. Acceptable exception
+ #types may be given as optional arguments. If the last argument is a
+ #String, it will be used as the error message.
+ #
+ # assert_raise do #Fails, no Exceptions are raised
+ # end
+ #
+ # assert_raise NameError do
+ # puts x #Raises NameError, so assertion succeeds
+ # end
+ def assert_raise(*exp, &b)
+ case exp.last
+ when String, Proc
+ msg = exp.pop
+ end
+
+ begin
+ yield
+ rescue Test::Unit::PendedError => e
+ return e if exp.include? Test::Unit::PendedError
+ raise e
+ rescue Exception => e
+ expected = exp.any? { |ex|
+ if ex.instance_of? Module then
+ e.kind_of? ex
+ else
+ e.instance_of? ex
+ end
+ }
+
+ assert expected, proc {
+ flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"})
+ }
+
+ return e
+ ensure
+ unless e
+ exp = exp.first if exp.size == 1
+
+ flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"})
+ end
+ end
+ end
+
+ # :call-seq:
+ # assert_raise_with_message(exception, expected, msg = nil, &block)
+ #
+ #Tests if the given block raises an exception with the expected
+ #message.
+ #
+ # assert_raise_with_message(RuntimeError, "foo") do
+ # nil #Fails, no Exceptions are raised
+ # end
+ #
+ # assert_raise_with_message(RuntimeError, "foo") do
+ # raise ArgumentError, "foo" #Fails, different Exception is raised
+ # end
+ #
+ # assert_raise_with_message(RuntimeError, "foo") do
+ # raise "bar" #Fails, RuntimeError is raised but the message differs
+ # end
+ #
+ # assert_raise_with_message(RuntimeError, "foo") do
+ # raise "foo" #Raises RuntimeError with the message, so assertion succeeds
+ # end
+ def assert_raise_with_message(exception, expected, msg = nil, &block)
+ case expected
+ when String
+ assert = :assert_equal
+ when Regexp
+ assert = :assert_match
+ else
+ raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}"
+ end
+
+ ex = m = nil
+ EnvUtil.with_default_internal(expected.encoding) do
+ ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do
+ yield
+ end
+ m = ex.message
+ end
+ msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"}
+
+ if assert == :assert_equal
+ assert_equal(expected, m, msg)
+ else
+ msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" }
+ assert expected =~ m, msg
+ block.binding.eval("proc{|_|$~=_}").call($~)
+ end
+ ex
+ end
+
+ TEST_DIR = File.join(__dir__, "test/unit") #:nodoc:
+
+ # :call-seq:
+ # assert(test, [failure_message])
+ #
+ #Tests if +test+ is true.
+ #
+ #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used
+ #as the failure message. Otherwise, the result of calling +msg+ will be
+ #used as the message if the assertion fails.
+ #
+ #If no +msg+ is given, a default message will be used.
+ #
+ # assert(false, "This was expected to be true")
+ def assert(test, *msgs)
+ case msg = msgs.first
+ when String, Proc
+ when nil
+ msgs.shift
+ else
+ bt = caller.reject { |s| s.start_with?(TEST_DIR) }
+ raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt
+ end unless msgs.empty?
+ super
+ end
+
+ # :call-seq:
+ # assert_respond_to( object, method, failure_message = nil )
+ #
+ #Tests if the given Object responds to +method+.
+ #
+ #An optional failure message may be provided as the final argument.
+ #
+ # assert_respond_to("hello", :reverse) #Succeeds
+ # assert_respond_to("hello", :does_not_exist) #Fails
+ def assert_respond_to(obj, (meth, *priv), msg = nil)
+ unless priv.empty?
+ msg = message(msg) {
+ "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}"
+ }
+ return assert obj.respond_to?(meth, *priv), msg
+ end
+ #get rid of overcounting
+ if caller_locations(1, 1)[0].path.start_with?(TEST_DIR)
+ return if obj.respond_to?(meth)
+ end
+ super(obj, meth, msg)
+ end
+
+ # :call-seq:
+ # assert_not_respond_to( object, method, failure_message = nil )
+ #
+ #Tests if the given Object does not respond to +method+.
+ #
+ #An optional failure message may be provided as the final argument.
+ #
+ # assert_not_respond_to("hello", :reverse) #Fails
+ # assert_not_respond_to("hello", :does_not_exist) #Succeeds
+ def assert_not_respond_to(obj, (meth, *priv), msg = nil)
+ unless priv.empty?
+ msg = message(msg) {
+ "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}"
+ }
+ return assert !obj.respond_to?(meth, *priv), msg
+ end
+ #get rid of overcounting
+ if caller_locations(1, 1)[0].path.start_with?(TEST_DIR)
+ return unless obj.respond_to?(meth)
+ end
+ refute_respond_to(obj, meth, msg)
+ end
+
+ # pattern_list is an array which contains regexp and :*.
+ # :* means any sequence.
+ #
+ # pattern_list is anchored.
+ # Use [:*, regexp, :*] for non-anchored match.
+ def assert_pattern_list(pattern_list, actual, message=nil)
+ rest = actual
+ anchored = true
+ pattern_list.each_with_index {|pattern, i|
+ if pattern == :*
+ anchored = false
+ else
+ if anchored
+ match = /\A#{pattern}/.match(rest)
+ else
+ match = pattern.match(rest)
+ end
+ unless match
+ msg = message(msg) {
+ expect_msg = "Expected #{mu_pp pattern}\n"
+ if /\n[^\n]/ =~ rest
+ actual_mesg = +"to match\n"
+ rest.scan(/.*\n+/) {
+ actual_mesg << ' ' << $&.inspect << "+\n"
+ }
+ actual_mesg.sub!(/\+\n\z/, '')
+ else
+ actual_mesg = "to match " + mu_pp(rest)
+ end
+ actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters"
+ expect_msg + actual_mesg
+ }
+ assert false, msg
+ end
+ rest = match.post_match
+ anchored = true
+ end
+ }
+ if anchored
+ assert_equal("", rest)
+ end
+ end
+
+ def assert_warning(pat, msg = nil)
+ result = nil
+ stderr = EnvUtil.with_default_internal(pat.encoding) {
+ EnvUtil.verbose_warning {
+ result = yield
+ }
+ }
+ msg = message(msg) {diff pat, stderr}
+ assert(pat === stderr, msg)
+ result
+ end
+
+ def assert_warn(*args)
+ assert_warning(*args) {$VERBOSE = false; yield}
+ end
+
+ def assert_deprecated_warning(mesg = /deprecated/)
+ assert_warning(mesg) do
+ Warning[:deprecated] = true
+ yield
+ end
+ end
+
+ def assert_deprecated_warn(mesg = /deprecated/)
+ assert_warn(mesg) do
+ Warning[:deprecated] = true
+ yield
+ end
+ end
+
+ class << (AssertFile = Struct.new(:failure_message).new)
+ include Assertions
+ include CoreAssertions
+ def assert_file_predicate(predicate, *args)
+ if /\Anot_/ =~ predicate
+ predicate = $'
+ neg = " not"
+ end
+ result = File.__send__(predicate, *args)
+ result = !result if neg
+ mesg = "Expected file ".dup << args.shift.inspect
+ mesg << "#{neg} to be #{predicate}"
+ mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty?
+ mesg << " #{failure_message}" if failure_message
+ assert(result, mesg)
+ end
+ alias method_missing assert_file_predicate
+
+ def for(message)
+ clone.tap {|a| a.failure_message = message}
+ end
+ end
+
+ class AllFailures
+ attr_reader :failures
+
+ def initialize
+ @count = 0
+ @failures = {}
+ end
+
+ def for(key)
+ @count += 1
+ yield key
+ rescue Exception => e
+ @failures[key] = [@count, e]
+ end
+
+ def foreach(*keys)
+ keys.each do |key|
+ @count += 1
+ begin
+ yield key
+ rescue Exception => e
+ @failures[key] = [@count, e]
+ end
+ end
+ end
+
+ def message
+ i = 0
+ total = @count.to_s
+ fmt = "%#{total.size}d"
+ @failures.map {|k, (n, v)|
+ v = v.message
+ "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}"
+ }.join("\n")
+ end
+
+ def pass?
+ @failures.empty?
+ end
+ end
+
+ # threads should respond to shift method.
+ # Array can be used.
+ def assert_join_threads(threads, message = nil)
+ errs = []
+ values = []
+ while th = threads.shift
+ begin
+ values << th.value
+ rescue Exception
+ errs << [th, $!]
+ th = nil
+ end
+ end
+ values
+ ensure
+ if th&.alive?
+ th.raise(Timeout::Error.new)
+ th.join rescue errs << [th, $!]
+ end
+ if !errs.empty?
+ msg = "exceptions on #{errs.length} threads:\n" +
+ errs.map {|t, err|
+ "#{t.inspect}:\n" +
+ RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message
+ }.join("\n---\n")
+ if message
+ msg = "#{message}\n#{msg}"
+ end
+ raise Test::Unit::AssertionFailedError, msg
+ end
+ end
+
+ def assert_all?(obj, m = nil, &blk)
+ failed = []
+ obj.each do |*a, &b|
+ unless blk.call(*a, &b)
+ failed << (a.size > 1 ? a : a[0])
+ end
+ end
+ assert(failed.empty?, message(m) {failed.pretty_inspect})
+ end
+
+ def assert_all_assertions(msg = nil)
+ all = AllFailures.new
+ yield all
+ ensure
+ assert(all.pass?, message(msg) {all.message.chomp(".")})
+ end
+ alias all_assertions assert_all_assertions
+
+ def assert_all_assertions_foreach(msg = nil, *keys, &block)
+ all = AllFailures.new
+ all.foreach(*keys, &block)
+ ensure
+ assert(all.pass?, message(msg) {all.message.chomp(".")})
+ end
+ alias all_assertions_foreach assert_all_assertions_foreach
+
+ %w[
+ CLOCK_THREAD_CPUTIME_ID CLOCK_PROCESS_CPUTIME_ID
+ CLOCK_MONOTONIC
+ ].find do |c|
+ if Process.const_defined?(c)
+ [c.to_sym, Process.const_get(c)].find do |clk|
+ begin
+ Process.clock_gettime(clk)
+ rescue
+ # Constants may be defined but not implemented, e.g., mingw.
+ else
+ PERFORMANCE_CLOCK = clk
+ end
+ end
+ end
+ end
+
+ # Expect +seq+ to respond to +first+ and +each+ methods, e.g.,
+ # Array, Range, Enumerator::ArithmeticSequence and other
+ # Enumerable-s, and each elements should be size factors.
+ #
+ # :yield: each elements of +seq+.
+ def assert_linear_performance(seq, rehearsal: nil, pre: ->(n) {n})
+ pend "No PERFORMANCE_CLOCK found" unless defined?(PERFORMANCE_CLOCK)
+
+ # Timeout testing generally doesn't work when RJIT compilation happens.
+ rjit_enabled = defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled?
+ measure = proc do |arg, message|
+ st = Process.clock_gettime(PERFORMANCE_CLOCK)
+ yield(*arg)
+ t = (Process.clock_gettime(PERFORMANCE_CLOCK) - st)
+ assert_operator 0, :<=, t, message unless rjit_enabled
+ t
+ end
+
+ first = seq.first
+ *arg = pre.call(first)
+ times = (0..(rehearsal || (2 * first))).map do
+ measure[arg, "rehearsal"].nonzero?
+ end
+ times.compact!
+ tmin, tmax = times.minmax
+
+ # safe_factor * tmax * rehearsal_time_variance_factor(equals to 1 when variance is small)
+ tbase = 10 * tmax * [(tmax / tmin) ** 2 / 4, 1].max
+ info = "(tmin: #{tmin}, tmax: #{tmax}, tbase: #{tbase})"
+
+ seq.each do |i|
+ next if i == first
+ t = tbase * i.fdiv(first)
+ *arg = pre.call(i)
+ message = "[#{i}]: in #{t}s #{info}"
+ Timeout.timeout(t, Timeout::Error, message) do
+ measure[arg, message]
+ end
+ end
+ end
+
+ def diff(exp, act)
+ require 'pp'
+ q = PP.new(+"")
+ q.guard_inspect_key do
+ q.group(2, "expected: ") do
+ q.pp exp
+ end
+ q.text q.newline
+ q.group(2, "actual: ") do
+ q.pp act
+ end
+ q.flush
+ end
+ q.output
+ end
+
+ def new_test_token
+ token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m"
+ return token.dump, Regexp.quote(token)
+ end
+ end
+ end
+end
diff --git a/tool/lib/envutil.rb b/tool/lib/envutil.rb
new file mode 100644
index 0000000000..0391b90c1c
--- /dev/null
+++ b/tool/lib/envutil.rb
@@ -0,0 +1,367 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+require "open3"
+require "timeout"
+require_relative "find_executable"
+begin
+ require 'rbconfig'
+rescue LoadError
+end
+begin
+ require "rbconfig/sizeof"
+rescue LoadError
+end
+
+module EnvUtil
+ def rubybin
+ if ruby = ENV["RUBY"]
+ return ruby
+ end
+ ruby = "ruby"
+ exeext = RbConfig::CONFIG["EXEEXT"]
+ rubyexe = (ruby + exeext if exeext and !exeext.empty?)
+ 3.times do
+ if File.exist? ruby and File.executable? ruby and !File.directory? ruby
+ return File.expand_path(ruby)
+ end
+ if rubyexe and File.exist? rubyexe and File.executable? rubyexe
+ return File.expand_path(rubyexe)
+ end
+ ruby = File.join("..", ruby)
+ end
+ if defined?(RbConfig.ruby)
+ RbConfig.ruby
+ else
+ "ruby"
+ end
+ end
+ module_function :rubybin
+
+ LANG_ENVS = %w"LANG LC_ALL LC_CTYPE"
+
+ DEFAULT_SIGNALS = Signal.list
+ DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM
+
+ RUBYLIB = ENV["RUBYLIB"]
+
+ class << self
+ attr_accessor :timeout_scale
+ attr_reader :original_internal_encoding, :original_external_encoding,
+ :original_verbose, :original_warning
+
+ def capture_global_values
+ @original_internal_encoding = Encoding.default_internal
+ @original_external_encoding = Encoding.default_external
+ @original_verbose = $VERBOSE
+ @original_warning = defined?(Warning.[]) ? %i[deprecated experimental].to_h {|i| [i, Warning[i]]} : nil
+ end
+ end
+
+ def apply_timeout_scale(t)
+ if scale = EnvUtil.timeout_scale
+ t * scale
+ else
+ t
+ end
+ end
+ module_function :apply_timeout_scale
+
+ def timeout(sec, klass = nil, message = nil, &blk)
+ return yield(sec) if sec == nil or sec.zero?
+ sec = apply_timeout_scale(sec)
+ Timeout.timeout(sec, klass, message, &blk)
+ end
+ module_function :timeout
+
+ def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1)
+ reprieve = apply_timeout_scale(reprieve) if reprieve
+
+ signals = Array(signal).select do |sig|
+ DEFAULT_SIGNALS[sig.to_s] or
+ DEFAULT_SIGNALS[Signal.signame(sig)] rescue false
+ end
+ signals |= [:ABRT, :KILL]
+ case pgroup
+ when 0, true
+ pgroup = -pid
+ when nil, false
+ pgroup = pid
+ end
+
+ lldb = true if /darwin/ =~ RUBY_PLATFORM
+
+ while signal = signals.shift
+
+ if lldb and [:ABRT, :KILL].include?(signal)
+ lldb = false
+ # sudo -n: --non-interactive
+ # lldb -p: attach
+ # -o: run command
+ system(*%W[sudo -n lldb -p #{pid} --batch -o bt\ all -o call\ rb_vmdebug_stack_dump_all_threads() -o quit])
+ true
+ end
+
+ begin
+ Process.kill signal, pgroup
+ rescue Errno::EINVAL
+ next
+ rescue Errno::ESRCH
+ break
+ end
+ if signals.empty? or !reprieve
+ Process.wait(pid)
+ else
+ begin
+ Timeout.timeout(reprieve) {Process.wait(pid)}
+ rescue Timeout::Error
+ else
+ break
+ end
+ end
+ end
+ $?
+ end
+ module_function :terminate
+
+ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false,
+ encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error,
+ stdout_filter: nil, stderr_filter: nil, ios: nil,
+ signal: :TERM,
+ rubybin: EnvUtil.rubybin, precommand: nil,
+ **opt)
+ timeout = apply_timeout_scale(timeout)
+
+ in_c, in_p = IO.pipe
+ out_p, out_c = IO.pipe if capture_stdout
+ err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout
+ opt[:in] = in_c
+ opt[:out] = out_c if capture_stdout
+ opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr
+ if encoding
+ out_p.set_encoding(encoding) if out_p
+ err_p.set_encoding(encoding) if err_p
+ end
+ ios.each {|i, o = i|opt[i] = o} if ios
+
+ c = "C"
+ child_env = {}
+ LANG_ENVS.each {|lc| child_env[lc] = c}
+ if Array === args and Hash === args.first
+ child_env.update(args.shift)
+ end
+ if RUBYLIB and lib = child_env["RUBYLIB"]
+ child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR)
+ end
+ child_env['ASAN_OPTIONS'] = ENV['ASAN_OPTIONS'] if ENV['ASAN_OPTIONS']
+ args = [args] if args.kind_of?(String)
+ pid = spawn(child_env, *precommand, rubybin, *args, opt)
+ in_c.close
+ out_c&.close
+ out_c = nil
+ err_c&.close
+ err_c = nil
+ if block_given?
+ return yield in_p, out_p, err_p, pid
+ else
+ th_stdout = Thread.new { out_p.read } if capture_stdout
+ th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout
+ in_p.write stdin_data.to_str unless stdin_data.empty?
+ in_p.close
+ if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout))
+ timeout_error = nil
+ else
+ status = terminate(pid, signal, opt[:pgroup], reprieve)
+ terminated = Time.now
+ end
+ stdout = th_stdout.value if capture_stdout
+ stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout
+ out_p.close if capture_stdout
+ err_p.close if capture_stderr && capture_stderr != :merge_to_stdout
+ status ||= Process.wait2(pid)[1]
+ stdout = stdout_filter.call(stdout) if stdout_filter
+ stderr = stderr_filter.call(stderr) if stderr_filter
+ if timeout_error
+ bt = caller_locations
+ msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)"
+ msg = failure_description(status, terminated, msg, [stdout, stderr].join("\n"))
+ raise timeout_error, msg, bt.map(&:to_s)
+ end
+ return stdout, stderr, status
+ end
+ ensure
+ [th_stdout, th_stderr].each do |th|
+ th.kill if th
+ end
+ [in_c, in_p, out_c, out_p, err_c, err_p].each do |io|
+ io&.close
+ end
+ [th_stdout, th_stderr].each do |th|
+ th.join if th
+ end
+ end
+ module_function :invoke_ruby
+
+ def verbose_warning
+ class << (stderr = "".dup)
+ alias write concat
+ def flush; end
+ end
+ stderr, $stderr = $stderr, stderr
+ $VERBOSE = true
+ yield stderr
+ return $stderr
+ ensure
+ stderr, $stderr = $stderr, stderr
+ $VERBOSE = EnvUtil.original_verbose
+ EnvUtil.original_warning&.each {|i, v| Warning[i] = v}
+ end
+ module_function :verbose_warning
+
+ def default_warning
+ $VERBOSE = false
+ yield
+ ensure
+ $VERBOSE = EnvUtil.original_verbose
+ end
+ module_function :default_warning
+
+ def suppress_warning
+ $VERBOSE = nil
+ yield
+ ensure
+ $VERBOSE = EnvUtil.original_verbose
+ end
+ module_function :suppress_warning
+
+ def under_gc_stress(stress = true)
+ stress, GC.stress = GC.stress, stress
+ yield
+ ensure
+ GC.stress = stress
+ end
+ module_function :under_gc_stress
+
+ def with_default_external(enc)
+ suppress_warning { Encoding.default_external = enc }
+ yield
+ ensure
+ suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding }
+ end
+ module_function :with_default_external
+
+ def with_default_internal(enc)
+ suppress_warning { Encoding.default_internal = enc }
+ yield
+ ensure
+ suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding }
+ end
+ module_function :with_default_internal
+
+ def labeled_module(name, &block)
+ Module.new do
+ singleton_class.class_eval {
+ define_method(:to_s) {name}
+ alias inspect to_s
+ alias name to_s
+ }
+ class_eval(&block) if block
+ end
+ end
+ module_function :labeled_module
+
+ def labeled_class(name, superclass = Object, &block)
+ Class.new(superclass) do
+ singleton_class.class_eval {
+ define_method(:to_s) {name}
+ alias inspect to_s
+ alias name to_s
+ }
+ class_eval(&block) if block
+ end
+ end
+ module_function :labeled_class
+
+ if /darwin/ =~ RUBY_PLATFORM
+ DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports")
+ DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S'
+ @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME']
+
+ def self.diagnostic_reports(signame, pid, now)
+ return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame)
+ cmd = File.basename(rubybin)
+ cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd
+ path = DIAGNOSTIC_REPORTS_PATH
+ timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT
+ pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash"
+ first = true
+ 30.times do
+ first ? (first = false) : sleep(0.1)
+ Dir.glob(pat) do |name|
+ log = File.read(name) rescue next
+ if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log
+ File.unlink(name)
+ File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil
+ return log
+ end
+ end
+ end
+ nil
+ end
+ else
+ def self.diagnostic_reports(signame, pid, now)
+ end
+ end
+
+ def self.failure_description(status, now, message = "", out = "")
+ pid = status.pid
+ if signo = status.termsig
+ signame = Signal.signame(signo)
+ sigdesc = "signal #{signo}"
+ end
+ log = diagnostic_reports(signame, pid, now)
+ if signame
+ sigdesc = "SIG#{signame} (#{sigdesc})"
+ end
+ if status.coredump?
+ sigdesc = "#{sigdesc} (core dumped)"
+ end
+ full_message = ''.dup
+ message = message.call if Proc === message
+ if message and !message.empty?
+ full_message << message << "\n"
+ end
+ full_message << "pid #{pid}"
+ full_message << " exit #{status.exitstatus}" if status.exited?
+ full_message << " killed by #{sigdesc}" if sigdesc
+ if out and !out.empty?
+ full_message << "\n" << out.b.gsub(/^/, '| ')
+ full_message.sub!(/(?<!\n)\z/, "\n")
+ end
+ if log
+ full_message << "Diagnostic reports:\n" << log.b.gsub(/^/, '| ')
+ end
+ full_message
+ end
+
+ def self.gc_stress_to_class?
+ unless defined?(@gc_stress_to_class)
+ _, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"])
+ @gc_stress_to_class = status.success?
+ end
+ @gc_stress_to_class
+ end
+end
+
+if defined?(RbConfig)
+ module RbConfig
+ @ruby = EnvUtil.rubybin
+ class << self
+ undef ruby if method_defined?(:ruby)
+ attr_reader :ruby
+ end
+ dir = File.dirname(ruby)
+ CONFIG['bindir'] = dir
+ end
+end
+
+EnvUtil.capture_global_values
diff --git a/tool/lib/find_executable.rb b/tool/lib/find_executable.rb
new file mode 100644
index 0000000000..89c6fb8f3b
--- /dev/null
+++ b/tool/lib/find_executable.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+require "rbconfig"
+
+module EnvUtil
+ def find_executable(cmd, *args)
+ exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]]
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
+ next if path.empty?
+ path = File.join(path, cmd)
+ exts.each do |ext|
+ cmdline = [path + ext, *args]
+ begin
+ return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read))
+ rescue
+ next
+ end
+ end
+ end
+ nil
+ end
+ module_function :find_executable
+end
diff --git a/tool/lib/gc_checker.rb b/tool/lib/gc_checker.rb
new file mode 100644
index 0000000000..719da8cac0
--- /dev/null
+++ b/tool/lib/gc_checker.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module GCDisabledChecker
+ def before_setup
+ if @__gc_disabled__ = GC.enable # return true if GC is disabled
+ GC.disable
+ end
+
+ super
+ end
+
+ def after_teardown
+ super
+
+ disabled = GC.enable
+ GC.disable if @__gc_disabled__
+
+ if @__gc_disabled__ != disabled
+ label = {
+ true => 'disabled',
+ false => 'enabled',
+ }
+ raise "GC was #{label[@__gc_disabled__]}, but is #{label[disabled]} after the test."
+ end
+ end
+end
+
+module GCCompactChecker
+ def after_teardown
+ super
+ GC.compact
+ end
+end
+
+Test::Unit::TestCase.include GCDisabledChecker
+Test::Unit::TestCase.include GCCompactChecker if ENV['RUBY_TEST_GC_COMPACT']
diff --git a/tool/lib/iseq_loader_checker.rb b/tool/lib/iseq_loader_checker.rb
new file mode 100644
index 0000000000..3f07b3a999
--- /dev/null
+++ b/tool/lib/iseq_loader_checker.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+begin
+ require '-test-/iseq_load/iseq_load'
+rescue LoadError
+end
+require 'tempfile'
+
+class RubyVM::InstructionSequence
+ def disasm_if_possible
+ begin
+ self.disasm
+ rescue Encoding::CompatibilityError, EncodingError, SecurityError
+ nil
+ end
+ end
+
+ def self.compare_dump_and_load i1, dumper, loader
+ dump = dumper.call(i1)
+ return i1 unless dump
+ i2 = loader.call(dump)
+
+ # compare disassembled result
+ d1 = i1.disasm_if_possible
+ d2 = i2.disasm_if_possible
+
+ if d1 != d2
+ STDERR.puts "expected:"
+ STDERR.puts d1
+ STDERR.puts "actual:"
+ STDERR.puts d2
+
+ t1 = Tempfile.new("expected"); t1.puts d1; t1.close
+ t2 = Tempfile.new("actual"); t2.puts d2; t2.close
+ system("diff -u #{t1.path} #{t2.path}") # use diff if available
+ exit(1)
+ end
+ i2
+ end
+
+ opt = ENV['RUBY_ISEQ_DUMP_DEBUG']
+
+ if opt && caller.any?{|e| /test\/runner\.rb/ =~ e}
+ puts "RUBY_ISEQ_DUMP_DEBUG = #{opt}" if opt
+ end
+
+ CHECK_TO_A = 'to_a' == opt
+ CHECK_TO_BINARY = 'to_binary' == opt
+
+ def self.translate i1
+ # check to_a/load_iseq
+ compare_dump_and_load(i1,
+ proc{|iseq|
+ ary = iseq.to_a
+ ary[9] == :top ? ary : nil
+ },
+ proc{|ary|
+ RubyVM::InstructionSequence.iseq_load(ary)
+ }) if CHECK_TO_A && defined?(RubyVM::InstructionSequence.iseq_load)
+
+ # check to_binary
+ i2_bin = compare_dump_and_load(i1,
+ proc{|iseq|
+ begin
+ iseq.to_binary
+ rescue RuntimeError # not a toplevel
+ # STDERR.puts [:failed, $!, iseq].inspect
+ nil
+ end
+ },
+ proc{|bin|
+ iseq = RubyVM::InstructionSequence.load_from_binary(bin)
+ # STDERR.puts iseq.inspect
+ iseq
+ }) if CHECK_TO_BINARY
+ # return value
+ i2_bin if CHECK_TO_BINARY
+ end if CHECK_TO_A || CHECK_TO_BINARY
+end
+
+#require_relative 'x'; exit(1)
diff --git a/tool/lib/jisx0208.rb b/tool/lib/jisx0208.rb
new file mode 100644
index 0000000000..30185fb81b
--- /dev/null
+++ b/tool/lib/jisx0208.rb
@@ -0,0 +1,86 @@
+# Library used by tools/enc-emoji-citrus-gen.rb
+
+module JISX0208
+ class Char
+ class << self
+ def from_sjis(sjis)
+ unless 0x8140 <= sjis && sjis <= 0xFCFC
+ raise ArgumentError, "out of the range of JIS X 0208: 0x#{sjis.to_s(16)}"
+ end
+ sjis_hi, sjis_lo = sjis >> 8, sjis & 0xFF
+ sjis_hi = (sjis_hi - ((sjis_hi <= 0x9F) ? 0x80 : 0xC0)) << 1
+ if sjis_lo <= 0x9E
+ sjis_hi -= 1
+ sjis_lo -= (sjis_lo <= 0x7E) ? 0x3F : 0x40
+ else
+ sjis_lo -= 0x9E
+ end
+ return self.new(sjis_hi, sjis_lo)
+ end
+ end
+
+ def initialize(row, cell=nil)
+ if cell
+ @code = row_cell_to_code(row, cell)
+ else
+ @code = row.to_int
+ end
+ end
+
+ def ==(other)
+ if self.class === other
+ return Integer(self) == Integer(other)
+ end
+ return super(other)
+ end
+
+ def to_int
+ return @code
+ end
+
+ def hi
+ Integer(self) >> 8
+ end
+
+ def lo
+ Integer(self) & 0xFF
+ end
+
+ def row
+ self.hi - 0x20
+ end
+
+ def cell
+ self.lo - 0x20
+ end
+
+ def succ
+ succ_hi, succ_lo = self.hi, self.lo + 1
+ if succ_lo > 0x7E
+ succ_lo = 0x21
+ succ_hi += 1
+ end
+ return self.class.new(succ_hi << 8 | succ_lo)
+ end
+
+ def to_sjis
+ h, l = self.hi, self.lo
+ h = (h + 1) / 2 + ((0x21..0x5E).include?(h) ? 0x70 : 0xB0)
+ l += self.hi.odd? ? 0x1F + ((l >= 0x60) ? 1 : 0) : 0x7E
+ return h << 8 | l
+ end
+
+ def inspect
+ "#<JISX0208::Char:#{self.object_id.to_s(16)} sjis=#{self.to_sjis.to_s(16)} jis=#{self.to_int.to_s(16)}>"
+ end
+
+ private
+
+ def row_cell_to_code(row, cell)
+ unless 0 < row && (1..94).include?(cell)
+ raise ArgumentError, "out of row-cell range: #{row}-#{cell}"
+ end
+ return (row + 0x20) << 8 | (cell + 0x20)
+ end
+ end
+end
diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb
new file mode 100644
index 0000000000..ed50796940
--- /dev/null
+++ b/tool/lib/leakchecker.rb
@@ -0,0 +1,314 @@
+# frozen_string_literal: true
+class LeakChecker
+ @@try_lsof = nil # not-tried-yet
+
+ def initialize
+ @fd_info = find_fds
+ @@skip = false
+ @tempfile_info = find_tempfiles
+ @thread_info = find_threads
+ @env_info = find_env
+ @encoding_info = find_encodings
+ @old_verbose = $VERBOSE
+ @old_warning_flags = find_warning_flags
+ end
+
+ def check(test_name)
+ if /i386-solaris/ =~ RUBY_PLATFORM && /TestGem/ =~ test_name
+ GC.verify_internal_consistency
+ end
+
+ leaks = [
+ check_fd_leak(test_name),
+ check_thread_leak(test_name),
+ check_tempfile_leak(test_name),
+ check_env(test_name),
+ check_encodings(test_name),
+ check_verbose(test_name),
+ check_warning_flags(test_name),
+ ]
+ GC.start if leaks.any?
+ end
+
+ def check_verbose test_name
+ puts "#{test_name}: $VERBOSE == #{$VERBOSE}" unless @old_verbose == $VERBOSE
+ end
+
+ def find_fds
+ if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
+ m[:close]
+ end
+ %w"/proc/self/fd /dev/fd".each do |fd_dir|
+ if File.directory?(fd_dir)
+ fds = Dir.open(fd_dir) {|d|
+ a = d.grep(/\A\d+\z/, &:to_i)
+ if d.respond_to? :fileno
+ a -= [d.fileno]
+ end
+ a
+ }
+ return fds.sort
+ end
+ end
+ []
+ end
+
+ def check_fd_leak(test_name)
+ leaked = false
+ live1 = @fd_info
+ live2 = find_fds
+ fd_closed = live1 - live2
+ if !fd_closed.empty?
+ fd_closed.each {|fd|
+ puts "Closed file descriptor: #{test_name}: #{fd}"
+ }
+ end
+ fd_leaked = live2 - live1
+ if !@@skip && !fd_leaked.empty?
+ leaked = true
+ h = {}
+ ObjectSpace.each_object(IO) {|io|
+ inspect = io.inspect
+ begin
+ autoclose = io.autoclose?
+ fd = io.fileno
+ rescue IOError # closed IO object
+ next
+ end
+ (h[fd] ||= []) << [io, autoclose, inspect]
+ }
+ fd_leaked.select! {|fd|
+ str = ''.dup
+ pos = nil
+ if h[fd]
+ str << ' :'
+ h[fd].map {|io, autoclose, inspect|
+ if ENV["LEAK_CHECKER_TRACE_OBJECT_ALLOCATION"]
+ pos = "#{ObjectSpace.allocation_sourcefile(io)}:#{ObjectSpace.allocation_sourceline(io)}"
+ end
+ s = ' ' + inspect
+ s << "(not-autoclose)" if !autoclose
+ s
+ }.sort.each {|s|
+ str << s
+ }
+ else
+ begin
+ io = IO.for_fd(fd, autoclose: false)
+ s = io.stat
+ rescue Errno::EBADF
+ # something un-stat-able
+ next
+ else
+ next if /darwin/ =~ RUBY_PLATFORM and [0, -1].include?(s.dev)
+ str << ' ' << s.inspect
+ ensure
+ io&.close
+ end
+ end
+ puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
+ puts " The IO was created at #{pos}" if pos
+ true
+ }
+ unless fd_leaked.empty?
+ unless @@try_lsof == false
+ @@try_lsof |= system(*%W[lsof -a -d #{fd_leaked.minmax.uniq.join("-")} -p #$$], out: Test::Unit::Runner.output)
+ end
+ end
+ h.each {|fd, list|
+ next if list.length <= 1
+ if 1 < list.count {|io, autoclose, inspect| autoclose }
+ str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
+ puts "Multiple autoclose IO objects for a file descriptor in: #{test_name}: #{str}"
+ end
+ }
+ end
+ @fd_info = live2
+ @@skip = false
+ return leaked
+ end
+
+ def extend_tempfile_counter
+ return if defined? LeakChecker::TempfileCounter
+ m = Module.new {
+ @count = 0
+ class << self
+ attr_accessor :count
+ end
+
+ def new(data)
+ LeakChecker::TempfileCounter.count += 1
+ super(data)
+ end
+ }
+ LeakChecker.const_set(:TempfileCounter, m)
+
+ class << Tempfile::Remover
+ prepend LeakChecker::TempfileCounter
+ end
+ end
+
+ def find_tempfiles(prev_count=-1)
+ return [prev_count, []] unless defined? Tempfile
+ extend_tempfile_counter
+ count = TempfileCounter.count
+ if prev_count == count
+ [prev_count, []]
+ else
+ tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t|
+ t.instance_variable_defined?(:@tmpfile) and t.path
+ }
+ [count, tempfiles]
+ end
+ end
+
+ def check_tempfile_leak(test_name)
+ return false unless defined? Tempfile
+ count1, initial_tempfiles = @tempfile_info
+ count2, current_tempfiles = find_tempfiles(count1)
+ leaked = false
+ tempfiles_leaked = current_tempfiles - initial_tempfiles
+ if !tempfiles_leaked.empty?
+ leaked = true
+ list = tempfiles_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Leaked tempfile: #{test_name}: #{str}"
+ }
+ tempfiles_leaked.each {|t| t.close! }
+ end
+ @tempfile_info = [count2, initial_tempfiles]
+ return leaked
+ end
+
+ def find_threads
+ Thread.list.find_all {|t|
+ t != Thread.current && t.alive?
+ }
+ end
+
+ def check_thread_leak(test_name)
+ live1 = @thread_info
+ live2 = find_threads
+ thread_finished = live1 - live2
+ leaked = false
+ if !thread_finished.empty?
+ list = thread_finished.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Finished thread: #{test_name}: #{str}"
+ }
+ end
+ thread_leaked = live2 - live1
+ if !thread_leaked.empty?
+ leaked = true
+ list = thread_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Leaked thread: #{test_name}: #{str}"
+ }
+ end
+ @thread_info = live2
+ return leaked
+ end
+
+ e = ENV["_Ruby_Env_Ignorecase_"], ENV["_RUBY_ENV_IGNORECASE_"]
+ begin
+ ENV["_Ruby_Env_Ignorecase_"] = ENV["_RUBY_ENV_IGNORECASE_"] = nil
+ ENV["_RUBY_ENV_IGNORECASE_"] = "ENV_CASE_TEST"
+ ENV_IGNORECASE = ENV["_Ruby_Env_Ignorecase_"] == "ENV_CASE_TEST"
+ ensure
+ ENV["_Ruby_Env_Ignorecase_"], ENV["_RUBY_ENV_IGNORECASE_"] = e
+ end
+
+ if ENV_IGNORECASE
+ def find_env
+ ENV.to_h {|k, v| [k.upcase, v]}
+ end
+ else
+ def find_env
+ ENV.to_h
+ end
+ end
+
+ def check_env(test_name)
+ old_env = @env_info
+ new_env = find_env
+ return false if old_env == new_env
+ (old_env.keys | new_env.keys).sort.each {|k|
+ if old_env.has_key?(k)
+ if new_env.has_key?(k)
+ if old_env[k] != new_env[k]
+ puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
+ end
+ else
+ puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
+ end
+ else
+ if new_env.has_key?(k)
+ puts "Environment variable changed: #{test_name} : #{k.inspect} added"
+ else
+ flunk "unreachable"
+ end
+ end
+ }
+ @env_info = new_env
+ return true
+ end
+
+ def find_encodings
+ {
+ 'Encoding.default_internal' => Encoding.default_internal,
+ 'Encoding.default_external' => Encoding.default_external,
+ 'STDIN.internal_encoding' => STDIN.internal_encoding,
+ 'STDIN.external_encoding' => STDIN.external_encoding,
+ 'STDOUT.internal_encoding' => STDOUT.internal_encoding,
+ 'STDOUT.external_encoding' => STDOUT.external_encoding,
+ 'STDERR.internal_encoding' => STDERR.internal_encoding,
+ 'STDERR.external_encoding' => STDERR.external_encoding,
+ }
+ end
+
+ def check_encodings(test_name)
+ old_encoding_info = @encoding_info
+ @encoding_info = find_encodings
+ leaked = false
+ @encoding_info.each do |key, new_encoding|
+ old_encoding = old_encoding_info[key]
+ if new_encoding != old_encoding
+ leaked = true
+ puts "#{key} changed: #{test_name} : #{old_encoding.inspect} to #{new_encoding.inspect}"
+ end
+ end
+ leaked
+ end
+
+ WARNING_CATEGORIES = (Warning.respond_to?(:[]) ? %i[deprecated experimental] : []).freeze
+
+ def find_warning_flags
+ WARNING_CATEGORIES.to_h do |category|
+ [category, Warning[category]]
+ end
+ end
+
+ def check_warning_flags(test_name)
+ new_warning_flags = find_warning_flags
+ leaked = false
+ WARNING_CATEGORIES.each do |category|
+ if new_warning_flags[category] != @old_warning_flags[category]
+ leaked = true
+ puts "Warning[#{category.inspect}] changed: #{test_name} : #{@old_warning_flags[category]} to #{new_warning_flags[category]}"
+ end
+ end
+ return leaked
+ end
+
+ def puts(*a)
+ output = Test::Unit::Runner.output
+ if defined?(output.set_encoding)
+ output.set_encoding(nil, nil)
+ end
+ output.puts(*a)
+ end
+
+ def self.skip
+ @@skip = true
+ end
+end
diff --git a/tool/lib/memory_status.rb b/tool/lib/memory_status.rb
new file mode 100644
index 0000000000..5e9e80a68a
--- /dev/null
+++ b/tool/lib/memory_status.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+begin
+ require '-test-/memory_status.so'
+rescue LoadError
+end
+
+module Memory
+ keys = []
+
+ case
+ when File.exist?(procfile = "/proc/self/status") && (pat = /^Vm(\w+):\s+(\d+)/) =~ (data = File.binread(procfile))
+ PROC_FILE = procfile
+ VM_PAT = pat
+ def self.read_status
+ IO.foreach(PROC_FILE, encoding: Encoding::ASCII_8BIT) do |l|
+ yield($1.downcase.intern, $2.to_i * 1024) if VM_PAT =~ l
+ end
+ end
+
+ data.scan(pat) {|k, v| keys << k.downcase.intern}
+
+ when /mswin|mingw/ =~ RUBY_PLATFORM
+ require 'fiddle/import'
+ require 'fiddle/types'
+
+ module Win32
+ extend Fiddle::Importer
+ dlload "kernel32.dll", "psapi.dll"
+ include Fiddle::Win32Types
+ typealias "SIZE_T", "size_t"
+
+ PROCESS_MEMORY_COUNTERS = struct [
+ "DWORD cb",
+ "DWORD PageFaultCount",
+ "SIZE_T PeakWorkingSetSize",
+ "SIZE_T WorkingSetSize",
+ "SIZE_T QuotaPeakPagedPoolUsage",
+ "SIZE_T QuotaPagedPoolUsage",
+ "SIZE_T QuotaPeakNonPagedPoolUsage",
+ "SIZE_T QuotaNonPagedPoolUsage",
+ "SIZE_T PagefileUsage",
+ "SIZE_T PeakPagefileUsage",
+ ]
+
+ typealias "PPROCESS_MEMORY_COUNTERS", "PROCESS_MEMORY_COUNTERS*"
+
+ extern "HANDLE GetCurrentProcess()", :stdcall
+ extern "BOOL GetProcessMemoryInfo(HANDLE, PPROCESS_MEMORY_COUNTERS, DWORD)", :stdcall
+
+ module_function
+ def memory_info
+ size = PROCESS_MEMORY_COUNTERS.size
+ data = PROCESS_MEMORY_COUNTERS.malloc
+ data.cb = size
+ data if GetProcessMemoryInfo(GetCurrentProcess(), data, size)
+ end
+ end
+
+ keys.push(:size, :rss, :peak)
+ def self.read_status
+ if info = Win32.memory_info
+ yield :size, info.PagefileUsage
+ yield :rss, info.WorkingSetSize
+ yield :peak, info.PeakWorkingSetSize
+ end
+ end
+ when (require_relative 'find_executable'
+ pat = /^\s*(\d+)\s+(\d+)$/
+ pscmd = EnvUtil.find_executable("ps", "-ovsz=", "-orss=", "-p", $$.to_s) {|out| pat =~ out})
+ pscmd.pop
+ PAT = pat
+ PSCMD = pscmd
+
+ keys << :size << :rss
+ def self.read_status
+ if PAT =~ IO.popen(PSCMD + [$$.to_s], "r", err: [:child, :out], &:read)
+ yield :size, $1.to_i*1024
+ yield :rss, $2.to_i*1024
+ end
+ end
+ else
+ def self.read_status
+ raise NotImplementedError, "unsupported platform"
+ end
+ end
+
+ if !keys.empty?
+ Status = Struct.new(*keys)
+ end
+end unless defined?(Memory::Status)
+
+if defined?(Memory::Status)
+ class Memory::Status
+ def _update
+ Memory.read_status do |key, val|
+ self[key] = val
+ end
+ self
+ end unless method_defined?(:_update)
+
+ Header = members.map {|k| k.to_s.upcase.rjust(6)}.join('')
+ Format = "%6d"
+
+ def initialize
+ _update
+ end
+
+ def to_s
+ status = each_pair.map {|n,v|
+ "#{n}:#{v}"
+ }
+ "{#{status.join(",")}}"
+ end
+
+ def self.parse(str)
+ status = allocate
+ str.scan(/(?:\A\{|\G,)(#{members.join('|')}):(\d+)(?=,|\}\z)/) do
+ status[$1] = $2.to_i
+ end
+ status
+ end
+ end
+
+ # On some platforms (e.g. Solaris), libc malloc does not return
+ # freed memory to OS because of efficiency, and linking with extra
+ # malloc library is needed to detect memory leaks.
+ #
+ case RUBY_PLATFORM
+ when /solaris2\.(?:9|[1-9][0-9])/i # Solaris 9, 10, 11,...
+ bits = [nil].pack('p').size == 8 ? 64 : 32
+ if ENV['LD_PRELOAD'].to_s.empty? &&
+ ENV["LD_PRELOAD_#{bits}"].to_s.empty? &&
+ (ENV['UMEM_OPTIONS'].to_s.empty? ||
+ ENV['UMEM_OPTIONS'] == 'backend=mmap') then
+ envs = {
+ 'LD_PRELOAD' => 'libumem.so',
+ 'UMEM_OPTIONS' => 'backend=mmap'
+ }
+ args = [
+ envs,
+ "--disable=gems",
+ "-v", "-",
+ ]
+ _, err, status = EnvUtil.invoke_ruby(args, "exit(0)", true, true)
+ if status.exitstatus == 0 && err.to_s.empty? then
+ Memory::NO_MEMORY_LEAK_ENVS = envs
+ end
+ end
+ end #case RUBY_PLATFORM
+
+end
diff --git a/tool/lib/profile_test_all.rb b/tool/lib/profile_test_all.rb
new file mode 100644
index 0000000000..fb434e314d
--- /dev/null
+++ b/tool/lib/profile_test_all.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+#
+# purpose:
+# Profile memory usage of each tests.
+#
+# usage:
+# RUBY_TEST_ALL_PROFILE=[file] make test-all
+#
+# output:
+# [file] specified by RUBY_TEST_ALL_PROFILE
+# If [file] is 'true', then it is ./test_all_profile
+#
+# collected information:
+# - ObjectSpace.memsize_of_all
+# - GC.stat
+# - /proc/meminfo (some fields, if exists)
+# - /proc/self/status (some fields, if exists)
+# - /proc/self/statm (if exists)
+#
+
+require 'objspace'
+
+class Test::Unit::TestCase
+ alias orig_run run
+
+ file = ENV['RUBY_TEST_ALL_PROFILE']
+ file = 'test-all-profile-result' if file == 'true'
+ TEST_ALL_PROFILE_OUT = open(file, 'w')
+ TEST_ALL_PROFILE_GC_STAT_HASH = {}
+ TEST_ALL_PROFILE_BANNER = ['name']
+ TEST_ALL_PROFILE_PROCS = []
+
+ def self.add *name, &b
+ TEST_ALL_PROFILE_BANNER.concat name
+ TEST_ALL_PROFILE_PROCS << b
+ end
+
+ add 'failed?' do |result, tc|
+ result << (tc.passed? ? 0 : 1)
+ end
+
+ add 'memsize_of_all' do |result, *|
+ result << ObjectSpace.memsize_of_all
+ end
+
+ add(*GC.stat.keys) do |result, *|
+ GC.stat(TEST_ALL_PROFILE_GC_STAT_HASH)
+ result.concat TEST_ALL_PROFILE_GC_STAT_HASH.values
+ end
+
+ def self.add_proc_meminfo file, fields
+ return unless FileTest.exist?(file)
+ regexp = /(#{fields.join("|")}):\s*(\d+) kB/
+ # check = {}; fields.each{|e| check[e] = true}
+ add(*fields) do |result, *|
+ text = File.read(file)
+ text.scan(regexp){
+ # check.delete $1
+ result << $2
+ ''
+ }
+ # raise check.inspect unless check.empty?
+ end
+ end
+
+ add_proc_meminfo '/proc/meminfo', %w(MemTotal MemFree)
+ add_proc_meminfo '/proc/self/status', %w(VmPeak VmSize VmHWM VmRSS)
+
+ if FileTest.exist?('/proc/self/statm')
+ add 'size', 'resident', 'share', 'text', 'lib', 'data', 'dt' do |result, *|
+ result.concat File.read('/proc/self/statm').split(/\s+/)
+ end
+ end
+
+ def memprofile_test_all_result_result
+ result = ["#{self.class}\##{self.__name__.to_s.gsub(/\s+/, '')}"]
+ TEST_ALL_PROFILE_PROCS.each{|proc|
+ proc.call(result, self)
+ }
+ result.join("\t")
+ end
+
+ def run runner
+ result = orig_run(runner)
+ TEST_ALL_PROFILE_OUT.puts memprofile_test_all_result_result
+ TEST_ALL_PROFILE_OUT.flush
+ result
+ end
+
+ TEST_ALL_PROFILE_OUT.puts TEST_ALL_PROFILE_BANNER.join("\t")
+end
diff --git a/tool/lib/test/unit.rb b/tool/lib/test/unit.rb
new file mode 100644
index 0000000000..3bb2692b43
--- /dev/null
+++ b/tool/lib/test/unit.rb
@@ -0,0 +1,1762 @@
+# frozen_string_literal: true
+
+require_relative '../envutil'
+require_relative '../colorize'
+require_relative '../leakchecker'
+require_relative '../test/unit/testcase'
+require 'optparse'
+
+# See Test::Unit
+module Test
+
+ class << self
+ ##
+ # Filter object for backtraces.
+
+ attr_accessor :backtrace_filter
+ end
+
+ class BacktraceFilter # :nodoc:
+ def filter bt
+ return ["No backtrace"] unless bt
+
+ new_bt = []
+ pattern = %r[/(?:lib\/test/|core_assertions\.rb:)]
+
+ unless $DEBUG then
+ bt.each do |line|
+ break if pattern.match?(line)
+ new_bt << line
+ end
+
+ new_bt = bt.reject { |line| pattern.match?(line) } if new_bt.empty?
+ new_bt = bt.dup if new_bt.empty?
+ else
+ new_bt = bt.dup
+ end
+
+ new_bt
+ end
+ end
+
+ self.backtrace_filter = BacktraceFilter.new
+
+ def self.filter_backtrace bt # :nodoc:
+ backtrace_filter.filter bt
+ end
+
+ ##
+ # Test::Unit is an implementation of the xUnit testing framework for Ruby.
+ module Unit
+ ##
+ # Assertion base class
+
+ class AssertionFailedError < Exception; end
+
+ ##
+ # Assertion raised when skipping a test
+
+ class PendedError < AssertionFailedError; end
+
+ module Order
+ class NoSort
+ def initialize(seed)
+ end
+
+ def sort_by_name(list)
+ list
+ end
+
+ alias sort_by_string sort_by_name
+
+ def group(list)
+ list
+ end
+ end
+
+ module JITFirst
+ def group(list)
+ # JIT first
+ jit, others = list.partition {|e| /test_jit/ =~ e}
+ jit + others
+ end
+ end
+
+ class Alpha < NoSort
+ include JITFirst
+
+ def sort_by_name(list)
+ list.sort_by(&:name)
+ end
+
+ def sort_by_string(list)
+ list.sort
+ end
+
+ end
+
+ # shuffle test suites based on CRC32 of their names
+ Shuffle = Struct.new(:seed, :salt) do
+ include JITFirst
+
+ def initialize(seed)
+ self.class::CRC_TBL ||= (0..255).map {|i|
+ (0..7).inject(i) {|c,| (c & 1 == 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1) }
+ }.freeze
+
+ salt = [seed].pack("V").unpack1("H*")
+ super(seed, "\n#{salt}".freeze).freeze
+ end
+
+ def sort_by_name(list)
+ list.sort_by {|e| randomize_key(e.name)}
+ end
+
+ def sort_by_string(list)
+ list.sort_by {|e| randomize_key(e)}
+ end
+
+ private
+
+ def crc32(str, crc32 = 0xffffffff)
+ crc_tbl = self.class::CRC_TBL
+ str.each_byte do |data|
+ crc32 = crc_tbl[(crc32 ^ data) & 0xff] ^ (crc32 >> 8)
+ end
+ crc32
+ end
+
+ def randomize_key(name)
+ crc32(salt, crc32(name)) ^ 0xffffffff
+ end
+ end
+
+ Types = {
+ random: Shuffle,
+ alpha: Alpha,
+ sorted: Alpha,
+ nosort: NoSort,
+ }
+ Types.default_proc = proc {|_, order|
+ raise "Unknown test_order: #{order.inspect}"
+ }
+ end
+
+ module RunCount # :nodoc: all
+ @@run_count = 0
+
+ def self.have_run?
+ @@run_count.nonzero?
+ end
+
+ def run(*)
+ @@run_count += 1
+ super
+ end
+
+ def run_once
+ return if have_run?
+ return if $! # don't run if there was an exception
+ yield
+ end
+ module_function :run_once
+ end
+
+ module Options # :nodoc: all
+ def initialize(*, &block)
+ @init_hook = block
+ @options = nil
+ super(&nil)
+ end
+
+ def option_parser
+ @option_parser ||= OptionParser.new
+ end
+
+ def process_args(args = [])
+ return @options if @options
+ orig_args = args.dup
+ options = {}
+ opts = option_parser
+ setup_options(opts, options)
+ opts.parse!(args)
+ orig_args -= args
+ args = @init_hook.call(args, options) if @init_hook
+ non_options(args, options)
+ @run_options = orig_args
+
+ order = options[:test_order]
+ if seed = options[:seed]
+ order ||= :random
+ elsif (order ||= :random) == :random
+ seed = options[:seed] = rand(0x10000)
+ orig_args.unshift "--seed=#{seed}"
+ end
+ Test::Unit::TestCase.test_order = order if order
+ order = Test::Unit::TestCase.test_order
+ @order = Test::Unit::Order::Types[order].new(seed)
+
+ @help = "\n" + orig_args.map { |s|
+ " " + (s =~ /[\s|&<>$()]/ ? s.inspect : s)
+ }.join("\n")
+ @options = options
+ end
+
+ private
+ def setup_options(opts, options)
+ opts.separator 'test-unit options:'
+
+ opts.on '-h', '--help', 'Display this help.' do
+ puts opts
+ exit
+ end
+
+ opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m|
+ options[:seed] = m.to_i
+ end
+
+ opts.on '-v', '--verbose', "Verbose. Show progress processing files." do
+ options[:verbose] = true
+ self.verbose = options[:verbose]
+ end
+
+ opts.on '-n', '--name PATTERN', "Filter test method names on pattern: /REGEXP/, !/REGEXP/ or STRING" do |a|
+ (options[:filter] ||= []) << a
+ end
+
+ orders = Test::Unit::Order::Types.keys
+ opts.on "--test-order=#{orders.join('|')}", orders do |a|
+ options[:test_order] = a
+ end
+ end
+
+ def non_options(files, options)
+ filter = options[:filter]
+ if filter
+ pos_pat = /\A\/(.*)\/\z/
+ neg_pat = /\A!\/(.*)\/\z/
+ negative, positive = filter.partition {|s| neg_pat =~ s}
+ if positive.empty?
+ filter = nil
+ elsif negative.empty? and positive.size == 1 and pos_pat !~ positive[0]
+ filter = positive[0]
+ unless /\A[A-Z]\w*(?:::[A-Z]\w*)*#/ =~ filter
+ filter = /##{Regexp.quote(filter)}\z/
+ end
+ else
+ filter = Regexp.union(*positive.map! {|s| Regexp.new(s[pos_pat, 1] || "\\A#{Regexp.quote(s)}\\z")})
+ end
+ unless negative.empty?
+ negative = Regexp.union(*negative.map! {|s| Regexp.new(s[neg_pat, 1])})
+ filter = /\A(?=.*#{filter})(?!.*#{negative})/
+ end
+ options[:filter] = filter
+ end
+ true
+ end
+ end
+
+ module Parallel # :nodoc: all
+ def process_args(args = [])
+ return @options if @options
+ options = super
+ if @options[:parallel]
+ @files = args
+ end
+ options
+ end
+
+ def non_options(files, options)
+ @jobserver = nil
+ makeflags = ENV.delete("MAKEFLAGS")
+ if !options[:parallel] and
+ /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ makeflags
+ begin
+ r = IO.for_fd($1.to_i(10), "rb", autoclose: false)
+ w = IO.for_fd($2.to_i(10), "wb", autoclose: false)
+ rescue
+ r.close if r
+ nil
+ else
+ r.close_on_exec = true
+ w.close_on_exec = true
+ @jobserver = [r, w]
+ options[:parallel] ||= 1
+ end
+ end
+ @worker_timeout = EnvUtil.apply_timeout_scale(options[:worker_timeout] || 180)
+ super
+ end
+
+ def status(*args)
+ result = super
+ raise @interrupt if @interrupt
+ result
+ end
+
+ private
+ def setup_options(opts, options)
+ super
+
+ opts.separator "parallel test options:"
+
+ options[:retry] = true
+
+ opts.on '-j N', '--jobs N', /\A(t)?(\d+)\z/, "Allow run tests with N jobs at once" do |_, t, a|
+ options[:testing] = true & t # For testing
+ options[:parallel] = a.to_i
+ end
+
+ opts.on '--worker-timeout=N', Integer, "Timeout workers not responding in N seconds" do |a|
+ options[:worker_timeout] = a
+ end
+
+ opts.on '--separate', "Restart job process after one testcase has done" do
+ options[:parallel] ||= 1
+ options[:separate] = true
+ end
+
+ opts.on '--retry', "Retry running testcase when --jobs specified" do
+ options[:retry] = true
+ end
+
+ opts.on '--no-retry', "Disable --retry" do
+ options[:retry] = false
+ end
+
+ opts.on '--ruby VAL', "Path to ruby which is used at -j option" do |a|
+ options[:ruby] = a.split(/ /).reject(&:empty?)
+ end
+
+ opts.on '--timetable-data=FILE', "Path to timetable data" do |a|
+ options[:timetable_data] = a
+ end
+ end
+
+ class Worker
+ def self.launch(ruby,args=[])
+ scale = EnvUtil.timeout_scale
+ io = IO.popen([*ruby, "-W1",
+ "#{__dir__}/unit/parallel.rb",
+ *("--timeout-scale=#{scale}" if scale),
+ *args], "rb+")
+ new(io, io.pid, :waiting)
+ end
+
+ attr_reader :quit_called
+ attr_accessor :start_time
+ attr_accessor :response_at
+ attr_accessor :current
+
+ @@worker_number = 0
+
+ def initialize(io, pid, status)
+ @num = (@@worker_number += 1)
+ @io = io
+ @pid = pid
+ @status = status
+ @file = nil
+ @real_file = nil
+ @loadpath = []
+ @hooks = {}
+ @quit_called = false
+ @response_at = nil
+ end
+
+ def name
+ "Worker #{@num}"
+ end
+
+ def puts(*args)
+ @io.puts(*args)
+ end
+
+ def run(task,type)
+ @file = File.basename(task, ".rb")
+ @real_file = task
+ begin
+ puts "loadpath #{[Marshal.dump($:-@loadpath)].pack("m0")}"
+ @loadpath = $:.dup
+ puts "run #{task} #{type}"
+ @status = :prepare
+ @start_time = Time.now
+ @response_at = @start_time
+ rescue Errno::EPIPE
+ died
+ rescue IOError
+ raise unless /stream closed|closed stream/ =~ $!.message
+ died
+ end
+ end
+
+ def hook(id,&block)
+ @hooks[id] ||= []
+ @hooks[id] << block
+ self
+ end
+
+ def read
+ res = (@status == :quit) ? @io.read : @io.gets
+ @response_at = Time.now
+ res && res.chomp
+ end
+
+ def close
+ @io.close unless @io.closed?
+ self
+ rescue IOError
+ end
+
+ def quit
+ return if @io.closed?
+ @quit_called = true
+ @io.puts "quit"
+ rescue Errno::EPIPE => e
+ warn "#{@pid}:#{@status.to_s.ljust(7)}:#{@file}: #{e.message}"
+ end
+
+ def kill
+ Process.kill(:KILL, @pid)
+ rescue Errno::ESRCH
+ end
+
+ def died(*additional)
+ @status = :quit
+ @io.close
+ status = $?
+ if status and status.signaled?
+ additional[0] ||= SignalException.new(status.termsig)
+ end
+
+ call_hook(:dead,*additional)
+ end
+
+ def to_s
+ if @file and @status != :ready
+ "#{@pid}=#{@file}"
+ else
+ "#{@pid}:#{@status.to_s.ljust(7)}"
+ end
+ end
+
+ attr_reader :io, :pid
+ attr_accessor :status, :file, :real_file, :loadpath
+
+ private
+
+ def call_hook(id,*additional)
+ @hooks[id] ||= []
+ @hooks[id].each{|hook| hook[self,additional] }
+ self
+ end
+
+ end
+
+ def flush_job_tokens
+ if @jobserver
+ r, w = @jobserver.shift(2)
+ @jobserver = nil
+ w << @job_tokens.slice!(0..-1)
+ r.close
+ w.close
+ end
+ end
+
+ def after_worker_down(worker, e=nil, c=false)
+ return unless @options[:parallel]
+ return if @interrupt
+ flush_job_tokens
+ warn e if e
+ real_file = worker.real_file and warn "running file: #{real_file}"
+ @need_quit = true
+ warn ""
+ warn "Some worker was crashed. It seems ruby interpreter's bug"
+ warn "or, a bug of test/unit/parallel.rb. try again without -j"
+ warn "option."
+ warn ""
+ if File.exist?('core')
+ require 'fileutils'
+ require 'time'
+ Dir.glob('/tmp/test-unit-core.*').each do |f|
+ if Time.now - File.mtime(f) > 7 * 24 * 60 * 60 # 7 days
+ warn "Deleting an old core file: #{f}"
+ FileUtils.rm(f)
+ end
+ end
+ core_path = "/tmp/test-unit-core.#{Time.now.utc.iso8601}"
+ warn "A core file is found. Saving it at: #{core_path.dump}"
+ FileUtils.mv('core', core_path)
+ cmd = ['gdb', RbConfig.ruby, '-c', core_path, '-ex', 'bt', '-batch']
+ p cmd # debugging why it's not working
+ system(*cmd)
+ end
+ STDERR.flush
+ exit c
+ end
+
+ def after_worker_quit(worker)
+ return unless @options[:parallel]
+ return if @interrupt
+ worker.close
+ if @jobserver and (token = @job_tokens.slice!(0))
+ @jobserver[1] << token
+ end
+ @workers.delete(worker)
+ @dead_workers << worker
+ @ios = @workers.map(&:io)
+ end
+
+ def launch_worker
+ begin
+ worker = Worker.launch(@options[:ruby], @run_options)
+ rescue => e
+ abort "ERROR: Failed to launch job process - #{e.class}: #{e.message}"
+ end
+ worker.hook(:dead) do |w,info|
+ after_worker_quit w
+ after_worker_down w, *info if !info.empty? && !worker.quit_called
+ end
+ @workers << worker
+ @ios << worker.io
+ @workers_hash[worker.io] = worker
+ worker
+ end
+
+ def delete_worker(worker)
+ @workers_hash.delete worker.io
+ @workers.delete worker
+ @ios.delete worker.io
+ end
+
+ def quit_workers(&cond)
+ return if @workers.empty?
+ closed = [] if cond
+ @workers.reject! do |worker|
+ next unless cond&.call(worker)
+ begin
+ Timeout.timeout(1) do
+ worker.quit
+ end
+ rescue Errno::EPIPE
+ rescue Timeout::Error
+ end
+ closed&.push worker
+ begin
+ Timeout.timeout(0.2) do
+ worker.close
+ end
+ rescue Timeout::Error
+ worker.kill
+ retry
+ end
+ @ios.delete worker.io
+ end
+
+ return if (closed ||= @workers).empty?
+ pids = closed.map(&:pid)
+ begin
+ Timeout.timeout(0.2 * closed.size) do
+ Process.waitall
+ end
+ rescue Timeout::Error
+ if pids
+ Process.kill(:KILL, *pids) rescue nil
+ pids = nil
+ retry
+ end
+ end
+ @workers.clear unless cond
+ closed
+ end
+
+ FakeClass = Struct.new(:name)
+ def fake_class(name)
+ (@fake_classes ||= {})[name] ||= FakeClass.new(name)
+ end
+
+ def deal(io, type, result, rep, shutting_down = false)
+ worker = @workers_hash[io]
+ cmd = worker.read
+ cmd.sub!(/\A\.+/, '') if cmd # read may return nil
+
+ case cmd
+ when ''
+ # just only dots, ignore
+ when /^okay$/
+ worker.status = :running
+ when /^ready(!)?$/
+ bang = $1
+ worker.status = :ready
+
+ unless task = @tasks.shift
+ worker.quit
+ return nil
+ end
+ if @options[:separate] and not bang
+ worker.quit
+ worker = launch_worker
+ end
+ worker.run(task, type)
+ @test_count += 1
+
+ jobs_status(worker)
+ when /^start (.+?)$/
+ worker.current = Marshal.load($1.unpack1("m"))
+ when /^done (.+?)$/
+ begin
+ r = Marshal.load($1.unpack1("m"))
+ rescue
+ print "unknown object: #{$1.unpack1("m").dump}"
+ return true
+ end
+ result << r[0..1] unless r[0..1] == [nil,nil]
+ rep << {file: worker.real_file, report: r[2], result: r[3], testcase: r[5]}
+ $:.push(*r[4]).uniq!
+ jobs_status(worker) if @options[:job_status] == :replace
+
+ return true
+ when /^record (.+?)$/
+ begin
+ r = Marshal.load($1.unpack1("m"))
+
+ suite = r.first
+ key = [worker.name, suite]
+ if @records[key]
+ @records[key][1] = worker.start_time = Time.now
+ else
+ @records[key] = [worker.start_time, Time.now]
+ end
+ rescue => e
+ print "unknown record: #{e.message} #{$1.unpack1("m").dump}"
+ return true
+ end
+ record(fake_class(r[0]), *r[1..-1])
+ when /^p (.+?)$/
+ del_jobs_status
+ print $1.unpack1("m")
+ jobs_status(worker) if @options[:job_status] == :replace
+ when /^after (.+?)$/
+ @warnings << Marshal.load($1.unpack1("m"))
+ when /^bye (.+?)$/
+ after_worker_down worker, Marshal.load($1.unpack1("m"))
+ when /^bye$/, nil
+ if shutting_down || worker.quit_called
+ after_worker_quit worker
+ else
+ after_worker_down worker
+ end
+ else
+ print "unknown command: #{cmd.dump}\n"
+ end
+ return false
+ end
+
+ def _run_parallel suites, type, result
+ @records = {}
+
+ if @options[:parallel] < 1
+ warn "Error: parameter of -j option should be greater than 0."
+ return
+ end
+
+ # Require needed thing for parallel running
+ require 'timeout'
+ @tasks = @order.group(@order.sort_by_string(@files)) # Array of filenames.
+
+ @need_quit = false
+ @dead_workers = [] # Array of dead workers.
+ @warnings = []
+ @total_tests = @tasks.size.to_s(10)
+ rep = [] # FIXME: more good naming
+
+ @workers = [] # Array of workers.
+ @workers_hash = {} # out-IO => worker
+ @ios = [] # Array of worker IOs
+ @job_tokens = String.new(encoding: Encoding::ASCII_8BIT) if @jobserver
+ begin
+ [@tasks.size, @options[:parallel]].min.times {launch_worker}
+
+ while true
+ timeout = [(@workers.filter_map {|w| w.response_at}.min&.-(Time.now) || 0) + @worker_timeout, 1].max
+
+ if !(_io = IO.select(@ios, nil, nil, timeout))
+ timeout = Time.now - @worker_timeout
+ quit_workers {|w| w.response_at < timeout}&.map {|w|
+ rep << {file: w.real_file, result: nil, testcase: w.current[0], error: w.current}
+ }
+ elsif _io.first.any? {|io|
+ @need_quit or
+ (deal(io, type, result, rep).nil? and
+ !@workers.any? {|x| [:running, :prepare].include? x.status})
+ }
+ break
+ end
+ break if @tasks.empty? and @workers.empty?
+ if @jobserver and @job_tokens and !@tasks.empty? and
+ ((newjobs = [@tasks.size, @options[:parallel]].min) > @workers.size or
+ !@workers.any? {|x| x.status == :ready})
+ t = @jobserver[0].read_nonblock(newjobs, exception: false)
+ if String === t
+ @job_tokens << t
+ t.size.times {launch_worker}
+ end
+ end
+ end
+ rescue Interrupt => ex
+ @interrupt = ex
+ return result
+ ensure
+ if file = @options[:timetable_data]
+ open(file, 'w'){|f|
+ @records.each{|(worker, suite), (st, ed)|
+ f.puts '[' + [worker.dump, suite.dump, st.to_f * 1_000, ed.to_f * 1_000].join(", ") + '],'
+ }
+ }
+ end
+
+ if @interrupt
+ @ios.select!{|x| @workers_hash[x].status == :running }
+ while !@ios.empty? && (__io = IO.select(@ios,[],[],10))
+ __io[0].reject! {|io| deal(io, type, result, rep, true)}
+ end
+ end
+
+ quit_workers
+ flush_job_tokens
+
+ unless @interrupt || !@options[:retry] || @need_quit
+ parallel = @options[:parallel]
+ @options[:parallel] = false
+ suites, rep = rep.partition {|r|
+ r[:testcase] && r[:file] &&
+ (!r.key?(:report) || r[:report].any? {|e| !e[2].is_a?(Test::Unit::PendedError)})
+ }
+ suites.map {|r| File.realpath(r[:file])}.uniq.each {|file| require file}
+ del_status_line or puts
+ error, suites = suites.partition {|r| r[:error]}
+ unless suites.empty?
+ puts "\n""Retrying..."
+ @verbose = options[:verbose]
+ suites.map! {|r| ::Object.const_get(r[:testcase])}
+ _run_suites(suites, type)
+ end
+ unless error.empty?
+ puts "\n""Retrying hung up testcases..."
+ error.map! {|r| ::Object.const_get(r[:testcase])}
+ verbose = @verbose
+ job_status = options[:job_status]
+ options[:verbose] = @verbose = true
+ options[:job_status] = :normal
+ result.concat _run_suites(error, type)
+ options[:verbose] = @verbose = verbose
+ options[:job_status] = job_status
+ end
+ @options[:parallel] = parallel
+ end
+ unless @options[:retry]
+ del_status_line or puts
+ end
+ unless rep.empty?
+ rep.each do |r|
+ if r[:error]
+ puke(*r[:error], Timeout::Error)
+ next
+ end
+ r[:report]&.each do |f|
+ puke(*f) if f
+ end
+ end
+ if @options[:retry]
+ rep.each do |x|
+ (e, f, s = x[:result]) or next
+ @errors += e
+ @failures += f
+ @skips += s
+ end
+ end
+ end
+ unless @warnings.empty?
+ warn ""
+ @warnings.uniq! {|w| w[1].message}
+ @warnings.each do |w|
+ warn "#{w[0]}: #{w[1].message} (#{w[1].class})"
+ end
+ warn ""
+ end
+ end
+ end
+
+ def _run_suites suites, type
+ _prepare_run(suites, type)
+ @interrupt = nil
+ result = []
+ GC.start
+ if @options[:parallel]
+ _run_parallel suites, type, result
+ else
+ suites.each {|suite|
+ begin
+ result << _run_suite(suite, type)
+ rescue Interrupt => e
+ @interrupt = e
+ break
+ end
+ }
+ end
+ del_status_line
+ result
+ end
+ end
+
+ module Skipping # :nodoc: all
+ def failed(s)
+ super if !s or @options[:hide_skip]
+ end
+
+ private
+ def setup_options(opts, options)
+ super
+
+ opts.separator "skipping options:"
+
+ options[:hide_skip] = true
+
+ opts.on '-q', '--hide-skip', 'Hide skipped tests' do
+ options[:hide_skip] = true
+ end
+
+ opts.on '--show-skip', 'Show skipped tests' do
+ options[:hide_skip] = false
+ end
+ end
+
+ def _run_suites(suites, type)
+ result = super
+ report.reject!{|r| r.start_with? "Skipped:" } if @options[:hide_skip]
+ report.sort_by!{|r| r.start_with?("Skipped:") ? 0 : \
+ (r.start_with?("Failure:") ? 1 : 2) }
+ failed(nil)
+ result
+ end
+ end
+
+ module Statistics
+ def update_list(list, rec, max)
+ if i = list.empty? ? 0 : list.bsearch_index {|*a| yield(*a)}
+ list[i, 0] = [rec]
+ list[max..-1] = [] if list.size >= max
+ end
+ end
+
+ def record(suite, method, assertions, time, error)
+ if @options.values_at(:longest, :most_asserted).any?
+ @tops ||= {}
+ rec = [suite.name, method, assertions, time, error]
+ if max = @options[:longest]
+ update_list(@tops[:longest] ||= [], rec, max) {|_,_,_,t,_|t<time}
+ end
+ if max = @options[:most_asserted]
+ update_list(@tops[:most_asserted] ||= [], rec, max) {|_,_,a,_,_|a<assertions}
+ end
+ end
+ # (((@record ||= {})[suite] ||= {})[method]) = [assertions, time, error]
+ super
+ end
+
+ def run(*args)
+ result = super
+ if @tops ||= nil
+ @tops.each do |t, list|
+ if list
+ puts "#{t.to_s.tr('_', ' ')} tests:"
+ list.each {|suite, method, assertions, time, error|
+ printf "%5.2fsec(%d): %s#%s\n", time, assertions, suite, method
+ }
+ end
+ end
+ end
+ result
+ end
+
+ private
+ def setup_options(opts, options)
+ super
+ opts.separator "statistics options:"
+ opts.on '--longest=N', Integer, 'Show longest N tests' do |n|
+ options[:longest] = n
+ end
+ opts.on '--most-asserted=N', Integer, 'Show most asserted N tests' do |n|
+ options[:most_asserted] = n
+ end
+ end
+ end
+
+ module StatusLine # :nodoc: all
+ def terminal_width
+ unless @terminal_width ||= nil
+ begin
+ require 'io/console'
+ width = $stdout.winsize[1]
+ rescue LoadError, NoMethodError, Errno::ENOTTY, Errno::EBADF, Errno::EINVAL
+ width = ENV["COLUMNS"].to_i.nonzero? || 80
+ end
+ width -= 1 if /mswin|mingw/ =~ RUBY_PLATFORM
+ @terminal_width = width
+ end
+ @terminal_width
+ end
+
+ def del_status_line(flush = true)
+ @status_line_size ||= 0
+ if @options[:job_status] == :replace
+ $stdout.print "\r"+" "*@status_line_size+"\r"
+ else
+ $stdout.puts if @status_line_size > 0
+ end
+ $stdout.flush if flush
+ @status_line_size = 0
+ end
+
+ def add_status(line)
+ @status_line_size ||= 0
+ if @options[:job_status] == :replace
+ line = line[0...(terminal_width-@status_line_size)]
+ end
+ print line
+ @status_line_size += line.size
+ end
+
+ def jobs_status(worker)
+ return if !@options[:job_status] or @verbose
+ if @options[:job_status] == :replace
+ status_line = @workers.map(&:to_s).join(" ")
+ else
+ status_line = worker.to_s
+ end
+ update_status(status_line) or (puts; nil)
+ end
+
+ def del_jobs_status
+ return unless @options[:job_status] == :replace && @status_line_size.nonzero?
+ del_status_line
+ end
+
+ def output
+ (@output ||= nil) || super
+ end
+
+ def _prepare_run(suites, type)
+ options[:job_status] ||= :replace if @tty && !@verbose
+ case options[:color]
+ when :always
+ color = true
+ when :auto, nil
+ color = true if @tty || @options[:job_status] == :replace
+ else
+ color = false
+ end
+ @colorize = Colorize.new(color, colors_file: File.join(__dir__, "../../colors"))
+ if color or @options[:job_status] == :replace
+ @verbose = !options[:parallel]
+ end
+ @output = Output.new(self) unless @options[:testing]
+ filter = options[:filter]
+ type = "#{type}_methods"
+ total = if filter
+ suites.inject(0) {|n, suite| n + suite.send(type).grep(filter).size}
+ else
+ suites.inject(0) {|n, suite| n + suite.send(type).size}
+ end
+ @test_count = 0
+ @total_tests = total.to_s(10)
+ end
+
+ def new_test(s)
+ @test_count += 1
+ update_status(s)
+ end
+
+ def update_status(s)
+ count = @test_count.to_s(10).rjust(@total_tests.size)
+ del_status_line(false)
+ add_status(@colorize.pass("[#{count}/#{@total_tests}]"))
+ add_status(" #{s}")
+ $stdout.print "\r" if @options[:job_status] == :replace and !@verbose
+ $stdout.flush
+ end
+
+ def _print(s); $stdout.print(s); end
+ def succeed; del_status_line; end
+
+ def failed(s)
+ return if s and @options[:job_status] != :replace
+ sep = "\n"
+ @report_count ||= 0
+ report.each do |msg|
+ if msg.start_with? "Skipped:"
+ if @options[:hide_skip]
+ del_status_line
+ next
+ end
+ color = :skip
+ else
+ color = :fail
+ end
+ first, msg = msg.split(/$/, 2)
+ first = sprintf("%3d) %s", @report_count += 1, first)
+ $stdout.print(sep, @colorize.decorate(first, color), msg, "\n")
+ sep = nil
+ end
+ report.clear
+ end
+
+ def initialize
+ super
+ @tty = $stdout.tty?
+ end
+
+ def run(*args)
+ result = super
+ puts "\nruby -v: #{RUBY_DESCRIPTION}"
+ result
+ end
+
+ private
+ def setup_options(opts, options)
+ super
+
+ opts.separator "status line options:"
+
+ options[:job_status] = nil
+
+ opts.on '--jobs-status [TYPE]', [:normal, :replace, :none],
+ "Show status of jobs every file; Disabled when --jobs isn't specified." do |type|
+ options[:job_status] = (type || :normal if type != :none)
+ end
+
+ opts.on '--color[=WHEN]',
+ [:always, :never, :auto],
+ "colorize the output. WHEN defaults to 'always'", "or can be 'never' or 'auto'." do |c|
+ options[:color] = c || :always
+ end
+
+ opts.on '--tty[=WHEN]',
+ [:yes, :no],
+ "force to output tty control. WHEN defaults to 'yes'", "or can be 'no'." do |c|
+ @tty = c != :no
+ end
+ end
+
+ class Output < Struct.new(:runner) # :nodoc: all
+ def puts(*a) $stdout.puts(*a) unless a.empty? end
+ def respond_to_missing?(*a) $stdout.respond_to?(*a) end
+ def method_missing(*a, &b) $stdout.__send__(*a, &b) end
+
+ def print(s)
+ case s
+ when /\A(.*\#.*) = \z/
+ runner.new_test($1)
+ when /\A(.* s) = \z/
+ runner.add_status(" = #$1")
+ when /\A\.+\z/
+ runner.succeed
+ when /\A\.*[EFS][EFS.]*\z/
+ runner.failed(s)
+ else
+ $stdout.print(s)
+ end
+ end
+ end
+ end
+
+ module LoadPathOption # :nodoc: all
+ def non_options(files, options)
+ begin
+ require "rbconfig"
+ rescue LoadError
+ warn "#{caller(1, 1)[0]}: warning: Parallel running disabled because can't get path to ruby; run specify with --ruby argument"
+ options[:parallel] = nil
+ else
+ options[:ruby] ||= [RbConfig.ruby]
+ end
+
+ super
+ end
+
+ def setup_options(parser, options)
+ super
+ parser.separator "load path options:"
+ parser.on '-Idirectory', 'Add library load path' do |dirs|
+ dirs.split(':').each { |d| $LOAD_PATH.unshift d }
+ end
+ end
+ end
+
+ module GlobOption # :nodoc: all
+ @@testfile_prefix = "test"
+ @@testfile_suffix = "test"
+
+ def setup_options(parser, options)
+ super
+ parser.separator "globbing options:"
+ parser.on '-B', '--base-directory DIR', 'Base directory to glob.' do |dir|
+ raise OptionParser::InvalidArgument, "not a directory: #{dir}" unless File.directory?(dir)
+ options[:base_directory] = dir
+ end
+ parser.on '-x', '--exclude REGEXP', 'Exclude test files on pattern.' do |pattern|
+ (options[:reject] ||= []) << pattern
+ end
+ end
+
+ def complement_test_name f, orig_f
+ basename = File.basename(f)
+
+ if /\.rb\z/ !~ basename
+ return File.join(File.dirname(f), basename+'.rb')
+ elsif /\Atest_/ !~ basename
+ return File.join(File.dirname(f), 'test_'+basename)
+ end if f.end_with?(basename) # otherwise basename is dirname/
+
+ raise ArgumentError, "file not found: #{orig_f}"
+ end
+
+ def non_options(files, options)
+ paths = [options.delete(:base_directory), nil].uniq
+ if reject = options.delete(:reject)
+ reject_pat = Regexp.union(reject.map {|r| %r"#{r}"})
+ end
+ files.map! {|f|
+ f = f.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
+ orig_f = f
+ while true
+ ret = ((paths if /\A\.\.?(?:\z|\/)/ !~ f) || [nil]).any? do |prefix|
+ if prefix
+ path = f.empty? ? prefix : "#{prefix}/#{f}"
+ else
+ next if f.empty?
+ path = f
+ end
+ if f.end_with?(File::SEPARATOR) or !f.include?(File::SEPARATOR) or File.directory?(path)
+ match = (Dir["#{path}/**/#{@@testfile_prefix}_*.rb"] + Dir["#{path}/**/*_#{@@testfile_suffix}.rb"]).uniq
+ else
+ match = Dir[path]
+ end
+ if !match.empty?
+ if reject
+ match.reject! {|n|
+ n = n[(prefix.length+1)..-1] if prefix
+ reject_pat =~ n
+ }
+ end
+ break match
+ elsif !reject or reject_pat !~ f and File.exist? path
+ break path
+ end
+ end
+ if !ret
+ f = complement_test_name(f, orig_f)
+ else
+ break ret
+ end
+ end
+ }
+ files.flatten!
+ super(files, options)
+ end
+ end
+
+ module GCOption # :nodoc: all
+ def setup_options(parser, options)
+ super
+ parser.separator "GC options:"
+ parser.on '--[no-]gc-stress', 'Set GC.stress as true' do |flag|
+ options[:gc_stress] = flag
+ end
+ parser.on '--[no-]gc-compact', 'GC.compact every time' do |flag|
+ options[:gc_compact] = flag
+ end
+ end
+
+ def non_options(files, options)
+ if options.delete(:gc_stress)
+ Test::Unit::TestCase.class_eval do
+ oldrun = instance_method(:run)
+ define_method(:run) do |runner|
+ begin
+ gc_stress, GC.stress = GC.stress, true
+ oldrun.bind_call(self, runner)
+ ensure
+ GC.stress = gc_stress
+ end
+ end
+ end
+ end
+ if options.delete(:gc_compact)
+ Test::Unit::TestCase.class_eval do
+ oldrun = instance_method(:run)
+ define_method(:run) do |runner|
+ begin
+ oldrun.bind_call(self, runner)
+ ensure
+ GC.compact
+ end
+ end
+ end
+ end
+ super
+ end
+ end
+
+ module RequireFiles # :nodoc: all
+ def non_options(files, options)
+ return false if !super
+ errors = {}
+ result = false
+ files.each {|f|
+ d = File.dirname(path = File.realpath(f))
+ unless $:.include? d
+ $: << d
+ end
+ begin
+ require path unless options[:parallel]
+ result = true
+ rescue LoadError
+ next if errors[$!.message]
+ errors[$!.message] = true
+ puts "#{f}: #{$!}"
+ end
+ }
+ result
+ end
+ end
+
+ module RepeatOption # :nodoc: all
+ def setup_options(parser, options)
+ super
+ options[:repeat_count] = nil
+ parser.separator "repeat options:"
+ parser.on '--repeat-count=NUM', "Number of times to repeat", Integer do |n|
+ options[:repeat_count] = n
+ end
+ end
+
+ def _run_anything(type)
+ @repeat_count = @options[:repeat_count]
+ super
+ end
+ end
+
+ module ExcludesOption # :nodoc: all
+ class ExcludedMethods < Struct.new(:excludes)
+ def exclude(name, reason)
+ excludes[name] = reason
+ end
+
+ def exclude_from(klass)
+ excludes = self.excludes
+ pattern = excludes.keys.grep(Regexp).tap {|k|
+ break (Regexp.new(k.join('|')) unless k.empty?)
+ }
+ klass.class_eval do
+ public_instance_methods(false).each do |method|
+ if excludes[method] or (pattern and pattern =~ method)
+ remove_method(method)
+ end
+ end
+ public_instance_methods(true).each do |method|
+ if excludes[method] or (pattern and pattern =~ method)
+ undef_method(method)
+ end
+ end
+ end
+ end
+
+ def self.load(dirs, name)
+ return unless dirs and name
+ instance = nil
+ dirs.each do |dir|
+ path = File.join(dir, name.gsub(/::/, '/') + ".rb")
+ begin
+ src = File.read(path)
+ rescue Errno::ENOENT
+ nil
+ else
+ instance ||= new({})
+ instance.instance_eval(src, path)
+ end
+ end
+ instance
+ end
+ end
+
+ def setup_options(parser, options)
+ super
+ if excludes = ENV["EXCLUDES"]
+ excludes = excludes.split(File::PATH_SEPARATOR)
+ end
+ options[:excludes] = excludes || []
+ parser.separator "excludes options:"
+ parser.on '-X', '--excludes-dir DIRECTORY', "Directory name of exclude files" do |d|
+ options[:excludes].concat d.split(File::PATH_SEPARATOR)
+ end
+ end
+
+ def _run_suite(suite, type)
+ if ex = ExcludedMethods.load(@options[:excludes], suite.name)
+ ex.exclude_from(suite)
+ end
+ super
+ end
+ end
+
+ module TimeoutOption
+ def setup_options(parser, options)
+ super
+ parser.separator "timeout options:"
+ parser.on '--timeout-scale NUM', '--subprocess-timeout-scale NUM', "Scale timeout", Float do |scale|
+ raise OptionParser::InvalidArgument, "timeout scale must be positive" unless scale > 0
+ options[:timeout_scale] = scale
+ end
+ end
+
+ def non_options(files, options)
+ if scale = options[:timeout_scale] or
+ (scale = ENV["RUBY_TEST_TIMEOUT_SCALE"] || ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"] and
+ (scale = scale.to_f) > 0)
+ EnvUtil.timeout_scale = scale
+ end
+ super
+ end
+ end
+
+ class Runner # :nodoc: all
+
+ attr_accessor :report, :failures, :errors, :skips # :nodoc:
+ attr_accessor :assertion_count # :nodoc:
+ attr_writer :test_count # :nodoc:
+ attr_accessor :start_time # :nodoc:
+ attr_accessor :help # :nodoc:
+ attr_accessor :verbose # :nodoc:
+ attr_writer :options # :nodoc:
+
+ ##
+ # :attr:
+ #
+ # if true, installs an "INFO" signal handler (only available to BSD and
+ # OS X users) which prints diagnostic information about the test run.
+ #
+ # This is auto-detected by default but may be overridden by custom
+ # runners.
+
+ attr_accessor :info_signal
+
+ ##
+ # Lazy accessor for options.
+
+ def options
+ @options ||= {seed: 42}
+ end
+
+ @@installed_at_exit ||= false
+ @@out = $stdout
+ @@after_tests = []
+ @@current_repeat_count = 0
+
+ ##
+ # A simple hook allowing you to run a block of code after _all_ of
+ # the tests are done. Eg:
+ #
+ # Test::Unit::Runner.after_tests { p $debugging_info }
+
+ def self.after_tests &block
+ @@after_tests << block
+ end
+
+ ##
+ # Returns the stream to use for output.
+
+ def self.output
+ @@out
+ end
+
+ ##
+ # Sets Test::Unit::Runner to write output to +stream+. $stdout is the default
+ # output
+
+ def self.output= stream
+ @@out = stream
+ end
+
+ ##
+ # Tells Test::Unit::Runner to delegate to +runner+, an instance of a
+ # Test::Unit::Runner subclass, when Test::Unit::Runner#run is called.
+
+ def self.runner= runner
+ @@runner = runner
+ end
+
+ ##
+ # Returns the Test::Unit::Runner subclass instance that will be used
+ # to run the tests. A Test::Unit::Runner instance is the default
+ # runner.
+
+ def self.runner
+ @@runner ||= self.new
+ end
+
+ ##
+ # Return all plugins' run methods (methods that start with "run_").
+
+ def self.plugins
+ @@plugins ||= (["run_tests"] +
+ public_instance_methods(false).
+ grep(/^run_/).map { |s| s.to_s }).uniq
+ end
+
+ ##
+ # Return the IO for output.
+
+ def output
+ self.class.output
+ end
+
+ def puts *a # :nodoc:
+ output.puts(*a)
+ end
+
+ def print *a # :nodoc:
+ output.print(*a)
+ end
+
+ def test_count # :nodoc:
+ @test_count ||= 0
+ end
+
+ ##
+ # Runner for a given +type+ (eg, test vs bench).
+
+ def self.current_repeat_count
+ @@current_repeat_count
+ end
+
+ def _run_anything type
+ suites = Test::Unit::TestCase.send "#{type}_suites"
+ return if suites.empty?
+
+ suites = @order.sort_by_name(suites)
+
+ puts
+ puts "# Running #{type}s:"
+ puts
+
+ @test_count, @assertion_count = 0, 0
+ test_count = assertion_count = 0
+ sync = output.respond_to? :"sync=" # stupid emacs
+ old_sync, output.sync = output.sync, true if sync
+
+ @@current_repeat_count = 0
+ begin
+ start = Time.now
+
+ results = _run_suites suites, type
+
+ @test_count = results.inject(0) { |sum, (tc, _)| sum + tc }
+ @assertion_count = results.inject(0) { |sum, (_, ac)| sum + ac }
+ test_count += @test_count
+ assertion_count += @assertion_count
+ t = Time.now - start
+ @@current_repeat_count += 1
+ unless @repeat_count
+ puts
+ puts
+ end
+ puts "Finished%s %ss in %.6fs, %.4f tests/s, %.4f assertions/s.\n" %
+ [(@repeat_count ? "(#{@@current_repeat_count}/#{@repeat_count}) " : ""), type,
+ t, @test_count.fdiv(t), @assertion_count.fdiv(t)]
+ end while @repeat_count && @@current_repeat_count < @repeat_count &&
+ report.empty? && failures.zero? && errors.zero?
+
+ output.sync = old_sync if sync
+
+ report.each_with_index do |msg, i|
+ puts "\n%3d) %s" % [i + 1, msg]
+ end
+
+ puts
+ @test_count = test_count
+ @assertion_count = assertion_count
+
+ status
+ end
+
+ ##
+ # Run a single +suite+ for a given +type+.
+
+ def _run_suite suite, type
+ header = "#{type}_suite_header"
+ puts send(header, suite) if respond_to? header
+
+ filter = options[:filter]
+
+ all_test_methods = suite.send "#{type}_methods"
+ if filter
+ all_test_methods.select! {|method|
+ filter === "#{suite}##{method}"
+ }
+ end
+ all_test_methods = @order.sort_by_name(all_test_methods)
+
+ leakchecker = LeakChecker.new
+ if ENV["LEAK_CHECKER_TRACE_OBJECT_ALLOCATION"]
+ require "objspace"
+ trace = true
+ end
+
+ assertions = all_test_methods.map { |method|
+
+ inst = suite.new method
+ _start_method(inst)
+ inst._assertions = 0
+
+ print "#{suite}##{method} = " if @verbose
+
+ start_time = Time.now if @verbose
+ result =
+ if trace
+ ObjectSpace.trace_object_allocations {inst.run self}
+ else
+ inst.run self
+ end
+
+ print "%.2f s = " % (Time.now - start_time) if @verbose
+ print result
+ puts if @verbose
+ $stdout.flush
+
+ unless defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # compiler process is wrongly considered as leak
+ leakchecker.check("#{inst.class}\##{inst.__name__}")
+ end
+
+ _end_method(inst)
+
+ inst._assertions
+ }
+ return assertions.size, assertions.inject(0) { |sum, n| sum + n }
+ end
+
+ def _start_method(inst)
+ end
+ def _end_method(inst)
+ end
+
+ ##
+ # Record the result of a single test. Makes it very easy to gather
+ # information. Eg:
+ #
+ # class StatisticsRecorder < Test::Unit::Runner
+ # def record suite, method, assertions, time, error
+ # # ... record the results somewhere ...
+ # end
+ # end
+ #
+ # Test::Unit::Runner.runner = StatisticsRecorder.new
+ #
+ # NOTE: record might be sent more than once per test. It will be
+ # sent once with the results from the test itself. If there is a
+ # failure or error in teardown, it will be sent again with the
+ # error or failure.
+
+ def record suite, method, assertions, time, error
+ end
+
+ def location e # :nodoc:
+ last_before_assertion = ""
+
+ return '<empty>' unless e.backtrace # SystemStackError can return nil.
+
+ e.backtrace.reverse_each do |s|
+ break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
+ last_before_assertion = s
+ end
+ last_before_assertion.sub(/:in .*$/, '')
+ end
+
+ ##
+ # Writes status for failed test +meth+ in +klass+ which finished with
+ # exception +e+
+
+ def initialize # :nodoc:
+ @report = []
+ @errors = @failures = @skips = 0
+ @verbose = false
+ @mutex = Thread::Mutex.new
+ @info_signal = Signal.list['INFO']
+ @repeat_count = nil
+ end
+
+ def synchronize # :nodoc:
+ if @mutex then
+ @mutex.synchronize { yield }
+ else
+ yield
+ end
+ end
+
+ def inspect
+ "#<#{self.class.name}: " <<
+ instance_variables.filter_map do |var|
+ next if var == :@option_parser # too big
+ "#{var}=#{instance_variable_get(var).inspect}"
+ end.join(", ") << ">"
+ end
+
+ ##
+ # Top level driver, controls all output and filtering.
+
+ def _run args = []
+ args = process_args args # ARGH!! blame test/unit process_args
+ self.options.merge! args
+
+ puts "Run options: #{help}"
+
+ self.class.plugins.each do |plugin|
+ send plugin
+ break unless report.empty?
+ end
+
+ return failures + errors if self.test_count > 0 # or return nil...
+ rescue Interrupt
+ abort 'Interrupted'
+ end
+
+ ##
+ # Runs test suites matching +filter+.
+
+ def run_tests
+ _run_anything :test
+ end
+
+ ##
+ # Writes status to +io+
+
+ def status io = self.output
+ format = "%d tests, %d assertions, %d failures, %d errors, %d skips"
+ io.puts format % [test_count, assertion_count, failures, errors, skips]
+ end
+
+ prepend Test::Unit::Options
+ prepend Test::Unit::StatusLine
+ prepend Test::Unit::Parallel
+ prepend Test::Unit::Statistics
+ prepend Test::Unit::Skipping
+ prepend Test::Unit::GlobOption
+ prepend Test::Unit::RepeatOption
+ prepend Test::Unit::LoadPathOption
+ prepend Test::Unit::GCOption
+ prepend Test::Unit::ExcludesOption
+ prepend Test::Unit::TimeoutOption
+ prepend Test::Unit::RunCount
+
+ ##
+ # Begins the full test run. Delegates to +runner+'s #_run method.
+
+ def run(argv = [])
+ self.class.runner._run(argv)
+ rescue NoMemoryError
+ system("cat /proc/meminfo") if File.exist?("/proc/meminfo")
+ system("ps x -opid,args,%cpu,%mem,nlwp,rss,vsz,wchan,stat,start,time,etime,blocked,caught,ignored,pending,f") if File.exist?("/bin/ps")
+ raise
+ end
+
+ @@stop_auto_run = false
+ def self.autorun
+ at_exit {
+ Test::Unit::RunCount.run_once {
+ exit(Test::Unit::Runner.new.run(ARGV) || true)
+ } unless @@stop_auto_run
+ } unless @@installed_at_exit
+ @@installed_at_exit = true
+ end
+
+ alias orig_run_suite _run_suite
+
+ # Overriding of Test::Unit::Runner#puke
+ def puke klass, meth, e
+ n = report.size
+ e = case e
+ when Test::Unit::PendedError then
+ @skips += 1
+ return "S" unless @verbose
+ "Skipped:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n"
+ when Test::Unit::AssertionFailedError then
+ @failures += 1
+ "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n"
+ else
+ @errors += 1
+ bt = Test::filter_backtrace(e.backtrace).join "\n "
+ "Error:\n#{klass}##{meth}:\n#{e.class}: #{e.message.b}\n #{bt}\n"
+ end
+ @report << e
+ rep = e[0, 1]
+ if Test::Unit::PendedError === e and /no message given\z/ =~ e.message
+ report.slice!(n..-1)
+ rep = "."
+ end
+ rep
+ end
+ end
+
+ class AutoRunner # :nodoc: all
+ class Runner < Test::Unit::Runner
+ include Test::Unit::RequireFiles
+ end
+
+ attr_accessor :to_run, :options
+
+ def initialize(force_standalone = false, default_dir = nil, argv = ARGV)
+ @force_standalone = force_standalone
+ @runner = Runner.new do |files, options|
+ base = options[:base_directory] ||= default_dir
+ files << default_dir if files.empty? and default_dir
+ @to_run = files
+ yield self if block_given?
+ $LOAD_PATH.unshift base if base
+ files
+ end
+ Runner.runner = @runner
+ @options = @runner.option_parser
+ if @force_standalone
+ @options.banner.sub!(/\[options\]/, '\& tests...')
+ end
+ @argv = argv
+ end
+
+ def process_args(*args)
+ @runner.process_args(*args)
+ !@to_run.empty?
+ end
+
+ def run
+ if @force_standalone and not process_args(@argv)
+ abort @options.banner
+ end
+ @runner.run(@argv) || true
+ end
+
+ def self.run(*args)
+ new(*args).run
+ end
+ end
+
+ class ProxyError < StandardError # :nodoc: all
+ def initialize(ex)
+ @message = ex.message
+ @backtrace = ex.backtrace
+ end
+
+ attr_accessor :message, :backtrace
+ end
+ end
+end
+
+Test::Unit::Runner.autorun
diff --git a/tool/lib/test/unit/assertions.rb b/tool/lib/test/unit/assertions.rb
new file mode 100644
index 0000000000..dcf3e6fcb9
--- /dev/null
+++ b/tool/lib/test/unit/assertions.rb
@@ -0,0 +1,839 @@
+# frozen_string_literal: true
+require 'pp'
+
+module Test
+ module Unit
+ module Assertions
+
+ ##
+ # Returns the diff command to use in #diff. Tries to intelligently
+ # figure out what diff to use.
+
+ def self.diff
+ unless defined? @diff
+ exe = RbConfig::CONFIG['EXEEXT']
+ @diff = %W"gdiff#{exe} diff#{exe}".find do |diff|
+ if system(diff, "-u", __FILE__, __FILE__)
+ break "#{diff} -u"
+ end
+ end
+ end
+
+ @diff
+ end
+
+ ##
+ # Set the diff command to use in #diff.
+
+ def self.diff= o
+ @diff = o
+ end
+
+ ##
+ # Returns a diff between +exp+ and +act+. If there is no known
+ # diff command or if it doesn't make sense to diff the output
+ # (single line, short output), then it simply returns a basic
+ # comparison between the two.
+
+ def diff exp, act
+ require "tempfile"
+
+ expect = mu_pp_for_diff exp
+ butwas = mu_pp_for_diff act
+ result = nil
+
+ need_to_diff =
+ self.class.diff &&
+ (expect.include?("\n") ||
+ butwas.include?("\n") ||
+ expect.size > 30 ||
+ butwas.size > 30 ||
+ expect == butwas)
+
+ return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
+ need_to_diff
+
+ tempfile_a = nil
+ tempfile_b = nil
+
+ Tempfile.open("expect") do |a|
+ tempfile_a = a
+ a.puts expect
+ a.flush
+
+ Tempfile.open("butwas") do |b|
+ tempfile_b = b
+ b.puts butwas
+ b.flush
+
+ result = `#{self.class.diff} #{a.path} #{b.path}`
+ result.sub!(/^\-\-\- .+/, "--- expected")
+ result.sub!(/^\+\+\+ .+/, "+++ actual")
+
+ if result.empty? then
+ klass = exp.class
+ result = [
+ "No visible difference in the #{klass}#inspect output.\n",
+ "You should look at the implementation of #== on ",
+ "#{klass} or its members.\n",
+ expect,
+ ].join
+ end
+ end
+ end
+
+ result
+ ensure
+ tempfile_a.close! if tempfile_a
+ tempfile_b.close! if tempfile_b
+ end
+
+ ##
+ # This returns a diff-able human-readable version of +obj+. This
+ # differs from the regular mu_pp because it expands escaped
+ # newlines and makes hex-values generic (like object_ids). This
+ # uses mu_pp to do the first pass and then cleans it up.
+
+ def mu_pp_for_diff obj
+ mu_pp(obj).gsub(/(?<!\\)(?:\\\\)*\K\\n/, "\n").gsub(/:0x[a-fA-F0-9]{4,}/m, ':0xXXXXXX')
+ end
+
+ ##
+ # Fails unless +test+ is a true value.
+
+ def assert test, msg = nil
+ msg ||= "Failed assertion, no message given."
+ self._assertions += 1
+ unless test then
+ msg = msg.call if Proc === msg
+ raise Test::Unit::AssertionFailedError, msg
+ end
+ true
+ end
+
+ ##
+ # Fails unless +obj+ is empty.
+
+ def assert_empty obj, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
+ assert_respond_to obj, :empty?
+ assert obj.empty?, msg
+ end
+
+ ##
+ # For comparing Floats. Fails unless +exp+ and +act+ are within +delta+
+ # of each other.
+ #
+ # assert_in_delta Math::PI, (22.0 / 7.0), 0.01
+
+ def assert_in_delta exp, act, delta = 0.001, msg = nil
+ n = (exp - act).abs
+ msg = message(msg) {
+ "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
+ }
+ assert delta >= n, msg
+ end
+
+ ##
+ # For comparing Floats. Fails unless +exp+ and +act+ have a relative
+ # error less than +epsilon+.
+
+ def assert_in_epsilon a, b, epsilon = 0.001, msg = nil
+ assert_in_delta a, b, [a.abs, b.abs].min * epsilon, msg
+ end
+
+ ##
+ # Fails unless +collection+ includes +obj+.
+
+ def assert_includes collection, obj, msg = nil
+ msg = message(msg) {
+ "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
+ }
+ assert_respond_to collection, :include?
+ assert collection.include?(obj), msg
+ end
+
+ ##
+ # Fails unless +obj+ is an instance of +cls+.
+
+ def assert_instance_of cls, obj, msg = nil
+ msg = message(msg) {
+ "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
+ }
+
+ assert obj.instance_of?(cls), msg
+ end
+
+ ##
+ # Fails unless +obj+ is a kind of +cls+.
+
+ def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of
+ msg = message(msg) {
+ "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
+
+ assert obj.kind_of?(cls), msg
+ end
+
+ ##
+ # Fails unless +matcher+ <tt>=~</tt> +obj+.
+
+ def assert_match matcher, obj, msg = nil
+ msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" }
+ assert_respond_to matcher, :"=~"
+ matcher = Regexp.new Regexp.escape matcher if String === matcher
+ assert matcher =~ obj, msg
+ end
+
+ ##
+ # Fails unless +obj+ is nil
+
+ def assert_nil obj, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
+ assert obj.nil?, msg
+ end
+
+ ##
+ # Fails unless +obj+ is true
+
+ def assert_true obj, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(obj)} to be true" }
+ assert obj == true, msg
+ end
+
+ ##
+ # Fails unless +obj+ is false
+
+ def assert_false obj, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(obj)} to be false" }
+ assert obj == false, msg
+ end
+
+ ##
+ # For testing with binary operators.
+ #
+ # assert_operator 5, :<=, 4
+
+ def assert_operator o1, op, o2 = (predicate = true; nil), msg = nil
+ return assert_predicate o1, op, msg if predicate
+ msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
+ assert o1.__send__(op, o2), msg
+ end
+
+ ##
+ # Fails if stdout or stderr do not output the expected results.
+ # Pass in nil if you don't care about that streams output. Pass in
+ # "" if you require it to be silent. Pass in a regexp if you want
+ # to pattern match.
+ #
+ # NOTE: this uses #capture_io, not #capture_subprocess_io.
+ #
+ # See also: #assert_silent
+
+ def assert_output stdout = nil, stderr = nil
+ out, err = capture_output do
+ yield
+ end
+
+ err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
+ out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
+
+ y = send err_msg, stderr, err, "In stderr" if err_msg
+ x = send out_msg, stdout, out, "In stdout" if out_msg
+
+ (!stdout || x) && (!stderr || y)
+ end
+
+ ##
+ # For testing with predicates.
+ #
+ # assert_predicate str, :empty?
+ #
+ # This is really meant for specs and is front-ended by assert_operator:
+ #
+ # str.must_be :empty?
+
+ def assert_predicate o1, op, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" }
+ assert o1.__send__(op), msg
+ end
+
+ ##
+ # Fails unless +obj+ responds to +meth+.
+
+ def assert_respond_to obj, meth, msg = nil
+ msg = message(msg) {
+ "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
+ }
+ assert obj.respond_to?(meth), msg
+ end
+
+ ##
+ # Fails unless +exp+ and +act+ are #equal?
+
+ def assert_same exp, act, msg = nil
+ msg = message(msg) {
+ data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
+ "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
+ }
+ assert exp.equal?(act), msg
+ end
+
+ ##
+ # Fails if the block outputs anything to stderr or stdout.
+ #
+ # See also: #assert_output
+
+ def assert_silent
+ assert_output "", "" do
+ yield
+ end
+ end
+
+ ##
+ # Fails unless the block throws +sym+
+
+ def assert_throws sym, msg = nil
+ default = "Expected #{mu_pp(sym)} to have been thrown"
+ caught = true
+ catch(sym) do
+ begin
+ yield
+ rescue ThreadError => e # wtf?!? 1.8 + threads == suck
+ default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
+ rescue ArgumentError => e # 1.9 exception
+ default += ", not #{e.message.split(/ /).last}"
+ rescue NameError => e # 1.8 exception
+ default += ", not #{e.name.inspect}"
+ end
+ caught = false
+ end
+
+ assert caught, message(msg) { default }
+ end
+
+ def assert_path_exists(path, msg = nil)
+ msg = message(msg) { "Expected path '#{path}' to exist" }
+ assert File.exist?(path), msg
+ end
+ alias assert_path_exist assert_path_exists
+ alias refute_path_not_exist assert_path_exists
+
+ def refute_path_exists(path, msg = nil)
+ msg = message(msg) { "Expected path '#{path}' to not exist" }
+ refute File.exist?(path), msg
+ end
+ alias refute_path_exist refute_path_exists
+ alias assert_path_not_exist refute_path_exists
+
+ ##
+ # Captures $stdout and $stderr into strings:
+ #
+ # out, err = capture_output do
+ # puts "Some info"
+ # warn "You did a bad thing"
+ # end
+ #
+ # assert_match %r%info%, out
+ # assert_match %r%bad%, err
+
+ def capture_output
+ require 'stringio'
+
+ captured_stdout, captured_stderr = StringIO.new, StringIO.new
+
+ synchronize do
+ orig_stdout, orig_stderr = $stdout, $stderr
+ $stdout, $stderr = captured_stdout, captured_stderr
+
+ begin
+ yield
+ ensure
+ $stdout = orig_stdout
+ $stderr = orig_stderr
+ end
+ end
+
+ return captured_stdout.string, captured_stderr.string
+ end
+
+ def capture_io
+ raise NoMethodError, "use capture_output"
+ end
+
+ ##
+ # Fails with +msg+
+
+ def flunk msg = nil
+ msg ||= "Epic Fail!"
+ assert false, msg
+ end
+
+ ##
+ # used for counting assertions
+
+ def pass msg = nil
+ assert true
+ end
+
+ ##
+ # Fails if +test+ is a true value
+
+ def refute test, msg = nil
+ msg ||= "Failed refutation, no message given"
+ not assert(! test, msg)
+ end
+
+ ##
+ # Fails if +obj+ is empty.
+
+ def refute_empty obj, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" }
+ assert_respond_to obj, :empty?
+ refute obj.empty?, msg
+ end
+
+ ##
+ # Fails if <tt>exp == act</tt>.
+ #
+ # For floats use refute_in_delta.
+
+ def refute_equal exp, act, msg = nil
+ msg = message(msg) {
+ "Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}"
+ }
+ refute exp == act, msg
+ end
+
+ ##
+ # For comparing Floats. Fails if +exp+ is within +delta+ of +act+.
+ #
+ # refute_in_delta Math::PI, (22.0 / 7.0)
+
+ def refute_in_delta exp, act, delta = 0.001, msg = nil
+ n = (exp - act).abs
+ msg = message(msg) {
+ "Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}"
+ }
+ refute delta >= n, msg
+ end
+
+ ##
+ # For comparing Floats. Fails if +exp+ and +act+ have a relative error
+ # less than +epsilon+.
+
+ def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
+ refute_in_delta a, b, a * epsilon, msg
+ end
+
+ ##
+ # Fails if +collection+ includes +obj+.
+
+ def refute_includes collection, obj, msg = nil
+ msg = message(msg) {
+ "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
+ }
+ assert_respond_to collection, :include?
+ refute collection.include?(obj), msg
+ end
+
+ ##
+ # Fails if +obj+ is an instance of +cls+.
+
+ def refute_instance_of cls, obj, msg = nil
+ msg = message(msg) {
+ "Expected #{mu_pp(obj)} to not be an instance of #{cls}"
+ }
+ refute obj.instance_of?(cls), msg
+ end
+
+ ##
+ # Fails if +obj+ is a kind of +cls+.
+
+ def refute_kind_of cls, obj, msg = nil # TODO: merge with instance_of
+ msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" }
+ refute obj.kind_of?(cls), msg
+ end
+
+ ##
+ # Fails if +matcher+ <tt>=~</tt> +obj+.
+
+ def refute_match matcher, obj, msg = nil
+ msg = message(msg) {"Expected #{mu_pp matcher} to not match #{mu_pp obj}"}
+ assert_respond_to matcher, :"=~"
+ matcher = Regexp.new Regexp.escape matcher if String === matcher
+ refute matcher =~ obj, msg
+ end
+
+ ##
+ # Fails if +obj+ is nil.
+
+ def refute_nil obj, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
+ refute obj.nil?, msg
+ end
+
+ ##
+ # Fails if +o1+ is not +op+ +o2+. Eg:
+ #
+ # refute_operator 1, :>, 2 #=> pass
+ # refute_operator 1, :<, 2 #=> fail
+
+ def refute_operator o1, op, o2 = (predicate = true; nil), msg = nil
+ return refute_predicate o1, op, msg if predicate
+ msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}"}
+ refute o1.__send__(op, o2), msg
+ end
+
+ ##
+ # For testing with predicates.
+ #
+ # refute_predicate str, :empty?
+ #
+ # This is really meant for specs and is front-ended by refute_operator:
+ #
+ # str.wont_be :empty?
+
+ def refute_predicate o1, op, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
+ refute o1.__send__(op), msg
+ end
+
+ ##
+ # Fails if +obj+ responds to the message +meth+.
+
+ def refute_respond_to obj, meth, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
+
+ refute obj.respond_to?(meth), msg
+ end
+
+ ##
+ # Fails if +exp+ is the same (by object identity) as +act+.
+
+ def refute_same exp, act, msg = nil
+ msg = message(msg) {
+ data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
+ "Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data
+ }
+ refute exp.equal?(act), msg
+ end
+
+ ##
+ # Skips the current test. Gets listed at the end of the run but
+ # doesn't cause a failure exit code.
+
+ def pend msg = nil, bt = caller
+ msg ||= "Skipped, no message given"
+ @skip = true
+ raise Test::Unit::PendedError, msg, bt
+ end
+ alias omit pend
+
+ # TODO: Removed this and enabled to raise NoMethodError with skip
+ alias skip pend
+ # def skip(msg = nil, bt = caller)
+ # raise NoMethodError, "use omit or pend", caller
+ # end
+
+ ##
+ # Was this testcase skipped? Meant for #teardown.
+
+ def skipped?
+ defined?(@skip) and @skip
+ end
+
+ ##
+ # Takes a block and wraps it with the runner's shared mutex.
+
+ def synchronize
+ Test::Unit::Runner.runner.synchronize do
+ yield
+ end
+ end
+
+ # :call-seq:
+ # assert_block( failure_message = nil )
+ #
+ #Tests the result of the given block. If the block does not return true,
+ #the assertion will fail. The optional +failure_message+ argument is the same as in
+ #Assertions#assert.
+ #
+ # assert_block do
+ # [1, 2, 3].any? { |num| num < 1 }
+ # end
+ def assert_block(*msgs)
+ assert yield, *msgs
+ end
+
+ def assert_raises(*exp, &b)
+ raise NoMethodError, "use assert_raise", caller
+ end
+
+ # :call-seq:
+ # assert_nothing_thrown( failure_message = nil, &block )
+ #
+ #Fails if the given block uses a call to Kernel#throw, and
+ #returns the result of the block otherwise.
+ #
+ #An optional failure message may be provided as the final argument.
+ #
+ # assert_nothing_thrown "Something was thrown!" do
+ # throw :problem?
+ # end
+ def assert_nothing_thrown(msg=nil)
+ begin
+ ret = yield
+ rescue ArgumentError => error
+ raise error if /\Auncaught throw (.+)\z/m !~ error.message
+ msg = message(msg) { "<#{$1}> was thrown when nothing was expected" }
+ flunk(msg)
+ end
+ assert(true, "Expected nothing to be thrown")
+ ret
+ end
+
+ # :call-seq:
+ # assert_equal( expected, actual, failure_message = nil )
+ #
+ #Tests if +expected+ is equal to +actual+.
+ #
+ #An optional failure message may be provided as the final argument.
+ def assert_equal(exp, act, msg = nil)
+ msg = message(msg) {
+ exp_str = mu_pp(exp)
+ act_str = mu_pp(act)
+ exp_comment = ''
+ act_comment = ''
+ if exp_str == act_str
+ if (exp.is_a?(String) && act.is_a?(String)) ||
+ (exp.is_a?(Regexp) && act.is_a?(Regexp))
+ exp_comment = " (#{exp.encoding})"
+ act_comment = " (#{act.encoding})"
+ elsif exp.is_a?(Float) && act.is_a?(Float)
+ exp_str = "%\#.#{Float::DIG+2}g" % exp
+ act_str = "%\#.#{Float::DIG+2}g" % act
+ elsif exp.is_a?(Time) && act.is_a?(Time)
+ if exp.subsec * 1000_000_000 == exp.nsec
+ exp_comment = " (#{exp.nsec}[ns])"
+ else
+ exp_comment = " (subsec=#{exp.subsec})"
+ end
+ if act.subsec * 1000_000_000 == act.nsec
+ act_comment = " (#{act.nsec}[ns])"
+ else
+ act_comment = " (subsec=#{act.subsec})"
+ end
+ elsif exp.class != act.class
+ # a subclass of Range, for example.
+ exp_comment = " (#{exp.class})"
+ act_comment = " (#{act.class})"
+ end
+ elsif !Encoding.compatible?(exp_str, act_str)
+ if exp.is_a?(String) && act.is_a?(String)
+ exp_str = exp.dump
+ act_str = act.dump
+ exp_comment = " (#{exp.encoding})"
+ act_comment = " (#{act.encoding})"
+ else
+ exp_str = exp_str.dump
+ act_str = act_str.dump
+ end
+ end
+ "<#{exp_str}>#{exp_comment} expected but was\n<#{act_str}>#{act_comment}"
+ }
+ assert(exp == act, msg)
+ end
+
+ # :call-seq:
+ # assert_not_nil( expression, failure_message = nil )
+ #
+ #Tests if +expression+ is not nil.
+ #
+ #An optional failure message may be provided as the final argument.
+ def assert_not_nil(exp, msg=nil)
+ msg = message(msg) { "<#{mu_pp(exp)}> expected to not be nil" }
+ assert(!exp.nil?, msg)
+ end
+
+ # :call-seq:
+ # assert_not_equal( expected, actual, failure_message = nil )
+ #
+ #Tests if +expected+ is not equal to +actual+.
+ #
+ #An optional failure message may be provided as the final argument.
+ def assert_not_equal(exp, act, msg=nil)
+ msg = message(msg) { "<#{mu_pp(exp)}> expected to be != to\n<#{mu_pp(act)}>" }
+ assert(exp != act, msg)
+ end
+
+ # :call-seq:
+ # assert_no_match( regexp, string, failure_message = nil )
+ #
+ #Tests if the given Regexp does not match a given String.
+ #
+ #An optional failure message may be provided as the final argument.
+ def assert_no_match(regexp, string, msg=nil)
+ assert_instance_of(Regexp, regexp, "The first argument to assert_no_match should be a Regexp.")
+ self._assertions -= 1
+ msg = message(msg) { "<#{mu_pp(regexp)}> expected to not match\n<#{mu_pp(string)}>" }
+ assert(regexp !~ string, msg)
+ end
+
+ # :call-seq:
+ # assert_not_same( expected, actual, failure_message = nil )
+ #
+ #Tests if +expected+ is not the same object as +actual+.
+ #This test uses Object#equal? to test equality.
+ #
+ #An optional failure message may be provided as the final argument.
+ #
+ # assert_not_same("x", "x") #Succeeds
+ def assert_not_same(expected, actual, message="")
+ msg = message(msg) { build_message(message, <<EOT, expected, expected.__id__, actual, actual.__id__) }
+<?>
+with id <?> expected to not be equal\\? to
+<?>
+with id <?>.
+EOT
+ assert(!actual.equal?(expected), msg)
+ end
+
+ # :call-seq:
+ # assert_send( +send_array+, failure_message = nil )
+ #
+ # Passes if the method send returns a true value.
+ #
+ # +send_array+ is composed of:
+ # * A receiver
+ # * A method
+ # * Arguments to the method
+ #
+ # Example:
+ # assert_send(["Hello world", :include?, "Hello"]) # -> pass
+ # assert_send(["Hello world", :include?, "Goodbye"]) # -> fail
+ def assert_send send_ary, m = nil
+ recv, msg, *args = send_ary
+ m = message(m) {
+ if args.empty?
+ argsstr = ""
+ else
+ (argsstr = mu_pp(args)).sub!(/\A\[(.*)\]\z/m, '(\1)')
+ end
+ "Expected #{mu_pp(recv)}.#{msg}#{argsstr} to return true"
+ }
+ assert recv.__send__(msg, *args), m
+ end
+
+ # :call-seq:
+ # assert_not_send( +send_array+, failure_message = nil )
+ #
+ # Passes if the method send doesn't return a true value.
+ #
+ # +send_array+ is composed of:
+ # * A receiver
+ # * A method
+ # * Arguments to the method
+ #
+ # Example:
+ # assert_not_send([[1, 2], :member?, 1]) # -> fail
+ # assert_not_send([[1, 2], :member?, 4]) # -> pass
+ def assert_not_send send_ary, m = nil
+ recv, msg, *args = send_ary
+ m = message(m) {
+ if args.empty?
+ argsstr = ""
+ else
+ (argsstr = mu_pp(args)).sub!(/\A\[(.*)\]\z/m, '(\1)')
+ end
+ "Expected #{mu_pp(recv)}.#{msg}#{argsstr} to return false"
+ }
+ assert !recv.__send__(msg, *args), m
+ end
+
+ ms = instance_methods(true).map {|sym| sym.to_s }
+ ms.grep(/\Arefute_/) do |m|
+ mname = ('assert_not_'.dup << m.to_s[/.*?_(.*)/, 1])
+ alias_method(mname, m) unless ms.include? mname
+ end
+ alias assert_include assert_includes
+ alias assert_not_include assert_not_includes
+
+ def assert_not_all?(obj, m = nil, &blk)
+ failed = []
+ obj.each do |*a, &b|
+ if blk.call(*a, &b)
+ failed << (a.size > 1 ? a : a[0])
+ end
+ end
+ assert(failed.empty?, message(m) {failed.pretty_inspect})
+ end
+
+ def assert_syntax_error(code, error, *args, **opt)
+ prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg|
+ yield if defined?(yield)
+ e = assert_raise(SyntaxError, mesg) do
+ syntax_check(src, fname, line)
+ end
+ assert_match(error, e.message, mesg)
+ e
+ end
+ end
+
+ def assert_no_warning(pat, msg = nil)
+ result = nil
+ stderr = EnvUtil.verbose_warning {
+ EnvUtil.with_default_internal(pat.encoding) {
+ result = yield
+ }
+ }
+ msg = message(msg) {diff pat, stderr}
+ refute(pat === stderr, msg)
+ result
+ end
+
+ # kernel resolution can limit the minimum time we can measure
+ # [ruby-core:81540]
+ MIN_HZ = /mswin|mingw/ =~ RUBY_PLATFORM ? 67 : 100
+ MIN_MEASURABLE = 1.0 / MIN_HZ
+
+ def assert_cpu_usage_low(msg = nil, pct: 0.05, wait: 1.0, stop: nil)
+ require 'benchmark'
+
+ wait = EnvUtil.apply_timeout_scale(wait)
+ if wait < 0.1 # TIME_QUANTUM_USEC in thread_pthread.c
+ warn "test #{msg || 'assert_cpu_usage_low'} too short to be accurate"
+ end
+ tms = Benchmark.measure(msg || '') do
+ if stop
+ th = Thread.start {sleep wait; stop.call}
+ yield
+ th.join
+ else
+ begin
+ Timeout.timeout(wait) {yield}
+ rescue Timeout::Error
+ end
+ end
+ end
+
+ max = pct * tms.real
+ min_measurable = MIN_MEASURABLE
+ min_measurable *= 1.30 # add a little (30%) to account for misc. overheads
+ if max < min_measurable
+ max = min_measurable
+ end
+
+ assert_operator tms.total, :<=, max, msg
+ end
+
+ def assert_is_minus_zero(f)
+ assert(1.0/f == -Float::INFINITY, "#{f} is not -0.0")
+ end
+
+ def build_message(head, template=nil, *arguments) #:nodoc:
+ template &&= template.chomp
+ template.gsub(/\G((?:[^\\]|\\.)*?)(\\)?\?/) { $1 + ($2 ? "?" : mu_pp(arguments.shift)) }
+ end
+ end
+ end
+end
diff --git a/tool/lib/test/unit/parallel.rb b/tool/lib/test/unit/parallel.rb
new file mode 100644
index 0000000000..b3a8957f26
--- /dev/null
+++ b/tool/lib/test/unit/parallel.rb
@@ -0,0 +1,212 @@
+# frozen_string_literal: true
+$LOAD_PATH.unshift "#{__dir__}/../.."
+require_relative '../../test/unit'
+
+require_relative '../../profile_test_all' if ENV.key?('RUBY_TEST_ALL_PROFILE')
+require_relative '../../tracepointchecker'
+require_relative '../../zombie_hunter'
+require_relative '../../iseq_loader_checker'
+require_relative '../../gc_checker'
+
+module Test
+ module Unit
+ class Worker < Runner # :nodoc:
+ class << self
+ undef autorun
+ end
+
+ undef _run_suite
+ undef _run_suites
+ undef run
+
+ def increment_io(orig) # :nodoc:
+ *rest, io = 32.times.inject([orig.dup]){|ios, | ios << ios.last.dup }
+ rest.each(&:close)
+ io
+ end
+
+ def _run_suites(suites, type) # :nodoc:
+ suites.map do |suite|
+ _run_suite(suite, type)
+ end
+ end
+
+ def _start_method(inst)
+ _report "start", Marshal.dump([inst.class.name, inst.__name__])
+ end
+
+ def _run_suite(suite, type) # :nodoc:
+ @partial_report = []
+ orig_testout = Test::Unit::Runner.output
+ i,o = IO.pipe
+
+ Test::Unit::Runner.output = o
+ orig_stdin, orig_stdout = $stdin, $stdout
+
+ th = Thread.new do
+ begin
+ while buf = (self.verbose ? i.gets : i.readpartial(1024))
+ _report "p", buf or break
+ end
+ rescue IOError
+ end
+ end
+
+ e, f, s = @errors, @failures, @skips
+
+ begin
+ result = orig_run_suite(suite, type)
+ rescue Interrupt
+ @need_exit = true
+ result = [nil,nil]
+ end
+
+ Test::Unit::Runner.output = orig_testout
+ $stdin = orig_stdin
+ $stdout = orig_stdout
+
+ o.close
+ begin
+ th.join
+ rescue IOError
+ raise unless /stream closed|closed stream/ =~ $!.message
+ end
+ i.close
+
+ result << @partial_report
+ @partial_report = nil
+ result << [@errors-e,@failures-f,@skips-s]
+ result << ($: - @old_loadpath)
+ result << suite.name
+
+ _report "done", Marshal.dump(result)
+ return result
+ ensure
+ Test::Unit::Runner.output = orig_stdout
+ $stdin = orig_stdin if orig_stdin
+ $stdout = orig_stdout if orig_stdout
+ o.close if o && !o.closed?
+ i.close if i && !i.closed?
+ end
+
+ def run(args = []) # :nodoc:
+ process_args args
+ @@stop_auto_run = true
+ @opts = @options.dup
+ @need_exit = false
+
+ @old_loadpath = []
+ begin
+ begin
+ @stdout = increment_io(STDOUT)
+ @stdin = increment_io(STDIN)
+ rescue
+ exit 2
+ end
+ exit 2 unless @stdout && @stdin
+
+ @stdout.sync = true
+ _report "ready!"
+ while buf = @stdin.gets
+ case buf.chomp
+ when /^loadpath (.+?)$/
+ @old_loadpath = $:.dup
+ $:.push(*Marshal.load($1.unpack1("m").force_encoding("ASCII-8BIT"))).uniq!
+ when /^run (.+?) (.+?)$/
+ _report "okay"
+
+ @options = @opts.dup
+ suites = Test::Unit::TestCase.test_suites
+
+ begin
+ require File.realpath($1)
+ rescue LoadError
+ _report "after", Marshal.dump([$1, ProxyError.new($!)])
+ _report "ready"
+ next
+ end
+ _run_suites Test::Unit::TestCase.test_suites-suites, $2.to_sym
+
+ if @need_exit
+ _report "bye"
+ exit
+ else
+ _report "ready"
+ end
+ when /^quit$/
+ _report "bye"
+ exit
+ end
+ end
+ rescue Exception => e
+ trace = e.backtrace || ['unknown method']
+ err = ["#{trace.shift}: #{e.message} (#{e.class})"] + trace.map{|t| "\t" + t }
+
+ if @stdout
+ _report "bye", Marshal.dump(err.join("\n"))
+ else
+ raise "failed to report a failure due to lack of @stdout"
+ end
+ exit
+ ensure
+ @stdin.close if @stdin
+ @stdout.close if @stdout
+ end
+ end
+
+ def _report(res, *args) # :nodoc:
+ @stdout.write(args.empty? ? "#{res}\n" : "#{res} #{args.pack("m0")}\n")
+ true
+ rescue Errno::EPIPE
+ rescue TypeError => e
+ abort("#{e.inspect} in _report(#{res.inspect}, #{args.inspect})\n#{e.backtrace.join("\n")}")
+ end
+
+ def puke(klass, meth, e) # :nodoc:
+ if e.is_a?(Test::Unit::PendedError)
+ new_e = Test::Unit::PendedError.new(e.message)
+ new_e.set_backtrace(e.backtrace)
+ e = new_e
+ end
+ @partial_report << [klass.name, meth, e.is_a?(Test::Unit::AssertionFailedError) ? e : ProxyError.new(e)]
+ super
+ end
+
+ def record(suite, method, assertions, time, error) # :nodoc:
+ case error
+ when nil
+ when Test::Unit::AssertionFailedError, Test::Unit::PendedError
+ case error.cause
+ when nil, Test::Unit::AssertionFailedError, Test::Unit::PendedError
+ else
+ bt = error.backtrace
+ error = error.class.new(error.message)
+ error.set_backtrace(bt)
+ end
+ else
+ error = ProxyError.new(error)
+ end
+ _report "record", Marshal.dump([suite.name, method, assertions, time, error])
+ super
+ end
+ end
+ end
+end
+
+if $0 == __FILE__
+ module Test
+ module Unit
+ class TestCase # :nodoc: all
+ undef on_parallel_worker?
+ def on_parallel_worker?
+ true
+ end
+ def self.on_parallel_worker?
+ true
+ end
+ end
+ end
+ end
+ require 'rubygems'
+ Test::Unit::Worker.new.run(ARGV)
+end
diff --git a/tool/lib/test/unit/testcase.rb b/tool/lib/test/unit/testcase.rb
new file mode 100644
index 0000000000..44d9ba7fdb
--- /dev/null
+++ b/tool/lib/test/unit/testcase.rb
@@ -0,0 +1,296 @@
+# frozen_string_literal: true
+require_relative 'assertions'
+require_relative '../../core_assertions'
+
+module Test
+ module Unit
+
+ ##
+ # Provides a simple set of guards that you can use in your tests
+ # to skip execution if it is not applicable. These methods are
+ # mixed into TestCase as both instance and class methods so you
+ # can use them inside or outside of the test methods.
+ #
+ # def test_something_for_mri
+ # skip "bug 1234" if jruby?
+ # # ...
+ # end
+ #
+ # if windows? then
+ # # ... lots of test methods ...
+ # end
+
+ module Guard
+
+ ##
+ # Is this running on jruby?
+
+ def jruby? platform = RUBY_PLATFORM
+ "java" == platform
+ end
+
+ ##
+ # Is this running on mri?
+
+ def mri? platform = RUBY_DESCRIPTION
+ /^ruby/ =~ platform
+ end
+
+ ##
+ # Is this running on windows?
+
+ def windows? platform = RUBY_PLATFORM
+ /mswin|mingw/ =~ platform
+ end
+
+ ##
+ # Is this running on mingw?
+
+ def mingw? platform = RUBY_PLATFORM
+ /mingw/ =~ platform
+ end
+
+ end
+
+ ##
+ # Provides before/after hooks for setup and teardown. These are
+ # meant for library writers, NOT for regular test authors. See
+ # #before_setup for an example.
+
+ module LifecycleHooks
+ ##
+ # Runs before every test, after setup. This hook is meant for
+ # libraries to extend Test::Unit. It is not meant to be used by
+ # test developers.
+ #
+ # See #before_setup for an example.
+
+ def after_setup; end
+
+ ##
+ # Runs before every test, before setup. This hook is meant for
+ # libraries to extend Test::Unit. It is not meant to be used by
+ # test developers.
+ #
+ # As a simplistic example:
+ #
+ # module MyTestUnitPlugin
+ # def before_setup
+ # super
+ # # ... stuff to do before setup is run
+ # end
+ #
+ # def after_setup
+ # # ... stuff to do after setup is run
+ # super
+ # end
+ #
+ # def before_teardown
+ # super
+ # # ... stuff to do before teardown is run
+ # end
+ #
+ # def after_teardown
+ # # ... stuff to do after teardown is run
+ # super
+ # end
+ # end
+ #
+ # class Test::Unit::Runner::TestCase
+ # include MyTestUnitPlugin
+ # end
+
+ def before_setup; end
+
+ ##
+ # Runs after every test, before teardown. This hook is meant for
+ # libraries to extend Test::Unit. It is not meant to be used by
+ # test developers.
+ #
+ # See #before_setup for an example.
+
+ def before_teardown; end
+
+ ##
+ # Runs after every test, after teardown. This hook is meant for
+ # libraries to extend Test::Unit. It is not meant to be used by
+ # test developers.
+ #
+ # See #before_setup for an example.
+
+ def after_teardown; end
+ end
+
+ ##
+ # Subclass TestCase to create your own tests. Typically you'll want a
+ # TestCase subclass per implementation class.
+ #
+ # See <code>Test::Unit::AssertionFailedError</code>s
+
+ class TestCase
+ include Assertions
+ include CoreAssertions
+
+ include LifecycleHooks
+ include Guard
+ extend Guard
+
+ attr_reader :__name__ # :nodoc:
+
+ PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException,
+ Interrupt, SystemExit] # :nodoc:
+
+ ##
+ # Runs the tests reporting the status to +runner+
+
+ def run runner
+ @options = runner.options
+
+ trap "INFO" do
+ runner.report.each_with_index do |msg, i|
+ warn "\n%3d) %s" % [i + 1, msg]
+ end
+ warn ''
+ time = runner.start_time ? Time.now - runner.start_time : 0
+ warn "Current Test: %s#%s %.2fs" % [self.class, self.__name__, time]
+ runner.status $stderr
+ end if runner.info_signal
+
+ start_time = Time.now
+
+ result = ""
+
+ begin
+ @passed = nil
+ self.before_setup
+ self.setup
+ self.after_setup
+ self.run_test self.__name__
+ result = "." unless io?
+ time = Time.now - start_time
+ runner.record self.class, self.__name__, self._assertions, time, nil
+ @passed = true
+ rescue *PASSTHROUGH_EXCEPTIONS
+ raise
+ rescue Exception => e
+ @passed = Test::Unit::PendedError === e
+ time = Time.now - start_time
+ runner.record self.class, self.__name__, self._assertions, time, e
+ result = runner.puke self.class, self.__name__, e
+ ensure
+ %w{ before_teardown teardown after_teardown }.each do |hook|
+ begin
+ self.send hook
+ rescue *PASSTHROUGH_EXCEPTIONS
+ raise
+ rescue Exception => e
+ @passed = false
+ runner.record self.class, self.__name__, self._assertions, time, e
+ result = runner.puke self.class, self.__name__, e
+ end
+ end
+ trap 'INFO', 'DEFAULT' if runner.info_signal
+ end
+ result
+ end
+
+ RUN_TEST_TRACE = "#{__FILE__}:#{__LINE__+3}:in `run_test'".freeze
+ def run_test(name)
+ progname, $0 = $0, "#{$0}: #{self.class}##{name}"
+ self.__send__(name)
+ ensure
+ $@.delete(RUN_TEST_TRACE) if $@
+ $0 = progname
+ end
+
+ def initialize name # :nodoc:
+ @__name__ = name
+ @__io__ = nil
+ @passed = nil
+ @@current = self # FIX: make thread local
+ end
+
+ def self.current # :nodoc:
+ @@current # FIX: make thread local
+ end
+
+ ##
+ # Return the output IO object
+
+ def io
+ @__io__ = true
+ Test::Unit::Runner.output
+ end
+
+ ##
+ # Have we hooked up the IO yet?
+
+ def io?
+ @__io__
+ end
+
+ def self.reset # :nodoc:
+ @@test_suites = {}
+ @@test_suites[self] = true
+ end
+
+ reset
+
+ def self.inherited klass # :nodoc:
+ @@test_suites[klass] = true
+ super
+ end
+
+ @test_order = :sorted
+
+ class << self
+ attr_writer :test_order
+ end
+
+ def self.test_order
+ defined?(@test_order) ? @test_order : superclass.test_order
+ end
+
+ def self.test_suites # :nodoc:
+ @@test_suites.keys
+ end
+
+ def self.test_methods # :nodoc:
+ public_instance_methods(true).grep(/^test/)
+ end
+
+ ##
+ # Returns true if the test passed.
+
+ def passed?
+ @passed
+ end
+
+ ##
+ # Runs before every test. Use this to set up before each test
+ # run.
+
+ def setup; end
+
+ ##
+ # Runs after every test. Use this to clean up after each test
+ # run.
+
+ def teardown; end
+
+ def on_parallel_worker?
+ false
+ end
+
+ def self.method_added(name)
+ super
+ return unless name.to_s.start_with?("test_")
+ @test_methods ||= {}
+ if @test_methods[name]
+ raise AssertionFailedError, "test/unit: method #{ self }##{ name } is redefined"
+ end
+ @test_methods[name] = true
+ end
+ end
+ end
+end
diff --git a/tool/lib/tracepointchecker.rb b/tool/lib/tracepointchecker.rb
new file mode 100644
index 0000000000..3254e59357
--- /dev/null
+++ b/tool/lib/tracepointchecker.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+module TracePointChecker
+ STATE = {
+ count: 0,
+ running: false,
+ }
+
+ module ZombieTraceHunter
+ def tracepoint_capture_stat_get
+ TracePoint.stat.map{|k, (activated, deleted)|
+ deleted = 0 unless @tracepoint_captured_singlethread
+ [k, activated, deleted]
+ }
+ end
+
+ def before_setup
+ @tracepoint_captured_singlethread = (Thread.list.size == 1)
+ @tracepoint_captured_stat = tracepoint_capture_stat_get()
+ super
+ end
+
+ def after_teardown
+ super
+
+ # detect zombie traces.
+ assert_equal(
+ @tracepoint_captured_stat,
+ tracepoint_capture_stat_get(),
+ "The number of active/deleted trace events was changed"
+ )
+ # puts "TracePoint - deleted: #{deleted}" if deleted > 0
+
+ TracePointChecker.check if STATE[:running]
+ end
+ end
+
+ MAIN_THREAD = Thread.current
+ TRACES = []
+
+ def self.prefix event
+ case event
+ when :call, :return
+ :n
+ when :c_call, :c_return
+ :c
+ when :b_call, :b_return
+ :b
+ end
+ end
+
+ def self.clear_call_stack
+ Thread.current[:call_stack] = []
+ end
+
+ def self.call_stack
+ stack = Thread.current[:call_stack]
+ stack = clear_call_stack unless stack
+ stack
+ end
+
+ def self.verbose_out label, method
+ puts label => call_stack, :count => STATE[:count], :method => method
+ end
+
+ def self.method_label tp
+ "#{prefix(tp.event)}##{tp.method_id}"
+ end
+
+ def self.start verbose: false, stop_at_failure: false
+ call_events = %i(a_call)
+ return_events = %i(a_return)
+ clear_call_stack
+
+ STATE[:running] = true
+
+ TRACES << TracePoint.new(*call_events){|tp|
+ next if Thread.current != MAIN_THREAD
+
+ method = method_label(tp)
+ call_stack.push method
+ STATE[:count] += 1
+
+ verbose_out :push, method if verbose
+ }
+
+ TRACES << TracePoint.new(*return_events){|tp|
+ next if Thread.current != MAIN_THREAD
+ STATE[:count] += 1
+
+ method = "#{prefix(tp.event)}##{tp.method_id}"
+ verbose_out :pop1, method if verbose
+
+ stored_method = call_stack.pop
+ next if stored_method.nil?
+
+ verbose_out :pop2, method if verbose
+
+ if stored_method != method
+ stop if stop_at_failure
+ RubyVM::SDR() if defined? RubyVM::SDR()
+ call_stack.clear
+ raise "#{stored_method} is expected, but #{method} (count: #{STATE[:count]})"
+ end
+ }
+
+ TRACES.each{|trace| trace.enable}
+ end
+
+ def self.stop
+ STATE[:running] = true
+ TRACES.each{|trace| trace.disable}
+ TRACES.clear
+ end
+
+ def self.check
+ TRACES.each{|trace|
+ raise "trace #{trace} should not be deactivated" unless trace.enabled?
+ }
+ end
+end if defined?(TracePoint.stat)
+
+class ::Test::Unit::TestCase
+ include TracePointChecker::ZombieTraceHunter
+end if defined?(TracePointChecker)
+
+# TracePointChecker.start verbose: false
diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb
new file mode 100644
index 0000000000..c41276e3b4
--- /dev/null
+++ b/tool/lib/vcs.rb
@@ -0,0 +1,733 @@
+# vcs
+require 'fileutils'
+require 'optparse'
+
+# This library is used by several other tools/ scripts to detect the current
+# VCS in use (e.g. SVN, Git) or to interact with that VCS.
+
+ENV.delete('PWD')
+
+class VCS
+ DEBUG_OUT = STDERR.dup
+end
+
+unless File.respond_to? :realpath
+ require 'pathname'
+ def File.realpath(arg)
+ Pathname(arg).realpath.to_s
+ end
+end
+
+def IO.pread(*args)
+ VCS::DEBUG_OUT.puts(args.inspect) if $DEBUG
+ popen(*args) {|f|f.read}
+end
+
+module DebugPOpen
+ refine IO.singleton_class do
+ def popen(*args)
+ VCS::DEBUG_OUT.puts args.inspect if $DEBUG
+ super
+ end
+ end
+end
+using DebugPOpen
+module DebugSystem
+ def system(*args)
+ VCS::DEBUG_OUT.puts args.inspect if $DEBUG
+ exception = false
+ opts = Hash.try_convert(args[-1])
+ if RUBY_VERSION >= "2.6"
+ unless opts
+ opts = {}
+ args << opts
+ end
+ exception = opts.fetch(:exception) {opts[:exception] = true}
+ elsif opts
+ exception = opts.delete(:exception) {true}
+ args.pop if opts.empty?
+ end
+ ret = super(*args)
+ raise "Command failed with status (#$?): #{args[0]}" if exception and !ret
+ ret
+ end
+end
+
+class VCS
+ prepend(DebugSystem) if defined?(DebugSystem)
+ class NotFoundError < RuntimeError; end
+
+ @@dirs = []
+ def self.register(dir, &pred)
+ @@dirs << [dir, self, pred]
+ end
+
+ def self.detect(path = '.', options = {}, parser = nil, **opts)
+ options.update(opts)
+ uplevel_limit = options.fetch(:uplevel_limit, 0)
+ curr = path
+ begin
+ @@dirs.each do |dir, klass, pred|
+ if pred ? pred[curr, dir] : File.directory?(File.join(curr, dir))
+ vcs = klass.new(curr)
+ vcs.define_options(parser) if parser
+ vcs.set_options(options)
+ return vcs
+ end
+ end
+ if uplevel_limit
+ break if uplevel_limit.zero?
+ uplevel_limit -= 1
+ end
+ prev, curr = curr, File.realpath(File.join(curr, '..'))
+ end until curr == prev # stop at the root directory
+ raise VCS::NotFoundError, "does not seem to be under a vcs: #{path}"
+ end
+
+ def self.local_path?(path)
+ String === path or path.respond_to?(:to_path)
+ end
+
+ def self.define_options(parser, opts = {})
+ parser.separator(" VCS common options:")
+ parser.define("--[no-]dryrun") {|v| opts[:dryrun] = v}
+ parser.define("--[no-]debug") {|v| opts[:debug] = v}
+ opts
+ end
+
+ attr_reader :srcdir
+
+ def initialize(path)
+ @srcdir = path
+ super()
+ end
+
+ def chdir(path)
+ @srcdir = path
+ end
+
+ def define_options(parser)
+ end
+
+ def set_options(opts)
+ @debug = opts.fetch(:debug) {$DEBUG}
+ @dryrun = opts.fetch(:dryrun) {@debug}
+ end
+
+ attr_reader :dryrun, :debug
+ alias dryrun? dryrun
+ alias debug? debug
+
+ NullDevice = defined?(IO::NULL) ? IO::NULL :
+ %w[/dev/null NUL NIL: NL:].find {|dev| File.exist?(dev)}
+
+ # returns
+ # * the last revision of the current branch
+ # * the last revision in which +path+ was modified
+ # * the last modified time of +path+
+ # * the last commit title since the latest upstream
+ def get_revisions(path)
+ if self.class.local_path?(path)
+ path = relative_to(path)
+ end
+ last, changed, modified, *rest = (
+ begin
+ if NullDevice
+ save_stderr = STDERR.dup
+ STDERR.reopen NullDevice, 'w'
+ end
+ _get_revisions(path, @srcdir)
+ rescue Errno::ENOENT => e
+ raise VCS::NotFoundError, e.message
+ ensure
+ if save_stderr
+ STDERR.reopen save_stderr
+ save_stderr.close
+ end
+ end
+ )
+ last or raise VCS::NotFoundError, "last revision not found"
+ changed or raise VCS::NotFoundError, "changed revision not found"
+ if modified
+ /\A(\d+)-(\d+)-(\d+)\D(\d+):(\d+):(\d+(?:\.\d+)?)\s*(?:Z|([-+]\d\d)(\d\d))\z/ =~ modified or
+ raise "unknown time format - #{modified}"
+ match = $~[1..6].map { |x| x.to_i }
+ off = $7 ? "#{$7}:#{$8}" : "+00:00"
+ match << off
+ begin
+ modified = Time.new(*match)
+ rescue ArgumentError
+ modified = Time.utc(*$~[1..6]) + $7.to_i * 3600 + $8.to_i * 60
+ end
+ end
+ return last, changed, modified, *rest
+ end
+
+ def modified(path)
+ _, _, modified, * = get_revisions(path)
+ modified
+ end
+
+ def relative_to(path)
+ if path
+ srcdir = File.realpath(@srcdir)
+ path = File.realdirpath(path)
+ list1 = srcdir.split(%r{/})
+ list2 = path.split(%r{/})
+ while !list1.empty? && !list2.empty? && list1.first == list2.first
+ list1.shift
+ list2.shift
+ end
+ if list1.empty? && list2.empty?
+ "."
+ else
+ ([".."] * list1.length + list2).join("/")
+ end
+ else
+ '.'
+ end
+ end
+
+ def after_export(dir)
+ FileUtils.rm_rf(Dir.glob("#{dir}/.git*"))
+ end
+
+ def revision_handler(rev)
+ self.class
+ end
+
+ def revision_name(rev)
+ revision_handler(rev).revision_name(rev)
+ end
+
+ def short_revision(rev)
+ revision_handler(rev).short_revision(rev)
+ end
+
+ class SVN < self
+ register(".svn")
+ COMMAND = ENV['SVN'] || 'svn'
+
+ def self.revision_name(rev)
+ "r#{rev}"
+ end
+
+ def self.short_revision(rev)
+ rev
+ end
+
+ def _get_revisions(path, srcdir = nil)
+ if srcdir and self.class.local_path?(path)
+ path = File.join(srcdir, path)
+ end
+ if srcdir
+ info_xml = IO.pread(%W"#{COMMAND} info --xml #{srcdir}")
+ info_xml = nil unless info_xml[/<url>(.*)<\/url>/, 1] == path.to_s
+ end
+ info_xml ||= IO.pread(%W"#{COMMAND} info --xml #{path}")
+ _, last, _, changed, _ = info_xml.split(/revision="(\d+)"/)
+ modified = info_xml[/<date>([^<>]*)/, 1]
+ branch = info_xml[%r'<relative-url>\^/(?:branches/|tags/)?([^<>]+)', 1]
+ [Integer(last), Integer(changed), modified, branch]
+ end
+
+ def self.search_root(path)
+ return unless local_path?(path)
+ parent = File.realpath(path)
+ begin
+ parent = File.dirname(wkdir = parent)
+ return wkdir if File.directory?(wkdir + "/.svn")
+ end until parent == wkdir
+ end
+
+ def get_info
+ @info ||= IO.pread(%W"#{COMMAND} info --xml #{@srcdir}")
+ end
+
+ def url
+ @url ||= begin
+ url = get_info[/<root>(.*)<\/root>/, 1]
+ @url = URI.parse(url+"/") if url
+ end
+ end
+
+ def wcroot
+ @wcroot ||= begin
+ info = get_info
+ @wcroot = info[/<wcroot-abspath>(.*)<\/wcroot-abspath>/, 1]
+ @wcroot ||= self.class.search_root(@srcdir)
+ end
+ end
+
+ def branch(name)
+ return trunk if name == "trunk"
+ url + "branches/#{name}"
+ end
+
+ def tag(name)
+ url + "tags/#{name}"
+ end
+
+ def trunk
+ url + "trunk"
+ end
+ alias master trunk
+
+ def branch_list(pat)
+ IO.popen(%W"#{COMMAND} ls #{branch('')}") do |f|
+ f.each do |line|
+ line.chomp!
+ line.chomp!('/')
+ yield(line) if File.fnmatch?(pat, line)
+ end
+ end
+ end
+
+ def grep(pat, tag, *files, &block)
+ cmd = %W"#{COMMAND} cat"
+ files.map! {|n| File.join(tag, n)} if tag
+ set = block.binding.eval("proc {|match| $~ = match}")
+ IO.popen([cmd, *files]) do |f|
+ f.grep(pat) do |s|
+ set[$~]
+ yield s
+ end
+ end
+ end
+
+ def export(revision, url, dir, keep_temp = false)
+ if @srcdir and (rootdir = wcroot)
+ srcdir = File.realpath(@srcdir)
+ rootdir << "/"
+ if srcdir.start_with?(rootdir)
+ subdir = srcdir[rootdir.size..-1]
+ subdir = nil if subdir.empty?
+ FileUtils.mkdir_p(svndir = dir+"/.svn")
+ FileUtils.ln_s(Dir.glob(rootdir+"/.svn/*"), svndir)
+ system(COMMAND, "-q", "revert", "-R", subdir || ".", :chdir => dir) or return false
+ FileUtils.rm_rf(svndir) unless keep_temp
+ if subdir
+ tmpdir = Dir.mktmpdir("tmp-co.", "#{dir}/#{subdir}")
+ File.rename(tmpdir, tmpdir = "#{dir}/#{File.basename(tmpdir)}")
+ FileUtils.mv(Dir.glob("#{dir}/#{subdir}/{.[^.]*,..?*,*}"), tmpdir)
+ begin
+ Dir.rmdir("#{dir}/#{subdir}")
+ end until (subdir = File.dirname(subdir)) == '.'
+ FileUtils.mv(Dir.glob("#{tmpdir}/#{subdir}/{.[^.]*,..?*,*}"), dir)
+ Dir.rmdir(tmpdir)
+ end
+ return self
+ end
+ end
+ IO.popen(%W"#{COMMAND} export -r #{revision} #{url} #{dir}") do |pipe|
+ pipe.each {|line| /^A/ =~ line or yield line}
+ end
+ self if $?.success?
+ end
+
+ def after_export(dir)
+ super
+ FileUtils.rm_rf(dir+"/.svn")
+ end
+
+ def branch_beginning(url)
+ # `--limit` of svn-log is useless in this case, because it is
+ # applied before `--search`.
+ rev = IO.pread(%W[ #{COMMAND} log --xml
+ --search=matz --search-and=has\ started
+ -- #{url}/version.h])[/<logentry\s+revision="(\d+)"/m, 1]
+ rev.to_i if rev
+ end
+
+ def export_changelog(url = '.', from = nil, to = nil, _path = nil, path: _path)
+ range = [to || 'HEAD', (from ? from+1 : branch_beginning(url))].compact.join(':')
+ IO.popen({'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'},
+ %W"#{COMMAND} log -r#{range} #{url}") do |r|
+ IO.copy_stream(r, path)
+ end
+ end
+
+ def commit
+ args = %W"#{COMMAND} commit"
+ if dryrun?
+ VCS::DEBUG_OUT.puts(args.inspect)
+ return true
+ end
+ system(*args)
+ end
+ end
+
+ class GIT < self
+ register(".git") {|path, dir| File.exist?(File.join(path, dir))}
+ COMMAND = ENV["GIT"] || 'git'
+
+ def cmd_args(cmds, srcdir = nil)
+ (opts = cmds.last).kind_of?(Hash) or cmds << (opts = {})
+ opts[:external_encoding] ||= "UTF-8"
+ if srcdir
+ opts[:chdir] ||= srcdir
+ end
+ VCS::DEBUG_OUT.puts cmds.inspect if debug?
+ cmds
+ end
+
+ def cmd_pipe_at(srcdir, cmds, &block)
+ without_gitconfig { IO.popen(*cmd_args(cmds, srcdir), &block) }
+ end
+
+ def cmd_read_at(srcdir, cmds)
+ result = without_gitconfig { IO.pread(*cmd_args(cmds, srcdir)) }
+ VCS::DEBUG_OUT.puts result.inspect if debug?
+ result
+ end
+
+ def cmd_pipe(*cmds, &block)
+ cmd_pipe_at(@srcdir, cmds, &block)
+ end
+
+ def cmd_read(*cmds)
+ cmd_read_at(@srcdir, cmds)
+ end
+
+ def svn_revision(log)
+ if /^ *git-svn-id: .*@(\d+) .*\n+\z/ =~ log
+ $1.to_i
+ end
+ end
+
+ def _get_revisions(path, srcdir = nil)
+ ref = Branch === path ? path.to_str : 'HEAD'
+ gitcmd = [COMMAND]
+ last = cmd_read_at(srcdir, [[*gitcmd, 'rev-parse', ref]]).rstrip
+ log = cmd_read_at(srcdir, [[*gitcmd, 'log', '-n1', '--date=iso', '--pretty=fuller', *path]])
+ changed = log[/\Acommit (\h+)/, 1]
+ modified = log[/^CommitDate:\s+(.*)/, 1]
+ if rev = svn_revision(log)
+ if changed == last
+ last = rev
+ else
+ svn_rev = svn_revision(cmd_read_at(srcdir, [[*gitcmd, 'log', '-n1', '--format=%B', last]]))
+ last = svn_rev if svn_rev
+ end
+ changed = rev
+ end
+ branch = cmd_read_at(srcdir, [gitcmd + %W[symbolic-ref --short #{ref}]])
+ if branch.empty?
+ branch = cmd_read_at(srcdir, [gitcmd + %W[describe --contains #{ref}]]).strip
+ end
+ if branch.empty?
+ branch_list = cmd_read_at(srcdir, [gitcmd + %W[branch --list --contains #{ref}]]).lines.to_a
+ branch, = branch_list.grep(/\A\*/)
+ case branch
+ when /\A\* *\(\S+ detached at (.*)\)\Z/
+ branch = $1
+ branch = nil if last.start_with?(branch)
+ when /\A\* (\S+)\Z/
+ branch = $1
+ else
+ branch = nil
+ end
+ unless branch
+ branch_list.each {|b| b.strip!}
+ branch_list.delete_if {|b| / / =~ b}
+ branch = branch_list.min_by(&:length) || ""
+ end
+ end
+ branch.chomp!
+ branch = ":detached:" if branch.empty?
+ upstream = cmd_read_at(srcdir, [gitcmd + %W[branch --list --format=%(upstream:short) #{branch}]])
+ upstream.chomp!
+ title = cmd_read_at(srcdir, [gitcmd + %W[log --format=%s -n1 #{upstream}..#{ref}]])
+ title = nil if title.empty?
+ [last, changed, modified, branch, title]
+ end
+
+ def self.revision_name(rev)
+ short_revision(rev)
+ end
+
+ def self.short_revision(rev)
+ rev[0, 10]
+ end
+
+ def revision_handler(rev)
+ case rev
+ when Integer
+ SVN
+ else
+ super
+ end
+ end
+
+ def without_gitconfig
+ home = ENV.delete('HOME')
+ yield
+ ensure
+ ENV['HOME'] = home if home
+ end
+
+ def initialize(*)
+ super
+ @srcdir = File.realpath(@srcdir)
+ VCS::DEBUG_OUT.puts @srcdir.inspect if debug?
+ self
+ end
+
+ Branch = Struct.new(:to_str)
+
+ def branch(name)
+ Branch.new(name)
+ end
+
+ alias tag branch
+
+ def master
+ branch("master")
+ end
+ alias trunk master
+
+ def stable
+ cmd = %W"#{COMMAND} for-each-ref --format=\%(refname:short) refs/heads/ruby_[0-9]*"
+ branch(cmd_read(cmd)[/.*^(ruby_\d+_\d+)$/m, 1])
+ end
+
+ def branch_list(pat)
+ cmd = %W"#{COMMAND} for-each-ref --format=\%(refname:short) refs/heads/#{pat}"
+ cmd_pipe(cmd) {|f|
+ f.each {|line|
+ line.chomp!
+ yield line
+ }
+ }
+ end
+
+ def grep(pat, tag, *files, &block)
+ cmd = %W[#{COMMAND} grep -h --perl-regexp #{tag} --]
+ set = block.binding.eval("proc {|match| $~ = match}")
+ cmd_pipe(cmd+files) do |f|
+ f.grep(pat) do |s|
+ set[$~]
+ yield s
+ end
+ end
+ end
+
+ def export(revision, url, dir, keep_temp = false)
+ system(COMMAND, "clone", "-c", "advice.detachedHead=false", "-s", (@srcdir || '.').to_s, "-b", url, dir) or return
+ (Integer === revision ? GITSVN : GIT).new(File.expand_path(dir))
+ end
+
+ def branch_beginning(url)
+ cmd_read(%W[ #{COMMAND} log -n1 --format=format:%H
+ --author=matz --committer=matz --grep=started\\.$
+ #{url.to_str} -- version.h include/ruby/version.h])
+ end
+
+ def export_changelog(url = '@', from = nil, to = nil, _path = nil, path: _path, base_url: nil)
+ svn = nil
+ from, to = [from, to].map do |rev|
+ rev or next
+ if Integer === rev
+ svn = true
+ rev = cmd_read({'LANG' => 'C', 'LC_ALL' => 'C'},
+ %W"#{COMMAND} log -n1 --format=format:%H" <<
+ "--grep=^ *git-svn-id: .*@#{rev} ")
+ end
+ rev unless rev.empty?
+ end
+ unless (from && /./.match(from)) or ((from = branch_beginning(url)) && /./.match(from))
+ warn "no starting commit found", uplevel: 1
+ from = nil
+ end
+ if svn or system(*%W"#{COMMAND} fetch origin refs/notes/commits:refs/notes/commits",
+ chdir: @srcdir, exception: false)
+ system(*%W"#{COMMAND} fetch origin refs/notes/log-fix:refs/notes/log-fix",
+ chdir: @srcdir, exception: false)
+ else
+ warn "Could not fetch notes/commits tree", uplevel: 1
+ end
+ to ||= url.to_str
+ if from
+ arg = ["#{from}^..#{to}"]
+ else
+ arg = ["--since=25 Dec 00:00:00", to]
+ end
+ writer =
+ if svn
+ format_changelog_as_svn(path, arg)
+ else
+ if base_url == true
+ remote, = upstream
+ if remote &&= cmd_read(env, %W[#{COMMAND} remote get-url --no-push #{remote}])
+ remote.chomp!
+ # hack to redirect git.r-l.o to github
+ remote.sub!(/\Agit@git\.ruby-lang\.org:/, 'git@github.com:ruby/')
+ remote.sub!(/\Agit@(.*?):(.*?)(?:\.git)?\z/, 'https://\1/\2/commit/')
+ end
+ base_url = remote
+ end
+ format_changelog(path, arg, base_url)
+ end
+ if !path or path == '-'
+ writer[$stdout]
+ else
+ File.open(path, 'wb', &writer)
+ end
+ end
+
+ LOG_FIX_REGEXP_SEPARATORS = '/!:;|,#%&'
+
+ def format_changelog(path, arg, base_url = nil)
+ env = {'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'}
+ cmd = %W"#{COMMAND} log --format=fuller --notes=commits --notes=log-fix --topo-order --no-merges"
+ date = "--date=iso-local"
+ unless system(env, *cmd, date, chdir: @srcdir, out: NullDevice, exception: false)
+ date = "--date=iso"
+ end
+ cmd << date
+ cmd.concat(arg)
+ proc do |w|
+ w.print "-*- coding: utf-8 -*-\n\n"
+ w.print "base-url = #{base_url}\n\n" if base_url
+ cmd_pipe(env, cmd, chdir: @srcdir) do |r|
+ while s = r.gets("\ncommit ")
+ h, s = s.split(/^$/, 2)
+ h.gsub!(/^(?:(?:Author|Commit)(?:Date)?|Date): /, ' \&')
+ if s.sub!(/\nNotes \(log-fix\):\n((?: +.*\n)+)/, '')
+ fix = $1
+ s = s.lines
+ fix.each_line do |x|
+ case x
+ when %r[^ +(\d+)s([#{LOG_FIX_REGEXP_SEPARATORS}])(.+)\2(.*)\2]o
+ n = $1.to_i
+ wrong = $3
+ correct = $4
+ begin
+ s[n][wrong] = correct
+ rescue IndexError
+ message = ["format_changelog failed to replace #{wrong.dump} with #{correct.dump} at #$1\n"]
+ from = [1, n-2].max
+ to = [s.size-1, n+2].min
+ s.each_with_index do |e, i|
+ next if i < from
+ break if to < i
+ message << "#{i}:#{e}"
+ end
+ raise message.join('')
+ end
+ when %r[^( +)(\d+)i([#{LOG_FIX_REGEXP_SEPARATORS}])(.*)\3]o
+ s[$2.to_i, 0] = "#{$1}#{$4}\n"
+ when %r[^ +(\d+)(?:,(\d+))?d]
+ n = $1.to_i
+ e = $2
+ s[n..(e ? e.to_i : n)] = []
+ end
+ end
+ s = s.join('')
+ end
+
+ if %r[^ +(https://github\.com/[^/]+/[^/]+/)commit/\h+\n(?=(?: +\n(?i: +Co-authored-by: .*\n)+)?(?:\n|\Z))] =~ s
+ issue = "#{$1}pull/"
+ s.gsub!(/\b[Ff]ix(?:e[sd])? \K#(?=\d+)/) {issue}
+ end
+
+ s.gsub!(/ +\n/, "\n")
+ s.sub!(/^Notes:/, ' \&')
+ w.print h, s
+ end
+ end
+ end
+ end
+
+ def format_changelog_as_svn(path, arg)
+ cmd = %W"#{COMMAND} log --topo-order --no-notes -z --format=%an%n%at%n%B"
+ cmd.concat(arg)
+ proc do |w|
+ sep = "-"*72 + "\n"
+ w.print sep
+ cmd_pipe(cmd) do |r|
+ while s = r.gets("\0")
+ s.chomp!("\0")
+ author, time, s = s.split("\n", 3)
+ s.sub!(/\n\ngit-svn-id: .*@(\d+) .*\n\Z/, '')
+ rev = $1
+ time = Time.at(time.to_i).getlocal("+09:00").strftime("%F %T %z (%a, %d %b %Y)")
+ lines = s.count("\n") + 1
+ lines = "#{lines} line#{lines == 1 ? '' : 's'}"
+ w.print "r#{rev} | #{author} | #{time} | #{lines}\n\n", s, "\n", sep
+ end
+ end
+ end
+ end
+
+ def upstream
+ (branch = cmd_read(%W"#{COMMAND} symbolic-ref --short HEAD")).chomp!
+ (upstream = cmd_read(%W"#{COMMAND} branch --list --format=%(upstream) #{branch}")).chomp!
+ while ref = upstream[%r"\Arefs/heads/(.*)", 1]
+ upstream = cmd_read(%W"#{COMMAND} branch --list --format=%(upstream) #{ref}")
+ end
+ unless %r"\Arefs/remotes/([^/]+)/(.*)" =~ upstream
+ raise "Upstream not found"
+ end
+ [$1, $2]
+ end
+
+ def commit(opts = {})
+ args = [COMMAND, "push"]
+ args << "-n" if dryrun
+ remote, branch = upstream
+ args << remote
+ branches = %W[refs/notes/commits:refs/notes/commits HEAD:#{branch}]
+ if dryrun?
+ branches.each do |b|
+ VCS::DEBUG_OUT.puts((args + [b]).inspect)
+ end
+ return true
+ end
+ branches.each do |b|
+ system(*(args + [b])) or return false
+ end
+ true
+ end
+ end
+
+ class GITSVN < GIT
+ def self.revision_name(rev)
+ SVN.revision_name(rev)
+ end
+
+ def last_changed_revision
+ rev = cmd_read(%W"#{COMMAND} svn info"+[STDERR=>[:child, :out]])[/^Last Changed Rev: (\d+)/, 1]
+ com = cmd_read(%W"#{COMMAND} svn find-rev r#{rev}").chomp
+ return rev, com
+ end
+
+ def commit(opts = {})
+ rev, com = last_changed_revision
+ head = cmd_read(%W"#{COMMAND} symbolic-ref --short HEAD").chomp
+
+ commits = cmd_read([COMMAND, "log", "--reverse", "--format=%H %ae %ce", "#{com}..@"], "rb").split("\n")
+ commits.each_with_index do |l, i|
+ r, a, c = l.split(' ')
+ dcommit = [COMMAND, "svn", "dcommit"]
+ dcommit.insert(-2, "-n") if dryrun
+ dcommit << "--add-author-from" unless a == c
+ dcommit << r
+ system(*dcommit) or return false
+ system(COMMAND, "checkout", head) or return false
+ system(COMMAND, "rebase") or return false
+ end
+
+ if rev
+ old = [cmd_read(%W"#{COMMAND} log -1 --format=%H").chomp]
+ old << cmd_read(%W"#{COMMAND} svn reset -r#{rev}")[/^r#{rev} = (\h+)/, 1]
+ 3.times do
+ sleep 2
+ system(*%W"#{COMMAND} pull --no-edit --rebase")
+ break unless old.include?(cmd_read(%W"#{COMMAND} log -1 --format=%H").chomp)
+ end
+ end
+ true
+ end
+ end
+end
diff --git a/tool/lib/vpath.rb b/tool/lib/vpath.rb
new file mode 100644
index 0000000000..48ab148405
--- /dev/null
+++ b/tool/lib/vpath.rb
@@ -0,0 +1,87 @@
+# -*- coding: us-ascii -*-
+
+class VPath
+ attr_accessor :separator
+
+ def initialize(*list)
+ @list = list
+ @additional = []
+ @separator = nil
+ end
+
+ def inspect
+ list.inspect
+ end
+
+ def search(meth, base, *rest)
+ begin
+ meth.call(base, *rest)
+ rescue Errno::ENOENT => error
+ list.each do |dir|
+ return meth.call(File.join(dir, base), *rest) rescue nil
+ end
+ raise error
+ end
+ end
+
+ def process(*args, &block)
+ search(File.method(__callee__), *args, &block)
+ end
+
+ alias stat process
+ alias lstat process
+
+ def open(*args)
+ f = search(File.method(:open), *args)
+ if block_given?
+ begin
+ yield f
+ ensure
+ f.close unless f.closed?
+ end
+ else
+ f
+ end
+ end
+
+ def read(*args)
+ open(*args) {|f| f.read}
+ end
+
+ def foreach(file, *args, &block)
+ open(file) {|f| f.each(*args, &block)}
+ end
+
+ def def_options(opt)
+ opt.on("-I", "--srcdir=DIR", "add a directory to search path") {|dir|
+ @additional << dir
+ }
+ opt.on("-L", "--vpath=PATH LIST", "add directories to search path") {|dirs|
+ @additional << [dirs]
+ }
+ opt.on("--path-separator=SEP", /\A(?:\W\z|\.(\W).+)/, "separator for vpath") {|sep, vsep|
+ # hack for msys make.
+ @separator = vsep || sep
+ }
+ end
+
+ def list
+ @additional.reject! do |dirs|
+ case dirs
+ when String
+ @list << dirs
+ when Array
+ raise "--path-separator option is needed for vpath list" unless @separator
+ # @separator ||= (require 'rbconfig'; RbConfig::CONFIG["PATH_SEPARATOR"])
+ @list.concat(dirs[0].split(@separator))
+ end
+ true
+ end
+ @list
+ end
+
+ def strip(path)
+ prefix = list.map {|dir| Regexp.quote(dir)}
+ path.sub(/\A#{prefix.join('|')}(?:\/|\z)/, '')
+ end
+end
diff --git a/tool/lib/webrick.rb b/tool/lib/webrick.rb
new file mode 100644
index 0000000000..b854b68db4
--- /dev/null
+++ b/tool/lib/webrick.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: false
+##
+# = WEB server toolkit.
+#
+# WEBrick is an HTTP server toolkit that can be configured as an HTTPS server,
+# a proxy server, and a virtual-host server. WEBrick features complete
+# logging of both server operations and HTTP access. WEBrick supports both
+# basic and digest authentication in addition to algorithms not in RFC 2617.
+#
+# A WEBrick server can be composed of multiple WEBrick servers or servlets to
+# provide differing behavior on a per-host or per-path basis. WEBrick
+# includes servlets for handling CGI scripts, ERB pages, Ruby blocks and
+# directory listings.
+#
+# WEBrick also includes tools for daemonizing a process and starting a process
+# at a higher privilege level and dropping permissions.
+#
+# == Security
+#
+# *Warning:* WEBrick is not recommended for production. It only implements
+# basic security checks.
+#
+# == Starting an HTTP server
+#
+# To create a new WEBrick::HTTPServer that will listen to connections on port
+# 8000 and serve documents from the current user's public_html folder:
+#
+# require 'webrick'
+#
+# root = File.expand_path '~/public_html'
+# server = WEBrick::HTTPServer.new :Port => 8000, :DocumentRoot => root
+#
+# To run the server you will need to provide a suitable shutdown hook as
+# starting the server blocks the current thread:
+#
+# trap 'INT' do server.shutdown end
+#
+# server.start
+#
+# == Custom Behavior
+#
+# The easiest way to have a server perform custom operations is through
+# WEBrick::HTTPServer#mount_proc. The block given will be called with a
+# WEBrick::HTTPRequest with request info and a WEBrick::HTTPResponse which
+# must be filled in appropriately:
+#
+# server.mount_proc '/' do |req, res|
+# res.body = 'Hello, world!'
+# end
+#
+# Remember that +server.mount_proc+ must precede +server.start+.
+#
+# == Servlets
+#
+# Advanced custom behavior can be obtained through mounting a subclass of
+# WEBrick::HTTPServlet::AbstractServlet. Servlets provide more modularity
+# when writing an HTTP server than mount_proc allows. Here is a simple
+# servlet:
+#
+# class Simple < WEBrick::HTTPServlet::AbstractServlet
+# def do_GET request, response
+# status, content_type, body = do_stuff_with request
+#
+# response.status = 200
+# response['Content-Type'] = 'text/plain'
+# response.body = 'Hello, World!'
+# end
+# end
+#
+# To initialize the servlet you mount it on the server:
+#
+# server.mount '/simple', Simple
+#
+# See WEBrick::HTTPServlet::AbstractServlet for more details.
+#
+# == Virtual Hosts
+#
+# A server can act as a virtual host for multiple host names. After creating
+# the listening host, additional hosts that do not listen can be created and
+# attached as virtual hosts:
+#
+# server = WEBrick::HTTPServer.new # ...
+#
+# vhost = WEBrick::HTTPServer.new :ServerName => 'vhost.example',
+# :DoNotListen => true, # ...
+# vhost.mount '/', ...
+#
+# server.virtual_host vhost
+#
+# If no +:DocumentRoot+ is provided and no servlets or procs are mounted on the
+# main server it will return 404 for all URLs.
+#
+# == HTTPS
+#
+# To create an HTTPS server you only need to enable SSL and provide an SSL
+# certificate name:
+#
+# require 'webrick'
+# require 'webrick/https'
+#
+# cert_name = [
+# %w[CN localhost],
+# ]
+#
+# server = WEBrick::HTTPServer.new(:Port => 8000,
+# :SSLEnable => true,
+# :SSLCertName => cert_name)
+#
+# This will start the server with a self-generated self-signed certificate.
+# The certificate will be changed every time the server is restarted.
+#
+# To create a server with a pre-determined key and certificate you can provide
+# them:
+#
+# require 'webrick'
+# require 'webrick/https'
+# require 'openssl'
+#
+# cert = OpenSSL::X509::Certificate.new File.read '/path/to/cert.pem'
+# pkey = OpenSSL::PKey::RSA.new File.read '/path/to/pkey.pem'
+#
+# server = WEBrick::HTTPServer.new(:Port => 8000,
+# :SSLEnable => true,
+# :SSLCertificate => cert,
+# :SSLPrivateKey => pkey)
+#
+# == Proxy Server
+#
+# WEBrick can act as a proxy server:
+#
+# require 'webrick'
+# require 'webrick/httpproxy'
+#
+# proxy = WEBrick::HTTPProxyServer.new :Port => 8000
+#
+# trap 'INT' do proxy.shutdown end
+#
+# See WEBrick::HTTPProxy for further details including modifying proxied
+# responses.
+#
+# == Basic and Digest authentication
+#
+# WEBrick provides both Basic and Digest authentication for regular and proxy
+# servers. See WEBrick::HTTPAuth, WEBrick::HTTPAuth::BasicAuth and
+# WEBrick::HTTPAuth::DigestAuth.
+#
+# == WEBrick as a daemonized Web Server
+#
+# WEBrick can be run as a daemonized server for small loads.
+#
+# === Daemonizing
+#
+# To start a WEBrick server as a daemon simple run WEBrick::Daemon.start
+# before starting the server.
+#
+# === Dropping Permissions
+#
+# WEBrick can be started as one user to gain permission to bind to port 80 or
+# 443 for serving HTTP or HTTPS traffic then can drop these permissions for
+# regular operation. To listen on all interfaces for HTTP traffic:
+#
+# sockets = WEBrick::Utils.create_listeners nil, 80
+#
+# Then drop privileges:
+#
+# WEBrick::Utils.su 'www'
+#
+# Then create a server that does not listen by default:
+#
+# server = WEBrick::HTTPServer.new :DoNotListen => true, # ...
+#
+# Then overwrite the listening sockets with the port 80 sockets:
+#
+# server.listeners.replace sockets
+#
+# === Logging
+#
+# WEBrick can separately log server operations and end-user access. For
+# server operations:
+#
+# log_file = File.open '/var/log/webrick.log', 'a+'
+# log = WEBrick::Log.new log_file
+#
+# For user access logging:
+#
+# access_log = [
+# [log_file, WEBrick::AccessLog::COMBINED_LOG_FORMAT],
+# ]
+#
+# server = WEBrick::HTTPServer.new :Logger => log, :AccessLog => access_log
+#
+# See WEBrick::AccessLog for further log formats.
+#
+# === Log Rotation
+#
+# To rotate logs in WEBrick on a HUP signal (like syslogd can send), open the
+# log file in 'a+' mode (as above) and trap 'HUP' to reopen the log file:
+#
+# trap 'HUP' do log_file.reopen '/path/to/webrick.log', 'a+'
+#
+# == Copyright
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+#
+# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#--
+# $IPR: webrick.rb,v 1.12 2002/10/01 17:16:31 gotoyuzo Exp $
+
+module WEBrick
+end
+
+require 'webrick/compat.rb'
+
+require 'webrick/version.rb'
+require 'webrick/config.rb'
+require 'webrick/log.rb'
+require 'webrick/server.rb'
+require_relative 'webrick/utils.rb'
+require 'webrick/accesslog'
+
+require 'webrick/htmlutils.rb'
+require 'webrick/httputils.rb'
+require 'webrick/cookie.rb'
+require 'webrick/httpversion.rb'
+require 'webrick/httpstatus.rb'
+require 'webrick/httprequest.rb'
+require 'webrick/httpresponse.rb'
+require 'webrick/httpserver.rb'
+require 'webrick/httpservlet.rb'
+require 'webrick/httpauth.rb'
diff --git a/tool/lib/webrick/.document b/tool/lib/webrick/.document
new file mode 100644
index 0000000000..c62f89083b
--- /dev/null
+++ b/tool/lib/webrick/.document
@@ -0,0 +1,6 @@
+# Add files to this as they become documented
+
+*.rb
+
+httpauth
+httpservlet
diff --git a/tool/lib/webrick/accesslog.rb b/tool/lib/webrick/accesslog.rb
new file mode 100644
index 0000000000..e4849637f3
--- /dev/null
+++ b/tool/lib/webrick/accesslog.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: false
+#--
+# accesslog.rb -- Access log handling utilities
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2002 keita yamaguchi
+# Copyright (c) 2002 Internet Programming with Ruby writers
+#
+# $IPR: accesslog.rb,v 1.1 2002/10/01 17:16:32 gotoyuzo Exp $
+
+module WEBrick
+
+ ##
+ # AccessLog provides logging to various files in various formats.
+ #
+ # Multiple logs may be written to at the same time:
+ #
+ # access_log = [
+ # [$stderr, WEBrick::AccessLog::COMMON_LOG_FORMAT],
+ # [$stderr, WEBrick::AccessLog::REFERER_LOG_FORMAT],
+ # ]
+ #
+ # server = WEBrick::HTTPServer.new :AccessLog => access_log
+ #
+ # Custom log formats may be defined. WEBrick::AccessLog provides a subset
+ # of the formatting from Apache's mod_log_config
+ # http://httpd.apache.org/docs/mod/mod_log_config.html#formats. See
+ # AccessLog::setup_params for a list of supported options
+
+ module AccessLog
+
+ ##
+ # Raised if a parameter such as %e, %i, %o or %n is used without fetching
+ # a specific field.
+
+ class AccessLogError < StandardError; end
+
+ ##
+ # The Common Log Format's time format
+
+ CLF_TIME_FORMAT = "[%d/%b/%Y:%H:%M:%S %Z]"
+
+ ##
+ # Common Log Format
+
+ COMMON_LOG_FORMAT = "%h %l %u %t \"%r\" %s %b"
+
+ ##
+ # Short alias for Common Log Format
+
+ CLF = COMMON_LOG_FORMAT
+
+ ##
+ # Referer Log Format
+
+ REFERER_LOG_FORMAT = "%{Referer}i -> %U"
+
+ ##
+ # User-Agent Log Format
+
+ AGENT_LOG_FORMAT = "%{User-Agent}i"
+
+ ##
+ # Combined Log Format
+
+ COMBINED_LOG_FORMAT = "#{CLF} \"%{Referer}i\" \"%{User-agent}i\""
+
+ module_function
+
+ # This format specification is a subset of mod_log_config of Apache:
+ #
+ # %a:: Remote IP address
+ # %b:: Total response size
+ # %e{variable}:: Given variable in ENV
+ # %f:: Response filename
+ # %h:: Remote host name
+ # %{header}i:: Given request header
+ # %l:: Remote logname, always "-"
+ # %m:: Request method
+ # %{attr}n:: Given request attribute from <tt>req.attributes</tt>
+ # %{header}o:: Given response header
+ # %p:: Server's request port
+ # %{format}p:: The canonical port of the server serving the request or the
+ # actual port or the client's actual port. Valid formats are
+ # canonical, local or remote.
+ # %q:: Request query string
+ # %r:: First line of the request
+ # %s:: Request status
+ # %t:: Time the request was received
+ # %T:: Time taken to process the request
+ # %u:: Remote user from auth
+ # %U:: Unparsed URI
+ # %%:: Literal %
+
+ def setup_params(config, req, res)
+ params = Hash.new("")
+ params["a"] = req.peeraddr[3]
+ params["b"] = res.sent_size
+ params["e"] = ENV
+ params["f"] = res.filename || ""
+ params["h"] = req.peeraddr[2]
+ params["i"] = req
+ params["l"] = "-"
+ params["m"] = req.request_method
+ params["n"] = req.attributes
+ params["o"] = res
+ params["p"] = req.port
+ params["q"] = req.query_string
+ params["r"] = req.request_line.sub(/\x0d?\x0a\z/o, '')
+ params["s"] = res.status # won't support "%>s"
+ params["t"] = req.request_time
+ params["T"] = Time.now - req.request_time
+ params["u"] = req.user || "-"
+ params["U"] = req.unparsed_uri
+ params["v"] = config[:ServerName]
+ params
+ end
+
+ ##
+ # Formats +params+ according to +format_string+ which is described in
+ # setup_params.
+
+ def format(format_string, params)
+ format_string.gsub(/\%(?:\{(.*?)\})?>?([a-zA-Z%])/){
+ param, spec = $1, $2
+ case spec[0]
+ when ?e, ?i, ?n, ?o
+ raise AccessLogError,
+ "parameter is required for \"#{spec}\"" unless param
+ (param = params[spec][param]) ? escape(param) : "-"
+ when ?t
+ params[spec].strftime(param || CLF_TIME_FORMAT)
+ when ?p
+ case param
+ when 'remote'
+ escape(params["i"].peeraddr[1].to_s)
+ else
+ escape(params["p"].to_s)
+ end
+ when ?%
+ "%"
+ else
+ escape(params[spec].to_s)
+ end
+ }
+ end
+
+ ##
+ # Escapes control characters in +data+
+
+ def escape(data)
+ data = data.gsub(/[[:cntrl:]\\]+/) {$&.dump[1...-1]}
+ data.untaint if RUBY_VERSION < '2.7'
+ data
+ end
+ end
+end
diff --git a/tool/lib/webrick/cgi.rb b/tool/lib/webrick/cgi.rb
new file mode 100644
index 0000000000..bb0ae2fc84
--- /dev/null
+++ b/tool/lib/webrick/cgi.rb
@@ -0,0 +1,313 @@
+# frozen_string_literal: false
+#
+# cgi.rb -- Yet another CGI library
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $Id$
+
+require_relative "httprequest"
+require_relative "httpresponse"
+require_relative "config"
+require "stringio"
+
+module WEBrick
+
+ # A CGI library using WEBrick requests and responses.
+ #
+ # Example:
+ #
+ # class MyCGI < WEBrick::CGI
+ # def do_GET req, res
+ # res.body = 'it worked!'
+ # res.status = 200
+ # end
+ # end
+ #
+ # MyCGI.new.start
+
+ class CGI
+
+ # The CGI error exception class
+
+ CGIError = Class.new(StandardError)
+
+ ##
+ # The CGI configuration. This is based on WEBrick::Config::HTTP
+
+ attr_reader :config
+
+ ##
+ # The CGI logger
+
+ attr_reader :logger
+
+ ##
+ # Creates a new CGI interface.
+ #
+ # The first argument in +args+ is a configuration hash which would update
+ # WEBrick::Config::HTTP.
+ #
+ # Any remaining arguments are stored in the <code>@options</code> instance
+ # variable for use by a subclass.
+
+ def initialize(*args)
+ if defined?(MOD_RUBY)
+ unless ENV.has_key?("GATEWAY_INTERFACE")
+ Apache.request.setup_cgi_env
+ end
+ end
+ if %r{HTTP/(\d+\.\d+)} =~ ENV["SERVER_PROTOCOL"]
+ httpv = $1
+ end
+ @config = WEBrick::Config::HTTP.dup.update(
+ :ServerSoftware => ENV["SERVER_SOFTWARE"] || "null",
+ :HTTPVersion => HTTPVersion.new(httpv || "1.0"),
+ :RunOnCGI => true, # to detect if it runs on CGI.
+ :NPH => false # set true to run as NPH script.
+ )
+ if config = args.shift
+ @config.update(config)
+ end
+ @config[:Logger] ||= WEBrick::BasicLog.new($stderr)
+ @logger = @config[:Logger]
+ @options = args
+ end
+
+ ##
+ # Reads +key+ from the configuration
+
+ def [](key)
+ @config[key]
+ end
+
+ ##
+ # Starts the CGI process with the given environment +env+ and standard
+ # input and output +stdin+ and +stdout+.
+
+ def start(env=ENV, stdin=$stdin, stdout=$stdout)
+ sock = WEBrick::CGI::Socket.new(@config, env, stdin, stdout)
+ req = HTTPRequest.new(@config)
+ res = HTTPResponse.new(@config)
+ unless @config[:NPH] or defined?(MOD_RUBY)
+ def res.setup_header
+ unless @header["status"]
+ phrase = HTTPStatus::reason_phrase(@status)
+ @header["status"] = "#{@status} #{phrase}"
+ end
+ super
+ end
+ def res.status_line
+ ""
+ end
+ end
+
+ begin
+ req.parse(sock)
+ req.script_name = (env["SCRIPT_NAME"] || File.expand_path($0)).dup
+ req.path_info = (env["PATH_INFO"] || "").dup
+ req.query_string = env["QUERY_STRING"]
+ req.user = env["REMOTE_USER"]
+ res.request_method = req.request_method
+ res.request_uri = req.request_uri
+ res.request_http_version = req.http_version
+ res.keep_alive = req.keep_alive?
+ self.service(req, res)
+ rescue HTTPStatus::Error => ex
+ res.set_error(ex)
+ rescue HTTPStatus::Status => ex
+ res.status = ex.code
+ rescue Exception => ex
+ @logger.error(ex)
+ res.set_error(ex, true)
+ ensure
+ req.fixup
+ if defined?(MOD_RUBY)
+ res.setup_header
+ Apache.request.status_line = "#{res.status} #{res.reason_phrase}"
+ Apache.request.status = res.status
+ table = Apache.request.headers_out
+ res.header.each{|key, val|
+ case key
+ when /^content-encoding$/i
+ Apache::request.content_encoding = val
+ when /^content-type$/i
+ Apache::request.content_type = val
+ else
+ table[key] = val.to_s
+ end
+ }
+ res.cookies.each{|cookie|
+ table.add("Set-Cookie", cookie.to_s)
+ }
+ Apache.request.send_http_header
+ res.send_body(sock)
+ else
+ res.send_response(sock)
+ end
+ end
+ end
+
+ ##
+ # Services the request +req+ which will fill in the response +res+. See
+ # WEBrick::HTTPServlet::AbstractServlet#service for details.
+
+ def service(req, res)
+ method_name = "do_" + req.request_method.gsub(/-/, "_")
+ if respond_to?(method_name)
+ __send__(method_name, req, res)
+ else
+ raise HTTPStatus::MethodNotAllowed,
+ "unsupported method `#{req.request_method}'."
+ end
+ end
+
+ ##
+ # Provides HTTP socket emulation from the CGI environment
+
+ class Socket # :nodoc:
+ include Enumerable
+
+ private
+
+ def initialize(config, env, stdin, stdout)
+ @config = config
+ @env = env
+ @header_part = StringIO.new
+ @body_part = stdin
+ @out_port = stdout
+ @out_port.binmode
+
+ @server_addr = @env["SERVER_ADDR"] || "0.0.0.0"
+ @server_name = @env["SERVER_NAME"]
+ @server_port = @env["SERVER_PORT"]
+ @remote_addr = @env["REMOTE_ADDR"]
+ @remote_host = @env["REMOTE_HOST"] || @remote_addr
+ @remote_port = @env["REMOTE_PORT"] || 0
+
+ begin
+ @header_part << request_line << CRLF
+ setup_header
+ @header_part << CRLF
+ @header_part.rewind
+ rescue Exception
+ raise CGIError, "invalid CGI environment"
+ end
+ end
+
+ def request_line
+ meth = @env["REQUEST_METHOD"] || "GET"
+ unless url = @env["REQUEST_URI"]
+ url = (@env["SCRIPT_NAME"] || File.expand_path($0)).dup
+ url << @env["PATH_INFO"].to_s
+ url = WEBrick::HTTPUtils.escape_path(url)
+ if query_string = @env["QUERY_STRING"]
+ unless query_string.empty?
+ url << "?" << query_string
+ end
+ end
+ end
+ # we cannot get real HTTP version of client ;)
+ httpv = @config[:HTTPVersion]
+ return "#{meth} #{url} HTTP/#{httpv}"
+ end
+
+ def setup_header
+ @env.each{|key, value|
+ case key
+ when "CONTENT_TYPE", "CONTENT_LENGTH"
+ add_header(key.gsub(/_/, "-"), value)
+ when /^HTTP_(.*)/
+ add_header($1.gsub(/_/, "-"), value)
+ end
+ }
+ end
+
+ def add_header(hdrname, value)
+ unless value.empty?
+ @header_part << hdrname << ": " << value << CRLF
+ end
+ end
+
+ def input
+ @header_part.eof? ? @body_part : @header_part
+ end
+
+ public
+
+ def peeraddr
+ [nil, @remote_port, @remote_host, @remote_addr]
+ end
+
+ def addr
+ [nil, @server_port, @server_name, @server_addr]
+ end
+
+ def gets(eol=LF, size=nil)
+ input.gets(eol, size)
+ end
+
+ def read(size=nil)
+ input.read(size)
+ end
+
+ def each
+ input.each{|line| yield(line) }
+ end
+
+ def eof?
+ input.eof?
+ end
+
+ def <<(data)
+ @out_port << data
+ end
+
+ def write(data)
+ @out_port.write(data)
+ end
+
+ def cert
+ return nil unless defined?(OpenSSL)
+ if pem = @env["SSL_SERVER_CERT"]
+ OpenSSL::X509::Certificate.new(pem) unless pem.empty?
+ end
+ end
+
+ def peer_cert
+ return nil unless defined?(OpenSSL)
+ if pem = @env["SSL_CLIENT_CERT"]
+ OpenSSL::X509::Certificate.new(pem) unless pem.empty?
+ end
+ end
+
+ def peer_cert_chain
+ return nil unless defined?(OpenSSL)
+ if @env["SSL_CLIENT_CERT_CHAIN_0"]
+ keys = @env.keys
+ certs = keys.sort.collect{|k|
+ if /^SSL_CLIENT_CERT_CHAIN_\d+$/ =~ k
+ if pem = @env[k]
+ OpenSSL::X509::Certificate.new(pem) unless pem.empty?
+ end
+ end
+ }
+ certs.compact
+ end
+ end
+
+ def cipher
+ return nil unless defined?(OpenSSL)
+ if cipher = @env["SSL_CIPHER"]
+ ret = [ cipher ]
+ ret << @env["SSL_PROTOCOL"]
+ ret << @env["SSL_CIPHER_USEKEYSIZE"]
+ ret << @env["SSL_CIPHER_ALGKEYSIZE"]
+ ret
+ end
+ end
+ end
+ end
+end
diff --git a/tool/lib/webrick/compat.rb b/tool/lib/webrick/compat.rb
new file mode 100644
index 0000000000..c497a1933c
--- /dev/null
+++ b/tool/lib/webrick/compat.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: false
+#
+# compat.rb -- cross platform compatibility
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2002 GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: compat.rb,v 1.6 2002/10/01 17:16:32 gotoyuzo Exp $
+
+##
+# System call error module used by webrick for cross platform compatibility.
+#
+# EPROTO:: protocol error
+# ECONNRESET:: remote host reset the connection request
+# ECONNABORTED:: Client sent TCP reset (RST) before server has accepted the
+# connection requested by client.
+#
+module Errno
+ ##
+ # Protocol error.
+
+ class EPROTO < SystemCallError; end
+
+ ##
+ # Remote host reset the connection request.
+
+ class ECONNRESET < SystemCallError; end
+
+ ##
+ # Client sent TCP reset (RST) before server has accepted the connection
+ # requested by client.
+
+ class ECONNABORTED < SystemCallError; end
+end
diff --git a/tool/lib/webrick/config.rb b/tool/lib/webrick/config.rb
new file mode 100644
index 0000000000..9f2ab44f49
--- /dev/null
+++ b/tool/lib/webrick/config.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: false
+#
+# config.rb -- Default configurations.
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: config.rb,v 1.52 2003/07/22 19:20:42 gotoyuzo Exp $
+
+require_relative 'version'
+require_relative 'httpversion'
+require_relative 'httputils'
+require_relative 'utils'
+require_relative 'log'
+
+module WEBrick
+ module Config
+ LIBDIR = File::dirname(__FILE__) # :nodoc:
+
+ # for GenericServer
+ General = Hash.new { |hash, key|
+ case key
+ when :ServerName
+ hash[key] = Utils.getservername
+ else
+ nil
+ end
+ }.update(
+ :BindAddress => nil, # "0.0.0.0" or "::" or nil
+ :Port => nil, # users MUST specify this!!
+ :MaxClients => 100, # maximum number of the concurrent connections
+ :ServerType => nil, # default: WEBrick::SimpleServer
+ :Logger => nil, # default: WEBrick::Log.new
+ :ServerSoftware => "WEBrick/#{WEBrick::VERSION} " +
+ "(Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})",
+ :TempDir => ENV['TMPDIR']||ENV['TMP']||ENV['TEMP']||'/tmp',
+ :DoNotListen => false,
+ :StartCallback => nil,
+ :StopCallback => nil,
+ :AcceptCallback => nil,
+ :DoNotReverseLookup => true,
+ :ShutdownSocketWithoutClose => false,
+ )
+
+ # for HTTPServer, HTTPRequest, HTTPResponse ...
+ HTTP = General.dup.update(
+ :Port => 80,
+ :RequestTimeout => 30,
+ :HTTPVersion => HTTPVersion.new("1.1"),
+ :AccessLog => nil,
+ :MimeTypes => HTTPUtils::DefaultMimeTypes,
+ :DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"],
+ :DocumentRoot => nil,
+ :DocumentRootOptions => { :FancyIndexing => true },
+ :RequestCallback => nil,
+ :ServerAlias => nil,
+ :InputBufferSize => 65536, # input buffer size in reading request body
+ :OutputBufferSize => 65536, # output buffer size in sending File or IO
+
+ # for HTTPProxyServer
+ :ProxyAuthProc => nil,
+ :ProxyContentHandler => nil,
+ :ProxyVia => true,
+ :ProxyTimeout => true,
+ :ProxyURI => nil,
+
+ :CGIInterpreter => nil,
+ :CGIPathEnv => nil,
+
+ # workaround: if Request-URIs contain 8bit chars,
+ # they should be escaped before calling of URI::parse().
+ :Escape8bitURI => false
+ )
+
+ ##
+ # Default configuration for WEBrick::HTTPServlet::FileHandler
+ #
+ # :AcceptableLanguages::
+ # Array of languages allowed for accept-language. There is no default
+ # :DirectoryCallback::
+ # Allows preprocessing of directory requests. There is no default
+ # callback.
+ # :FancyIndexing::
+ # If true, show an index for directories. The default is true.
+ # :FileCallback::
+ # Allows preprocessing of file requests. There is no default callback.
+ # :HandlerCallback::
+ # Allows preprocessing of requests. There is no default callback.
+ # :HandlerTable::
+ # Maps file suffixes to file handlers. DefaultFileHandler is used by
+ # default but any servlet can be used.
+ # :NondisclosureName::
+ # Do not show files matching this array of globs. .ht* and *~ are
+ # excluded by default.
+ # :UserDir::
+ # Directory inside ~user to serve content from for /~user requests.
+ # Only works if mounted on /. Disabled by default.
+
+ FileHandler = {
+ :NondisclosureName => [".ht*", "*~"],
+ :FancyIndexing => false,
+ :HandlerTable => {},
+ :HandlerCallback => nil,
+ :DirectoryCallback => nil,
+ :FileCallback => nil,
+ :UserDir => nil, # e.g. "public_html"
+ :AcceptableLanguages => [] # ["en", "ja", ... ]
+ }
+
+ ##
+ # Default configuration for WEBrick::HTTPAuth::BasicAuth
+ #
+ # :AutoReloadUserDB:: Reload the user database provided by :UserDB
+ # automatically?
+
+ BasicAuth = {
+ :AutoReloadUserDB => true,
+ }
+
+ ##
+ # Default configuration for WEBrick::HTTPAuth::DigestAuth.
+ #
+ # :Algorithm:: MD5, MD5-sess (default), SHA1, SHA1-sess
+ # :Domain:: An Array of URIs that define the protected space
+ # :Qop:: 'auth' for authentication, 'auth-int' for integrity protection or
+ # both
+ # :UseOpaque:: Should the server send opaque values to the client? This
+ # helps prevent replay attacks.
+ # :CheckNc:: Should the server check the nonce count? This helps the
+ # server detect replay attacks.
+ # :UseAuthenticationInfoHeader:: Should the server send an
+ # AuthenticationInfo header?
+ # :AutoReloadUserDB:: Reload the user database provided by :UserDB
+ # automatically?
+ # :NonceExpirePeriod:: How long should we store used nonces? Default is
+ # 30 minutes.
+ # :NonceExpireDelta:: How long is a nonce valid? Default is 1 minute
+ # :InternetExplorerHack:: Hack which allows Internet Explorer to work.
+ # :OperaHack:: Hack which allows Opera to work.
+
+ DigestAuth = {
+ :Algorithm => 'MD5-sess', # or 'MD5'
+ :Domain => nil, # an array includes domain names.
+ :Qop => [ 'auth' ], # 'auth' or 'auth-int' or both.
+ :UseOpaque => true,
+ :UseNextNonce => false,
+ :CheckNc => false,
+ :UseAuthenticationInfoHeader => true,
+ :AutoReloadUserDB => true,
+ :NonceExpirePeriod => 30*60,
+ :NonceExpireDelta => 60,
+ :InternetExplorerHack => true,
+ :OperaHack => true,
+ }
+ end
+end
diff --git a/tool/lib/webrick/cookie.rb b/tool/lib/webrick/cookie.rb
new file mode 100644
index 0000000000..5fd3bfb228
--- /dev/null
+++ b/tool/lib/webrick/cookie.rb
@@ -0,0 +1,172 @@
+# frozen_string_literal: false
+#
+# cookie.rb -- Cookie class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: cookie.rb,v 1.16 2002/09/21 12:23:35 gotoyuzo Exp $
+
+require 'time'
+require_relative 'httputils'
+
+module WEBrick
+
+ ##
+ # Processes HTTP cookies
+
+ class Cookie
+
+ ##
+ # The cookie name
+
+ attr_reader :name
+
+ ##
+ # The cookie value
+
+ attr_accessor :value
+
+ ##
+ # The cookie version
+
+ attr_accessor :version
+
+ ##
+ # The cookie domain
+ attr_accessor :domain
+
+ ##
+ # The cookie path
+
+ attr_accessor :path
+
+ ##
+ # Is this a secure cookie?
+
+ attr_accessor :secure
+
+ ##
+ # The cookie comment
+
+ attr_accessor :comment
+
+ ##
+ # The maximum age of the cookie
+
+ attr_accessor :max_age
+
+ #attr_accessor :comment_url, :discard, :port
+
+ ##
+ # Creates a new cookie with the given +name+ and +value+
+
+ def initialize(name, value)
+ @name = name
+ @value = value
+ @version = 0 # Netscape Cookie
+
+ @domain = @path = @secure = @comment = @max_age =
+ @expires = @comment_url = @discard = @port = nil
+ end
+
+ ##
+ # Sets the cookie expiration to the time +t+. The expiration time may be
+ # a false value to disable expiration or a Time or HTTP format time string
+ # to set the expiration date.
+
+ def expires=(t)
+ @expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s)
+ end
+
+ ##
+ # Retrieves the expiration time as a Time
+
+ def expires
+ @expires && Time.parse(@expires)
+ end
+
+ ##
+ # The cookie string suitable for use in an HTTP header
+
+ def to_s
+ ret = ""
+ ret << @name << "=" << @value
+ ret << "; " << "Version=" << @version.to_s if @version > 0
+ ret << "; " << "Domain=" << @domain if @domain
+ ret << "; " << "Expires=" << @expires if @expires
+ ret << "; " << "Max-Age=" << @max_age.to_s if @max_age
+ ret << "; " << "Comment=" << @comment if @comment
+ ret << "; " << "Path=" << @path if @path
+ ret << "; " << "Secure" if @secure
+ ret
+ end
+
+ ##
+ # Parses a Cookie field sent from the user-agent. Returns an array of
+ # cookies.
+
+ def self.parse(str)
+ if str
+ ret = []
+ cookie = nil
+ ver = 0
+ str.split(/;\s+/).each{|x|
+ key, val = x.split(/=/,2)
+ val = val ? HTTPUtils::dequote(val) : ""
+ case key
+ when "$Version"; ver = val.to_i
+ when "$Path"; cookie.path = val
+ when "$Domain"; cookie.domain = val
+ when "$Port"; cookie.port = val
+ else
+ ret << cookie if cookie
+ cookie = self.new(key, val)
+ cookie.version = ver
+ end
+ }
+ ret << cookie if cookie
+ ret
+ end
+ end
+
+ ##
+ # Parses the cookie in +str+
+
+ def self.parse_set_cookie(str)
+ cookie_elem = str.split(/;/)
+ first_elem = cookie_elem.shift
+ first_elem.strip!
+ key, value = first_elem.split(/=/, 2)
+ cookie = new(key, HTTPUtils.dequote(value))
+ cookie_elem.each{|pair|
+ pair.strip!
+ key, value = pair.split(/=/, 2)
+ if value
+ value = HTTPUtils.dequote(value.strip)
+ end
+ case key.downcase
+ when "domain" then cookie.domain = value
+ when "path" then cookie.path = value
+ when "expires" then cookie.expires = value
+ when "max-age" then cookie.max_age = Integer(value)
+ when "comment" then cookie.comment = value
+ when "version" then cookie.version = Integer(value)
+ when "secure" then cookie.secure = true
+ end
+ }
+ return cookie
+ end
+
+ ##
+ # Parses the cookies in +str+
+
+ def self.parse_set_cookies(str)
+ return str.split(/,(?=[^;,]*=)|,$/).collect{|c|
+ parse_set_cookie(c)
+ }
+ end
+ end
+end
diff --git a/tool/lib/webrick/htmlutils.rb b/tool/lib/webrick/htmlutils.rb
new file mode 100644
index 0000000000..ed9f4ac0d3
--- /dev/null
+++ b/tool/lib/webrick/htmlutils.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: false
+#--
+# htmlutils.rb -- HTMLUtils Module
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: htmlutils.rb,v 1.7 2002/09/21 12:23:35 gotoyuzo Exp $
+
+module WEBrick
+ module HTMLUtils
+
+ ##
+ # Escapes &, ", > and < in +string+
+
+ def escape(string)
+ return "" unless string
+ str = string.b
+ str.gsub!(/&/n, '&amp;')
+ str.gsub!(/\"/n, '&quot;')
+ str.gsub!(/>/n, '&gt;')
+ str.gsub!(/</n, '&lt;')
+ str.force_encoding(string.encoding)
+ end
+ module_function :escape
+
+ end
+end
diff --git a/tool/lib/webrick/httpauth.rb b/tool/lib/webrick/httpauth.rb
new file mode 100644
index 0000000000..f8bf09a6f1
--- /dev/null
+++ b/tool/lib/webrick/httpauth.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: false
+#
+# httpauth.rb -- HTTP access authentication
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httpauth.rb,v 1.14 2003/07/22 19:20:42 gotoyuzo Exp $
+
+require_relative 'httpauth/basicauth'
+require_relative 'httpauth/digestauth'
+require_relative 'httpauth/htpasswd'
+require_relative 'httpauth/htdigest'
+require_relative 'httpauth/htgroup'
+
+module WEBrick
+
+ ##
+ # HTTPAuth provides both basic and digest authentication.
+ #
+ # To enable authentication for requests in WEBrick you will need a user
+ # database and an authenticator. To start, here's an Htpasswd database for
+ # use with a DigestAuth authenticator:
+ #
+ # config = { :Realm => 'DigestAuth example realm' }
+ #
+ # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file'
+ # htpasswd.auth_type = WEBrick::HTTPAuth::DigestAuth
+ # htpasswd.set_passwd config[:Realm], 'username', 'password'
+ # htpasswd.flush
+ #
+ # The +:Realm+ is used to provide different access to different groups
+ # across several resources on a server. Typically you'll need only one
+ # realm for a server.
+ #
+ # This database can be used to create an authenticator:
+ #
+ # config[:UserDB] = htpasswd
+ #
+ # digest_auth = WEBrick::HTTPAuth::DigestAuth.new config
+ #
+ # To authenticate a request call #authenticate with a request and response
+ # object in a servlet:
+ #
+ # def do_GET req, res
+ # @authenticator.authenticate req, res
+ # end
+ #
+ # For digest authentication the authenticator must not be created every
+ # request, it must be passed in as an option via WEBrick::HTTPServer#mount.
+
+ module HTTPAuth
+ module_function
+
+ def _basic_auth(req, res, realm, req_field, res_field, err_type,
+ block) # :nodoc:
+ user = pass = nil
+ if /^Basic\s+(.*)/o =~ req[req_field]
+ userpass = $1
+ user, pass = userpass.unpack("m*")[0].split(":", 2)
+ end
+ if block.call(user, pass)
+ req.user = user
+ return
+ end
+ res[res_field] = "Basic realm=\"#{realm}\""
+ raise err_type
+ end
+
+ ##
+ # Simple wrapper for providing basic authentication for a request. When
+ # called with a request +req+, response +res+, authentication +realm+ and
+ # +block+ the block will be called with a +username+ and +password+. If
+ # the block returns true the request is allowed to continue, otherwise an
+ # HTTPStatus::Unauthorized error is raised.
+
+ def basic_auth(req, res, realm, &block) # :yield: username, password
+ _basic_auth(req, res, realm, "Authorization", "WWW-Authenticate",
+ HTTPStatus::Unauthorized, block)
+ end
+
+ ##
+ # Simple wrapper for providing basic authentication for a proxied request.
+ # When called with a request +req+, response +res+, authentication +realm+
+ # and +block+ the block will be called with a +username+ and +password+.
+ # If the block returns true the request is allowed to continue, otherwise
+ # an HTTPStatus::ProxyAuthenticationRequired error is raised.
+
+ def proxy_basic_auth(req, res, realm, &block) # :yield: username, password
+ _basic_auth(req, res, realm, "Proxy-Authorization", "Proxy-Authenticate",
+ HTTPStatus::ProxyAuthenticationRequired, block)
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpauth/authenticator.rb b/tool/lib/webrick/httpauth/authenticator.rb
new file mode 100644
index 0000000000..8f0eaa3aca
--- /dev/null
+++ b/tool/lib/webrick/httpauth/authenticator.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: false
+#--
+# httpauth/authenticator.rb -- Authenticator mix-in module.
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: authenticator.rb,v 1.3 2003/02/20 07:15:47 gotoyuzo Exp $
+
+module WEBrick
+ module HTTPAuth
+
+ ##
+ # Module providing generic support for both Digest and Basic
+ # authentication schemes.
+
+ module Authenticator
+
+ RequestField = "Authorization" # :nodoc:
+ ResponseField = "WWW-Authenticate" # :nodoc:
+ ResponseInfoField = "Authentication-Info" # :nodoc:
+ AuthException = HTTPStatus::Unauthorized # :nodoc:
+
+ ##
+ # Method of authentication, must be overridden by the including class
+
+ AuthScheme = nil
+
+ ##
+ # The realm this authenticator covers
+
+ attr_reader :realm
+
+ ##
+ # The user database for this authenticator
+
+ attr_reader :userdb
+
+ ##
+ # The logger for this authenticator
+
+ attr_reader :logger
+
+ private
+
+ # :stopdoc:
+
+ ##
+ # Initializes the authenticator from +config+
+
+ def check_init(config)
+ [:UserDB, :Realm].each{|sym|
+ unless config[sym]
+ raise ArgumentError, "Argument #{sym.inspect} missing."
+ end
+ }
+ @realm = config[:Realm]
+ @userdb = config[:UserDB]
+ @logger = config[:Logger] || Log::new($stderr)
+ @reload_db = config[:AutoReloadUserDB]
+ @request_field = self::class::RequestField
+ @response_field = self::class::ResponseField
+ @resp_info_field = self::class::ResponseInfoField
+ @auth_exception = self::class::AuthException
+ @auth_scheme = self::class::AuthScheme
+ end
+
+ ##
+ # Ensures +req+ has credentials that can be authenticated.
+
+ def check_scheme(req)
+ unless credentials = req[@request_field]
+ error("no credentials in the request.")
+ return nil
+ end
+ unless match = /^#{@auth_scheme}\s+/i.match(credentials)
+ error("invalid scheme in %s.", credentials)
+ info("%s: %s", @request_field, credentials) if $DEBUG
+ return nil
+ end
+ return match.post_match
+ end
+
+ def log(meth, fmt, *args)
+ msg = format("%s %s: ", @auth_scheme, @realm)
+ msg << fmt % args
+ @logger.__send__(meth, msg)
+ end
+
+ def error(fmt, *args)
+ if @logger.error?
+ log(:error, fmt, *args)
+ end
+ end
+
+ def info(fmt, *args)
+ if @logger.info?
+ log(:info, fmt, *args)
+ end
+ end
+
+ # :startdoc:
+ end
+
+ ##
+ # Module providing generic support for both Digest and Basic
+ # authentication schemes for proxies.
+
+ module ProxyAuthenticator
+ RequestField = "Proxy-Authorization" # :nodoc:
+ ResponseField = "Proxy-Authenticate" # :nodoc:
+ InfoField = "Proxy-Authentication-Info" # :nodoc:
+ AuthException = HTTPStatus::ProxyAuthenticationRequired # :nodoc:
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpauth/basicauth.rb b/tool/lib/webrick/httpauth/basicauth.rb
new file mode 100644
index 0000000000..7d0a9cfc8f
--- /dev/null
+++ b/tool/lib/webrick/httpauth/basicauth.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: false
+#
+# httpauth/basicauth.rb -- HTTP basic access authentication
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: basicauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $
+
+require_relative '../config'
+require_relative '../httpstatus'
+require_relative 'authenticator'
+
+module WEBrick
+ module HTTPAuth
+
+ ##
+ # Basic Authentication for WEBrick
+ #
+ # Use this class to add basic authentication to a WEBrick servlet.
+ #
+ # Here is an example of how to set up a BasicAuth:
+ #
+ # config = { :Realm => 'BasicAuth example realm' }
+ #
+ # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file', password_hash: :bcrypt
+ # htpasswd.set_passwd config[:Realm], 'username', 'password'
+ # htpasswd.flush
+ #
+ # config[:UserDB] = htpasswd
+ #
+ # basic_auth = WEBrick::HTTPAuth::BasicAuth.new config
+
+ class BasicAuth
+ include Authenticator
+
+ AuthScheme = "Basic" # :nodoc:
+
+ ##
+ # Used by UserDB to create a basic password entry
+
+ def self.make_passwd(realm, user, pass)
+ pass ||= ""
+ pass.crypt(Utils::random_string(2))
+ end
+
+ attr_reader :realm, :userdb, :logger
+
+ ##
+ # Creates a new BasicAuth instance.
+ #
+ # See WEBrick::Config::BasicAuth for default configuration entries
+ #
+ # You must supply the following configuration entries:
+ #
+ # :Realm:: The name of the realm being protected.
+ # :UserDB:: A database of usernames and passwords.
+ # A WEBrick::HTTPAuth::Htpasswd instance should be used.
+
+ def initialize(config, default=Config::BasicAuth)
+ check_init(config)
+ @config = default.dup.update(config)
+ end
+
+ ##
+ # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if
+ # the authentication was not correct.
+
+ def authenticate(req, res)
+ unless basic_credentials = check_scheme(req)
+ challenge(req, res)
+ end
+ userid, password = basic_credentials.unpack("m*")[0].split(":", 2)
+ password ||= ""
+ if userid.empty?
+ error("user id was not given.")
+ challenge(req, res)
+ end
+ unless encpass = @userdb.get_passwd(@realm, userid, @reload_db)
+ error("%s: the user is not allowed.", userid)
+ challenge(req, res)
+ end
+
+ case encpass
+ when /\A\$2[aby]\$/
+ password_matches = BCrypt::Password.new(encpass.sub(/\A\$2[aby]\$/, '$2a$')) == password
+ else
+ password_matches = password.crypt(encpass) == encpass
+ end
+
+ unless password_matches
+ error("%s: password unmatch.", userid)
+ challenge(req, res)
+ end
+ info("%s: authentication succeeded.", userid)
+ req.user = userid
+ end
+
+ ##
+ # Returns a challenge response which asks for authentication information
+
+ def challenge(req, res)
+ res[@response_field] = "#{@auth_scheme} realm=\"#{@realm}\""
+ raise @auth_exception
+ end
+ end
+
+ ##
+ # Basic authentication for proxy servers. See BasicAuth for details.
+
+ class ProxyBasicAuth < BasicAuth
+ include ProxyAuthenticator
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpauth/digestauth.rb b/tool/lib/webrick/httpauth/digestauth.rb
new file mode 100644
index 0000000000..3cf12899d2
--- /dev/null
+++ b/tool/lib/webrick/httpauth/digestauth.rb
@@ -0,0 +1,395 @@
+# frozen_string_literal: false
+#
+# httpauth/digestauth.rb -- HTTP digest access authentication
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2003 Internet Programming with Ruby writers.
+# Copyright (c) 2003 H.M.
+#
+# The original implementation is provided by H.M.
+# URL: http://rwiki.jin.gr.jp/cgi-bin/rw-cgi.rb?cmd=view;name=
+# %C7%A7%BE%DA%B5%A1%C7%BD%A4%F2%B2%FE%C2%A4%A4%B7%A4%C6%A4%DF%A4%EB
+#
+# $IPR: digestauth.rb,v 1.5 2003/02/20 07:15:47 gotoyuzo Exp $
+
+require_relative '../config'
+require_relative '../httpstatus'
+require_relative 'authenticator'
+require 'digest/md5'
+require 'digest/sha1'
+
+module WEBrick
+ module HTTPAuth
+
+ ##
+ # RFC 2617 Digest Access Authentication for WEBrick
+ #
+ # Use this class to add digest authentication to a WEBrick servlet.
+ #
+ # Here is an example of how to set up DigestAuth:
+ #
+ # config = { :Realm => 'DigestAuth example realm' }
+ #
+ # htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
+ # htdigest.set_passwd config[:Realm], 'username', 'password'
+ # htdigest.flush
+ #
+ # config[:UserDB] = htdigest
+ #
+ # digest_auth = WEBrick::HTTPAuth::DigestAuth.new config
+ #
+ # When using this as with a servlet be sure not to create a new DigestAuth
+ # object in the servlet's #initialize. By default WEBrick creates a new
+ # servlet instance for every request and the DigestAuth object must be
+ # used across requests.
+
+ class DigestAuth
+ include Authenticator
+
+ AuthScheme = "Digest" # :nodoc:
+
+ ##
+ # Struct containing the opaque portion of the digest authentication
+
+ OpaqueInfo = Struct.new(:time, :nonce, :nc) # :nodoc:
+
+ ##
+ # Digest authentication algorithm
+
+ attr_reader :algorithm
+
+ ##
+ # Quality of protection. RFC 2617 defines "auth" and "auth-int"
+
+ attr_reader :qop
+
+ ##
+ # Used by UserDB to create a digest password entry
+
+ def self.make_passwd(realm, user, pass)
+ pass ||= ""
+ Digest::MD5::hexdigest([user, realm, pass].join(":"))
+ end
+
+ ##
+ # Creates a new DigestAuth instance. Be sure to use the same DigestAuth
+ # instance for multiple requests as it saves state between requests in
+ # order to perform authentication.
+ #
+ # See WEBrick::Config::DigestAuth for default configuration entries
+ #
+ # You must supply the following configuration entries:
+ #
+ # :Realm:: The name of the realm being protected.
+ # :UserDB:: A database of usernames and passwords.
+ # A WEBrick::HTTPAuth::Htdigest instance should be used.
+
+ def initialize(config, default=Config::DigestAuth)
+ check_init(config)
+ @config = default.dup.update(config)
+ @algorithm = @config[:Algorithm]
+ @domain = @config[:Domain]
+ @qop = @config[:Qop]
+ @use_opaque = @config[:UseOpaque]
+ @use_next_nonce = @config[:UseNextNonce]
+ @check_nc = @config[:CheckNc]
+ @use_auth_info_header = @config[:UseAuthenticationInfoHeader]
+ @nonce_expire_period = @config[:NonceExpirePeriod]
+ @nonce_expire_delta = @config[:NonceExpireDelta]
+ @internet_explorer_hack = @config[:InternetExplorerHack]
+
+ case @algorithm
+ when 'MD5','MD5-sess'
+ @h = Digest::MD5
+ when 'SHA1','SHA1-sess' # it is a bonus feature :-)
+ @h = Digest::SHA1
+ else
+ msg = format('Algorithm "%s" is not supported.', @algorithm)
+ raise ArgumentError.new(msg)
+ end
+
+ @instance_key = hexdigest(self.__id__, Time.now.to_i, Process.pid)
+ @opaques = {}
+ @last_nonce_expire = Time.now
+ @mutex = Thread::Mutex.new
+ end
+
+ ##
+ # Authenticates a +req+ and returns a 401 Unauthorized using +res+ if
+ # the authentication was not correct.
+
+ def authenticate(req, res)
+ unless result = @mutex.synchronize{ _authenticate(req, res) }
+ challenge(req, res)
+ end
+ if result == :nonce_is_stale
+ challenge(req, res, true)
+ end
+ return true
+ end
+
+ ##
+ # Returns a challenge response which asks for authentication information
+
+ def challenge(req, res, stale=false)
+ nonce = generate_next_nonce(req)
+ if @use_opaque
+ opaque = generate_opaque(req)
+ @opaques[opaque].nonce = nonce
+ end
+
+ param = Hash.new
+ param["realm"] = HTTPUtils::quote(@realm)
+ param["domain"] = HTTPUtils::quote(@domain.to_a.join(" ")) if @domain
+ param["nonce"] = HTTPUtils::quote(nonce)
+ param["opaque"] = HTTPUtils::quote(opaque) if opaque
+ param["stale"] = stale.to_s
+ param["algorithm"] = @algorithm
+ param["qop"] = HTTPUtils::quote(@qop.to_a.join(",")) if @qop
+
+ res[@response_field] =
+ "#{@auth_scheme} " + param.map{|k,v| "#{k}=#{v}" }.join(", ")
+ info("%s: %s", @response_field, res[@response_field]) if $DEBUG
+ raise @auth_exception
+ end
+
+ private
+
+ # :stopdoc:
+
+ MustParams = ['username','realm','nonce','uri','response']
+ MustParamsAuth = ['cnonce','nc']
+
+ def _authenticate(req, res)
+ unless digest_credentials = check_scheme(req)
+ return false
+ end
+
+ auth_req = split_param_value(digest_credentials)
+ if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
+ req_params = MustParams + MustParamsAuth
+ else
+ req_params = MustParams
+ end
+ req_params.each{|key|
+ unless auth_req.has_key?(key)
+ error('%s: parameter missing. "%s"', auth_req['username'], key)
+ raise HTTPStatus::BadRequest
+ end
+ }
+
+ if !check_uri(req, auth_req)
+ raise HTTPStatus::BadRequest
+ end
+
+ if auth_req['realm'] != @realm
+ error('%s: realm unmatch. "%s" for "%s"',
+ auth_req['username'], auth_req['realm'], @realm)
+ return false
+ end
+
+ auth_req['algorithm'] ||= 'MD5'
+ if auth_req['algorithm'].upcase != @algorithm.upcase
+ error('%s: algorithm unmatch. "%s" for "%s"',
+ auth_req['username'], auth_req['algorithm'], @algorithm)
+ return false
+ end
+
+ if (@qop.nil? && auth_req.has_key?('qop')) ||
+ (@qop && (! @qop.member?(auth_req['qop'])))
+ error('%s: the qop is not allowed. "%s"',
+ auth_req['username'], auth_req['qop'])
+ return false
+ end
+
+ password = @userdb.get_passwd(@realm, auth_req['username'], @reload_db)
+ unless password
+ error('%s: the user is not allowed.', auth_req['username'])
+ return false
+ end
+
+ nonce_is_invalid = false
+ if @use_opaque
+ info("@opaque = %s", @opaque.inspect) if $DEBUG
+ if !(opaque = auth_req['opaque'])
+ error('%s: opaque is not given.', auth_req['username'])
+ nonce_is_invalid = true
+ elsif !(opaque_struct = @opaques[opaque])
+ error('%s: invalid opaque is given.', auth_req['username'])
+ nonce_is_invalid = true
+ elsif !check_opaque(opaque_struct, req, auth_req)
+ @opaques.delete(auth_req['opaque'])
+ nonce_is_invalid = true
+ end
+ elsif !check_nonce(req, auth_req)
+ nonce_is_invalid = true
+ end
+
+ if /-sess$/i =~ auth_req['algorithm']
+ ha1 = hexdigest(password, auth_req['nonce'], auth_req['cnonce'])
+ else
+ ha1 = password
+ end
+
+ if auth_req['qop'] == "auth" || auth_req['qop'] == nil
+ ha2 = hexdigest(req.request_method, auth_req['uri'])
+ ha2_res = hexdigest("", auth_req['uri'])
+ elsif auth_req['qop'] == "auth-int"
+ body_digest = @h.new
+ req.body { |chunk| body_digest.update(chunk) }
+ body_digest = body_digest.hexdigest
+ ha2 = hexdigest(req.request_method, auth_req['uri'], body_digest)
+ ha2_res = hexdigest("", auth_req['uri'], body_digest)
+ end
+
+ if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
+ param2 = ['nonce', 'nc', 'cnonce', 'qop'].map{|key|
+ auth_req[key]
+ }.join(':')
+ digest = hexdigest(ha1, param2, ha2)
+ digest_res = hexdigest(ha1, param2, ha2_res)
+ else
+ digest = hexdigest(ha1, auth_req['nonce'], ha2)
+ digest_res = hexdigest(ha1, auth_req['nonce'], ha2_res)
+ end
+
+ if digest != auth_req['response']
+ error("%s: digest unmatch.", auth_req['username'])
+ return false
+ elsif nonce_is_invalid
+ error('%s: digest is valid, but nonce is not valid.',
+ auth_req['username'])
+ return :nonce_is_stale
+ elsif @use_auth_info_header
+ auth_info = {
+ 'nextnonce' => generate_next_nonce(req),
+ 'rspauth' => digest_res
+ }
+ if @use_opaque
+ opaque_struct.time = req.request_time
+ opaque_struct.nonce = auth_info['nextnonce']
+ opaque_struct.nc = "%08x" % (auth_req['nc'].hex + 1)
+ end
+ if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
+ ['qop','cnonce','nc'].each{|key|
+ auth_info[key] = auth_req[key]
+ }
+ end
+ res[@resp_info_field] = auth_info.keys.map{|key|
+ if key == 'nc'
+ key + '=' + auth_info[key]
+ else
+ key + "=" + HTTPUtils::quote(auth_info[key])
+ end
+ }.join(', ')
+ end
+ info('%s: authentication succeeded.', auth_req['username'])
+ req.user = auth_req['username']
+ return true
+ end
+
+ def split_param_value(string)
+ ret = {}
+ string.scan(/\G\s*([\w\-.*%!]+)=\s*(?:\"((?>\\.|[^\"])*)\"|([^,\"]*))\s*,?/) do
+ ret[$1] = $3 || $2.gsub(/\\(.)/, "\\1")
+ end
+ ret
+ end
+
+ def generate_next_nonce(req)
+ now = "%012d" % req.request_time.to_i
+ pk = hexdigest(now, @instance_key)[0,32]
+ nonce = [now + ":" + pk].pack("m0") # it has 60 length of chars.
+ nonce
+ end
+
+ def check_nonce(req, auth_req)
+ username = auth_req['username']
+ nonce = auth_req['nonce']
+
+ pub_time, pk = nonce.unpack("m*")[0].split(":", 2)
+ if (!pub_time || !pk)
+ error("%s: empty nonce is given", username)
+ return false
+ elsif (hexdigest(pub_time, @instance_key)[0,32] != pk)
+ error("%s: invalid private-key: %s for %s",
+ username, hexdigest(pub_time, @instance_key)[0,32], pk)
+ return false
+ end
+
+ diff_time = req.request_time.to_i - pub_time.to_i
+ if (diff_time < 0)
+ error("%s: difference of time-stamp is negative.", username)
+ return false
+ elsif diff_time > @nonce_expire_period
+ error("%s: nonce is expired.", username)
+ return false
+ end
+
+ return true
+ end
+
+ def generate_opaque(req)
+ @mutex.synchronize{
+ now = req.request_time
+ if now - @last_nonce_expire > @nonce_expire_delta
+ @opaques.delete_if{|key,val|
+ (now - val.time) > @nonce_expire_period
+ }
+ @last_nonce_expire = now
+ end
+ begin
+ opaque = Utils::random_string(16)
+ end while @opaques[opaque]
+ @opaques[opaque] = OpaqueInfo.new(now, nil, '00000001')
+ opaque
+ }
+ end
+
+ def check_opaque(opaque_struct, req, auth_req)
+ if (@use_next_nonce && auth_req['nonce'] != opaque_struct.nonce)
+ error('%s: nonce unmatched. "%s" for "%s"',
+ auth_req['username'], auth_req['nonce'], opaque_struct.nonce)
+ return false
+ elsif !check_nonce(req, auth_req)
+ return false
+ end
+ if (@check_nc && auth_req['nc'] != opaque_struct.nc)
+ error('%s: nc unmatched."%s" for "%s"',
+ auth_req['username'], auth_req['nc'], opaque_struct.nc)
+ return false
+ end
+ true
+ end
+
+ def check_uri(req, auth_req)
+ uri = auth_req['uri']
+ if uri != req.request_uri.to_s && uri != req.unparsed_uri &&
+ (@internet_explorer_hack && uri != req.path)
+ error('%s: uri unmatch. "%s" for "%s"', auth_req['username'],
+ auth_req['uri'], req.request_uri.to_s)
+ return false
+ end
+ true
+ end
+
+ def hexdigest(*args)
+ @h.hexdigest(args.join(":"))
+ end
+
+ # :startdoc:
+ end
+
+ ##
+ # Digest authentication for proxy servers. See DigestAuth for details.
+
+ class ProxyDigestAuth < DigestAuth
+ include ProxyAuthenticator
+
+ private
+ def check_uri(req, auth_req) # :nodoc:
+ return true
+ end
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpauth/htdigest.rb b/tool/lib/webrick/httpauth/htdigest.rb
new file mode 100644
index 0000000000..93b18e2c75
--- /dev/null
+++ b/tool/lib/webrick/httpauth/htdigest.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: false
+#
+# httpauth/htdigest.rb -- Apache compatible htdigest file
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: htdigest.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $
+
+require_relative 'userdb'
+require_relative 'digestauth'
+require 'tempfile'
+
+module WEBrick
+ module HTTPAuth
+
+ ##
+ # Htdigest accesses apache-compatible digest password files. Passwords are
+ # matched to a realm where they are valid. For security, the path for a
+ # digest password database should be stored outside of the paths available
+ # to the HTTP server.
+ #
+ # Htdigest is intended for use with WEBrick::HTTPAuth::DigestAuth and
+ # stores passwords using cryptographic hashes.
+ #
+ # htpasswd = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
+ # htpasswd.set_passwd 'my realm', 'username', 'password'
+ # htpasswd.flush
+
+ class Htdigest
+ include UserDB
+
+ ##
+ # Open a digest password database at +path+
+
+ def initialize(path)
+ @path = path
+ @mtime = Time.at(0)
+ @digest = Hash.new
+ @mutex = Thread::Mutex::new
+ @auth_type = DigestAuth
+ File.open(@path,"a").close unless File.exist?(@path)
+ reload
+ end
+
+ ##
+ # Reloads passwords from the database
+
+ def reload
+ mtime = File::mtime(@path)
+ if mtime > @mtime
+ @digest.clear
+ File.open(@path){|io|
+ while line = io.gets
+ line.chomp!
+ user, realm, pass = line.split(/:/, 3)
+ unless @digest[realm]
+ @digest[realm] = Hash.new
+ end
+ @digest[realm][user] = pass
+ end
+ }
+ @mtime = mtime
+ end
+ end
+
+ ##
+ # Flush the password database. If +output+ is given the database will
+ # be written there instead of to the original path.
+
+ def flush(output=nil)
+ output ||= @path
+ tmp = Tempfile.create("htpasswd", File::dirname(output))
+ renamed = false
+ begin
+ each{|item| tmp.puts(item.join(":")) }
+ tmp.close
+ File::rename(tmp.path, output)
+ renamed = true
+ ensure
+ tmp.close
+ File.unlink(tmp.path) if !renamed
+ end
+ end
+
+ ##
+ # Retrieves a password from the database for +user+ in +realm+. If
+ # +reload_db+ is true the database will be reloaded first.
+
+ def get_passwd(realm, user, reload_db)
+ reload() if reload_db
+ if hash = @digest[realm]
+ hash[user]
+ end
+ end
+
+ ##
+ # Sets a password in the database for +user+ in +realm+ to +pass+.
+
+ def set_passwd(realm, user, pass)
+ @mutex.synchronize{
+ unless @digest[realm]
+ @digest[realm] = Hash.new
+ end
+ @digest[realm][user] = make_passwd(realm, user, pass)
+ }
+ end
+
+ ##
+ # Removes a password from the database for +user+ in +realm+.
+
+ def delete_passwd(realm, user)
+ if hash = @digest[realm]
+ hash.delete(user)
+ end
+ end
+
+ ##
+ # Iterate passwords in the database.
+
+ def each # :yields: [user, realm, password_hash]
+ @digest.keys.sort.each{|realm|
+ hash = @digest[realm]
+ hash.keys.sort.each{|user|
+ yield([user, realm, hash[user]])
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpauth/htgroup.rb b/tool/lib/webrick/httpauth/htgroup.rb
new file mode 100644
index 0000000000..e06c441b18
--- /dev/null
+++ b/tool/lib/webrick/httpauth/htgroup.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: false
+#
+# httpauth/htgroup.rb -- Apache compatible htgroup file
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: htgroup.rb,v 1.1 2003/02/16 22:22:56 gotoyuzo Exp $
+
+require 'tempfile'
+
+module WEBrick
+ module HTTPAuth
+
+ ##
+ # Htgroup accesses apache-compatible group files. Htgroup can be used to
+ # provide group-based authentication for users. Currently Htgroup is not
+ # directly integrated with any authenticators in WEBrick. For security,
+ # the path for a digest password database should be stored outside of the
+ # paths available to the HTTP server.
+ #
+ # Example:
+ #
+ # htgroup = WEBrick::HTTPAuth::Htgroup.new 'my_group_file'
+ # htgroup.add 'superheroes', %w[spiderman batman]
+ #
+ # htgroup.members('superheroes').include? 'magneto' # => false
+
+ class Htgroup
+
+ ##
+ # Open a group database at +path+
+
+ def initialize(path)
+ @path = path
+ @mtime = Time.at(0)
+ @group = Hash.new
+ File.open(@path,"a").close unless File.exist?(@path)
+ reload
+ end
+
+ ##
+ # Reload groups from the database
+
+ def reload
+ if (mtime = File::mtime(@path)) > @mtime
+ @group.clear
+ File.open(@path){|io|
+ while line = io.gets
+ line.chomp!
+ group, members = line.split(/:\s*/)
+ @group[group] = members.split(/\s+/)
+ end
+ }
+ @mtime = mtime
+ end
+ end
+
+ ##
+ # Flush the group database. If +output+ is given the database will be
+ # written there instead of to the original path.
+
+ def flush(output=nil)
+ output ||= @path
+ tmp = Tempfile.create("htgroup", File::dirname(output))
+ begin
+ @group.keys.sort.each{|group|
+ tmp.puts(format("%s: %s", group, self.members(group).join(" ")))
+ }
+ ensure
+ tmp.close
+ if $!
+ File.unlink(tmp.path)
+ else
+ return File.rename(tmp.path, output)
+ end
+ end
+ end
+
+ ##
+ # Retrieve the list of members from +group+
+
+ def members(group)
+ reload
+ @group[group] || []
+ end
+
+ ##
+ # Add an Array of +members+ to +group+
+
+ def add(group, members)
+ @group[group] = members(group) | members
+ end
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpauth/htpasswd.rb b/tool/lib/webrick/httpauth/htpasswd.rb
new file mode 100644
index 0000000000..abca30532e
--- /dev/null
+++ b/tool/lib/webrick/httpauth/htpasswd.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: false
+#
+# httpauth/htpasswd -- Apache compatible htpasswd file
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: htpasswd.rb,v 1.4 2003/07/22 19:20:45 gotoyuzo Exp $
+
+require_relative 'userdb'
+require_relative 'basicauth'
+require 'tempfile'
+
+module WEBrick
+ module HTTPAuth
+
+ ##
+ # Htpasswd accesses apache-compatible password files. Passwords are
+ # matched to a realm where they are valid. For security, the path for a
+ # password database should be stored outside of the paths available to the
+ # HTTP server.
+ #
+ # Htpasswd is intended for use with WEBrick::HTTPAuth::BasicAuth.
+ #
+ # To create an Htpasswd database with a single user:
+ #
+ # htpasswd = WEBrick::HTTPAuth::Htpasswd.new 'my_password_file'
+ # htpasswd.set_passwd 'my realm', 'username', 'password'
+ # htpasswd.flush
+
+ class Htpasswd
+ include UserDB
+
+ ##
+ # Open a password database at +path+
+
+ def initialize(path, password_hash: nil)
+ @path = path
+ @mtime = Time.at(0)
+ @passwd = Hash.new
+ @auth_type = BasicAuth
+ @password_hash = password_hash
+
+ case @password_hash
+ when nil
+ # begin
+ # require "string/crypt"
+ # rescue LoadError
+ # warn("Unable to load string/crypt, proceeding with deprecated use of String#crypt, consider using password_hash: :bcrypt")
+ # end
+ @password_hash = :crypt
+ when :crypt
+ # require "string/crypt"
+ when :bcrypt
+ require "bcrypt"
+ else
+ raise ArgumentError, "only :crypt and :bcrypt are supported for password_hash keyword argument"
+ end
+
+ File.open(@path,"a").close unless File.exist?(@path)
+ reload
+ end
+
+ ##
+ # Reload passwords from the database
+
+ def reload
+ mtime = File::mtime(@path)
+ if mtime > @mtime
+ @passwd.clear
+ File.open(@path){|io|
+ while line = io.gets
+ line.chomp!
+ case line
+ when %r!\A[^:]+:[a-zA-Z0-9./]{13}\z!
+ if @password_hash == :bcrypt
+ raise StandardError, ".htpasswd file contains crypt password, only bcrypt passwords supported"
+ end
+ user, pass = line.split(":")
+ when %r!\A[^:]+:\$2[aby]\$\d{2}\$.{53}\z!
+ if @password_hash == :crypt
+ raise StandardError, ".htpasswd file contains bcrypt password, only crypt passwords supported"
+ end
+ user, pass = line.split(":")
+ when /:\$/, /:{SHA}/
+ raise NotImplementedError,
+ 'MD5, SHA1 .htpasswd file not supported'
+ else
+ raise StandardError, 'bad .htpasswd file'
+ end
+ @passwd[user] = pass
+ end
+ }
+ @mtime = mtime
+ end
+ end
+
+ ##
+ # Flush the password database. If +output+ is given the database will
+ # be written there instead of to the original path.
+
+ def flush(output=nil)
+ output ||= @path
+ tmp = Tempfile.create("htpasswd", File::dirname(output))
+ renamed = false
+ begin
+ each{|item| tmp.puts(item.join(":")) }
+ tmp.close
+ File::rename(tmp.path, output)
+ renamed = true
+ ensure
+ tmp.close
+ File.unlink(tmp.path) if !renamed
+ end
+ end
+
+ ##
+ # Retrieves a password from the database for +user+ in +realm+. If
+ # +reload_db+ is true the database will be reloaded first.
+
+ def get_passwd(realm, user, reload_db)
+ reload() if reload_db
+ @passwd[user]
+ end
+
+ ##
+ # Sets a password in the database for +user+ in +realm+ to +pass+.
+
+ def set_passwd(realm, user, pass)
+ if @password_hash == :bcrypt
+ # Cost of 5 to match Apache default, and because the
+ # bcrypt default of 10 will introduce significant delays
+ # for every request.
+ @passwd[user] = BCrypt::Password.create(pass, :cost=>5)
+ else
+ @passwd[user] = make_passwd(realm, user, pass)
+ end
+ end
+
+ ##
+ # Removes a password from the database for +user+ in +realm+.
+
+ def delete_passwd(realm, user)
+ @passwd.delete(user)
+ end
+
+ ##
+ # Iterate passwords in the database.
+
+ def each # :yields: [user, password]
+ @passwd.keys.sort.each{|user|
+ yield([user, @passwd[user]])
+ }
+ end
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpauth/userdb.rb b/tool/lib/webrick/httpauth/userdb.rb
new file mode 100644
index 0000000000..7a17715cdf
--- /dev/null
+++ b/tool/lib/webrick/httpauth/userdb.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: false
+#--
+# httpauth/userdb.rb -- UserDB mix-in module.
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: userdb.rb,v 1.2 2003/02/20 07:15:48 gotoyuzo Exp $
+
+module WEBrick
+ module HTTPAuth
+
+ ##
+ # User database mixin for HTTPAuth. This mixin dispatches user record
+ # access to the underlying auth_type for this database.
+
+ module UserDB
+
+ ##
+ # The authentication type.
+ #
+ # WEBrick::HTTPAuth::BasicAuth or WEBrick::HTTPAuth::DigestAuth are
+ # built-in.
+
+ attr_accessor :auth_type
+
+ ##
+ # Creates an obscured password in +realm+ with +user+ and +password+
+ # using the auth_type of this database.
+
+ def make_passwd(realm, user, pass)
+ @auth_type::make_passwd(realm, user, pass)
+ end
+
+ ##
+ # Sets a password in +realm+ with +user+ and +password+ for the
+ # auth_type of this database.
+
+ def set_passwd(realm, user, pass)
+ self[user] = pass
+ end
+
+ ##
+ # Retrieves a password in +realm+ for +user+ for the auth_type of this
+ # database. +reload_db+ is a dummy value.
+
+ def get_passwd(realm, user, reload_db=false)
+ make_passwd(realm, user, self[user])
+ end
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpproxy.rb b/tool/lib/webrick/httpproxy.rb
new file mode 100644
index 0000000000..7607c3df88
--- /dev/null
+++ b/tool/lib/webrick/httpproxy.rb
@@ -0,0 +1,354 @@
+# frozen_string_literal: false
+#
+# httpproxy.rb -- HTTPProxy Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2002 GOTO Kentaro
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httpproxy.rb,v 1.18 2003/03/08 18:58:10 gotoyuzo Exp $
+# $kNotwork: straw.rb,v 1.3 2002/02/12 15:13:07 gotoken Exp $
+
+require_relative "httpserver"
+require "net/http"
+
+module WEBrick
+
+ NullReader = Object.new # :nodoc:
+ class << NullReader # :nodoc:
+ def read(*args)
+ nil
+ end
+ alias gets read
+ end
+
+ FakeProxyURI = Object.new # :nodoc:
+ class << FakeProxyURI # :nodoc:
+ def method_missing(meth, *args)
+ if %w(scheme host port path query userinfo).member?(meth.to_s)
+ return nil
+ end
+ super
+ end
+ end
+
+ # :startdoc:
+
+ ##
+ # An HTTP Proxy server which proxies GET, HEAD and POST requests.
+ #
+ # To create a simple proxy server:
+ #
+ # require 'webrick'
+ # require 'webrick/httpproxy'
+ #
+ # proxy = WEBrick::HTTPProxyServer.new Port: 8000
+ #
+ # trap 'INT' do proxy.shutdown end
+ # trap 'TERM' do proxy.shutdown end
+ #
+ # proxy.start
+ #
+ # See ::new for proxy-specific configuration items.
+ #
+ # == Modifying proxied responses
+ #
+ # To modify content the proxy server returns use the +:ProxyContentHandler+
+ # option:
+ #
+ # handler = proc do |req, res|
+ # if res['content-type'] == 'text/plain' then
+ # res.body << "\nThis content was proxied!\n"
+ # end
+ # end
+ #
+ # proxy =
+ # WEBrick::HTTPProxyServer.new Port: 8000, ProxyContentHandler: handler
+
+ class HTTPProxyServer < HTTPServer
+
+ ##
+ # Proxy server configurations. The proxy server handles the following
+ # configuration items in addition to those supported by HTTPServer:
+ #
+ # :ProxyAuthProc:: Called with a request and response to authorize a
+ # request
+ # :ProxyVia:: Appended to the via header
+ # :ProxyURI:: The proxy server's URI
+ # :ProxyContentHandler:: Called with a request and response and allows
+ # modification of the response
+ # :ProxyTimeout:: Sets the proxy timeouts to 30 seconds for open and 60
+ # seconds for read operations
+
+ def initialize(config={}, default=Config::HTTP)
+ super(config, default)
+ c = @config
+ @via = "#{c[:HTTPVersion]} #{c[:ServerName]}:#{c[:Port]}"
+ end
+
+ # :stopdoc:
+ def service(req, res)
+ if req.request_method == "CONNECT"
+ do_CONNECT(req, res)
+ elsif req.unparsed_uri =~ %r!^http://!
+ proxy_service(req, res)
+ else
+ super(req, res)
+ end
+ end
+
+ def proxy_auth(req, res)
+ if proc = @config[:ProxyAuthProc]
+ proc.call(req, res)
+ end
+ req.header.delete("proxy-authorization")
+ end
+
+ def proxy_uri(req, res)
+ # should return upstream proxy server's URI
+ return @config[:ProxyURI]
+ end
+
+ def proxy_service(req, res)
+ # Proxy Authentication
+ proxy_auth(req, res)
+
+ begin
+ public_send("do_#{req.request_method}", req, res)
+ rescue NoMethodError
+ raise HTTPStatus::MethodNotAllowed,
+ "unsupported method `#{req.request_method}'."
+ rescue => err
+ logger.debug("#{err.class}: #{err.message}")
+ raise HTTPStatus::ServiceUnavailable, err.message
+ end
+
+ # Process contents
+ if handler = @config[:ProxyContentHandler]
+ handler.call(req, res)
+ end
+ end
+
+ def do_CONNECT(req, res)
+ # Proxy Authentication
+ proxy_auth(req, res)
+
+ ua = Thread.current[:WEBrickSocket] # User-Agent
+ raise HTTPStatus::InternalServerError,
+ "[BUG] cannot get socket" unless ua
+
+ host, port = req.unparsed_uri.split(":", 2)
+ # Proxy authentication for upstream proxy server
+ if proxy = proxy_uri(req, res)
+ proxy_request_line = "CONNECT #{host}:#{port} HTTP/1.0"
+ if proxy.userinfo
+ credentials = "Basic " + [proxy.userinfo].pack("m0")
+ end
+ host, port = proxy.host, proxy.port
+ end
+
+ begin
+ @logger.debug("CONNECT: upstream proxy is `#{host}:#{port}'.")
+ os = TCPSocket.new(host, port) # origin server
+
+ if proxy
+ @logger.debug("CONNECT: sending a Request-Line")
+ os << proxy_request_line << CRLF
+ @logger.debug("CONNECT: > #{proxy_request_line}")
+ if credentials
+ @logger.debug("CONNECT: sending credentials")
+ os << "Proxy-Authorization: " << credentials << CRLF
+ end
+ os << CRLF
+ proxy_status_line = os.gets(LF)
+ @logger.debug("CONNECT: read Status-Line from the upstream server")
+ @logger.debug("CONNECT: < #{proxy_status_line}")
+ if %r{^HTTP/\d+\.\d+\s+200\s*} =~ proxy_status_line
+ while line = os.gets(LF)
+ break if /\A(#{CRLF}|#{LF})\z/om =~ line
+ end
+ else
+ raise HTTPStatus::BadGateway
+ end
+ end
+ @logger.debug("CONNECT #{host}:#{port}: succeeded")
+ res.status = HTTPStatus::RC_OK
+ rescue => ex
+ @logger.debug("CONNECT #{host}:#{port}: failed `#{ex.message}'")
+ res.set_error(ex)
+ raise HTTPStatus::EOFError
+ ensure
+ if handler = @config[:ProxyContentHandler]
+ handler.call(req, res)
+ end
+ res.send_response(ua)
+ access_log(@config, req, res)
+
+ # Should clear request-line not to send the response twice.
+ # see: HTTPServer#run
+ req.parse(NullReader) rescue nil
+ end
+
+ begin
+ while fds = IO::select([ua, os])
+ if fds[0].member?(ua)
+ buf = ua.readpartial(1024);
+ @logger.debug("CONNECT: #{buf.bytesize} byte from User-Agent")
+ os.write(buf)
+ elsif fds[0].member?(os)
+ buf = os.readpartial(1024);
+ @logger.debug("CONNECT: #{buf.bytesize} byte from #{host}:#{port}")
+ ua.write(buf)
+ end
+ end
+ rescue
+ os.close
+ @logger.debug("CONNECT #{host}:#{port}: closed")
+ end
+
+ raise HTTPStatus::EOFError
+ end
+
+ def do_GET(req, res)
+ perform_proxy_request(req, res, Net::HTTP::Get)
+ end
+
+ def do_HEAD(req, res)
+ perform_proxy_request(req, res, Net::HTTP::Head)
+ end
+
+ def do_POST(req, res)
+ perform_proxy_request(req, res, Net::HTTP::Post, req.body_reader)
+ end
+
+ def do_OPTIONS(req, res)
+ res['allow'] = "GET,HEAD,POST,OPTIONS,CONNECT"
+ end
+
+ private
+
+ # Some header fields should not be transferred.
+ HopByHop = %w( connection keep-alive proxy-authenticate upgrade
+ proxy-authorization te trailers transfer-encoding )
+ ShouldNotTransfer = %w( set-cookie proxy-connection )
+ def split_field(f) f ? f.split(/,\s+/).collect{|i| i.downcase } : [] end
+
+ def choose_header(src, dst)
+ connections = split_field(src['connection'])
+ src.each{|key, value|
+ key = key.downcase
+ if HopByHop.member?(key) || # RFC2616: 13.5.1
+ connections.member?(key) || # RFC2616: 14.10
+ ShouldNotTransfer.member?(key) # pragmatics
+ @logger.debug("choose_header: `#{key}: #{value}'")
+ next
+ end
+ dst[key] = value
+ }
+ end
+
+ # Net::HTTP is stupid about the multiple header fields.
+ # Here is workaround:
+ def set_cookie(src, dst)
+ if str = src['set-cookie']
+ cookies = []
+ str.split(/,\s*/).each{|token|
+ if /^[^=]+;/o =~ token
+ cookies[-1] << ", " << token
+ elsif /=/o =~ token
+ cookies << token
+ else
+ cookies[-1] << ", " << token
+ end
+ }
+ dst.cookies.replace(cookies)
+ end
+ end
+
+ def set_via(h)
+ if @config[:ProxyVia]
+ if h['via']
+ h['via'] << ", " << @via
+ else
+ h['via'] = @via
+ end
+ end
+ end
+
+ def setup_proxy_header(req, res)
+ # Choose header fields to transfer
+ header = Hash.new
+ choose_header(req, header)
+ set_via(header)
+ return header
+ end
+
+ def setup_upstream_proxy_authentication(req, res, header)
+ if upstream = proxy_uri(req, res)
+ if upstream.userinfo
+ header['proxy-authorization'] =
+ "Basic " + [upstream.userinfo].pack("m0")
+ end
+ return upstream
+ end
+ return FakeProxyURI
+ end
+
+ def create_net_http(uri, upstream)
+ Net::HTTP.new(uri.host, uri.port, upstream.host, upstream.port)
+ end
+
+ def perform_proxy_request(req, res, req_class, body_stream = nil)
+ uri = req.request_uri
+ path = uri.path.dup
+ path << "?" << uri.query if uri.query
+ header = setup_proxy_header(req, res)
+ upstream = setup_upstream_proxy_authentication(req, res, header)
+
+ body_tmp = []
+ http = create_net_http(uri, upstream)
+ req_fib = Fiber.new do
+ http.start do
+ if @config[:ProxyTimeout]
+ ################################## these issues are
+ http.open_timeout = 30 # secs # necessary (maybe because
+ http.read_timeout = 60 # secs # Ruby's bug, but why?)
+ ##################################
+ end
+ if body_stream && req['transfer-encoding'] =~ /\bchunked\b/i
+ header['Transfer-Encoding'] = 'chunked'
+ end
+ http_req = req_class.new(path, header)
+ http_req.body_stream = body_stream if body_stream
+ http.request(http_req) do |response|
+ # Persistent connection requirements are mysterious for me.
+ # So I will close the connection in every response.
+ res['proxy-connection'] = "close"
+ res['connection'] = "close"
+
+ # stream Net::HTTP::HTTPResponse to WEBrick::HTTPResponse
+ res.status = response.code.to_i
+ res.chunked = response.chunked?
+ choose_header(response, res)
+ set_cookie(response, res)
+ set_via(res)
+ response.read_body do |buf|
+ body_tmp << buf
+ Fiber.yield # wait for res.body Proc#call
+ end
+ end # http.request
+ end
+ end
+ req_fib.resume # read HTTP response headers and first chunk of the body
+ res.body = ->(socket) do
+ while buf = body_tmp.shift
+ socket.write(buf)
+ buf.clear
+ req_fib.resume # continue response.read_body
+ end
+ end
+ end
+ # :stopdoc:
+ end
+end
diff --git a/tool/lib/webrick/httprequest.rb b/tool/lib/webrick/httprequest.rb
new file mode 100644
index 0000000000..d34eac7ecf
--- /dev/null
+++ b/tool/lib/webrick/httprequest.rb
@@ -0,0 +1,636 @@
+# frozen_string_literal: false
+#
+# httprequest.rb -- HTTPRequest Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httprequest.rb,v 1.64 2003/07/13 17:18:22 gotoyuzo Exp $
+
+require 'fiber'
+require 'uri'
+require_relative 'httpversion'
+require_relative 'httpstatus'
+require_relative 'httputils'
+require_relative 'cookie'
+
+module WEBrick
+
+ ##
+ # An HTTP request. This is consumed by service and do_* methods in
+ # WEBrick servlets
+
+ class HTTPRequest
+
+ BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ] # :nodoc:
+
+ # :section: Request line
+
+ ##
+ # The complete request line such as:
+ #
+ # GET / HTTP/1.1
+
+ attr_reader :request_line
+
+ ##
+ # The request method, GET, POST, PUT, etc.
+
+ attr_reader :request_method
+
+ ##
+ # The unparsed URI of the request
+
+ attr_reader :unparsed_uri
+
+ ##
+ # The HTTP version of the request
+
+ attr_reader :http_version
+
+ # :section: Request-URI
+
+ ##
+ # The parsed URI of the request
+
+ attr_reader :request_uri
+
+ ##
+ # The request path
+
+ attr_reader :path
+
+ ##
+ # The script name (CGI variable)
+
+ attr_accessor :script_name
+
+ ##
+ # The path info (CGI variable)
+
+ attr_accessor :path_info
+
+ ##
+ # The query from the URI of the request
+
+ attr_accessor :query_string
+
+ # :section: Header and entity body
+
+ ##
+ # The raw header of the request
+
+ attr_reader :raw_header
+
+ ##
+ # The parsed header of the request
+
+ attr_reader :header
+
+ ##
+ # The parsed request cookies
+
+ attr_reader :cookies
+
+ ##
+ # The Accept header value
+
+ attr_reader :accept
+
+ ##
+ # The Accept-Charset header value
+
+ attr_reader :accept_charset
+
+ ##
+ # The Accept-Encoding header value
+
+ attr_reader :accept_encoding
+
+ ##
+ # The Accept-Language header value
+
+ attr_reader :accept_language
+
+ # :section:
+
+ ##
+ # The remote user (CGI variable)
+
+ attr_accessor :user
+
+ ##
+ # The socket address of the server
+
+ attr_reader :addr
+
+ ##
+ # The socket address of the client
+
+ attr_reader :peeraddr
+
+ ##
+ # Hash of request attributes
+
+ attr_reader :attributes
+
+ ##
+ # Is this a keep-alive connection?
+
+ attr_reader :keep_alive
+
+ ##
+ # The local time this request was received
+
+ attr_reader :request_time
+
+ ##
+ # Creates a new HTTP request. WEBrick::Config::HTTP is the default
+ # configuration.
+
+ def initialize(config)
+ @config = config
+ @buffer_size = @config[:InputBufferSize]
+ @logger = config[:Logger]
+
+ @request_line = @request_method =
+ @unparsed_uri = @http_version = nil
+
+ @request_uri = @host = @port = @path = nil
+ @script_name = @path_info = nil
+ @query_string = nil
+ @query = nil
+ @form_data = nil
+
+ @raw_header = Array.new
+ @header = nil
+ @cookies = []
+ @accept = []
+ @accept_charset = []
+ @accept_encoding = []
+ @accept_language = []
+ @body = ""
+
+ @addr = @peeraddr = nil
+ @attributes = {}
+ @user = nil
+ @keep_alive = false
+ @request_time = nil
+
+ @remaining_size = nil
+ @socket = nil
+
+ @forwarded_proto = @forwarded_host = @forwarded_port =
+ @forwarded_server = @forwarded_for = nil
+ end
+
+ ##
+ # Parses a request from +socket+. This is called internally by
+ # WEBrick::HTTPServer.
+
+ def parse(socket=nil)
+ @socket = socket
+ begin
+ @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []
+ @addr = socket.respond_to?(:addr) ? socket.addr : []
+ rescue Errno::ENOTCONN
+ raise HTTPStatus::EOFError
+ end
+
+ read_request_line(socket)
+ if @http_version.major > 0
+ read_header(socket)
+ @header['cookie'].each{|cookie|
+ @cookies += Cookie::parse(cookie)
+ }
+ @accept = HTTPUtils.parse_qvalues(self['accept'])
+ @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset'])
+ @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding'])
+ @accept_language = HTTPUtils.parse_qvalues(self['accept-language'])
+ end
+ return if @request_method == "CONNECT"
+ return if @unparsed_uri == "*"
+
+ begin
+ setup_forwarded_info
+ @request_uri = parse_uri(@unparsed_uri)
+ @path = HTTPUtils::unescape(@request_uri.path)
+ @path = HTTPUtils::normalize_path(@path)
+ @host = @request_uri.host
+ @port = @request_uri.port
+ @query_string = @request_uri.query
+ @script_name = ""
+ @path_info = @path.dup
+ rescue
+ raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'."
+ end
+
+ if /\Aclose\z/io =~ self["connection"]
+ @keep_alive = false
+ elsif /\Akeep-alive\z/io =~ self["connection"]
+ @keep_alive = true
+ elsif @http_version < "1.1"
+ @keep_alive = false
+ else
+ @keep_alive = true
+ end
+ end
+
+ ##
+ # Generate HTTP/1.1 100 continue response if the client expects it,
+ # otherwise does nothing.
+
+ def continue # :nodoc:
+ if self['expect'] == '100-continue' && @config[:HTTPVersion] >= "1.1"
+ @socket << "HTTP/#{@config[:HTTPVersion]} 100 continue#{CRLF}#{CRLF}"
+ @header.delete('expect')
+ end
+ end
+
+ ##
+ # Returns the request body.
+
+ def body(&block) # :yields: body_chunk
+ block ||= Proc.new{|chunk| @body << chunk }
+ read_body(@socket, block)
+ @body.empty? ? nil : @body
+ end
+
+ ##
+ # Prepares the HTTPRequest object for use as the
+ # source for IO.copy_stream
+
+ def body_reader
+ @body_tmp = []
+ @body_rd = Fiber.new do
+ body do |buf|
+ @body_tmp << buf
+ Fiber.yield
+ end
+ end
+ @body_rd.resume # grab the first chunk and yield
+ self
+ end
+
+ # for IO.copy_stream.
+ def readpartial(size, buf = ''.b) # :nodoc
+ res = @body_tmp.shift or raise EOFError, 'end of file reached'
+ if res.length > size
+ @body_tmp.unshift(res[size..-1])
+ res = res[0..size - 1]
+ end
+ buf.replace(res)
+ res.clear
+ # get more chunks - check alive? because we can take a partial chunk
+ @body_rd.resume if @body_rd.alive?
+ buf
+ end
+
+ ##
+ # Request query as a Hash
+
+ def query
+ unless @query
+ parse_query()
+ end
+ @query
+ end
+
+ ##
+ # The content-length header
+
+ def content_length
+ return Integer(self['content-length'])
+ end
+
+ ##
+ # The content-type header
+
+ def content_type
+ return self['content-type']
+ end
+
+ ##
+ # Retrieves +header_name+
+
+ def [](header_name)
+ if @header
+ value = @header[header_name.downcase]
+ value.empty? ? nil : value.join(", ")
+ end
+ end
+
+ ##
+ # Iterates over the request headers
+
+ def each
+ if @header
+ @header.each{|k, v|
+ value = @header[k]
+ yield(k, value.empty? ? nil : value.join(", "))
+ }
+ end
+ end
+
+ ##
+ # The host this request is for
+
+ def host
+ return @forwarded_host || @host
+ end
+
+ ##
+ # The port this request is for
+
+ def port
+ return @forwarded_port || @port
+ end
+
+ ##
+ # The server name this request is for
+
+ def server_name
+ return @forwarded_server || @config[:ServerName]
+ end
+
+ ##
+ # The client's IP address
+
+ def remote_ip
+ return self["client-ip"] || @forwarded_for || @peeraddr[3]
+ end
+
+ ##
+ # Is this an SSL request?
+
+ def ssl?
+ return @request_uri.scheme == "https"
+ end
+
+ ##
+ # Should the connection this request was made on be kept alive?
+
+ def keep_alive?
+ @keep_alive
+ end
+
+ def to_s # :nodoc:
+ ret = @request_line.dup
+ @raw_header.each{|line| ret << line }
+ ret << CRLF
+ ret << body if body
+ ret
+ end
+
+ ##
+ # Consumes any remaining body and updates keep-alive status
+
+ def fixup() # :nodoc:
+ begin
+ body{|chunk| } # read remaining body
+ rescue HTTPStatus::Error => ex
+ @logger.error("HTTPRequest#fixup: #{ex.class} occurred.")
+ @keep_alive = false
+ rescue => ex
+ @logger.error(ex)
+ @keep_alive = false
+ end
+ end
+
+ # This method provides the metavariables defined by the revision 3
+ # of "The WWW Common Gateway Interface Version 1.1"
+ # To browse the current document of CGI Version 1.1, see below:
+ # http://tools.ietf.org/html/rfc3875
+
+ def meta_vars
+ meta = Hash.new
+
+ cl = self["Content-Length"]
+ ct = self["Content-Type"]
+ meta["CONTENT_LENGTH"] = cl if cl.to_i > 0
+ meta["CONTENT_TYPE"] = ct.dup if ct
+ meta["GATEWAY_INTERFACE"] = "CGI/1.1"
+ meta["PATH_INFO"] = @path_info ? @path_info.dup : ""
+ #meta["PATH_TRANSLATED"] = nil # no plan to be provided
+ meta["QUERY_STRING"] = @query_string ? @query_string.dup : ""
+ meta["REMOTE_ADDR"] = @peeraddr[3]
+ meta["REMOTE_HOST"] = @peeraddr[2]
+ #meta["REMOTE_IDENT"] = nil # no plan to be provided
+ meta["REMOTE_USER"] = @user
+ meta["REQUEST_METHOD"] = @request_method.dup
+ meta["REQUEST_URI"] = @request_uri.to_s
+ meta["SCRIPT_NAME"] = @script_name.dup
+ meta["SERVER_NAME"] = @host
+ meta["SERVER_PORT"] = @port.to_s
+ meta["SERVER_PROTOCOL"] = "HTTP/" + @config[:HTTPVersion].to_s
+ meta["SERVER_SOFTWARE"] = @config[:ServerSoftware].dup
+
+ self.each{|key, val|
+ next if /^content-type$/i =~ key
+ next if /^content-length$/i =~ key
+ name = "HTTP_" + key
+ name.gsub!(/-/o, "_")
+ name.upcase!
+ meta[name] = val
+ }
+
+ meta
+ end
+
+ private
+
+ # :stopdoc:
+
+ MAX_URI_LENGTH = 2083 # :nodoc:
+
+ # same as Mongrel, Thin and Puma
+ MAX_HEADER_LENGTH = (112 * 1024) # :nodoc:
+
+ def read_request_line(socket)
+ @request_line = read_line(socket, MAX_URI_LENGTH) if socket
+ raise HTTPStatus::EOFError unless @request_line
+
+ @request_bytes = @request_line.bytesize
+ if @request_bytes >= MAX_URI_LENGTH and @request_line[-1, 1] != LF
+ raise HTTPStatus::RequestURITooLarge
+ end
+
+ @request_time = Time.now
+ if /^(\S+)\s+(\S++)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
+ @request_method = $1
+ @unparsed_uri = $2
+ @http_version = HTTPVersion.new($3 ? $3 : "0.9")
+ else
+ rl = @request_line.sub(/\x0d?\x0a\z/o, '')
+ raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'."
+ end
+ end
+
+ def read_header(socket)
+ if socket
+ while line = read_line(socket)
+ break if /\A(#{CRLF}|#{LF})\z/om =~ line
+ if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH
+ raise HTTPStatus::RequestEntityTooLarge, 'headers too large'
+ end
+ @raw_header << line
+ end
+ end
+ @header = HTTPUtils::parse_header(@raw_header.join)
+ end
+
+ def parse_uri(str, scheme="http")
+ if @config[:Escape8bitURI]
+ str = HTTPUtils::escape8bit(str)
+ end
+ str.sub!(%r{\A/+}o, '/')
+ uri = URI::parse(str)
+ return uri if uri.absolute?
+ if @forwarded_host
+ host, port = @forwarded_host, @forwarded_port
+ elsif self["host"]
+ pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n
+ host, port = *self['host'].scan(pattern)[0]
+ elsif @addr.size > 0
+ host, port = @addr[2], @addr[1]
+ else
+ host, port = @config[:ServerName], @config[:Port]
+ end
+ uri.scheme = @forwarded_proto || scheme
+ uri.host = host
+ uri.port = port ? port.to_i : nil
+ return URI::parse(uri.to_s)
+ end
+
+ def read_body(socket, block)
+ return unless socket
+ if tc = self['transfer-encoding']
+ case tc
+ when /\Achunked\z/io then read_chunked(socket, block)
+ else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
+ end
+ elsif self['content-length'] || @remaining_size
+ @remaining_size ||= self['content-length'].to_i
+ while @remaining_size > 0
+ sz = [@buffer_size, @remaining_size].min
+ break unless buf = read_data(socket, sz)
+ @remaining_size -= buf.bytesize
+ block.call(buf)
+ end
+ if @remaining_size > 0 && @socket.eof?
+ raise HTTPStatus::BadRequest, "invalid body size."
+ end
+ elsif BODY_CONTAINABLE_METHODS.member?(@request_method) && !@socket.eof
+ raise HTTPStatus::LengthRequired
+ end
+ return @body
+ end
+
+ def read_chunk_size(socket)
+ line = read_line(socket)
+ if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line
+ chunk_size = $1.hex
+ chunk_ext = $2
+ [ chunk_size, chunk_ext ]
+ else
+ raise HTTPStatus::BadRequest, "bad chunk `#{line}'."
+ end
+ end
+
+ def read_chunked(socket, block)
+ chunk_size, = read_chunk_size(socket)
+ while chunk_size > 0
+ begin
+ sz = [ chunk_size, @buffer_size ].min
+ data = read_data(socket, sz) # read chunk-data
+ if data.nil? || data.bytesize != sz
+ raise HTTPStatus::BadRequest, "bad chunk data size."
+ end
+ block.call(data)
+ end while (chunk_size -= sz) > 0
+
+ read_line(socket) # skip CRLF
+ chunk_size, = read_chunk_size(socket)
+ end
+ read_header(socket) # trailer + CRLF
+ @header.delete("transfer-encoding")
+ @remaining_size = 0
+ end
+
+ def _read_data(io, method, *arg)
+ begin
+ WEBrick::Utils.timeout(@config[:RequestTimeout]){
+ return io.__send__(method, *arg)
+ }
+ rescue Errno::ECONNRESET
+ return nil
+ rescue Timeout::Error
+ raise HTTPStatus::RequestTimeout
+ end
+ end
+
+ def read_line(io, size=4096)
+ _read_data(io, :gets, LF, size)
+ end
+
+ def read_data(io, size)
+ _read_data(io, :read, size)
+ end
+
+ def parse_query()
+ begin
+ if @request_method == "GET" || @request_method == "HEAD"
+ @query = HTTPUtils::parse_query(@query_string)
+ elsif self['content-type'] =~ /^application\/x-www-form-urlencoded/
+ @query = HTTPUtils::parse_query(body)
+ elsif self['content-type'] =~ /^multipart\/form-data; boundary=(.+)/
+ boundary = HTTPUtils::dequote($1)
+ @query = HTTPUtils::parse_form_data(body, boundary)
+ else
+ @query = Hash.new
+ end
+ rescue => ex
+ raise HTTPStatus::BadRequest, ex.message
+ end
+ end
+
+ PrivateNetworkRegexp = /
+ ^unknown$|
+ ^((::ffff:)?127.0.0.1|::1)$|
+ ^(::ffff:)?(10|172\.(1[6-9]|2[0-9]|3[01])|192\.168)\.
+ /ixo
+
+ # It's said that all X-Forwarded-* headers will contain more than one
+ # (comma-separated) value if the original request already contained one of
+ # these headers. Since we could use these values as Host header, we choose
+ # the initial(first) value. (apr_table_mergen() adds new value after the
+ # existing value with ", " prefix)
+ def setup_forwarded_info
+ if @forwarded_server = self["x-forwarded-server"]
+ @forwarded_server = @forwarded_server.split(",", 2).first
+ end
+ if @forwarded_proto = self["x-forwarded-proto"]
+ @forwarded_proto = @forwarded_proto.split(",", 2).first
+ end
+ if host_port = self["x-forwarded-host"]
+ host_port = host_port.split(",", 2).first
+ if host_port =~ /\A(\[[0-9a-fA-F:]+\])(?::(\d+))?\z/
+ @forwarded_host = $1
+ tmp = $2
+ else
+ @forwarded_host, tmp = host_port.split(":", 2)
+ end
+ @forwarded_port = (tmp || (@forwarded_proto == "https" ? 443 : 80)).to_i
+ end
+ if addrs = self["x-forwarded-for"]
+ addrs = addrs.split(",").collect(&:strip)
+ addrs.reject!{|ip| PrivateNetworkRegexp =~ ip }
+ @forwarded_for = addrs.first
+ end
+ end
+
+ # :startdoc:
+ end
+end
diff --git a/tool/lib/webrick/httpresponse.rb b/tool/lib/webrick/httpresponse.rb
new file mode 100644
index 0000000000..ba4494ab74
--- /dev/null
+++ b/tool/lib/webrick/httpresponse.rb
@@ -0,0 +1,564 @@
+# frozen_string_literal: false
+#
+# httpresponse.rb -- HTTPResponse Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httpresponse.rb,v 1.45 2003/07/11 11:02:25 gotoyuzo Exp $
+
+require 'time'
+require 'uri'
+require_relative 'httpversion'
+require_relative 'htmlutils'
+require_relative 'httputils'
+require_relative 'httpstatus'
+
+module WEBrick
+ ##
+ # An HTTP response. This is filled in by the service or do_* methods of a
+ # WEBrick HTTP Servlet.
+
+ class HTTPResponse
+ class InvalidHeader < StandardError
+ end
+
+ ##
+ # HTTP Response version
+
+ attr_reader :http_version
+
+ ##
+ # Response status code (200)
+
+ attr_reader :status
+
+ ##
+ # Response header
+
+ attr_reader :header
+
+ ##
+ # Response cookies
+
+ attr_reader :cookies
+
+ ##
+ # Response reason phrase ("OK")
+
+ attr_accessor :reason_phrase
+
+ ##
+ # Body may be:
+ # * a String;
+ # * an IO-like object that responds to +#read+ and +#readpartial+;
+ # * a Proc-like object that responds to +#call+.
+ #
+ # In the latter case, either #chunked= should be set to +true+,
+ # or <code>header['content-length']</code> explicitly provided.
+ # Example:
+ #
+ # server.mount_proc '/' do |req, res|
+ # res.chunked = true
+ # # or
+ # # res.header['content-length'] = 10
+ # res.body = proc { |out| out.write(Time.now.to_s) }
+ # end
+
+ attr_accessor :body
+
+ ##
+ # Request method for this response
+
+ attr_accessor :request_method
+
+ ##
+ # Request URI for this response
+
+ attr_accessor :request_uri
+
+ ##
+ # Request HTTP version for this response
+
+ attr_accessor :request_http_version
+
+ ##
+ # Filename of the static file in this response. Only used by the
+ # FileHandler servlet.
+
+ attr_accessor :filename
+
+ ##
+ # Is this a keep-alive response?
+
+ attr_accessor :keep_alive
+
+ ##
+ # Configuration for this response
+
+ attr_reader :config
+
+ ##
+ # Bytes sent in this response
+
+ attr_reader :sent_size
+
+ ##
+ # Creates a new HTTP response object. WEBrick::Config::HTTP is the
+ # default configuration.
+
+ def initialize(config)
+ @config = config
+ @buffer_size = config[:OutputBufferSize]
+ @logger = config[:Logger]
+ @header = Hash.new
+ @status = HTTPStatus::RC_OK
+ @reason_phrase = nil
+ @http_version = HTTPVersion::convert(@config[:HTTPVersion])
+ @body = ''
+ @keep_alive = true
+ @cookies = []
+ @request_method = nil
+ @request_uri = nil
+ @request_http_version = @http_version # temporary
+ @chunked = false
+ @filename = nil
+ @sent_size = 0
+ @bodytempfile = nil
+ end
+
+ ##
+ # The response's HTTP status line
+
+ def status_line
+ "HTTP/#@http_version #@status #@reason_phrase".rstrip << CRLF
+ end
+
+ ##
+ # Sets the response's status to the +status+ code
+
+ def status=(status)
+ @status = status
+ @reason_phrase = HTTPStatus::reason_phrase(status)
+ end
+
+ ##
+ # Retrieves the response header +field+
+
+ def [](field)
+ @header[field.downcase]
+ end
+
+ ##
+ # Sets the response header +field+ to +value+
+
+ def []=(field, value)
+ @chunked = value.to_s.downcase == 'chunked' if field.downcase == 'transfer-encoding'
+ @header[field.downcase] = value.to_s
+ end
+
+ ##
+ # The content-length header
+
+ def content_length
+ if len = self['content-length']
+ return Integer(len)
+ end
+ end
+
+ ##
+ # Sets the content-length header to +len+
+
+ def content_length=(len)
+ self['content-length'] = len.to_s
+ end
+
+ ##
+ # The content-type header
+
+ def content_type
+ self['content-type']
+ end
+
+ ##
+ # Sets the content-type header to +type+
+
+ def content_type=(type)
+ self['content-type'] = type
+ end
+
+ ##
+ # Iterates over each header in the response
+
+ def each
+ @header.each{|field, value| yield(field, value) }
+ end
+
+ ##
+ # Will this response body be returned using chunked transfer-encoding?
+
+ def chunked?
+ @chunked
+ end
+
+ ##
+ # Enables chunked transfer encoding.
+
+ def chunked=(val)
+ @chunked = val ? true : false
+ end
+
+ ##
+ # Will this response's connection be kept alive?
+
+ def keep_alive?
+ @keep_alive
+ end
+
+ ##
+ # Sends the response on +socket+
+
+ def send_response(socket) # :nodoc:
+ begin
+ setup_header()
+ send_header(socket)
+ send_body(socket)
+ rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN => ex
+ @logger.debug(ex)
+ @keep_alive = false
+ rescue Exception => ex
+ @logger.error(ex)
+ @keep_alive = false
+ end
+ end
+
+ ##
+ # Sets up the headers for sending
+
+ def setup_header() # :nodoc:
+ @reason_phrase ||= HTTPStatus::reason_phrase(@status)
+ @header['server'] ||= @config[:ServerSoftware]
+ @header['date'] ||= Time.now.httpdate
+
+ # HTTP/0.9 features
+ if @request_http_version < "1.0"
+ @http_version = HTTPVersion.new("0.9")
+ @keep_alive = false
+ end
+
+ # HTTP/1.0 features
+ if @request_http_version < "1.1"
+ if chunked?
+ @chunked = false
+ ver = @request_http_version.to_s
+ msg = "chunked is set for an HTTP/#{ver} request. (ignored)"
+ @logger.warn(msg)
+ end
+ end
+
+ # Determine the message length (RFC2616 -- 4.4 Message Length)
+ if @status == 304 || @status == 204 || HTTPStatus::info?(@status)
+ @header.delete('content-length')
+ @body = ""
+ elsif chunked?
+ @header["transfer-encoding"] = "chunked"
+ @header.delete('content-length')
+ elsif %r{^multipart/byteranges} =~ @header['content-type']
+ @header.delete('content-length')
+ elsif @header['content-length'].nil?
+ if @body.respond_to? :readpartial
+ elsif @body.respond_to? :call
+ make_body_tempfile
+ else
+ @header['content-length'] = (@body ? @body.bytesize : 0).to_s
+ end
+ end
+
+ # Keep-Alive connection.
+ if @header['connection'] == "close"
+ @keep_alive = false
+ elsif keep_alive?
+ if chunked? || @header['content-length'] || @status == 304 || @status == 204 || HTTPStatus.info?(@status)
+ @header['connection'] = "Keep-Alive"
+ else
+ msg = "Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true"
+ @logger.warn(msg)
+ @header['connection'] = "close"
+ @keep_alive = false
+ end
+ else
+ @header['connection'] = "close"
+ end
+
+ # Location is a single absoluteURI.
+ if location = @header['location']
+ if @request_uri
+ @header['location'] = @request_uri.merge(location).to_s
+ end
+ end
+ end
+
+ def make_body_tempfile # :nodoc:
+ return if @bodytempfile
+ bodytempfile = Tempfile.create("webrick")
+ if @body.nil?
+ # nothing
+ elsif @body.respond_to? :readpartial
+ IO.copy_stream(@body, bodytempfile)
+ @body.close
+ elsif @body.respond_to? :call
+ @body.call(bodytempfile)
+ else
+ bodytempfile.write @body
+ end
+ bodytempfile.rewind
+ @body = @bodytempfile = bodytempfile
+ @header['content-length'] = bodytempfile.stat.size.to_s
+ end
+
+ def remove_body_tempfile # :nodoc:
+ if @bodytempfile
+ @bodytempfile.close
+ File.unlink @bodytempfile.path
+ @bodytempfile = nil
+ end
+ end
+
+
+ ##
+ # Sends the headers on +socket+
+
+ def send_header(socket) # :nodoc:
+ if @http_version.major > 0
+ data = status_line()
+ @header.each{|key, value|
+ tmp = key.gsub(/\bwww|^te$|\b\w/){ $&.upcase }
+ data << "#{tmp}: #{check_header(value)}" << CRLF
+ }
+ @cookies.each{|cookie|
+ data << "Set-Cookie: " << check_header(cookie.to_s) << CRLF
+ }
+ data << CRLF
+ socket.write(data)
+ end
+ rescue InvalidHeader => e
+ @header.clear
+ @cookies.clear
+ set_error e
+ retry
+ end
+
+ ##
+ # Sends the body on +socket+
+
+ def send_body(socket) # :nodoc:
+ if @body.respond_to? :readpartial then
+ send_body_io(socket)
+ elsif @body.respond_to?(:call) then
+ send_body_proc(socket)
+ else
+ send_body_string(socket)
+ end
+ end
+
+ ##
+ # Redirects to +url+ with a WEBrick::HTTPStatus::Redirect +status+.
+ #
+ # Example:
+ #
+ # res.set_redirect WEBrick::HTTPStatus::TemporaryRedirect
+
+ def set_redirect(status, url)
+ url = URI(url).to_s
+ @body = "<HTML><A HREF=\"#{url}\">#{url}</A>.</HTML>\n"
+ @header['location'] = url
+ raise status
+ end
+
+ ##
+ # Creates an error page for exception +ex+ with an optional +backtrace+
+
+ def set_error(ex, backtrace=false)
+ case ex
+ when HTTPStatus::Status
+ @keep_alive = false if HTTPStatus::error?(ex.code)
+ self.status = ex.code
+ else
+ @keep_alive = false
+ self.status = HTTPStatus::RC_INTERNAL_SERVER_ERROR
+ end
+ @header['content-type'] = "text/html; charset=ISO-8859-1"
+
+ if respond_to?(:create_error_page)
+ create_error_page()
+ return
+ end
+
+ if @request_uri
+ host, port = @request_uri.host, @request_uri.port
+ else
+ host, port = @config[:ServerName], @config[:Port]
+ end
+
+ error_body(backtrace, ex, host, port)
+ end
+
+ private
+
+ def check_header(header_value)
+ header_value = header_value.to_s
+ if /[\r\n]/ =~ header_value
+ raise InvalidHeader
+ else
+ header_value
+ end
+ end
+
+ # :stopdoc:
+
+ def error_body(backtrace, ex, host, port)
+ @body = ''
+ @body << <<-_end_of_html_
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
+<HTML>
+ <HEAD><TITLE>#{HTMLUtils::escape(@reason_phrase)}</TITLE></HEAD>
+ <BODY>
+ <H1>#{HTMLUtils::escape(@reason_phrase)}</H1>
+ #{HTMLUtils::escape(ex.message)}
+ <HR>
+ _end_of_html_
+
+ if backtrace && $DEBUG
+ @body << "backtrace of `#{HTMLUtils::escape(ex.class.to_s)}' "
+ @body << "#{HTMLUtils::escape(ex.message)}"
+ @body << "<PRE>"
+ ex.backtrace.each{|line| @body << "\t#{line}\n"}
+ @body << "</PRE><HR>"
+ end
+
+ @body << <<-_end_of_html_
+ <ADDRESS>
+ #{HTMLUtils::escape(@config[:ServerSoftware])} at
+ #{host}:#{port}
+ </ADDRESS>
+ </BODY>
+</HTML>
+ _end_of_html_
+ end
+
+ def send_body_io(socket)
+ begin
+ if @request_method == "HEAD"
+ # do nothing
+ elsif chunked?
+ buf = ''
+ begin
+ @body.readpartial(@buffer_size, buf)
+ size = buf.bytesize
+ data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
+ socket.write(data)
+ data.clear
+ @sent_size += size
+ rescue EOFError
+ break
+ end while true
+ buf.clear
+ socket.write("0#{CRLF}#{CRLF}")
+ else
+ if %r{\Abytes (\d+)-(\d+)/\d+\z} =~ @header['content-range']
+ offset = $1.to_i
+ size = $2.to_i - offset + 1
+ else
+ offset = nil
+ size = @header['content-length']
+ size = size.to_i if size
+ end
+ begin
+ @sent_size = IO.copy_stream(@body, socket, size, offset)
+ rescue NotImplementedError
+ @body.seek(offset, IO::SEEK_SET)
+ @sent_size = IO.copy_stream(@body, socket, size)
+ end
+ end
+ ensure
+ @body.close
+ end
+ remove_body_tempfile
+ end
+
+ def send_body_string(socket)
+ if @request_method == "HEAD"
+ # do nothing
+ elsif chunked?
+ body ? @body.bytesize : 0
+ while buf = @body[@sent_size, @buffer_size]
+ break if buf.empty?
+ size = buf.bytesize
+ data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
+ buf.clear
+ socket.write(data)
+ @sent_size += size
+ end
+ socket.write("0#{CRLF}#{CRLF}")
+ else
+ if @body && @body.bytesize > 0
+ socket.write(@body)
+ @sent_size = @body.bytesize
+ end
+ end
+ end
+
+ def send_body_proc(socket)
+ if @request_method == "HEAD"
+ # do nothing
+ elsif chunked?
+ @body.call(ChunkedWrapper.new(socket, self))
+ socket.write("0#{CRLF}#{CRLF}")
+ else
+ size = @header['content-length'].to_i
+ if @bodytempfile
+ @bodytempfile.rewind
+ IO.copy_stream(@bodytempfile, socket)
+ else
+ @body.call(socket)
+ end
+ @sent_size = size
+ end
+ end
+
+ class ChunkedWrapper
+ def initialize(socket, resp)
+ @socket = socket
+ @resp = resp
+ end
+
+ def write(buf)
+ return 0 if buf.empty?
+ socket = @socket
+ @resp.instance_eval {
+ size = buf.bytesize
+ data = "#{size.to_s(16)}#{CRLF}#{buf}#{CRLF}"
+ socket.write(data)
+ data.clear
+ @sent_size += size
+ size
+ }
+ end
+
+ def <<(*buf)
+ write(buf)
+ self
+ end
+ end
+
+ # preserved for compatibility with some 3rd-party handlers
+ def _write_data(socket, data)
+ socket << data
+ end
+
+ # :startdoc:
+ end
+
+end
diff --git a/tool/lib/webrick/https.rb b/tool/lib/webrick/https.rb
new file mode 100644
index 0000000000..b0a49bc40b
--- /dev/null
+++ b/tool/lib/webrick/https.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: false
+#
+# https.rb -- SSL/TLS enhancement for HTTPServer
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2001 GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: https.rb,v 1.15 2003/07/22 19:20:42 gotoyuzo Exp $
+
+require_relative 'ssl'
+require_relative 'httpserver'
+
+module WEBrick
+ module Config
+ HTTP.update(SSL)
+ end
+
+ ##
+ #--
+ # Adds SSL functionality to WEBrick::HTTPRequest
+
+ class HTTPRequest
+
+ ##
+ # HTTP request SSL cipher
+
+ attr_reader :cipher
+
+ ##
+ # HTTP request server certificate
+
+ attr_reader :server_cert
+
+ ##
+ # HTTP request client certificate
+
+ attr_reader :client_cert
+
+ # :stopdoc:
+
+ alias orig_parse parse
+
+ def parse(socket=nil)
+ if socket.respond_to?(:cert)
+ @server_cert = socket.cert || @config[:SSLCertificate]
+ @client_cert = socket.peer_cert
+ @client_cert_chain = socket.peer_cert_chain
+ @cipher = socket.cipher
+ end
+ orig_parse(socket)
+ end
+
+ alias orig_parse_uri parse_uri
+
+ def parse_uri(str, scheme="https")
+ if server_cert
+ return orig_parse_uri(str, scheme)
+ end
+ return orig_parse_uri(str)
+ end
+ private :parse_uri
+
+ alias orig_meta_vars meta_vars
+
+ def meta_vars
+ meta = orig_meta_vars
+ if server_cert
+ meta["HTTPS"] = "on"
+ meta["SSL_SERVER_CERT"] = @server_cert.to_pem
+ meta["SSL_CLIENT_CERT"] = @client_cert ? @client_cert.to_pem : ""
+ if @client_cert_chain
+ @client_cert_chain.each_with_index{|cert, i|
+ meta["SSL_CLIENT_CERT_CHAIN_#{i}"] = cert.to_pem
+ }
+ end
+ meta["SSL_CIPHER"] = @cipher[0]
+ meta["SSL_PROTOCOL"] = @cipher[1]
+ meta["SSL_CIPHER_USEKEYSIZE"] = @cipher[2].to_s
+ meta["SSL_CIPHER_ALGKEYSIZE"] = @cipher[3].to_s
+ end
+ meta
+ end
+
+ # :startdoc:
+ end
+
+ ##
+ #--
+ # Fake WEBrick::HTTPRequest for lookup_server
+
+ class SNIRequest
+
+ ##
+ # The SNI hostname
+
+ attr_reader :host
+
+ ##
+ # The socket address of the server
+
+ attr_reader :addr
+
+ ##
+ # The port this request is for
+
+ attr_reader :port
+
+ ##
+ # Creates a new SNIRequest.
+
+ def initialize(sslsocket, hostname)
+ @host = hostname
+ @addr = sslsocket.addr
+ @port = @addr[1]
+ end
+ end
+
+
+ ##
+ #--
+ # Adds SSL functionality to WEBrick::HTTPServer
+
+ class HTTPServer < ::WEBrick::GenericServer
+ ##
+ # ServerNameIndication callback
+
+ def ssl_servername_callback(sslsocket, hostname = nil)
+ req = SNIRequest.new(sslsocket, hostname)
+ server = lookup_server(req)
+ server ? server.ssl_context : nil
+ end
+
+ # :stopdoc:
+
+ ##
+ # Check whether +server+ is also SSL server.
+ # Also +server+'s SSL context will be created.
+
+ alias orig_virtual_host virtual_host
+
+ def virtual_host(server)
+ if @config[:SSLEnable] && !server.ssl_context
+ raise ArgumentError, "virtual host must set SSLEnable to true"
+ end
+ orig_virtual_host(server)
+ end
+
+ # :startdoc:
+ end
+end
diff --git a/tool/lib/webrick/httpserver.rb b/tool/lib/webrick/httpserver.rb
new file mode 100644
index 0000000000..e85d059319
--- /dev/null
+++ b/tool/lib/webrick/httpserver.rb
@@ -0,0 +1,294 @@
+# frozen_string_literal: false
+#
+# httpserver.rb -- HTTPServer Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httpserver.rb,v 1.63 2002/10/01 17:16:32 gotoyuzo Exp $
+
+require 'io/wait'
+require_relative 'server'
+require_relative 'httputils'
+require_relative 'httpstatus'
+require_relative 'httprequest'
+require_relative 'httpresponse'
+require_relative 'httpservlet'
+require_relative 'accesslog'
+
+module WEBrick
+ class HTTPServerError < ServerError; end
+
+ ##
+ # An HTTP Server
+
+ class HTTPServer < ::WEBrick::GenericServer
+ ##
+ # Creates a new HTTP server according to +config+
+ #
+ # An HTTP server uses the following attributes:
+ #
+ # :AccessLog:: An array of access logs. See WEBrick::AccessLog
+ # :BindAddress:: Local address for the server to bind to
+ # :DocumentRoot:: Root path to serve files from
+ # :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler
+ # :HTTPVersion:: The HTTP version of this server
+ # :Port:: Port to listen on
+ # :RequestCallback:: Called with a request and response before each
+ # request is serviced.
+ # :RequestTimeout:: Maximum time to wait between requests
+ # :ServerAlias:: Array of alternate names for this server for virtual
+ # hosting
+ # :ServerName:: Name for this server for virtual hosting
+
+ def initialize(config={}, default=Config::HTTP)
+ super(config, default)
+ @http_version = HTTPVersion::convert(@config[:HTTPVersion])
+
+ @mount_tab = MountTable.new
+ if @config[:DocumentRoot]
+ mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot],
+ @config[:DocumentRootOptions])
+ end
+
+ unless @config[:AccessLog]
+ @config[:AccessLog] = [
+ [ $stderr, AccessLog::COMMON_LOG_FORMAT ],
+ [ $stderr, AccessLog::REFERER_LOG_FORMAT ]
+ ]
+ end
+
+ @virtual_hosts = Array.new
+ end
+
+ ##
+ # Processes requests on +sock+
+
+ def run(sock)
+ while true
+ req = create_request(@config)
+ res = create_response(@config)
+ server = self
+ begin
+ timeout = @config[:RequestTimeout]
+ while timeout > 0
+ break if sock.to_io.wait_readable(0.5)
+ break if @status != :Running
+ timeout -= 0.5
+ end
+ raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running
+ raise HTTPStatus::EOFError if sock.eof?
+ req.parse(sock)
+ res.request_method = req.request_method
+ res.request_uri = req.request_uri
+ res.request_http_version = req.http_version
+ res.keep_alive = req.keep_alive?
+ server = lookup_server(req) || self
+ if callback = server[:RequestCallback]
+ callback.call(req, res)
+ elsif callback = server[:RequestHandler]
+ msg = ":RequestHandler is deprecated, please use :RequestCallback"
+ @logger.warn(msg)
+ callback.call(req, res)
+ end
+ server.service(req, res)
+ rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex
+ res.set_error(ex)
+ rescue HTTPStatus::Error => ex
+ @logger.error(ex.message)
+ res.set_error(ex)
+ rescue HTTPStatus::Status => ex
+ res.status = ex.code
+ rescue StandardError => ex
+ @logger.error(ex)
+ res.set_error(ex, true)
+ ensure
+ if req.request_line
+ if req.keep_alive? && res.keep_alive?
+ req.fixup()
+ end
+ res.send_response(sock)
+ server.access_log(@config, req, res)
+ end
+ end
+ break if @http_version < "1.1"
+ break unless req.keep_alive?
+ break unless res.keep_alive?
+ end
+ end
+
+ ##
+ # Services +req+ and fills in +res+
+
+ def service(req, res)
+ if req.unparsed_uri == "*"
+ if req.request_method == "OPTIONS"
+ do_OPTIONS(req, res)
+ raise HTTPStatus::OK
+ end
+ raise HTTPStatus::NotFound, "`#{req.unparsed_uri}' not found."
+ end
+
+ servlet, options, script_name, path_info = search_servlet(req.path)
+ raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet
+ req.script_name = script_name
+ req.path_info = path_info
+ si = servlet.get_instance(self, *options)
+ @logger.debug(format("%s is invoked.", si.class.name))
+ si.service(req, res)
+ end
+
+ ##
+ # The default OPTIONS request handler says GET, HEAD, POST and OPTIONS
+ # requests are allowed.
+
+ def do_OPTIONS(req, res)
+ res["allow"] = "GET,HEAD,POST,OPTIONS"
+ end
+
+ ##
+ # Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation
+ # time
+
+ def mount(dir, servlet, *options)
+ @logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir))
+ @mount_tab[dir] = [ servlet, options ]
+ end
+
+ ##
+ # Mounts +proc+ or +block+ on +dir+ and calls it with a
+ # WEBrick::HTTPRequest and WEBrick::HTTPResponse
+
+ def mount_proc(dir, proc=nil, &block)
+ proc ||= block
+ raise HTTPServerError, "must pass a proc or block" unless proc
+ mount(dir, HTTPServlet::ProcHandler.new(proc))
+ end
+
+ ##
+ # Unmounts +dir+
+
+ def unmount(dir)
+ @logger.debug(sprintf("unmount %s.", dir))
+ @mount_tab.delete(dir)
+ end
+ alias umount unmount
+
+ ##
+ # Finds a servlet for +path+
+
+ def search_servlet(path)
+ script_name, path_info = @mount_tab.scan(path)
+ servlet, options = @mount_tab[script_name]
+ if servlet
+ [ servlet, options, script_name, path_info ]
+ end
+ end
+
+ ##
+ # Adds +server+ as a virtual host.
+
+ def virtual_host(server)
+ @virtual_hosts << server
+ @virtual_hosts = @virtual_hosts.sort_by{|s|
+ num = 0
+ num -= 4 if s[:BindAddress]
+ num -= 2 if s[:Port]
+ num -= 1 if s[:ServerName]
+ num
+ }
+ end
+
+ ##
+ # Finds the appropriate virtual host to handle +req+
+
+ def lookup_server(req)
+ @virtual_hosts.find{|s|
+ (s[:BindAddress].nil? || req.addr[3] == s[:BindAddress]) &&
+ (s[:Port].nil? || req.port == s[:Port]) &&
+ ((s[:ServerName].nil? || req.host == s[:ServerName]) ||
+ (!s[:ServerAlias].nil? && s[:ServerAlias].find{|h| h === req.host}))
+ }
+ end
+
+ ##
+ # Logs +req+ and +res+ in the access logs. +config+ is used for the
+ # server name.
+
+ def access_log(config, req, res)
+ param = AccessLog::setup_params(config, req, res)
+ @config[:AccessLog].each{|logger, fmt|
+ logger << AccessLog::format(fmt+"\n", param)
+ }
+ end
+
+ ##
+ # Creates the HTTPRequest used when handling the HTTP
+ # request. Can be overridden by subclasses.
+ def create_request(with_webrick_config)
+ HTTPRequest.new(with_webrick_config)
+ end
+
+ ##
+ # Creates the HTTPResponse used when handling the HTTP
+ # request. Can be overridden by subclasses.
+ def create_response(with_webrick_config)
+ HTTPResponse.new(with_webrick_config)
+ end
+
+ ##
+ # Mount table for the path a servlet is mounted on in the directory space
+ # of the server. Users of WEBrick can only access this indirectly via
+ # WEBrick::HTTPServer#mount, WEBrick::HTTPServer#unmount and
+ # WEBrick::HTTPServer#search_servlet
+
+ class MountTable # :nodoc:
+ def initialize
+ @tab = Hash.new
+ compile
+ end
+
+ def [](dir)
+ dir = normalize(dir)
+ @tab[dir]
+ end
+
+ def []=(dir, val)
+ dir = normalize(dir)
+ @tab[dir] = val
+ compile
+ val
+ end
+
+ def delete(dir)
+ dir = normalize(dir)
+ res = @tab.delete(dir)
+ compile
+ res
+ end
+
+ def scan(path)
+ @scanner =~ path
+ [ $&, $' ]
+ end
+
+ private
+
+ def compile
+ k = @tab.keys
+ k.sort!
+ k.reverse!
+ k.collect!{|path| Regexp.escape(path) }
+ @scanner = Regexp.new("\\A(" + k.join("|") +")(?=/|\\z)")
+ end
+
+ def normalize(dir)
+ ret = dir ? dir.dup : ""
+ ret.sub!(%r|/+\z|, "")
+ ret
+ end
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpservlet.rb b/tool/lib/webrick/httpservlet.rb
new file mode 100644
index 0000000000..da49a1405b
--- /dev/null
+++ b/tool/lib/webrick/httpservlet.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: false
+#
+# httpservlet.rb -- HTTPServlet Utility File
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httpservlet.rb,v 1.21 2003/02/23 12:24:46 gotoyuzo Exp $
+
+require_relative 'httpservlet/abstract'
+require_relative 'httpservlet/filehandler'
+require_relative 'httpservlet/cgihandler'
+require_relative 'httpservlet/erbhandler'
+require_relative 'httpservlet/prochandler'
+
+module WEBrick
+ module HTTPServlet
+ FileHandler.add_handler("cgi", CGIHandler)
+ FileHandler.add_handler("rhtml", ERBHandler)
+ end
+end
diff --git a/tool/lib/webrick/httpservlet/abstract.rb b/tool/lib/webrick/httpservlet/abstract.rb
new file mode 100644
index 0000000000..bccb091861
--- /dev/null
+++ b/tool/lib/webrick/httpservlet/abstract.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: false
+#
+# httpservlet.rb -- HTTPServlet Module
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: abstract.rb,v 1.24 2003/07/11 11:16:46 gotoyuzo Exp $
+
+require_relative '../htmlutils'
+require_relative '../httputils'
+require_relative '../httpstatus'
+
+module WEBrick
+ module HTTPServlet
+ class HTTPServletError < StandardError; end
+
+ ##
+ # AbstractServlet allows HTTP server modules to be reused across multiple
+ # servers and allows encapsulation of functionality.
+ #
+ # By default a servlet will respond to GET, HEAD (through an alias to GET)
+ # and OPTIONS requests.
+ #
+ # By default a new servlet is initialized for every request. A servlet
+ # instance can be reused by overriding ::get_instance in the
+ # AbstractServlet subclass.
+ #
+ # == A Simple Servlet
+ #
+ # class Simple < WEBrick::HTTPServlet::AbstractServlet
+ # def do_GET request, response
+ # status, content_type, body = do_stuff_with request
+ #
+ # response.status = status
+ # response['Content-Type'] = content_type
+ # response.body = body
+ # end
+ #
+ # def do_stuff_with request
+ # return 200, 'text/plain', 'you got a page'
+ # end
+ # end
+ #
+ # This servlet can be mounted on a server at a given path:
+ #
+ # server.mount '/simple', Simple
+ #
+ # == Servlet Configuration
+ #
+ # Servlets can be configured via initialize. The first argument is the
+ # HTTP server the servlet is being initialized for.
+ #
+ # class Configurable < Simple
+ # def initialize server, color, size
+ # super server
+ # @color = color
+ # @size = size
+ # end
+ #
+ # def do_stuff_with request
+ # content = "<p " \
+ # %q{style="color: #{@color}; font-size: #{@size}"} \
+ # ">Hello, World!"
+ #
+ # return 200, "text/html", content
+ # end
+ # end
+ #
+ # This servlet must be provided two arguments at mount time:
+ #
+ # server.mount '/configurable', Configurable, 'red', '2em'
+
+ class AbstractServlet
+
+ ##
+ # Factory for servlet instances that will handle a request from +server+
+ # using +options+ from the mount point. By default a new servlet
+ # instance is created for every call.
+
+ def self.get_instance(server, *options)
+ self.new(server, *options)
+ end
+
+ ##
+ # Initializes a new servlet for +server+ using +options+ which are
+ # stored as-is in +@options+. +@logger+ is also provided.
+
+ def initialize(server, *options)
+ @server = @config = server
+ @logger = @server[:Logger]
+ @options = options
+ end
+
+ ##
+ # Dispatches to a +do_+ method based on +req+ if such a method is
+ # available. (+do_GET+ for a GET request). Raises a MethodNotAllowed
+ # exception if the method is not implemented.
+
+ def service(req, res)
+ method_name = "do_" + req.request_method.gsub(/-/, "_")
+ if respond_to?(method_name)
+ __send__(method_name, req, res)
+ else
+ raise HTTPStatus::MethodNotAllowed,
+ "unsupported method `#{req.request_method}'."
+ end
+ end
+
+ ##
+ # Raises a NotFound exception
+
+ def do_GET(req, res)
+ raise HTTPStatus::NotFound, "not found."
+ end
+
+ ##
+ # Dispatches to do_GET
+
+ def do_HEAD(req, res)
+ do_GET(req, res)
+ end
+
+ ##
+ # Returns the allowed HTTP request methods
+
+ def do_OPTIONS(req, res)
+ m = self.methods.grep(/\Ado_([A-Z]+)\z/) {$1}
+ m.sort!
+ res["allow"] = m.join(",")
+ end
+
+ private
+
+ ##
+ # Redirects to a path ending in /
+
+ def redirect_to_directory_uri(req, res)
+ if req.path[-1] != ?/
+ location = WEBrick::HTTPUtils.escape_path(req.path + "/")
+ if req.query_string && req.query_string.bytesize > 0
+ location << "?" << req.query_string
+ end
+ res.set_redirect(HTTPStatus::MovedPermanently, location)
+ end
+ end
+ end
+
+ end
+end
diff --git a/tool/lib/webrick/httpservlet/cgi_runner.rb b/tool/lib/webrick/httpservlet/cgi_runner.rb
new file mode 100644
index 0000000000..0398c16749
--- /dev/null
+++ b/tool/lib/webrick/httpservlet/cgi_runner.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: false
+#
+# cgi_runner.rb -- CGI launcher.
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: cgi_runner.rb,v 1.9 2002/09/25 11:33:15 gotoyuzo Exp $
+
+def sysread(io, size)
+ buf = ""
+ while size > 0
+ tmp = io.sysread(size)
+ buf << tmp
+ size -= tmp.bytesize
+ end
+ return buf
+end
+
+STDIN.binmode
+
+len = sysread(STDIN, 8).to_i
+out = sysread(STDIN, len)
+STDOUT.reopen(File.open(out, "w"))
+
+len = sysread(STDIN, 8).to_i
+err = sysread(STDIN, len)
+STDERR.reopen(File.open(err, "w"))
+
+len = sysread(STDIN, 8).to_i
+dump = sysread(STDIN, len)
+hash = Marshal.restore(dump)
+ENV.keys.each{|name| ENV.delete(name) }
+hash.each{|k, v| ENV[k] = v if v }
+
+dir = File::dirname(ENV["SCRIPT_FILENAME"])
+Dir::chdir dir
+
+if ARGV[0]
+ argv = ARGV.dup
+ argv << ENV["SCRIPT_FILENAME"]
+ exec(*argv)
+ # NOTREACHED
+end
+exec ENV["SCRIPT_FILENAME"]
diff --git a/tool/lib/webrick/httpservlet/cgihandler.rb b/tool/lib/webrick/httpservlet/cgihandler.rb
new file mode 100644
index 0000000000..4457770b7a
--- /dev/null
+++ b/tool/lib/webrick/httpservlet/cgihandler.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: false
+#
+# cgihandler.rb -- CGIHandler Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: cgihandler.rb,v 1.27 2003/03/21 19:56:01 gotoyuzo Exp $
+
+require 'rbconfig'
+require 'tempfile'
+require_relative '../config'
+require_relative 'abstract'
+
+module WEBrick
+ module HTTPServlet
+
+ ##
+ # Servlet for handling CGI scripts
+ #
+ # Example:
+ #
+ # server.mount('/cgi/my_script', WEBrick::HTTPServlet::CGIHandler,
+ # '/path/to/my_script')
+
+ class CGIHandler < AbstractServlet
+ Ruby = RbConfig.ruby # :nodoc:
+ CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc:
+ CGIRunnerArray = [Ruby, "#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb".freeze].freeze # :nodoc:
+
+ ##
+ # Creates a new CGI script servlet for the script at +name+
+
+ def initialize(server, name)
+ super(server, name)
+ @script_filename = name
+ @tempdir = server[:TempDir]
+ interpreter = server[:CGIInterpreter]
+ if interpreter.is_a?(Array)
+ @cgicmd = CGIRunnerArray + interpreter
+ else
+ @cgicmd = "#{CGIRunner} #{interpreter}"
+ end
+ end
+
+ # :stopdoc:
+
+ def do_GET(req, res)
+ cgi_in = IO::popen(@cgicmd, "wb")
+ cgi_out = Tempfile.new("webrick.cgiout.", @tempdir, mode: IO::BINARY)
+ cgi_out.set_encoding("ASCII-8BIT")
+ cgi_err = Tempfile.new("webrick.cgierr.", @tempdir, mode: IO::BINARY)
+ cgi_err.set_encoding("ASCII-8BIT")
+ begin
+ cgi_in.sync = true
+ meta = req.meta_vars
+ meta["SCRIPT_FILENAME"] = @script_filename
+ meta["PATH"] = @config[:CGIPathEnv]
+ meta.delete("HTTP_PROXY")
+ if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM
+ meta["SystemRoot"] = ENV["SystemRoot"]
+ end
+ dump = Marshal.dump(meta)
+
+ cgi_in.write("%8d" % cgi_out.path.bytesize)
+ cgi_in.write(cgi_out.path)
+ cgi_in.write("%8d" % cgi_err.path.bytesize)
+ cgi_in.write(cgi_err.path)
+ cgi_in.write("%8d" % dump.bytesize)
+ cgi_in.write(dump)
+
+ req.body { |chunk| cgi_in.write(chunk) }
+ ensure
+ cgi_in.close
+ status = $?.exitstatus
+ sleep 0.1 if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM
+ data = cgi_out.read
+ cgi_out.close(true)
+ if errmsg = cgi_err.read
+ if errmsg.bytesize > 0
+ @logger.error("CGIHandler: #{@script_filename}:\n" + errmsg)
+ end
+ end
+ cgi_err.close(true)
+ end
+
+ if status != 0
+ @logger.error("CGIHandler: #{@script_filename} exit with #{status}")
+ end
+
+ data = "" unless data
+ raw_header, body = data.split(/^[\xd\xa]+/, 2)
+ raise HTTPStatus::InternalServerError,
+ "Premature end of script headers: #{@script_filename}" if body.nil?
+
+ begin
+ header = HTTPUtils::parse_header(raw_header)
+ if /^(\d+)/ =~ header['status'][0]
+ res.status = $1.to_i
+ header.delete('status')
+ end
+ if header.has_key?('location')
+ # RFC 3875 6.2.3, 6.2.4
+ res.status = 302 unless (300...400) === res.status
+ end
+ if header.has_key?('set-cookie')
+ header['set-cookie'].each{|k|
+ res.cookies << Cookie.parse_set_cookie(k)
+ }
+ header.delete('set-cookie')
+ end
+ header.each{|key, val| res[key] = val.join(", ") }
+ rescue => ex
+ raise HTTPStatus::InternalServerError, ex.message
+ end
+ res.body = body
+ end
+ alias do_POST do_GET
+
+ # :startdoc:
+ end
+
+ end
+end
diff --git a/tool/lib/webrick/httpservlet/erbhandler.rb b/tool/lib/webrick/httpservlet/erbhandler.rb
new file mode 100644
index 0000000000..cd09e5f216
--- /dev/null
+++ b/tool/lib/webrick/httpservlet/erbhandler.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: false
+#
+# erbhandler.rb -- ERBHandler Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: erbhandler.rb,v 1.25 2003/02/24 19:25:31 gotoyuzo Exp $
+
+require_relative 'abstract'
+
+require 'erb'
+
+module WEBrick
+ module HTTPServlet
+
+ ##
+ # ERBHandler evaluates an ERB file and returns the result. This handler
+ # is automatically used if there are .rhtml files in a directory served by
+ # the FileHandler.
+ #
+ # ERBHandler supports GET and POST methods.
+ #
+ # The ERB file is evaluated with the local variables +servlet_request+ and
+ # +servlet_response+ which are a WEBrick::HTTPRequest and
+ # WEBrick::HTTPResponse respectively.
+ #
+ # Example .rhtml file:
+ #
+ # Request to <%= servlet_request.request_uri %>
+ #
+ # Query params <%= servlet_request.query.inspect %>
+
+ class ERBHandler < AbstractServlet
+
+ ##
+ # Creates a new ERBHandler on +server+ that will evaluate and serve the
+ # ERB file +name+
+
+ def initialize(server, name)
+ super(server, name)
+ @script_filename = name
+ end
+
+ ##
+ # Handles GET requests
+
+ def do_GET(req, res)
+ unless defined?(ERB)
+ @logger.warn "#{self.class}: ERB not defined."
+ raise HTTPStatus::Forbidden, "ERBHandler cannot work."
+ end
+ begin
+ data = File.open(@script_filename, &:read)
+ res.body = evaluate(ERB.new(data), req, res)
+ res['content-type'] ||=
+ HTTPUtils::mime_type(@script_filename, @config[:MimeTypes])
+ rescue StandardError
+ raise
+ rescue Exception => ex
+ @logger.error(ex)
+ raise HTTPStatus::InternalServerError, ex.message
+ end
+ end
+
+ ##
+ # Handles POST requests
+
+ alias do_POST do_GET
+
+ private
+
+ ##
+ # Evaluates +erb+ providing +servlet_request+ and +servlet_response+ as
+ # local variables.
+
+ def evaluate(erb, servlet_request, servlet_response)
+ Module.new.module_eval{
+ servlet_request.meta_vars
+ servlet_request.query
+ erb.result(binding)
+ }
+ end
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpservlet/filehandler.rb b/tool/lib/webrick/httpservlet/filehandler.rb
new file mode 100644
index 0000000000..010df0e918
--- /dev/null
+++ b/tool/lib/webrick/httpservlet/filehandler.rb
@@ -0,0 +1,552 @@
+# frozen_string_literal: false
+#
+# filehandler.rb -- FileHandler Module
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: filehandler.rb,v 1.44 2003/06/07 01:34:51 gotoyuzo Exp $
+
+require 'time'
+
+require_relative '../htmlutils'
+require_relative '../httputils'
+require_relative '../httpstatus'
+
+module WEBrick
+ module HTTPServlet
+
+ ##
+ # Servlet for serving a single file. You probably want to use the
+ # FileHandler servlet instead as it handles directories and fancy indexes.
+ #
+ # Example:
+ #
+ # server.mount('/my_page.txt', WEBrick::HTTPServlet::DefaultFileHandler,
+ # '/path/to/my_page.txt')
+ #
+ # This servlet handles If-Modified-Since and Range requests.
+
+ class DefaultFileHandler < AbstractServlet
+
+ ##
+ # Creates a DefaultFileHandler instance for the file at +local_path+.
+
+ def initialize(server, local_path)
+ super(server, local_path)
+ @local_path = local_path
+ end
+
+ # :stopdoc:
+
+ def do_GET(req, res)
+ st = File::stat(@local_path)
+ mtime = st.mtime
+ res['etag'] = sprintf("%x-%x-%x", st.ino, st.size, st.mtime.to_i)
+
+ if not_modified?(req, res, mtime, res['etag'])
+ res.body = ''
+ raise HTTPStatus::NotModified
+ elsif req['range']
+ make_partial_content(req, res, @local_path, st.size)
+ raise HTTPStatus::PartialContent
+ else
+ mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes])
+ res['content-type'] = mtype
+ res['content-length'] = st.size.to_s
+ res['last-modified'] = mtime.httpdate
+ res.body = File.open(@local_path, "rb")
+ end
+ end
+
+ def not_modified?(req, res, mtime, etag)
+ if ir = req['if-range']
+ begin
+ if Time.httpdate(ir) >= mtime
+ return true
+ end
+ rescue
+ if HTTPUtils::split_header_value(ir).member?(res['etag'])
+ return true
+ end
+ end
+ end
+
+ if (ims = req['if-modified-since']) && Time.parse(ims) >= mtime
+ return true
+ end
+
+ if (inm = req['if-none-match']) &&
+ HTTPUtils::split_header_value(inm).member?(res['etag'])
+ return true
+ end
+
+ return false
+ end
+
+ # returns a lambda for webrick/httpresponse.rb send_body_proc
+ def multipart_body(body, parts, boundary, mtype, filesize)
+ lambda do |socket|
+ begin
+ begin
+ first = parts.shift
+ last = parts.shift
+ socket.write(
+ "--#{boundary}#{CRLF}" \
+ "Content-Type: #{mtype}#{CRLF}" \
+ "Content-Range: bytes #{first}-#{last}/#{filesize}#{CRLF}" \
+ "#{CRLF}"
+ )
+
+ begin
+ IO.copy_stream(body, socket, last - first + 1, first)
+ rescue NotImplementedError
+ body.seek(first, IO::SEEK_SET)
+ IO.copy_stream(body, socket, last - first + 1)
+ end
+ socket.write(CRLF)
+ end while parts[0]
+ socket.write("--#{boundary}--#{CRLF}")
+ ensure
+ body.close
+ end
+ end
+ end
+
+ def make_partial_content(req, res, filename, filesize)
+ mtype = HTTPUtils::mime_type(filename, @config[:MimeTypes])
+ unless ranges = HTTPUtils::parse_range_header(req['range'])
+ raise HTTPStatus::BadRequest,
+ "Unrecognized range-spec: \"#{req['range']}\""
+ end
+ File.open(filename, "rb"){|io|
+ if ranges.size > 1
+ time = Time.now
+ boundary = "#{time.sec}_#{time.usec}_#{Process::pid}"
+ parts = []
+ ranges.each {|range|
+ prange = prepare_range(range, filesize)
+ next if prange[0] < 0
+ parts.concat(prange)
+ }
+ raise HTTPStatus::RequestRangeNotSatisfiable if parts.empty?
+ res["content-type"] = "multipart/byteranges; boundary=#{boundary}"
+ if req.http_version < '1.1'
+ res['connection'] = 'close'
+ else
+ res.chunked = true
+ end
+ res.body = multipart_body(io.dup, parts, boundary, mtype, filesize)
+ elsif range = ranges[0]
+ first, last = prepare_range(range, filesize)
+ raise HTTPStatus::RequestRangeNotSatisfiable if first < 0
+ res['content-type'] = mtype
+ res['content-range'] = "bytes #{first}-#{last}/#{filesize}"
+ res['content-length'] = (last - first + 1).to_s
+ res.body = io.dup
+ else
+ raise HTTPStatus::BadRequest
+ end
+ }
+ end
+
+ def prepare_range(range, filesize)
+ first = range.first < 0 ? filesize + range.first : range.first
+ return -1, -1 if first < 0 || first >= filesize
+ last = range.last < 0 ? filesize + range.last : range.last
+ last = filesize - 1 if last >= filesize
+ return first, last
+ end
+
+ # :startdoc:
+ end
+
+ ##
+ # Serves a directory including fancy indexing and a variety of other
+ # options.
+ #
+ # Example:
+ #
+ # server.mount('/assets', WEBrick::HTTPServlet::FileHandler,
+ # '/path/to/assets')
+
+ class FileHandler < AbstractServlet
+ HandlerTable = Hash.new # :nodoc:
+
+ ##
+ # Allow custom handling of requests for files with +suffix+ by class
+ # +handler+
+
+ def self.add_handler(suffix, handler)
+ HandlerTable[suffix] = handler
+ end
+
+ ##
+ # Remove custom handling of requests for files with +suffix+
+
+ def self.remove_handler(suffix)
+ HandlerTable.delete(suffix)
+ end
+
+ ##
+ # Creates a FileHandler servlet on +server+ that serves files starting
+ # at directory +root+
+ #
+ # +options+ may be a Hash containing keys from
+ # WEBrick::Config::FileHandler or +true+ or +false+.
+ #
+ # If +options+ is true or false then +:FancyIndexing+ is enabled or
+ # disabled respectively.
+
+ def initialize(server, root, options={}, default=Config::FileHandler)
+ @config = server.config
+ @logger = @config[:Logger]
+ @root = File.expand_path(root)
+ if options == true || options == false
+ options = { :FancyIndexing => options }
+ end
+ @options = default.dup.update(options)
+ end
+
+ # :stopdoc:
+
+ def set_filesystem_encoding(str)
+ enc = Encoding.find('filesystem')
+ if enc == Encoding::US_ASCII
+ str.b
+ else
+ str.dup.force_encoding(enc)
+ end
+ end
+
+ def service(req, res)
+ # if this class is mounted on "/" and /~username is requested.
+ # we're going to override path information before invoking service.
+ if defined?(Etc) && @options[:UserDir] && req.script_name.empty?
+ if %r|^(/~([^/]+))| =~ req.path_info
+ script_name, user = $1, $2
+ path_info = $'
+ begin
+ passwd = Etc::getpwnam(user)
+ @root = File::join(passwd.dir, @options[:UserDir])
+ req.script_name = script_name
+ req.path_info = path_info
+ rescue
+ @logger.debug "#{self.class}#do_GET: getpwnam(#{user}) failed"
+ end
+ end
+ end
+ prevent_directory_traversal(req, res)
+ super(req, res)
+ end
+
+ def do_GET(req, res)
+ unless exec_handler(req, res)
+ set_dir_list(req, res)
+ end
+ end
+
+ def do_POST(req, res)
+ unless exec_handler(req, res)
+ raise HTTPStatus::NotFound, "`#{req.path}' not found."
+ end
+ end
+
+ def do_OPTIONS(req, res)
+ unless exec_handler(req, res)
+ super(req, res)
+ end
+ end
+
+ # ToDo
+ # RFC2518: HTTP Extensions for Distributed Authoring -- WEBDAV
+ #
+ # PROPFIND PROPPATCH MKCOL DELETE PUT COPY MOVE
+ # LOCK UNLOCK
+
+ # RFC3253: Versioning Extensions to WebDAV
+ # (Web Distributed Authoring and Versioning)
+ #
+ # VERSION-CONTROL REPORT CHECKOUT CHECK_IN UNCHECKOUT
+ # MKWORKSPACE UPDATE LABEL MERGE ACTIVITY
+
+ private
+
+ def trailing_pathsep?(path)
+ # check for trailing path separator:
+ # File.dirname("/aaaa/bbbb/") #=> "/aaaa")
+ # File.dirname("/aaaa/bbbb/x") #=> "/aaaa/bbbb")
+ # File.dirname("/aaaa/bbbb") #=> "/aaaa")
+ # File.dirname("/aaaa/bbbbx") #=> "/aaaa")
+ return File.dirname(path) != File.dirname(path+"x")
+ end
+
+ def prevent_directory_traversal(req, res)
+ # Preventing directory traversal on Windows platforms;
+ # Backslashes (0x5c) in path_info are not interpreted as special
+ # character in URI notation. So the value of path_info should be
+ # normalize before accessing to the filesystem.
+
+ # dirty hack for filesystem encoding; in nature, File.expand_path
+ # should not be used for path normalization. [Bug #3345]
+ path = req.path_info.dup.force_encoding(Encoding.find("filesystem"))
+ if trailing_pathsep?(req.path_info)
+ # File.expand_path removes the trailing path separator.
+ # Adding a character is a workaround to save it.
+ # File.expand_path("/aaa/") #=> "/aaa"
+ # File.expand_path("/aaa/" + "x") #=> "/aaa/x"
+ expanded = File.expand_path(path + "x")
+ expanded.chop! # remove trailing "x"
+ else
+ expanded = File.expand_path(path)
+ end
+ expanded.force_encoding(req.path_info.encoding)
+ req.path_info = expanded
+ end
+
+ def exec_handler(req, res)
+ raise HTTPStatus::NotFound, "`#{req.path}' not found." unless @root
+ if set_filename(req, res)
+ handler = get_handler(req, res)
+ call_callback(:HandlerCallback, req, res)
+ h = handler.get_instance(@config, res.filename)
+ h.service(req, res)
+ return true
+ end
+ call_callback(:HandlerCallback, req, res)
+ return false
+ end
+
+ def get_handler(req, res)
+ suffix1 = (/\.(\w+)\z/ =~ res.filename) && $1.downcase
+ if /\.(\w+)\.([\w\-]+)\z/ =~ res.filename
+ if @options[:AcceptableLanguages].include?($2.downcase)
+ suffix2 = $1.downcase
+ end
+ end
+ handler_table = @options[:HandlerTable]
+ return handler_table[suffix1] || handler_table[suffix2] ||
+ HandlerTable[suffix1] || HandlerTable[suffix2] ||
+ DefaultFileHandler
+ end
+
+ def set_filename(req, res)
+ res.filename = @root
+ path_info = req.path_info.scan(%r|/[^/]*|)
+
+ path_info.unshift("") # dummy for checking @root dir
+ while base = path_info.first
+ base = set_filesystem_encoding(base)
+ break if base == "/"
+ break unless File.directory?(File.expand_path(res.filename + base))
+ shift_path_info(req, res, path_info)
+ call_callback(:DirectoryCallback, req, res)
+ end
+
+ if base = path_info.first
+ base = set_filesystem_encoding(base)
+ if base == "/"
+ if file = search_index_file(req, res)
+ shift_path_info(req, res, path_info, file)
+ call_callback(:FileCallback, req, res)
+ return true
+ end
+ shift_path_info(req, res, path_info)
+ elsif file = search_file(req, res, base)
+ shift_path_info(req, res, path_info, file)
+ call_callback(:FileCallback, req, res)
+ return true
+ else
+ raise HTTPStatus::NotFound, "`#{req.path}' not found."
+ end
+ end
+
+ return false
+ end
+
+ def check_filename(req, res, name)
+ if nondisclosure_name?(name) || windows_ambiguous_name?(name)
+ @logger.warn("the request refers nondisclosure name `#{name}'.")
+ raise HTTPStatus::NotFound, "`#{req.path}' not found."
+ end
+ end
+
+ def shift_path_info(req, res, path_info, base=nil)
+ tmp = path_info.shift
+ base = base || set_filesystem_encoding(tmp)
+ req.path_info = path_info.join
+ req.script_name << base
+ res.filename = File.expand_path(res.filename + base)
+ check_filename(req, res, File.basename(res.filename))
+ end
+
+ def search_index_file(req, res)
+ @config[:DirectoryIndex].each{|index|
+ if file = search_file(req, res, "/"+index)
+ return file
+ end
+ }
+ return nil
+ end
+
+ def search_file(req, res, basename)
+ langs = @options[:AcceptableLanguages]
+ path = res.filename + basename
+ if File.file?(path)
+ return basename
+ elsif langs.size > 0
+ req.accept_language.each{|lang|
+ path_with_lang = path + ".#{lang}"
+ if langs.member?(lang) && File.file?(path_with_lang)
+ return basename + ".#{lang}"
+ end
+ }
+ (langs - req.accept_language).each{|lang|
+ path_with_lang = path + ".#{lang}"
+ if File.file?(path_with_lang)
+ return basename + ".#{lang}"
+ end
+ }
+ end
+ return nil
+ end
+
+ def call_callback(callback_name, req, res)
+ if cb = @options[callback_name]
+ cb.call(req, res)
+ end
+ end
+
+ def windows_ambiguous_name?(name)
+ return true if /[. ]+\z/ =~ name
+ return true if /::\$DATA\z/ =~ name
+ return false
+ end
+
+ def nondisclosure_name?(name)
+ @options[:NondisclosureName].each{|pattern|
+ if File.fnmatch(pattern, name, File::FNM_CASEFOLD)
+ return true
+ end
+ }
+ return false
+ end
+
+ def set_dir_list(req, res)
+ redirect_to_directory_uri(req, res)
+ unless @options[:FancyIndexing]
+ raise HTTPStatus::Forbidden, "no access permission to `#{req.path}'"
+ end
+ local_path = res.filename
+ list = Dir::entries(local_path).collect{|name|
+ next if name == "." || name == ".."
+ next if nondisclosure_name?(name)
+ next if windows_ambiguous_name?(name)
+ st = (File::stat(File.join(local_path, name)) rescue nil)
+ if st.nil?
+ [ name, nil, -1 ]
+ elsif st.directory?
+ [ name + "/", st.mtime, -1 ]
+ else
+ [ name, st.mtime, st.size ]
+ end
+ }
+ list.compact!
+
+ query = req.query
+
+ d0 = nil
+ idx = nil
+ %w[N M S].each_with_index do |q, i|
+ if d = query.delete(q)
+ idx ||= i
+ d0 ||= d
+ end
+ end
+ d0 ||= "A"
+ idx ||= 0
+ d1 = (d0 == "A") ? "D" : "A"
+
+ if d0 == "A"
+ list.sort!{|a,b| a[idx] <=> b[idx] }
+ else
+ list.sort!{|a,b| b[idx] <=> a[idx] }
+ end
+
+ namewidth = query["NameWidth"]
+ if namewidth == "*"
+ namewidth = nil
+ elsif !namewidth or (namewidth = namewidth.to_i) < 2
+ namewidth = 25
+ end
+ query = query.inject('') {|s, (k, v)| s << '&' << HTMLUtils::escape("#{k}=#{v}")}
+
+ type = "text/html"
+ case enc = Encoding.find('filesystem')
+ when Encoding::US_ASCII, Encoding::ASCII_8BIT
+ else
+ type << "; charset=\"#{enc.name}\""
+ end
+ res['content-type'] = type
+
+ title = "Index of #{HTMLUtils::escape(req.path)}"
+ res.body = <<-_end_of_html_
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<HTML>
+ <HEAD>
+ <TITLE>#{title}</TITLE>
+ <style type="text/css">
+ <!--
+ .name, .mtime { text-align: left; }
+ .size { text-align: right; }
+ td { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
+ table { border-collapse: collapse; }
+ tr th { border-bottom: 2px groove; }
+ //-->
+ </style>
+ </HEAD>
+ <BODY>
+ <H1>#{title}</H1>
+ _end_of_html_
+
+ res.body << "<TABLE width=\"100%\"><THEAD><TR>\n"
+ res.body << "<TH class=\"name\"><A HREF=\"?N=#{d1}#{query}\">Name</A></TH>"
+ res.body << "<TH class=\"mtime\"><A HREF=\"?M=#{d1}#{query}\">Last modified</A></TH>"
+ res.body << "<TH class=\"size\"><A HREF=\"?S=#{d1}#{query}\">Size</A></TH>\n"
+ res.body << "</TR></THEAD>\n"
+ res.body << "<TBODY>\n"
+
+ query.sub!(/\A&/, '?')
+ list.unshift [ "..", File::mtime(local_path+"/.."), -1 ]
+ list.each{ |name, time, size|
+ if name == ".."
+ dname = "Parent Directory"
+ elsif namewidth and name.size > namewidth
+ dname = name[0...(namewidth - 2)] << '..'
+ else
+ dname = name
+ end
+ s = "<TR><TD class=\"name\"><A HREF=\"#{HTTPUtils::escape(name)}#{query if name.end_with?('/')}\">#{HTMLUtils::escape(dname)}</A></TD>"
+ s << "<TD class=\"mtime\">" << (time ? time.strftime("%Y/%m/%d %H:%M") : "") << "</TD>"
+ s << "<TD class=\"size\">" << (size >= 0 ? size.to_s : "-") << "</TD></TR>\n"
+ res.body << s
+ }
+ res.body << "</TBODY></TABLE>"
+ res.body << "<HR>"
+
+ res.body << <<-_end_of_html_
+ <ADDRESS>
+ #{HTMLUtils::escape(@config[:ServerSoftware])}<BR>
+ at #{req.host}:#{req.port}
+ </ADDRESS>
+ </BODY>
+</HTML>
+ _end_of_html_
+ end
+
+ # :startdoc:
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpservlet/prochandler.rb b/tool/lib/webrick/httpservlet/prochandler.rb
new file mode 100644
index 0000000000..599ffc4340
--- /dev/null
+++ b/tool/lib/webrick/httpservlet/prochandler.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: false
+#
+# prochandler.rb -- ProcHandler Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: prochandler.rb,v 1.7 2002/09/21 12:23:42 gotoyuzo Exp $
+
+require_relative 'abstract'
+
+module WEBrick
+ module HTTPServlet
+
+ ##
+ # Mounts a proc at a path that accepts a request and response.
+ #
+ # Instead of mounting this servlet with WEBrick::HTTPServer#mount use
+ # WEBrick::HTTPServer#mount_proc:
+ #
+ # server.mount_proc '/' do |req, res|
+ # res.body = 'it worked!'
+ # res.status = 200
+ # end
+
+ class ProcHandler < AbstractServlet
+ # :stopdoc:
+ def get_instance(server, *options)
+ self
+ end
+
+ def initialize(proc)
+ @proc = proc
+ end
+
+ def do_GET(request, response)
+ @proc.call(request, response)
+ end
+
+ alias do_POST do_GET
+ # :startdoc:
+ end
+
+ end
+end
diff --git a/tool/lib/webrick/httpstatus.rb b/tool/lib/webrick/httpstatus.rb
new file mode 100644
index 0000000000..c811f21964
--- /dev/null
+++ b/tool/lib/webrick/httpstatus.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: false
+#--
+# httpstatus.rb -- HTTPStatus Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httpstatus.rb,v 1.11 2003/03/24 20:18:55 gotoyuzo Exp $
+
+require_relative 'accesslog'
+
+module WEBrick
+
+ ##
+ # This module is used to manager HTTP status codes.
+ #
+ # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for more
+ # information.
+ module HTTPStatus
+
+ ##
+ # Root of the HTTP status class hierarchy
+ class Status < StandardError
+ class << self
+ attr_reader :code, :reason_phrase # :nodoc:
+ end
+
+ # Returns the HTTP status code
+ def code() self::class::code end
+
+ # Returns the HTTP status description
+ def reason_phrase() self::class::reason_phrase end
+
+ alias to_i code # :nodoc:
+ end
+
+ # Root of the HTTP info statuses
+ class Info < Status; end
+ # Root of the HTTP success statuses
+ class Success < Status; end
+ # Root of the HTTP redirect statuses
+ class Redirect < Status; end
+ # Root of the HTTP error statuses
+ class Error < Status; end
+ # Root of the HTTP client error statuses
+ class ClientError < Error; end
+ # Root of the HTTP server error statuses
+ class ServerError < Error; end
+
+ class EOFError < StandardError; end
+
+ # HTTP status codes and descriptions
+ StatusMessage = { # :nodoc:
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Request Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 426 => 'Upgrade Required',
+ 428 => 'Precondition Required',
+ 429 => 'Too Many Requests',
+ 431 => 'Request Header Fields Too Large',
+ 451 => 'Unavailable For Legal Reasons',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 507 => 'Insufficient Storage',
+ 511 => 'Network Authentication Required',
+ }
+
+ # Maps a status code to the corresponding Status class
+ CodeToError = {} # :nodoc:
+
+ # Creates a status or error class for each status code and
+ # populates the CodeToError map.
+ StatusMessage.each{|code, message|
+ message.freeze
+ var_name = message.gsub(/[ \-]/,'_').upcase
+ err_name = message.gsub(/[ \-]/,'')
+
+ case code
+ when 100...200; parent = Info
+ when 200...300; parent = Success
+ when 300...400; parent = Redirect
+ when 400...500; parent = ClientError
+ when 500...600; parent = ServerError
+ end
+
+ const_set("RC_#{var_name}", code)
+ err_class = Class.new(parent)
+ err_class.instance_variable_set(:@code, code)
+ err_class.instance_variable_set(:@reason_phrase, message)
+ const_set(err_name, err_class)
+ CodeToError[code] = err_class
+ }
+
+ ##
+ # Returns the description corresponding to the HTTP status +code+
+ #
+ # WEBrick::HTTPStatus.reason_phrase 404
+ # => "Not Found"
+ def reason_phrase(code)
+ StatusMessage[code.to_i]
+ end
+
+ ##
+ # Is +code+ an informational status?
+ def info?(code)
+ code.to_i >= 100 and code.to_i < 200
+ end
+
+ ##
+ # Is +code+ a successful status?
+ def success?(code)
+ code.to_i >= 200 and code.to_i < 300
+ end
+
+ ##
+ # Is +code+ a redirection status?
+ def redirect?(code)
+ code.to_i >= 300 and code.to_i < 400
+ end
+
+ ##
+ # Is +code+ an error status?
+ def error?(code)
+ code.to_i >= 400 and code.to_i < 600
+ end
+
+ ##
+ # Is +code+ a client error status?
+ def client_error?(code)
+ code.to_i >= 400 and code.to_i < 500
+ end
+
+ ##
+ # Is +code+ a server error status?
+ def server_error?(code)
+ code.to_i >= 500 and code.to_i < 600
+ end
+
+ ##
+ # Returns the status class corresponding to +code+
+ #
+ # WEBrick::HTTPStatus[302]
+ # => WEBrick::HTTPStatus::NotFound
+ #
+ def self.[](code)
+ CodeToError[code]
+ end
+
+ module_function :reason_phrase
+ module_function :info?, :success?, :redirect?, :error?
+ module_function :client_error?, :server_error?
+ end
+end
diff --git a/tool/lib/webrick/httputils.rb b/tool/lib/webrick/httputils.rb
new file mode 100644
index 0000000000..f1b9ddf9f0
--- /dev/null
+++ b/tool/lib/webrick/httputils.rb
@@ -0,0 +1,512 @@
+# frozen_string_literal: false
+#
+# httputils.rb -- HTTPUtils Module
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httputils.rb,v 1.34 2003/06/05 21:34:08 gotoyuzo Exp $
+
+require 'socket'
+require 'tempfile'
+
+module WEBrick
+ CR = "\x0d" # :nodoc:
+ LF = "\x0a" # :nodoc:
+ CRLF = "\x0d\x0a" # :nodoc:
+
+ ##
+ # HTTPUtils provides utility methods for working with the HTTP protocol.
+ #
+ # This module is generally used internally by WEBrick
+
+ module HTTPUtils
+
+ ##
+ # Normalizes a request path. Raises an exception if the path cannot be
+ # normalized.
+
+ def normalize_path(path)
+ raise "abnormal path `#{path}'" if path[0] != ?/
+ ret = path.dup
+
+ ret.gsub!(%r{/+}o, '/') # // => /
+ while ret.sub!(%r'/\.(?:/|\Z)', '/'); end # /. => /
+ while ret.sub!(%r'/(?!\.\./)[^/]+/\.\.(?:/|\Z)', '/'); end # /foo/.. => /foo
+
+ raise "abnormal path `#{path}'" if %r{/\.\.(/|\Z)} =~ ret
+ ret
+ end
+ module_function :normalize_path
+
+ ##
+ # Default mime types
+
+ DefaultMimeTypes = {
+ "ai" => "application/postscript",
+ "asc" => "text/plain",
+ "avi" => "video/x-msvideo",
+ "bin" => "application/octet-stream",
+ "bmp" => "image/bmp",
+ "class" => "application/octet-stream",
+ "cer" => "application/pkix-cert",
+ "crl" => "application/pkix-crl",
+ "crt" => "application/x-x509-ca-cert",
+ #"crl" => "application/x-pkcs7-crl",
+ "css" => "text/css",
+ "dms" => "application/octet-stream",
+ "doc" => "application/msword",
+ "dvi" => "application/x-dvi",
+ "eps" => "application/postscript",
+ "etx" => "text/x-setext",
+ "exe" => "application/octet-stream",
+ "gif" => "image/gif",
+ "htm" => "text/html",
+ "html" => "text/html",
+ "jpe" => "image/jpeg",
+ "jpeg" => "image/jpeg",
+ "jpg" => "image/jpeg",
+ "js" => "application/javascript",
+ "json" => "application/json",
+ "lha" => "application/octet-stream",
+ "lzh" => "application/octet-stream",
+ "mjs" => "application/javascript",
+ "mov" => "video/quicktime",
+ "mpe" => "video/mpeg",
+ "mpeg" => "video/mpeg",
+ "mpg" => "video/mpeg",
+ "pbm" => "image/x-portable-bitmap",
+ "pdf" => "application/pdf",
+ "pgm" => "image/x-portable-graymap",
+ "png" => "image/png",
+ "pnm" => "image/x-portable-anymap",
+ "ppm" => "image/x-portable-pixmap",
+ "ppt" => "application/vnd.ms-powerpoint",
+ "ps" => "application/postscript",
+ "qt" => "video/quicktime",
+ "ras" => "image/x-cmu-raster",
+ "rb" => "text/plain",
+ "rd" => "text/plain",
+ "rtf" => "application/rtf",
+ "sgm" => "text/sgml",
+ "sgml" => "text/sgml",
+ "svg" => "image/svg+xml",
+ "tif" => "image/tiff",
+ "tiff" => "image/tiff",
+ "txt" => "text/plain",
+ "wasm" => "application/wasm",
+ "xbm" => "image/x-xbitmap",
+ "xhtml" => "text/html",
+ "xls" => "application/vnd.ms-excel",
+ "xml" => "text/xml",
+ "xpm" => "image/x-xpixmap",
+ "xwd" => "image/x-xwindowdump",
+ "zip" => "application/zip",
+ }
+
+ ##
+ # Loads Apache-compatible mime.types in +file+.
+
+ def load_mime_types(file)
+ # note: +file+ may be a "| command" for now; some people may
+ # rely on this, but currently we do not use this method by default.
+ open(file){ |io|
+ hash = Hash.new
+ io.each{ |line|
+ next if /^#/ =~ line
+ line.chomp!
+ mimetype, ext0 = line.split(/\s+/, 2)
+ next unless ext0
+ next if ext0.empty?
+ ext0.split(/\s+/).each{ |ext| hash[ext] = mimetype }
+ }
+ hash
+ }
+ end
+ module_function :load_mime_types
+
+ ##
+ # Returns the mime type of +filename+ from the list in +mime_tab+. If no
+ # mime type was found application/octet-stream is returned.
+
+ def mime_type(filename, mime_tab)
+ suffix1 = (/\.(\w+)$/ =~ filename && $1.downcase)
+ suffix2 = (/\.(\w+)\.[\w\-]+$/ =~ filename && $1.downcase)
+ mime_tab[suffix1] || mime_tab[suffix2] || "application/octet-stream"
+ end
+ module_function :mime_type
+
+ ##
+ # Parses an HTTP header +raw+ into a hash of header fields with an Array
+ # of values.
+
+ def parse_header(raw)
+ header = Hash.new([].freeze)
+ field = nil
+ raw.each_line{|line|
+ case line
+ when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
+ field, value = $1, $2
+ field.downcase!
+ header[field] = [] unless header.has_key?(field)
+ header[field] << value
+ when /^\s+(.*?)\s*\z/om
+ value = $1
+ unless field
+ raise HTTPStatus::BadRequest, "bad header '#{line}'."
+ end
+ header[field][-1] << " " << value
+ else
+ raise HTTPStatus::BadRequest, "bad header '#{line}'."
+ end
+ }
+ header.each{|key, values|
+ values.each(&:strip!)
+ }
+ header
+ end
+ module_function :parse_header
+
+ ##
+ # Splits a header value +str+ according to HTTP specification.
+
+ def split_header_value(str)
+ str.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+)
+ (?:,\s*|\Z)'xn).flatten
+ end
+ module_function :split_header_value
+
+ ##
+ # Parses a Range header value +ranges_specifier+
+
+ def parse_range_header(ranges_specifier)
+ if /^bytes=(.*)/ =~ ranges_specifier
+ byte_range_set = split_header_value($1)
+ byte_range_set.collect{|range_spec|
+ case range_spec
+ when /^(\d+)-(\d+)/ then $1.to_i .. $2.to_i
+ when /^(\d+)-/ then $1.to_i .. -1
+ when /^-(\d+)/ then -($1.to_i) .. -1
+ else return nil
+ end
+ }
+ end
+ end
+ module_function :parse_range_header
+
+ ##
+ # Parses q values in +value+ as used in Accept headers.
+
+ def parse_qvalues(value)
+ tmp = []
+ if value
+ parts = value.split(/,\s*/)
+ parts.each {|part|
+ if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
+ val = m[1]
+ q = (m[2] or 1).to_f
+ tmp.push([val, q])
+ end
+ }
+ tmp = tmp.sort_by{|val, q| -q}
+ tmp.collect!{|val, q| val}
+ end
+ return tmp
+ end
+ module_function :parse_qvalues
+
+ ##
+ # Removes quotes and escapes from +str+
+
+ def dequote(str)
+ ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
+ ret.gsub!(/\\(.)/, "\\1")
+ ret
+ end
+ module_function :dequote
+
+ ##
+ # Quotes and escapes quotes in +str+
+
+ def quote(str)
+ '"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
+ end
+ module_function :quote
+
+ ##
+ # Stores multipart form data. FormData objects are created when
+ # WEBrick::HTTPUtils.parse_form_data is called.
+
+ class FormData < String
+ EmptyRawHeader = [].freeze # :nodoc:
+ EmptyHeader = {}.freeze # :nodoc:
+
+ ##
+ # The name of the form data part
+
+ attr_accessor :name
+
+ ##
+ # The filename of the form data part
+
+ attr_accessor :filename
+
+ attr_accessor :next_data # :nodoc:
+ protected :next_data
+
+ ##
+ # Creates a new FormData object.
+ #
+ # +args+ is an Array of form data entries. One FormData will be created
+ # for each entry.
+ #
+ # This is called by WEBrick::HTTPUtils.parse_form_data for you
+
+ def initialize(*args)
+ @name = @filename = @next_data = nil
+ if args.empty?
+ @raw_header = []
+ @header = nil
+ super("")
+ else
+ @raw_header = EmptyRawHeader
+ @header = EmptyHeader
+ super(args.shift)
+ unless args.empty?
+ @next_data = self.class.new(*args)
+ end
+ end
+ end
+
+ ##
+ # Retrieves the header at the first entry in +key+
+
+ def [](*key)
+ begin
+ @header[key[0].downcase].join(", ")
+ rescue StandardError, NameError
+ super
+ end
+ end
+
+ ##
+ # Adds +str+ to this FormData which may be the body, a header or a
+ # header entry.
+ #
+ # This is called by WEBrick::HTTPUtils.parse_form_data for you
+
+ def <<(str)
+ if @header
+ super
+ elsif str == CRLF
+ @header = HTTPUtils::parse_header(@raw_header.join)
+ if cd = self['content-disposition']
+ if /\s+name="(.*?)"/ =~ cd then @name = $1 end
+ if /\s+filename="(.*?)"/ =~ cd then @filename = $1 end
+ end
+ else
+ @raw_header << str
+ end
+ self
+ end
+
+ ##
+ # Adds +data+ at the end of the chain of entries
+ #
+ # This is called by WEBrick::HTTPUtils.parse_form_data for you.
+
+ def append_data(data)
+ tmp = self
+ while tmp
+ unless tmp.next_data
+ tmp.next_data = data
+ break
+ end
+ tmp = tmp.next_data
+ end
+ self
+ end
+
+ ##
+ # Yields each entry in this FormData
+
+ def each_data
+ tmp = self
+ while tmp
+ next_data = tmp.next_data
+ yield(tmp)
+ tmp = next_data
+ end
+ end
+
+ ##
+ # Returns all the FormData as an Array
+
+ def list
+ ret = []
+ each_data{|data|
+ ret << data.to_s
+ }
+ ret
+ end
+
+ ##
+ # A FormData will behave like an Array
+
+ alias :to_ary :list
+
+ ##
+ # This FormData's body
+
+ def to_s
+ String.new(self)
+ end
+ end
+
+ ##
+ # Parses the query component of a URI in +str+
+
+ def parse_query(str)
+ query = Hash.new
+ if str
+ str.split(/[&;]/).each{|x|
+ next if x.empty?
+ key, val = x.split(/=/,2)
+ key = unescape_form(key)
+ val = unescape_form(val.to_s)
+ val = FormData.new(val)
+ val.name = key
+ if query.has_key?(key)
+ query[key].append_data(val)
+ next
+ end
+ query[key] = val
+ }
+ end
+ query
+ end
+ module_function :parse_query
+
+ ##
+ # Parses form data in +io+ with the given +boundary+
+
+ def parse_form_data(io, boundary)
+ boundary_regexp = /\A--#{Regexp.quote(boundary)}(--)?#{CRLF}\z/
+ form_data = Hash.new
+ return form_data unless io
+ data = nil
+ io.each_line{|line|
+ if boundary_regexp =~ line
+ if data
+ data.chop!
+ key = data.name
+ if form_data.has_key?(key)
+ form_data[key].append_data(data)
+ else
+ form_data[key] = data
+ end
+ end
+ data = FormData.new
+ next
+ else
+ if data
+ data << line
+ end
+ end
+ }
+ return form_data
+ end
+ module_function :parse_form_data
+
+ #####
+
+ reserved = ';/?:@&=+$,'
+ num = '0123456789'
+ lowalpha = 'abcdefghijklmnopqrstuvwxyz'
+ upalpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ mark = '-_.!~*\'()'
+ unreserved = num + lowalpha + upalpha + mark
+ control = (0x0..0x1f).collect{|c| c.chr }.join + "\x7f"
+ space = " "
+ delims = '<>#%"'
+ unwise = '{}|\\^[]`'
+ nonascii = (0x80..0xff).collect{|c| c.chr }.join
+
+ module_function
+
+ # :stopdoc:
+
+ def _make_regex(str) /([#{Regexp.escape(str)}])/n end
+ def _make_regex!(str) /([^#{Regexp.escape(str)}])/n end
+ def _escape(str, regex)
+ str = str.b
+ str.gsub!(regex) {"%%%02X" % $1.ord}
+ # %-escaped string should contain US-ASCII only
+ str.force_encoding(Encoding::US_ASCII)
+ end
+ def _unescape(str, regex)
+ str = str.b
+ str.gsub!(regex) {$1.hex.chr}
+ # encoding of %-unescaped string is unknown
+ str
+ end
+
+ UNESCAPED = _make_regex(control+space+delims+unwise+nonascii)
+ UNESCAPED_FORM = _make_regex(reserved+control+delims+unwise+nonascii)
+ NONASCII = _make_regex(nonascii)
+ ESCAPED = /%([0-9a-fA-F]{2})/
+ UNESCAPED_PCHAR = _make_regex!(unreserved+":@&=+$,")
+
+ # :startdoc:
+
+ ##
+ # Escapes HTTP reserved and unwise characters in +str+
+
+ def escape(str)
+ _escape(str, UNESCAPED)
+ end
+
+ ##
+ # Unescapes HTTP reserved and unwise characters in +str+
+
+ def unescape(str)
+ _unescape(str, ESCAPED)
+ end
+
+ ##
+ # Escapes form reserved characters in +str+
+
+ def escape_form(str)
+ ret = _escape(str, UNESCAPED_FORM)
+ ret.gsub!(/ /, "+")
+ ret
+ end
+
+ ##
+ # Unescapes form reserved characters in +str+
+
+ def unescape_form(str)
+ _unescape(str.gsub(/\+/, " "), ESCAPED)
+ end
+
+ ##
+ # Escapes path +str+
+
+ def escape_path(str)
+ result = ""
+ str.scan(%r{/([^/]*)}).each{|i|
+ result << "/" << _escape(i[0], UNESCAPED_PCHAR)
+ }
+ return result
+ end
+
+ ##
+ # Escapes 8 bit characters in +str+
+
+ def escape8bit(str)
+ _escape(str, NONASCII)
+ end
+ end
+end
diff --git a/tool/lib/webrick/httpversion.rb b/tool/lib/webrick/httpversion.rb
new file mode 100644
index 0000000000..8a251944a2
--- /dev/null
+++ b/tool/lib/webrick/httpversion.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: false
+#--
+# HTTPVersion.rb -- presentation of HTTP version
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httpversion.rb,v 1.5 2002/09/21 12:23:37 gotoyuzo Exp $
+
+module WEBrick
+
+ ##
+ # Represents an HTTP protocol version
+
+ class HTTPVersion
+ include Comparable
+
+ ##
+ # The major protocol version number
+
+ attr_accessor :major
+
+ ##
+ # The minor protocol version number
+
+ attr_accessor :minor
+
+ ##
+ # Converts +version+ into an HTTPVersion
+
+ def self.convert(version)
+ version.is_a?(self) ? version : new(version)
+ end
+
+ ##
+ # Creates a new HTTPVersion from +version+.
+
+ def initialize(version)
+ case version
+ when HTTPVersion
+ @major, @minor = version.major, version.minor
+ when String
+ if /^(\d+)\.(\d+)$/ =~ version
+ @major, @minor = $1.to_i, $2.to_i
+ end
+ end
+ if @major.nil? || @minor.nil?
+ raise ArgumentError,
+ format("cannot convert %s into %s", version.class, self.class)
+ end
+ end
+
+ ##
+ # Compares this version with +other+ according to the HTTP specification
+ # rules.
+
+ def <=>(other)
+ unless other.is_a?(self.class)
+ other = self.class.new(other)
+ end
+ if (ret = @major <=> other.major) == 0
+ return @minor <=> other.minor
+ end
+ return ret
+ end
+
+ ##
+ # The HTTP version as show in the HTTP request and response. For example,
+ # "1.1"
+
+ def to_s
+ format("%d.%d", @major, @minor)
+ end
+ end
+end
diff --git a/tool/lib/webrick/log.rb b/tool/lib/webrick/log.rb
new file mode 100644
index 0000000000..2c1fdfe602
--- /dev/null
+++ b/tool/lib/webrick/log.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: false
+#--
+# log.rb -- Log Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: log.rb,v 1.26 2002/10/06 17:06:10 gotoyuzo Exp $
+
+module WEBrick
+
+ ##
+ # A generic logging class
+
+ class BasicLog
+
+ # Fatal log level which indicates a server crash
+
+ FATAL = 1
+
+ # Error log level which indicates a recoverable error
+
+ ERROR = 2
+
+ # Warning log level which indicates a possible problem
+
+ WARN = 3
+
+ # Information log level which indicates possibly useful information
+
+ INFO = 4
+
+ # Debugging error level for messages used in server development or
+ # debugging
+
+ DEBUG = 5
+
+ # log-level, messages above this level will be logged
+ attr_accessor :level
+
+ ##
+ # Initializes a new logger for +log_file+ that outputs messages at +level+
+ # or higher. +log_file+ can be a filename, an IO-like object that
+ # responds to #<< or nil which outputs to $stderr.
+ #
+ # If no level is given INFO is chosen by default
+
+ def initialize(log_file=nil, level=nil)
+ @level = level || INFO
+ case log_file
+ when String
+ @log = File.open(log_file, "a+")
+ @log.sync = true
+ @opened = true
+ when NilClass
+ @log = $stderr
+ else
+ @log = log_file # requires "<<". (see BasicLog#log)
+ end
+ end
+
+ ##
+ # Closes the logger (also closes the log device associated to the logger)
+ def close
+ @log.close if @opened
+ @log = nil
+ end
+
+ ##
+ # Logs +data+ at +level+ if the given level is above the current log
+ # level.
+
+ def log(level, data)
+ if @log && level <= @level
+ data += "\n" if /\n\Z/ !~ data
+ @log << data
+ end
+ end
+
+ ##
+ # Synonym for log(INFO, obj.to_s)
+ def <<(obj)
+ log(INFO, obj.to_s)
+ end
+
+ # Shortcut for logging a FATAL message
+ def fatal(msg) log(FATAL, "FATAL " << format(msg)); end
+ # Shortcut for logging an ERROR message
+ def error(msg) log(ERROR, "ERROR " << format(msg)); end
+ # Shortcut for logging a WARN message
+ def warn(msg) log(WARN, "WARN " << format(msg)); end
+ # Shortcut for logging an INFO message
+ def info(msg) log(INFO, "INFO " << format(msg)); end
+ # Shortcut for logging a DEBUG message
+ def debug(msg) log(DEBUG, "DEBUG " << format(msg)); end
+
+ # Will the logger output FATAL messages?
+ def fatal?; @level >= FATAL; end
+ # Will the logger output ERROR messages?
+ def error?; @level >= ERROR; end
+ # Will the logger output WARN messages?
+ def warn?; @level >= WARN; end
+ # Will the logger output INFO messages?
+ def info?; @level >= INFO; end
+ # Will the logger output DEBUG messages?
+ def debug?; @level >= DEBUG; end
+
+ private
+
+ ##
+ # Formats +arg+ for the logger
+ #
+ # * If +arg+ is an Exception, it will format the error message and
+ # the back trace.
+ # * If +arg+ responds to #to_str, it will return it.
+ # * Otherwise it will return +arg+.inspect.
+ def format(arg)
+ if arg.is_a?(Exception)
+ "#{arg.class}: #{AccessLog.escape(arg.message)}\n\t" <<
+ arg.backtrace.join("\n\t") << "\n"
+ elsif arg.respond_to?(:to_str)
+ AccessLog.escape(arg.to_str)
+ else
+ arg.inspect
+ end
+ end
+ end
+
+ ##
+ # A logging class that prepends a timestamp to each message.
+
+ class Log < BasicLog
+ # Format of the timestamp which is applied to each logged line. The
+ # default is <tt>"[%Y-%m-%d %H:%M:%S]"</tt>
+ attr_accessor :time_format
+
+ ##
+ # Same as BasicLog#initialize
+ #
+ # You can set the timestamp format through #time_format
+ def initialize(log_file=nil, level=nil)
+ super(log_file, level)
+ @time_format = "[%Y-%m-%d %H:%M:%S]"
+ end
+
+ ##
+ # Same as BasicLog#log
+ def log(level, data)
+ tmp = Time.now.strftime(@time_format)
+ tmp << " " << data
+ super(level, tmp)
+ end
+ end
+end
diff --git a/tool/lib/webrick/server.rb b/tool/lib/webrick/server.rb
new file mode 100644
index 0000000000..fd6b7a61b5
--- /dev/null
+++ b/tool/lib/webrick/server.rb
@@ -0,0 +1,381 @@
+# frozen_string_literal: false
+#
+# server.rb -- GenericServer Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: server.rb,v 1.62 2003/07/22 19:20:43 gotoyuzo Exp $
+
+require 'socket'
+require_relative 'config'
+require_relative 'log'
+
+module WEBrick
+
+ ##
+ # Server error exception
+
+ class ServerError < StandardError; end
+
+ ##
+ # Base server class
+
+ class SimpleServer
+
+ ##
+ # A SimpleServer only yields when you start it
+
+ def SimpleServer.start
+ yield
+ end
+ end
+
+ ##
+ # A generic module for daemonizing a process
+
+ class Daemon
+
+ ##
+ # Performs the standard operations for daemonizing a process. Runs a
+ # block, if given.
+
+ def Daemon.start
+ Process.daemon
+ File.umask(0)
+ yield if block_given?
+ end
+ end
+
+ ##
+ # Base TCP server class. You must subclass GenericServer and provide a #run
+ # method.
+
+ class GenericServer
+
+ ##
+ # The server status. One of :Stop, :Running or :Shutdown
+
+ attr_reader :status
+
+ ##
+ # The server configuration
+
+ attr_reader :config
+
+ ##
+ # The server logger. This is independent from the HTTP access log.
+
+ attr_reader :logger
+
+ ##
+ # Tokens control the number of outstanding clients. The
+ # <code>:MaxClients</code> configuration sets this.
+
+ attr_reader :tokens
+
+ ##
+ # Sockets listening for connections.
+
+ attr_reader :listeners
+
+ ##
+ # Creates a new generic server from +config+. The default configuration
+ # comes from +default+.
+
+ def initialize(config={}, default=Config::General)
+ @config = default.dup.update(config)
+ @status = :Stop
+ @config[:Logger] ||= Log::new
+ @logger = @config[:Logger]
+
+ @tokens = Thread::SizedQueue.new(@config[:MaxClients])
+ @config[:MaxClients].times{ @tokens.push(nil) }
+
+ webrickv = WEBrick::VERSION
+ rubyv = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
+ @logger.info("WEBrick #{webrickv}")
+ @logger.info("ruby #{rubyv}")
+
+ @listeners = []
+ @shutdown_pipe = nil
+ unless @config[:DoNotListen]
+ raise ArgumentError, "Port must an integer" unless @config[:Port].to_s == @config[:Port].to_i.to_s
+
+ @config[:Port] = @config[:Port].to_i
+ if @config[:Listen]
+ warn(":Listen option is deprecated; use GenericServer#listen", uplevel: 1)
+ end
+ listen(@config[:BindAddress], @config[:Port])
+ if @config[:Port] == 0
+ @config[:Port] = @listeners[0].addr[1]
+ end
+ end
+ end
+
+ ##
+ # Retrieves +key+ from the configuration
+
+ def [](key)
+ @config[key]
+ end
+
+ ##
+ # Adds listeners from +address+ and +port+ to the server. See
+ # WEBrick::Utils::create_listeners for details.
+
+ def listen(address, port)
+ @listeners += Utils::create_listeners(address, port)
+ end
+
+ ##
+ # Starts the server and runs the +block+ for each connection. This method
+ # does not return until the server is stopped from a signal handler or
+ # another thread using #stop or #shutdown.
+ #
+ # If the block raises a subclass of StandardError the exception is logged
+ # and ignored. If an IOError or Errno::EBADF exception is raised the
+ # exception is ignored. If an Exception subclass is raised the exception
+ # is logged and re-raised which stops the server.
+ #
+ # To completely shut down a server call #shutdown from ensure:
+ #
+ # server = WEBrick::GenericServer.new
+ # # or WEBrick::HTTPServer.new
+ #
+ # begin
+ # server.start
+ # ensure
+ # server.shutdown
+ # end
+
+ def start(&block)
+ raise ServerError, "already started." if @status != :Stop
+ server_type = @config[:ServerType] || SimpleServer
+
+ setup_shutdown_pipe
+
+ server_type.start{
+ @logger.info \
+ "#{self.class}#start: pid=#{$$} port=#{@config[:Port]}"
+ @status = :Running
+ call_callback(:StartCallback)
+
+ shutdown_pipe = @shutdown_pipe
+
+ thgroup = ThreadGroup.new
+ begin
+ while @status == :Running
+ begin
+ sp = shutdown_pipe[0]
+ if svrs = IO.select([sp, *@listeners])
+ if svrs[0].include? sp
+ # swallow shutdown pipe
+ buf = String.new
+ nil while String ===
+ sp.read_nonblock([sp.nread, 8].max, buf, exception: false)
+ break
+ end
+ svrs[0].each{|svr|
+ @tokens.pop # blocks while no token is there.
+ if sock = accept_client(svr)
+ unless config[:DoNotReverseLookup].nil?
+ sock.do_not_reverse_lookup = !!config[:DoNotReverseLookup]
+ end
+ th = start_thread(sock, &block)
+ th[:WEBrickThread] = true
+ thgroup.add(th)
+ else
+ @tokens.push(nil)
+ end
+ }
+ end
+ rescue Errno::EBADF, Errno::ENOTSOCK, IOError => ex
+ # if the listening socket was closed in GenericServer#shutdown,
+ # IO::select raise it.
+ rescue StandardError => ex
+ msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
+ @logger.error msg
+ rescue Exception => ex
+ @logger.fatal ex
+ raise
+ end
+ end
+ ensure
+ cleanup_shutdown_pipe(shutdown_pipe)
+ cleanup_listener
+ @status = :Shutdown
+ @logger.info "going to shutdown ..."
+ thgroup.list.each{|th| th.join if th[:WEBrickThread] }
+ call_callback(:StopCallback)
+ @logger.info "#{self.class}#start done."
+ @status = :Stop
+ end
+ }
+ end
+
+ ##
+ # Stops the server from accepting new connections.
+
+ def stop
+ if @status == :Running
+ @status = :Shutdown
+ end
+
+ alarm_shutdown_pipe {|f| f.write_nonblock("\0")}
+ end
+
+ ##
+ # Shuts down the server and all listening sockets. New listeners must be
+ # provided to restart the server.
+
+ def shutdown
+ stop
+
+ alarm_shutdown_pipe(&:close)
+ end
+
+ ##
+ # You must subclass GenericServer and implement \#run which accepts a TCP
+ # client socket
+
+ def run(sock)
+ @logger.fatal "run() must be provided by user."
+ end
+
+ private
+
+ # :stopdoc:
+
+ ##
+ # Accepts a TCP client socket from the TCP server socket +svr+ and returns
+ # the client socket.
+
+ def accept_client(svr)
+ case sock = svr.to_io.accept_nonblock(exception: false)
+ when :wait_readable
+ nil
+ else
+ if svr.respond_to?(:start_immediately)
+ sock = OpenSSL::SSL::SSLSocket.new(sock, ssl_context)
+ sock.sync_close = true
+ # we cannot do OpenSSL::SSL::SSLSocket#accept here because
+ # a slow client can prevent us from accepting connections
+ # from other clients
+ end
+ sock
+ end
+ rescue Errno::ECONNRESET, Errno::ECONNABORTED,
+ Errno::EPROTO, Errno::EINVAL
+ nil
+ rescue StandardError => ex
+ msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
+ @logger.error msg
+ nil
+ end
+
+ ##
+ # Starts a server thread for the client socket +sock+ that runs the given
+ # +block+.
+ #
+ # Sets the socket to the <code>:WEBrickSocket</code> thread local variable
+ # in the thread.
+ #
+ # If any errors occur in the block they are logged and handled.
+
+ def start_thread(sock, &block)
+ Thread.start{
+ begin
+ Thread.current[:WEBrickSocket] = sock
+ begin
+ addr = sock.peeraddr
+ @logger.debug "accept: #{addr[3]}:#{addr[1]}"
+ rescue SocketError
+ @logger.debug "accept: <address unknown>"
+ raise
+ end
+ if sock.respond_to?(:sync_close=) && @config[:SSLStartImmediately]
+ WEBrick::Utils.timeout(@config[:RequestTimeout]) do
+ begin
+ sock.accept # OpenSSL::SSL::SSLSocket#accept
+ rescue Errno::ECONNRESET, Errno::ECONNABORTED,
+ Errno::EPROTO, Errno::EINVAL
+ Thread.exit
+ end
+ end
+ end
+ call_callback(:AcceptCallback, sock)
+ block ? block.call(sock) : run(sock)
+ rescue Errno::ENOTCONN
+ @logger.debug "Errno::ENOTCONN raised"
+ rescue ServerError => ex
+ msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
+ @logger.error msg
+ rescue Exception => ex
+ @logger.error ex
+ ensure
+ @tokens.push(nil)
+ Thread.current[:WEBrickSocket] = nil
+ if addr
+ @logger.debug "close: #{addr[3]}:#{addr[1]}"
+ else
+ @logger.debug "close: <address unknown>"
+ end
+ sock.close
+ end
+ }
+ end
+
+ ##
+ # Calls the callback +callback_name+ from the configuration with +args+
+
+ def call_callback(callback_name, *args)
+ @config[callback_name]&.call(*args)
+ end
+
+ def setup_shutdown_pipe
+ return @shutdown_pipe ||= IO.pipe
+ end
+
+ def cleanup_shutdown_pipe(shutdown_pipe)
+ @shutdown_pipe = nil
+ shutdown_pipe&.each(&:close)
+ end
+
+ def alarm_shutdown_pipe
+ _, pipe = @shutdown_pipe # another thread may modify @shutdown_pipe.
+ if pipe
+ if !pipe.closed?
+ begin
+ yield pipe
+ rescue IOError # closed by another thread.
+ end
+ end
+ end
+ end
+
+ def cleanup_listener
+ @listeners.each{|s|
+ if @logger.debug?
+ addr = s.addr
+ @logger.debug("close TCPSocket(#{addr[2]}, #{addr[1]})")
+ end
+ begin
+ s.shutdown
+ rescue Errno::ENOTCONN
+ # when `Errno::ENOTCONN: Socket is not connected' on some platforms,
+ # call #close instead of #shutdown.
+ # (ignore @config[:ShutdownSocketWithoutClose])
+ s.close
+ else
+ unless @config[:ShutdownSocketWithoutClose]
+ s.close
+ end
+ end
+ }
+ @listeners.clear
+ end
+ end # end of GenericServer
+end
diff --git a/tool/lib/webrick/ssl.rb b/tool/lib/webrick/ssl.rb
new file mode 100644
index 0000000000..e448095a12
--- /dev/null
+++ b/tool/lib/webrick/ssl.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: false
+#
+# ssl.rb -- SSL/TLS enhancement for GenericServer
+#
+# Copyright (c) 2003 GOTOU Yuuzou All rights reserved.
+#
+# $Id$
+
+require 'webrick'
+require 'openssl'
+
+module WEBrick
+ module Config
+ svrsoft = General[:ServerSoftware]
+ osslv = ::OpenSSL::OPENSSL_VERSION.split[1]
+
+ ##
+ # Default SSL server configuration.
+ #
+ # WEBrick can automatically create a self-signed certificate if
+ # <code>:SSLCertName</code> is set. For more information on the various
+ # SSL options see OpenSSL::SSL::SSLContext.
+ #
+ # :ServerSoftware ::
+ # The server software name used in the Server: header.
+ # :SSLEnable :: false,
+ # Enable SSL for this server. Defaults to false.
+ # :SSLCertificate ::
+ # The SSL certificate for the server.
+ # :SSLPrivateKey ::
+ # The SSL private key for the server certificate.
+ # :SSLClientCA :: nil,
+ # Array of certificates that will be sent to the client.
+ # :SSLExtraChainCert :: nil,
+ # Array of certificates that will be added to the certificate chain
+ # :SSLCACertificateFile :: nil,
+ # Path to a CA certificate file
+ # :SSLCACertificatePath :: nil,
+ # Path to a directory containing CA certificates
+ # :SSLCertificateStore :: nil,
+ # OpenSSL::X509::Store used for certificate validation of the client
+ # :SSLTmpDhCallback :: nil,
+ # Callback invoked when DH parameters are required.
+ # :SSLVerifyClient ::
+ # Sets whether the client is verified. This defaults to VERIFY_NONE
+ # which is typical for an HTTPS server.
+ # :SSLVerifyDepth ::
+ # Number of CA certificates to walk when verifying a certificate chain
+ # :SSLVerifyCallback ::
+ # Custom certificate verification callback
+ # :SSLServerNameCallback::
+ # Custom servername indication callback
+ # :SSLTimeout ::
+ # Maximum session lifetime
+ # :SSLOptions ::
+ # Various SSL options
+ # :SSLCiphers ::
+ # Ciphers to be used
+ # :SSLStartImmediately ::
+ # Immediately start SSL upon connection? Defaults to true
+ # :SSLCertName ::
+ # SSL certificate name. Must be set to enable automatic certificate
+ # creation.
+ # :SSLCertComment ::
+ # Comment used during automatic certificate creation.
+
+ SSL = {
+ :ServerSoftware => "#{svrsoft} OpenSSL/#{osslv}",
+ :SSLEnable => false,
+ :SSLCertificate => nil,
+ :SSLPrivateKey => nil,
+ :SSLClientCA => nil,
+ :SSLExtraChainCert => nil,
+ :SSLCACertificateFile => nil,
+ :SSLCACertificatePath => nil,
+ :SSLCertificateStore => nil,
+ :SSLTmpDhCallback => nil,
+ :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
+ :SSLVerifyDepth => nil,
+ :SSLVerifyCallback => nil, # custom verification
+ :SSLTimeout => nil,
+ :SSLOptions => nil,
+ :SSLCiphers => nil,
+ :SSLStartImmediately => true,
+ # Must specify if you use auto generated certificate.
+ :SSLCertName => nil,
+ :SSLCertComment => "Generated by Ruby/OpenSSL"
+ }
+ General.update(SSL)
+ end
+
+ module Utils
+ ##
+ # Creates a self-signed certificate with the given number of +bits+,
+ # the issuer +cn+ and a +comment+ to be stored in the certificate.
+
+ def create_self_signed_cert(bits, cn, comment)
+ rsa = OpenSSL::PKey::RSA.new(bits){|p, n|
+ case p
+ when 0; $stderr.putc "." # BN_generate_prime
+ when 1; $stderr.putc "+" # BN_generate_prime
+ when 2; $stderr.putc "*" # searching good prime,
+ # n = #of try,
+ # but also data from BN_generate_prime
+ when 3; $stderr.putc "\n" # found good prime, n==0 - p, n==1 - q,
+ # but also data from BN_generate_prime
+ else; $stderr.putc "*" # BN_generate_prime
+ end
+ }
+ cert = OpenSSL::X509::Certificate.new
+ cert.version = 2
+ cert.serial = 1
+ name = (cn.kind_of? String) ? OpenSSL::X509::Name.parse(cn)
+ : OpenSSL::X509::Name.new(cn)
+ cert.subject = name
+ cert.issuer = name
+ cert.not_before = Time.now
+ cert.not_after = Time.now + (365*24*60*60)
+ cert.public_key = rsa.public_key
+
+ ef = OpenSSL::X509::ExtensionFactory.new(nil,cert)
+ ef.issuer_certificate = cert
+ cert.extensions = [
+ ef.create_extension("basicConstraints","CA:FALSE"),
+ ef.create_extension("keyUsage", "keyEncipherment, digitalSignature, keyAgreement, dataEncipherment"),
+ ef.create_extension("subjectKeyIdentifier", "hash"),
+ ef.create_extension("extendedKeyUsage", "serverAuth"),
+ ef.create_extension("nsComment", comment),
+ ]
+ aki = ef.create_extension("authorityKeyIdentifier",
+ "keyid:always,issuer:always")
+ cert.add_extension(aki)
+ cert.sign(rsa, "SHA256")
+
+ return [ cert, rsa ]
+ end
+ module_function :create_self_signed_cert
+ end
+
+ ##
+ #--
+ # Updates WEBrick::GenericServer with SSL functionality
+
+ class GenericServer
+
+ ##
+ # SSL context for the server when run in SSL mode
+
+ def ssl_context # :nodoc:
+ @ssl_context ||= begin
+ if @config[:SSLEnable]
+ ssl_context = setup_ssl_context(@config)
+ @logger.info("\n" + @config[:SSLCertificate].to_text)
+ ssl_context
+ end
+ end
+ end
+
+ undef listen
+
+ ##
+ # Updates +listen+ to enable SSL when the SSL configuration is active.
+
+ def listen(address, port) # :nodoc:
+ listeners = Utils::create_listeners(address, port)
+ if @config[:SSLEnable]
+ listeners.collect!{|svr|
+ ssvr = ::OpenSSL::SSL::SSLServer.new(svr, ssl_context)
+ ssvr.start_immediately = @config[:SSLStartImmediately]
+ ssvr
+ }
+ end
+ @listeners += listeners
+ setup_shutdown_pipe
+ end
+
+ ##
+ # Sets up an SSL context for +config+
+
+ def setup_ssl_context(config) # :nodoc:
+ unless config[:SSLCertificate]
+ cn = config[:SSLCertName]
+ comment = config[:SSLCertComment]
+ cert, key = Utils::create_self_signed_cert(2048, cn, comment)
+ config[:SSLCertificate] = cert
+ config[:SSLPrivateKey] = key
+ end
+ ctx = OpenSSL::SSL::SSLContext.new
+ ctx.key = config[:SSLPrivateKey]
+ ctx.cert = config[:SSLCertificate]
+ ctx.client_ca = config[:SSLClientCA]
+ ctx.extra_chain_cert = config[:SSLExtraChainCert]
+ ctx.ca_file = config[:SSLCACertificateFile]
+ ctx.ca_path = config[:SSLCACertificatePath]
+ ctx.cert_store = config[:SSLCertificateStore]
+ ctx.tmp_dh_callback = config[:SSLTmpDhCallback]
+ ctx.verify_mode = config[:SSLVerifyClient]
+ ctx.verify_depth = config[:SSLVerifyDepth]
+ ctx.verify_callback = config[:SSLVerifyCallback]
+ ctx.servername_cb = config[:SSLServerNameCallback] || proc { |args| ssl_servername_callback(*args) }
+ ctx.timeout = config[:SSLTimeout]
+ ctx.options = config[:SSLOptions]
+ ctx.ciphers = config[:SSLCiphers]
+ ctx
+ end
+
+ ##
+ # ServerNameIndication callback
+
+ def ssl_servername_callback(sslsocket, hostname = nil)
+ # default
+ end
+
+ end
+end
diff --git a/tool/lib/webrick/utils.rb b/tool/lib/webrick/utils.rb
new file mode 100644
index 0000000000..a96d6f03fd
--- /dev/null
+++ b/tool/lib/webrick/utils.rb
@@ -0,0 +1,265 @@
+# frozen_string_literal: false
+#
+# utils.rb -- Miscellaneous utilities
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: utils.rb,v 1.10 2003/02/16 22:22:54 gotoyuzo Exp $
+
+require 'socket'
+require 'io/nonblock'
+require 'etc'
+
+module WEBrick
+ module Utils
+ ##
+ # Sets IO operations on +io+ to be non-blocking
+ def set_non_blocking(io)
+ io.nonblock = true if io.respond_to?(:nonblock=)
+ end
+ module_function :set_non_blocking
+
+ ##
+ # Sets the close on exec flag for +io+
+ def set_close_on_exec(io)
+ io.close_on_exec = true if io.respond_to?(:close_on_exec=)
+ end
+ module_function :set_close_on_exec
+
+ ##
+ # Changes the process's uid and gid to the ones of +user+
+ def su(user)
+ if pw = Etc.getpwnam(user)
+ Process::initgroups(user, pw.gid)
+ Process::Sys::setgid(pw.gid)
+ Process::Sys::setuid(pw.uid)
+ else
+ warn("WEBrick::Utils::su doesn't work on this platform", uplevel: 1)
+ end
+ end
+ module_function :su
+
+ ##
+ # The server hostname
+ def getservername
+ Socket::gethostname
+ end
+ module_function :getservername
+
+ ##
+ # Creates TCP server sockets bound to +address+:+port+ and returns them.
+ #
+ # It will create IPV4 and IPV6 sockets on all interfaces.
+ def create_listeners(address, port)
+ unless port
+ raise ArgumentError, "must specify port"
+ end
+ sockets = Socket.tcp_server_sockets(address, port)
+ sockets = sockets.map {|s|
+ s.autoclose = false
+ ts = TCPServer.for_fd(s.fileno)
+ s.close
+ ts
+ }
+ return sockets
+ end
+ module_function :create_listeners
+
+ ##
+ # Characters used to generate random strings
+ RAND_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
+ "0123456789" +
+ "abcdefghijklmnopqrstuvwxyz"
+
+ ##
+ # Generates a random string of length +len+
+ def random_string(len)
+ rand_max = RAND_CHARS.bytesize
+ ret = ""
+ len.times{ ret << RAND_CHARS[rand(rand_max)] }
+ ret
+ end
+ module_function :random_string
+
+ ###########
+
+ require "timeout"
+ require "singleton"
+
+ ##
+ # Class used to manage timeout handlers across multiple threads.
+ #
+ # Timeout handlers should be managed by using the class methods which are
+ # synchronized.
+ #
+ # id = TimeoutHandler.register(10, Timeout::Error)
+ # begin
+ # sleep 20
+ # puts 'foo'
+ # ensure
+ # TimeoutHandler.cancel(id)
+ # end
+ #
+ # will raise Timeout::Error
+ #
+ # id = TimeoutHandler.register(10, Timeout::Error)
+ # begin
+ # sleep 5
+ # puts 'foo'
+ # ensure
+ # TimeoutHandler.cancel(id)
+ # end
+ #
+ # will print 'foo'
+ #
+ class TimeoutHandler
+ include Singleton
+
+ ##
+ # Mutex used to synchronize access across threads
+ TimeoutMutex = Thread::Mutex.new # :nodoc:
+
+ ##
+ # Registers a new timeout handler
+ #
+ # +time+:: Timeout in seconds
+ # +exception+:: Exception to raise when timeout elapsed
+ def TimeoutHandler.register(seconds, exception)
+ at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
+ instance.register(Thread.current, at, exception)
+ end
+
+ ##
+ # Cancels the timeout handler +id+
+ def TimeoutHandler.cancel(id)
+ instance.cancel(Thread.current, id)
+ end
+
+ def self.terminate
+ instance.terminate
+ end
+
+ ##
+ # Creates a new TimeoutHandler. You should use ::register and ::cancel
+ # instead of creating the timeout handler directly.
+ def initialize
+ TimeoutMutex.synchronize{
+ @timeout_info = Hash.new
+ }
+ @queue = Thread::Queue.new
+ @watcher = nil
+ end
+
+ # :nodoc:
+ private \
+ def watch
+ to_interrupt = []
+ while true
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ wakeup = nil
+ to_interrupt.clear
+ TimeoutMutex.synchronize{
+ @timeout_info.each {|thread, ary|
+ next unless ary
+ ary.each{|info|
+ time, exception = *info
+ if time < now
+ to_interrupt.push [thread, info.object_id, exception]
+ elsif !wakeup || time < wakeup
+ wakeup = time
+ end
+ }
+ }
+ }
+ to_interrupt.each {|arg| interrupt(*arg)}
+ if !wakeup
+ @queue.pop
+ elsif (wakeup -= now) > 0
+ begin
+ (th = Thread.start {@queue.pop}).join(wakeup)
+ ensure
+ th&.kill&.join
+ end
+ end
+ @queue.clear
+ end
+ end
+
+ # :nodoc:
+ private \
+ def watcher
+ (w = @watcher)&.alive? and return w # usual case
+ TimeoutMutex.synchronize{
+ (w = @watcher)&.alive? and next w # pathological check
+ @watcher = Thread.start(&method(:watch))
+ }
+ end
+
+ ##
+ # Interrupts the timeout handler +id+ and raises +exception+
+ def interrupt(thread, id, exception)
+ if cancel(thread, id) && thread.alive?
+ thread.raise(exception, "execution timeout")
+ end
+ end
+
+ ##
+ # Registers a new timeout handler
+ #
+ # +time+:: Timeout in seconds
+ # +exception+:: Exception to raise when timeout elapsed
+ def register(thread, time, exception)
+ info = nil
+ TimeoutMutex.synchronize{
+ (@timeout_info[thread] ||= []) << (info = [time, exception])
+ }
+ @queue.push nil
+ watcher
+ return info.object_id
+ end
+
+ ##
+ # Cancels the timeout handler +id+
+ def cancel(thread, id)
+ TimeoutMutex.synchronize{
+ if ary = @timeout_info[thread]
+ ary.delete_if{|info| info.object_id == id }
+ if ary.empty?
+ @timeout_info.delete(thread)
+ end
+ return true
+ end
+ return false
+ }
+ end
+
+ ##
+ def terminate
+ TimeoutMutex.synchronize{
+ @timeout_info.clear
+ @watcher&.kill&.join
+ }
+ end
+ end
+
+ ##
+ # Executes the passed block and raises +exception+ if execution takes more
+ # than +seconds+.
+ #
+ # If +seconds+ is zero or nil, simply executes the block
+ def timeout(seconds, exception=Timeout::Error)
+ return yield if seconds.nil? or seconds.zero?
+ # raise ThreadError, "timeout within critical session" if Thread.critical
+ id = TimeoutHandler.register(seconds, exception)
+ begin
+ yield(seconds)
+ ensure
+ TimeoutHandler.cancel(id)
+ end
+ end
+ module_function :timeout
+ end
+end
diff --git a/tool/lib/webrick/version.rb b/tool/lib/webrick/version.rb
new file mode 100644
index 0000000000..b62988bdbb
--- /dev/null
+++ b/tool/lib/webrick/version.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: false
+#--
+# version.rb -- version and release date
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU
+# Copyright (c) 2003 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: version.rb,v 1.74 2003/07/22 19:20:43 gotoyuzo Exp $
+
+module WEBrick
+
+ ##
+ # The WEBrick version
+
+ VERSION = "1.7.0"
+end
diff --git a/tool/lib/zombie_hunter.rb b/tool/lib/zombie_hunter.rb
new file mode 100644
index 0000000000..33bc467941
--- /dev/null
+++ b/tool/lib/zombie_hunter.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module ZombieHunter
+ def after_teardown
+ super
+ assert_empty(Process.waitall)
+ end
+end
+
+Test::Unit::TestCase.include ZombieHunter
diff --git a/tool/ln_sr.rb b/tool/ln_sr.rb
new file mode 100644
index 0000000000..2aa8391e17
--- /dev/null
+++ b/tool/ln_sr.rb
@@ -0,0 +1,131 @@
+#!/usr/bin/ruby
+
+target_directory = true
+noop = false
+force = false
+quiet = false
+
+until ARGV.empty?
+ case ARGV[0]
+ when '-n'
+ noop = true
+ when '-f'
+ force = true
+ when '-T'
+ target_directory = false
+ when '-q'
+ quiet = true
+ else
+ break
+ end
+ ARGV.shift
+end
+
+unless ARGV.size == 2
+ abort "usage: #{$0} src destdir"
+end
+src, dest = ARGV
+
+require 'fileutils'
+
+include FileUtils
+unless respond_to?(:ln_sr)
+ def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil)
+ options = "#{force ? 'f' : ''}#{target_directory ? '' : 'T'}"
+ dest = File.path(dest)
+ srcs = Array(src)
+ link = proc do |s, target_dir_p = true|
+ s = File.path(s)
+ if target_dir_p
+ d = File.join(destdirs = dest, File.basename(s))
+ else
+ destdirs = File.dirname(d = dest)
+ end
+ destdirs = fu_split_path(File.realpath(destdirs))
+ if fu_starting_path?(s)
+ srcdirs = fu_split_path((File.realdirpath(s) rescue File.expand_path(s)))
+ base = fu_relative_components_from(srcdirs, destdirs)
+ s = File.join(*base)
+ else
+ srcdirs = fu_clean_components(*fu_split_path(s))
+ base = fu_relative_components_from(fu_split_path(Dir.pwd), destdirs)
+ while srcdirs.first&. == ".." and base.last&.!=("..") and !fu_starting_path?(base.last)
+ srcdirs.shift
+ base.pop
+ end
+ s = File.join(*base, *srcdirs)
+ end
+ fu_output_message "ln -s#{options} #{s} #{d}" if verbose
+ next if noop
+ remove_file d, true if force
+ File.symlink s, d
+ end
+ case srcs.size
+ when 0
+ when 1
+ link[srcs[0], target_directory && File.directory?(dest)]
+ else
+ srcs.each(&link)
+ end
+ end
+
+ def fu_split_path(path)
+ path = File.path(path)
+ list = []
+ until (parent, base = File.split(path); parent == path or parent == ".")
+ list << base
+ path = parent
+ end
+ list << path
+ list.reverse!
+ end
+
+ def fu_relative_components_from(target, base) #:nodoc:
+ i = 0
+ while target[i]&.== base[i]
+ i += 1
+ end
+ Array.new(base.size-i, '..').concat(target[i..-1])
+ end
+
+ def fu_clean_components(*comp)
+ comp.shift while comp.first == "."
+ return comp if comp.empty?
+ clean = [comp.shift]
+ path = File.join(*clean, "") # ending with File::SEPARATOR
+ while c = comp.shift
+ if c == ".." and clean.last != ".." and !(fu_have_symlink? && File.symlink?(path))
+ clean.pop
+ path.chomp!(%r((?<=\A|/)[^/]+/\z), "")
+ else
+ clean << c
+ path << c << "/"
+ end
+ end
+ clean
+ end
+
+ if fu_windows?
+ def fu_starting_path?(path)
+ path&.start_with?(%r(\w:|/))
+ end
+ else
+ def fu_starting_path?(path)
+ path&.start_with?("/")
+ end
+ end
+end
+
+if File.respond_to?(:symlink)
+ if quiet and File.identical?(src, dest)
+ exit
+ end
+ begin
+ ln_sr(src, dest, verbose: true, target_directory: target_directory, force: force, noop: noop)
+ rescue NotImplementedError, Errno::EPERM, Errno::EACCES
+ else
+ exit
+ end
+end
+
+cp_r(src, dest)
diff --git a/tool/m4/_colorize_result_prepare.m4 b/tool/m4/_colorize_result_prepare.m4
new file mode 100644
index 0000000000..8439acf3ed
--- /dev/null
+++ b/tool/m4/_colorize_result_prepare.m4
@@ -0,0 +1,34 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([_COLORIZE_RESULT_PREPARE], [
+ msg_checking= msg_result_yes= msg_result_no= msg_result_other= msg_reset=
+ AS_CASE(["x${CONFIGURE_TTY}"],
+ [xyes|xalways],[configure_tty=1],
+ [xno|xnever], [configure_tty=0],
+ [AS_IF([test -t 1],
+ [configure_tty=1],
+ [configure_tty=0])])
+ AS_IF([test $configure_tty -eq 1], [
+ msg_begin="`tput smso 2>/dev/null`"
+ AS_IF([test -z "$msg_begin"], [msg_begin="`tput so 2>/dev/null`"])
+ AS_CASE(["$msg_begin"], ['@<:@'*m],
+ [msg_begin="`echo "$msg_begin" | sed ['s/[0-9]*m$//']`"
+ msg_checking="${msg_begin}33m"
+ AS_IF([test ${TEST_COLORS:+set}], [
+ msg_result_yes=[`expr ":$TEST_COLORS:" : ".*:pass=\([^:]*\):"`]
+ msg_result_no=[`expr ":$TEST_COLORS:" : ".*:fail=\([^:]*\):"`]
+ msg_result_other=[`expr ":$TEST_COLORS:" : ".*:skip=\([^:]*\):"`]
+ ])
+ msg_result_yes="${msg_begin}${msg_result_yes:-32;1}m"
+ msg_result_no="${msg_begin}${msg_result_no:-31;1}m"
+ msg_result_other="${msg_begin}${msg_result_other:-33;1}m"
+ msg_reset="${msg_begin}m"
+ ])
+ AS_UNSET(msg_begin)
+ ])
+ AS_REQUIRE_SHELL_FN([colorize_result],
+ [AS_FUNCTION_DESCRIBE([colorize_result], [MSG], [Colorize result])],
+ [AS_CASE(["$[]1"],
+ [yes], [_AS_ECHO([${msg_result_yes}$[]1${msg_reset}])],
+ [no], [_AS_ECHO([${msg_result_no}$[]1${msg_reset}])],
+ [_AS_ECHO([${msg_result_other}$[]1${msg_reset}])])])
+])dnl
diff --git a/tool/m4/ac_msg_result.m4 b/tool/m4/ac_msg_result.m4
new file mode 100644
index 0000000000..12a3617c0d
--- /dev/null
+++ b/tool/m4/ac_msg_result.m4
@@ -0,0 +1,5 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([AC_MSG_RESULT], [dnl
+{ _AS_ECHO_LOG([result: $1])
+COLORIZE_RESULT([$1]); dnl
+}])dnl
diff --git a/tool/m4/colorize_result.m4 b/tool/m4/colorize_result.m4
new file mode 100644
index 0000000000..83912040e5
--- /dev/null
+++ b/tool/m4/colorize_result.m4
@@ -0,0 +1,9 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([COLORIZE_RESULT], [AC_REQUIRE([_COLORIZE_RESULT_PREPARE])dnl
+ AS_LITERAL_IF([$1],
+ [m4_case([$1],
+ [yes], [_AS_ECHO([${msg_result_yes}$1${msg_reset}])],
+ [no], [_AS_ECHO([${msg_result_no}$1${msg_reset}])],
+ [_AS_ECHO([${msg_result_other}$1${msg_reset}])])],
+ [colorize_result "$1"]) dnl
+])dnl
diff --git a/tool/m4/ruby_append_option.m4 b/tool/m4/ruby_append_option.m4
new file mode 100644
index 0000000000..ff828d2162
--- /dev/null
+++ b/tool/m4/ruby_append_option.m4
@@ -0,0 +1,5 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_APPEND_OPTION],
+ [# RUBY_APPEND_OPTION($1)
+ AS_CASE([" [$]{$1-} "],
+ [*" $2 "*], [], [' '], [ $1="$2"], [ $1="[$]$1 $2"])])dnl
diff --git a/tool/m4/ruby_append_options.m4 b/tool/m4/ruby_append_options.m4
new file mode 100644
index 0000000000..14213111ca
--- /dev/null
+++ b/tool/m4/ruby_append_options.m4
@@ -0,0 +1,7 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_APPEND_OPTIONS],
+ [# RUBY_APPEND_OPTIONS($1)
+ for rb_opt in $2; do
+ AS_CASE([" [$]{$1-} "],
+ [*" [$]{rb_opt} "*], [], [' '], [ $1="[$]{rb_opt}"], [ $1="[$]$1 [$]{rb_opt}"])
+ done])dnl
diff --git a/tool/m4/ruby_check_builtin_func.m4 b/tool/m4/ruby_check_builtin_func.m4
new file mode 100644
index 0000000000..40abc78ef8
--- /dev/null
+++ b/tool/m4/ruby_check_builtin_func.m4
@@ -0,0 +1,10 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_CHECK_BUILTIN_FUNC], [dnl
+AC_CACHE_CHECK([for $1], AS_TR_SH(rb_cv_builtin_$1),
+ [AC_LINK_IFELSE(
+ [AC_LANG_PROGRAM([int foo;], [$2;])],
+ [AS_TR_SH(rb_cv_builtin_$1)=yes],
+ [AS_TR_SH(rb_cv_builtin_$1)=no])])
+AS_IF([test "${AS_TR_SH(rb_cv_builtin_$1)}" != no], [
+ AC_DEFINE(AS_TR_CPP(HAVE_BUILTIN_$1))
+])])dnl
diff --git a/tool/m4/ruby_check_builtin_setjmp.m4 b/tool/m4/ruby_check_builtin_setjmp.m4
new file mode 100644
index 0000000000..05118e2243
--- /dev/null
+++ b/tool/m4/ruby_check_builtin_setjmp.m4
@@ -0,0 +1,27 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_CHECK_BUILTIN_SETJMP], [
+AS_IF([test x"${ac_cv_func___builtin_setjmp}" = xyes], [
+ unset ac_cv_func___builtin_setjmp
+])
+AC_CACHE_CHECK(for __builtin_setjmp, ac_cv_func___builtin_setjmp,
+ [
+ ac_cv_func___builtin_setjmp=no
+ for cast in "" "(void **)"; do
+ RUBY_WERROR_FLAG(
+ [AC_LINK_IFELSE([AC_LANG_PROGRAM([[@%:@include <setjmp.h>
+ @%:@include <stdio.h>
+ jmp_buf jb;
+ @%:@ifdef NORETURN
+ NORETURN(void t(void));
+ @%:@endif
+ void t(void) {__builtin_longjmp($cast jb, 1);}
+ int jump(void) {(void)(__builtin_setjmp($cast jb) ? 1 : 0); return 0;}]],
+ [[
+ void (*volatile f)(void) = t;
+ if (!jump()) printf("%d\n", f != 0);
+ ]])],
+ [ac_cv_func___builtin_setjmp="yes with cast ($cast)"])
+ ])
+ test "$ac_cv_func___builtin_setjmp" = no || break
+ done])
+])dnl
diff --git a/tool/m4/ruby_check_printf_prefix.m4 b/tool/m4/ruby_check_printf_prefix.m4
new file mode 100644
index 0000000000..15bb4aee87
--- /dev/null
+++ b/tool/m4/ruby_check_printf_prefix.m4
@@ -0,0 +1,29 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_CHECK_PRINTF_PREFIX], [
+AC_CACHE_CHECK([for printf prefix for $1], [rb_cv_pri_prefix_]AS_TR_SH($1),[
+ [rb_cv_pri_prefix_]AS_TR_SH($1)=[NONE]
+ RUBY_WERROR_FLAG(RUBY_APPEND_OPTIONS(CFLAGS, $rb_cv_wsuppress_flags)
+ for pri in $2; do
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[@%:@include <stdio.h>
+ @%:@include <stddef.h>
+ @%:@ifdef __GNUC__
+ @%:@if defined __MINGW_PRINTF_FORMAT
+ @%:@define PRINTF_ARGS(decl, string_index, first_to_check) \
+ decl __attribute__((format(__MINGW_PRINTF_FORMAT, string_index, first_to_check)))
+ @%:@else
+ @%:@define PRINTF_ARGS(decl, string_index, first_to_check) \
+ decl __attribute__((format(printf, string_index, first_to_check)))
+ @%:@endif
+ @%:@else
+ @%:@define PRINTF_ARGS(decl, string_index, first_to_check) decl
+ @%:@endif
+ PRINTF_ARGS(void test_sprintf(const char*, ...), 1, 2);]],
+ [[printf("%]${pri}[d", (]$1[)42);
+ test_sprintf("%]${pri}[d", (]$1[)42);]])],
+ [rb_cv_pri_prefix_]AS_TR_SH($1)[=[$pri]; break])
+ done)])
+AS_IF([test "[$rb_cv_pri_prefix_]AS_TR_SH($1)" != NONE], [
+ AC_DEFINE_UNQUOTED([PRI_]m4_ifval($3,$3,AS_TR_CPP(m4_bpatsubst([$1],[_t$])))[_PREFIX],
+ "[$rb_cv_pri_prefix_]AS_TR_SH($1)")
+])
+])dnl
diff --git a/tool/m4/ruby_check_setjmp.m4 b/tool/m4/ruby_check_setjmp.m4
new file mode 100644
index 0000000000..6020b766b8
--- /dev/null
+++ b/tool/m4/ruby_check_setjmp.m4
@@ -0,0 +1,17 @@
+dnl -*- Autoconf -*-
+dnl used for AC_ARG_WITH(setjmp-type)
+AC_DEFUN([RUBY_CHECK_SETJMP], [
+AC_CACHE_CHECK([for ]$1[ as a macro or function], ac_cv_func_$1,
+ [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
+@%:@include <setjmp.h>
+]AC_INCLUDES_DEFAULT([$3])[
+@%:@define JMPARGS_1 env
+@%:@define JMPARGS_2 env,1
+@%:@define JMPARGS JMPARGS_]m4_ifval($2,2,1)[
+]],
+ [m4_ifval($2,$2,jmp_buf)[ env; $1(JMPARGS);]])],
+ ac_cv_func_$1=yes,
+ ac_cv_func_$1=no)]
+)
+AS_IF([test "$ac_cv_func_]$1[" = yes], [AC_DEFINE([HAVE_]AS_TR_CPP($1), 1)])
+])dnl
diff --git a/tool/m4/ruby_check_signedness.m4 b/tool/m4/ruby_check_signedness.m4
new file mode 100644
index 0000000000..f9fbb3c088
--- /dev/null
+++ b/tool/m4/ruby_check_signedness.m4
@@ -0,0 +1,5 @@
+dnl -*- Autoconf -*-
+dnl RUBY_CHECK_SIGNEDNESS [typename] [if-signed] [if-unsigned] [included]
+AC_DEFUN([RUBY_CHECK_SIGNEDNESS], [dnl
+ AC_COMPILE_IFELSE([AC_LANG_BOOL_COMPILE_TRY([AC_INCLUDES_DEFAULT([$4])], [($1)-1 > 0])],
+ [$3], [$2])])dnl
diff --git a/tool/m4/ruby_check_sizeof.m4 b/tool/m4/ruby_check_sizeof.m4
new file mode 100644
index 0000000000..975ac6c9be
--- /dev/null
+++ b/tool/m4/ruby_check_sizeof.m4
@@ -0,0 +1,108 @@
+dnl -*- Autoconf -*-
+dnl RUBY_CHECK_SIZEOF [typename], [maybe same size types], [macros], [include]
+AC_DEFUN([RUBY_CHECK_SIZEOF],
+[dnl
+AS_VAR_PUSHDEF([rbcv_var], [rbcv_sizeof_var])dnl
+AS_VAR_PUSHDEF([cond], [rbcv_sizeof_cond])dnl
+AS_VAR_PUSHDEF([t], [rbcv_sizeof_type])dnl
+AS_VAR_PUSHDEF([s], [rbcv_sizeof_size])dnl
+]
+[m4_bmatch([$1], [\.], [], [if test "$universal_binary" = yes; then])
+AC_CACHE_CHECK([size of $1], [AS_TR_SH([ac_cv_sizeof_$1])], [
+ unset AS_TR_SH(ac_cv_sizeof_$1)
+ rbcv_var="
+typedef m4_bpatsubst([$1], [\..*]) ac__type_sizeof_;
+static ac__type_sizeof_ *rbcv_ptr;
+@%:@define AS_TR_CPP(SIZEOF_$1) sizeof((*rbcv_ptr)[]m4_bmatch([$1], [\.], .m4_bpatsubst([$1], [^[^.]*\.])))
+"
+ m4_ifval([$2], [test -z "${AS_TR_SH(ac_cv_sizeof_$1)+set}" && {
+ for t in $2; do
+ AC_COMPILE_IFELSE(
+ [AC_LANG_BOOL_COMPILE_TRY(AC_INCLUDES_DEFAULT([$4]
+ [$rbcv_var]),
+ [AS_TR_CPP(SIZEOF_$1) == sizeof($t)])], [
+ AS_TR_SH(ac_cv_sizeof_$1)=AS_TR_CPP([SIZEOF_]$t)
+ break])
+ done
+ }], [
+ AC_COMPUTE_INT([AS_TR_SH(ac_cv_sizeof_$1)], [AS_TR_CPP(SIZEOF_$1)],
+ [AC_INCLUDES_DEFAULT([$4])
+$rbcv_var],
+ [AS_TR_SH(ac_cv_sizeof_$1)=])
+ ])
+ unset cond
+ m4_ifval([$3], [test -z "${AS_TR_SH(ac_cv_sizeof_$1)+set}" && {
+ for s in 32 64 128; do
+ for t in $3; do
+ cond="${cond}
+@%:@${cond+el}if defined(__${t}${s}__) || defined(__${t}${s}) || defined(_${t}${s}) || defined(${t}${s})"
+ hdr="AC_INCLUDES_DEFAULT([$4
+@%:@if defined(__${t}${s}__) || defined(__${t}${s}) || defined(_${t}${s}) || defined(${t}${s})
+@%:@ define AS_TR_CPP(HAVE_$1) 1
+@%:@else
+@%:@ define AS_TR_CPP(HAVE_$1) 0
+@%:@endif])"
+ AC_COMPILE_IFELSE([AC_LANG_BOOL_COMPILE_TRY([$hdr], [!AS_TR_CPP(HAVE_$1)])], [continue])
+ AC_COMPILE_IFELSE([AC_LANG_BOOL_COMPILE_TRY([$hdr]
+ [$rbcv_var],
+ [AS_TR_CPP(HAVE_$1) == (AS_TR_CPP(SIZEOF_$1) == ($s / $rb_cv_char_bit))])],
+ [AS_TR_SH(ac_cv_sizeof_$1)="${AS_TR_SH(ac_cv_sizeof_$1)+${AS_TR_SH(ac_cv_sizeof_$1)-} }${t}${s}"; continue])
+ AC_COMPILE_IFELSE([AC_LANG_BOOL_COMPILE_TRY([$hdr]
+[
+@%:@if AS_TR_CPP(HAVE_$1)
+$rbcv_var
+@%:@else
+@%:@define AS_TR_CPP(SIZEOF_$1) 0
+@%:@endif
+],
+ [AS_TR_CPP(HAVE_$1) == (AS_TR_CPP(SIZEOF_$1) == (m4_bmatch([$2], [^[0-9][0-9]*$], [$2], [($s / $rb_cv_char_bit)])))])],
+ [AS_TR_SH(ac_cv_sizeof_$1)="${AS_TR_SH(ac_cv_sizeof_$1)+${AS_TR_SH(ac_cv_sizeof_$1)-} }${t}${s}m4_bmatch([$2], [^[0-9][0-9]*$], [:$2])"])
+ done
+ done
+ }])
+ test "${AS_TR_SH(ac_cv_sizeof_$1)@%:@@<:@1-9@:>@}" = "${AS_TR_SH(ac_cv_sizeof_$1)}" &&
+ m4_ifval([$2][$3],
+ [test "${AS_TR_SH(ac_cv_sizeof_$1)@%:@SIZEOF_}" = "${AS_TR_SH(ac_cv_sizeof_$1)}" && ]){
+ test "$universal_binary" = yes && cross_compiling=yes
+ AC_COMPUTE_INT([t], AS_TR_CPP(SIZEOF_$1), [AC_INCLUDES_DEFAULT([$4])]
+[${cond+$cond
+@%:@else}
+$rbcv_var
+${cond+@%:@endif}
+@%:@ifndef AS_TR_CPP(SIZEOF_$1)
+@%:@define AS_TR_CPP(SIZEOF_$1) 0
+@%:@endif], [t=0])
+ test "$universal_binary" = yes && cross_compiling=$real_cross_compiling
+ AS_IF([test ${t-0} != 0], [
+ AS_TR_SH(ac_cv_sizeof_$1)="${AS_TR_SH(ac_cv_sizeof_$1)+${AS_TR_SH(ac_cv_sizeof_$1)-} }${t}"
+ ])
+ }
+ : ${AS_TR_SH(ac_cv_sizeof_$1)=0}
+])
+{
+ unset cond
+ for t in ${AS_TR_SH(ac_cv_sizeof_$1)-}; do
+ AS_CASE(["$t"],
+ [[[0-9]*|SIZEOF_*]], [
+ ${cond+echo "@%:@else"}
+ echo "[@%:@define ]AS_TR_CPP(SIZEOF_$1) $t"
+ break
+ ],
+ [
+ s=`expr $t : ['.*[^0-9]\([0-9][0-9]*\)$']`
+ AS_CASE([$t], [*:*], [t="${t%:*}"], [s=`expr $s / $rb_cv_char_bit`])
+ echo "@%:@${cond+el}if defined(__${t}__) || defined(__${t}) || defined(_${t}) || defined($t)"
+ echo "@%:@define AS_TR_CPP(SIZEOF_$1) $s"
+ cond=1
+ ])
+ done
+ ${cond+echo "@%:@endif"}
+} >> confdefs.h
+m4_bmatch([$1], [\.], [], [else
+AC_CHECK_SIZEOF([$1], 0, [$4])
+fi])
+AS_VAR_POPDEF([rbcv_var])dnl
+AS_VAR_POPDEF([cond])dnl
+AS_VAR_POPDEF([t])dnl
+AS_VAR_POPDEF([s])dnl
+])dnl
diff --git a/tool/m4/ruby_check_sysconf.m4 b/tool/m4/ruby_check_sysconf.m4
new file mode 100644
index 0000000000..f554786e77
--- /dev/null
+++ b/tool/m4/ruby_check_sysconf.m4
@@ -0,0 +1,13 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_CHECK_SYSCONF], [dnl
+AC_CACHE_CHECK([whether _SC_$1 is supported], rb_cv_have_sc_[]m4_tolower($1),
+ [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <unistd.h>
+ ]],
+ [[_SC_$1 >= 0]])],
+ rb_cv_have_sc_[]m4_tolower($1)=yes,
+ rb_cv_have_sc_[]m4_tolower($1)=no)
+ ])
+AS_IF([test "$rb_cv_have_sc_[]m4_tolower($1)" = yes], [
+ AC_DEFINE(HAVE__SC_$1)
+])
+])dnl
diff --git a/tool/m4/ruby_cppoutfile.m4 b/tool/m4/ruby_cppoutfile.m4
new file mode 100644
index 0000000000..976cbb1c43
--- /dev/null
+++ b/tool/m4/ruby_cppoutfile.m4
@@ -0,0 +1,18 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_CPPOUTFILE],
+[AC_CACHE_CHECK(whether ${CPP} accepts -o, rb_cv_cppoutfile,
+[save_CPPFLAGS="$CPPFLAGS"
+CPPFLAGS='-o conftest-1.i'
+rb_cv_cppoutfile=no
+AC_PREPROC_IFELSE([AC_LANG_SOURCE([[test-for-cppout]])],
+ [grep test-for-cppout conftest-1.i > /dev/null && rb_cv_cppoutfile=yes])
+CPPFLAGS="$save_CPPFLAGS"
+rm -f conftest*])
+AS_IF([test "$rb_cv_cppoutfile" = yes], [
+ CPPOUTFILE='-o conftest.i'
+], [test "$rb_cv_cppoutfile" = no], [
+ CPPOUTFILE='> conftest.i'
+], [test -n "$rb_cv_cppoutfile"], [
+ CPPOUTFILE="$rb_cv_cppoutfile"
+])
+AC_SUBST(CPPOUTFILE)])dnl
diff --git a/tool/m4/ruby_decl_attribute.m4 b/tool/m4/ruby_decl_attribute.m4
new file mode 100644
index 0000000000..a8a73dc870
--- /dev/null
+++ b/tool/m4/ruby_decl_attribute.m4
@@ -0,0 +1,45 @@
+dnl -*- Autoconf -*-
+dnl RUBY_DECL_ATTRIBUTE(attrib, macroname, cachevar, condition, type, code)
+AC_DEFUN([RUBY_DECL_ATTRIBUTE], [dnl
+m4_ifval([$2], dnl
+ [AS_VAR_PUSHDEF([attrib], m4_bpatsubst([$2], [(.*)], []))], dnl
+ [AS_VAR_PUSHDEF([attrib], m4_toupper(m4_format(%.4s, [$5]))[_]AS_TR_CPP($1))] dnl
+)dnl
+m4_ifval([$3], dnl
+ [AS_VAR_PUSHDEF([rbcv],[$3])], dnl
+ [AS_VAR_PUSHDEF([rbcv],[rb_cv_]m4_format(%.4s, [$5])[_][$1])]dnl
+)dnl
+m4_pushdef([attrib_code],[m4_bpatsubst([$1],["],[\\"])])dnl
+m4_pushdef([attrib_params],[m4_bpatsubst([$2(x)],[^[^()]*(\([^()]*\)).*],[\1])])dnl
+m4_ifval([$4], [rbcv_cond=["$4"]; test "$rbcv_cond" || unset rbcv_cond])
+AC_CACHE_CHECK(for m4_ifval([$2],[m4_bpatsubst([$2], [(.*)], [])],[$1]) [$5] attribute, rbcv, dnl
+[rbcv=x
+RUBY_WERROR_FLAG([
+for mac in \
+ "__attribute__ ((attrib_code)) x" \
+ "x __attribute__ ((attrib_code))" \
+ "__declspec(attrib_code) x" \
+ x; do
+ m4_ifval([$4],mac="$mac"${rbcv_cond+" /* only if $rbcv_cond */"})
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([
+ m4_ifval([$4],${rbcv_cond+[@%:@if ]$rbcv_cond})
+[@%:@define ]attrib[](attrib_params)[ $mac]
+m4_ifval([$4],${rbcv_cond+[@%:@else]}
+${rbcv_cond+[@%:@define ]attrib[](attrib_params)[ x]}
+${rbcv_cond+[@%:@endif]})
+$6
+@%:@define mesg ("")
+@%:@define san "address"
+ attrib[](attrib_params)[;]], [[]])],
+ [rbcv="$mac"; break])
+done
+])])
+AS_IF([test "$rbcv" != x], [
+ RUBY_DEFINE_IF(m4_ifval([$4],[${rbcv_cond}]), attrib[](attrib_params)[], $rbcv)
+])
+m4_ifval([$4], [unset rbcv_cond]) dnl
+m4_popdef([attrib_params])dnl
+m4_popdef([attrib_code])dnl
+AS_VAR_POPDEF([attrib])dnl
+AS_VAR_POPDEF([rbcv])dnl
+])dnl
diff --git a/tool/m4/ruby_default_arch.m4 b/tool/m4/ruby_default_arch.m4
new file mode 100644
index 0000000000..03e52f7776
--- /dev/null
+++ b/tool/m4/ruby_default_arch.m4
@@ -0,0 +1,11 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_DEFAULT_ARCH], [
+AC_MSG_CHECKING([arch option])
+AS_CASE([$1],
+ [arm64], [],
+ [*64], [ARCH_FLAG=-m64],
+ [[i[3-6]86]], [ARCH_FLAG=-m32],
+ [AC_MSG_ERROR(unknown target architecture: $target_archs)]
+ )
+AC_MSG_RESULT([$ARCH_FLAG])
+])dnl
diff --git a/tool/m4/ruby_define_if.m4 b/tool/m4/ruby_define_if.m4
new file mode 100644
index 0000000000..aba55783a2
--- /dev/null
+++ b/tool/m4/ruby_define_if.m4
@@ -0,0 +1,6 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_DEFINE_IF], [dnl
+ m4_ifval([$1], [AS_LITERAL_IF([$1], [], [test "X$1" = X || ])printf "@%:@if %s\n" "$1" >>confdefs.h])
+AC_DEFINE_UNQUOTED($2, $3)dnl
+ m4_ifval([$1], [AS_LITERAL_IF([$1], [], [test "X$1" = X || ])printf "@%:@endif /* %s */\n" "$1" >>confdefs.h])
+])dnl
diff --git a/tool/m4/ruby_defint.m4 b/tool/m4/ruby_defint.m4
new file mode 100644
index 0000000000..e9ed68e5b8
--- /dev/null
+++ b/tool/m4/ruby_defint.m4
@@ -0,0 +1,40 @@
+dnl -*- Autoconf -*-
+dnl RUBY_DEFINT TYPENAME, SIZE, [UNSIGNED], [INCLUDES = DEFAULT-INCLUDES]
+AC_DEFUN([RUBY_DEFINT], [dnl
+AS_VAR_PUSHDEF([cond], [rb_defint_cond])dnl
+AS_VAR_PUSHDEF([type], [rb_defint_type])dnl
+AC_CACHE_CHECK([for $1], [rb_cv_type_$1],
+[AC_COMPILE_IFELSE([AC_LANG_PROGRAM([AC_INCLUDES_DEFAULT([$4])
+typedef $1 t; int s = sizeof(t) == 42;])],
+ [rb_cv_type_$1=yes],
+ [AS_CASE([m4_bmatch([$2], [^[1-9][0-9]*$], $2, [$ac_cv_sizeof_]AS_TR_SH($2))],
+ ["1"], [ rb_cv_type_$1="m4_if([$3], [], [signed ], [$3 ])char"],
+ ["$ac_cv_sizeof_short"], [ rb_cv_type_$1="m4_if([$3], [], [], [$3 ])short"],
+ ["$ac_cv_sizeof_int"], [ rb_cv_type_$1="m4_if([$3], [], [], [$3 ])int"],
+ ["$ac_cv_sizeof_long"], [ rb_cv_type_$1="m4_if([$3], [], [], [$3 ])long"],
+ ["$ac_cv_sizeof_long_long"], [ rb_cv_type_$1="m4_if([$3], [], [], [$3 ])long long"],
+ ["${ac_cv_sizeof___int64@%:@*:}"], [ rb_cv_type_$1="m4_if([$3], [], [], [$3 ])__int64"],
+ ["${ac_cv_sizeof___int128@%:@*:}"], [ rb_cv_type_$1="m4_if([$3], [], [], [$3 ])__int128"],
+ [ rb_cv_type_$1=no])])])
+AS_IF([test "${rb_cv_type_$1}" != no], [
+ type="${rb_cv_type_$1@%:@@%:@unsigned }"
+ AS_IF([test "$type" != yes && eval 'test -n "${ac_cv_sizeof_'$type'+set}"'], [
+ eval cond='"${ac_cv_sizeof_'$type'}"'
+ AS_CASE([$cond], [*:*], [
+ cond=AS_TR_CPP($type)
+ echo "@%:@if defined SIZEOF_"$cond" && SIZEOF_"$cond" > 0" >> confdefs.h
+ ], [cond=])
+ ], [cond=])
+ AC_DEFINE([HAVE_]AS_TR_CPP($1), 1)
+ AS_IF([test "${rb_cv_type_$1}" = yes], [
+ m4_bmatch([$2], [^[1-9][0-9]*$], [AC_CHECK_SIZEOF([$1], 0, [AC_INCLUDES_DEFAULT([$4])])],
+ [RUBY_CHECK_SIZEOF([$1], [$2], [], [AC_INCLUDES_DEFAULT([$4])])])
+ ], [
+ AC_DEFINE_UNQUOTED($1, [$rb_cv_type_$1])
+ AC_DEFINE_UNQUOTED([SIZEOF_]AS_TR_CPP($1), [SIZEOF_]AS_TR_CPP([$type]))
+ ])
+ test -n "$cond" && echo "@%:@endif /* $cond */" >> confdefs.h
+])
+AS_VAR_POPDEF([cond])dnl
+AS_VAR_POPDEF([type])dnl
+])dnl
diff --git a/tool/m4/ruby_dtrace_available.m4 b/tool/m4/ruby_dtrace_available.m4
new file mode 100644
index 0000000000..e03b7762fc
--- /dev/null
+++ b/tool/m4/ruby_dtrace_available.m4
@@ -0,0 +1,20 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_DTRACE_AVAILABLE],
+[AC_CACHE_CHECK(whether dtrace USDT is available, rb_cv_dtrace_available,
+[
+ echo "provider conftest{ probe fire(); };" > conftest_provider.d
+ rb_cv_dtrace_available=no
+ AS_FOR(opt, rb_dtrace_opt, ["-xnolibs" ""], [dnl
+ AS_IF([$DTRACE opt -h -o conftest_provider.h -s conftest_provider.d >/dev/null 2>/dev/null],
+ [], [continue])
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[@%:@include "conftest_provider.h"]], [[CONFTEST_FIRE();]])],
+ [], [continue])
+ # DTrace is available on the system
+ rb_cv_dtrace_available=yes${rb_dtrace_opt:+"(opt)"}
+ break
+ ])
+ rm -f conftest.[co] conftest_provider.[dho]
+])
+AS_CASE(["$rb_cv_dtrace_available"], ["yes("*")"],
+ [DTRACE_OPT=`expr "$rb_cv_dtrace_available" : "yes(\(.*\))"`])
+])dnl
diff --git a/tool/m4/ruby_dtrace_postprocess.m4 b/tool/m4/ruby_dtrace_postprocess.m4
new file mode 100644
index 0000000000..6fd6de7c9a
--- /dev/null
+++ b/tool/m4/ruby_dtrace_postprocess.m4
@@ -0,0 +1,30 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_DTRACE_POSTPROCESS],
+[AC_CACHE_CHECK(whether $DTRACE needs post processing, rb_cv_prog_dtrace_g,
+[
+ rb_cv_prog_dtrace_g=no
+ AS_IF([{
+ cat >conftest_provider.d <<_PROBES &&
+ provider conftest {
+ probe fire();
+ };
+_PROBES
+ $DTRACE ${DTRACE_OPT} -h -o conftest_provider.h -s conftest_provider.d >/dev/null 2>/dev/null &&
+ :
+ }], [
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[@%:@include "conftest_provider.h"]], [[CONFTEST_FIRE();]])],[
+ AS_IF([{
+ cp -p conftest.${ac_objext} conftest.${ac_objext}.save &&
+ $DTRACE ${DTRACE_OPT} -G -s conftest_provider.d conftest.${ac_objext} 2>/dev/null &&
+ :
+ }], [
+ AS_IF([cmp -s conftest.o conftest.${ac_objext}.save], [
+ rb_cv_prog_dtrace_g=yes
+ ], [
+ rb_cv_prog_dtrace_g=rebuild
+ ])
+ ])])
+ ])
+ rm -f conftest.[co] conftest_provider.[dho]
+])
+])dnl
diff --git a/tool/m4/ruby_func_attribute.m4 b/tool/m4/ruby_func_attribute.m4
new file mode 100644
index 0000000000..bce26fc16a
--- /dev/null
+++ b/tool/m4/ruby_func_attribute.m4
@@ -0,0 +1,7 @@
+dnl -*- Autoconf -*-
+dnl RUBY_FUNC_ATTRIBUTE(attrib, macroname, cachevar, condition)
+AC_DEFUN([RUBY_FUNC_ATTRIBUTE], [dnl
+ RUBY_DECL_ATTRIBUTE([$1], [$2], [$3], [$4],
+ [function], [@%:@define x int conftest_attribute_check(void)]
+ )
+])dnl
diff --git a/tool/m4/ruby_mingw32.m4 b/tool/m4/ruby_mingw32.m4
new file mode 100644
index 0000000000..98e922340b
--- /dev/null
+++ b/tool/m4/ruby_mingw32.m4
@@ -0,0 +1,24 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_MINGW32],
+[AS_CASE(["$host_os"],
+[cygwin*], [
+AC_CACHE_CHECK(for mingw32 environment, rb_cv_mingw32,
+[AC_PREPROC_IFELSE([AC_LANG_SOURCE([[
+#ifndef __MINGW32__
+# error
+#endif
+]])],[rb_cv_mingw32=yes],[rb_cv_mingw32=no])
+rm -f conftest*])
+AS_IF([test "$rb_cv_mingw32" = yes], [
+ target_os="mingw32"
+ : ${ac_tool_prefix:="`expr "$CC" : ['\(.*-\)g\?cc[^/]*$']`"}
+ AC_DEFINE(__USE_MINGW_ANSI_STDIO, 1) dnl for gnu_printf
+])
+])
+AS_CASE(["$target_os"], [mingw*msvc], [
+target_os="`echo ${target_os} | sed 's/msvc$//'`"
+])
+AS_CASE(["$target_cpu-$target_os"], [x86_64-mingw*], [
+target_cpu=x64
+])
+])dnl
diff --git a/tool/m4/ruby_prepend_option.m4 b/tool/m4/ruby_prepend_option.m4
new file mode 100644
index 0000000000..3b7030a473
--- /dev/null
+++ b/tool/m4/ruby_prepend_option.m4
@@ -0,0 +1,5 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_PREPEND_OPTION],
+ [# RUBY_PREPEND_OPTION($1)
+ AS_CASE([" [$]{$1-} "],
+ [*" $2 "*], [], [' '], [ $1="$2"], [ $1="$2 [$]$1"])])dnl
diff --git a/tool/m4/ruby_prog_gnu_ld.m4 b/tool/m4/ruby_prog_gnu_ld.m4
new file mode 100644
index 0000000000..b38fb3d527
--- /dev/null
+++ b/tool/m4/ruby_prog_gnu_ld.m4
@@ -0,0 +1,10 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_PROG_GNU_LD],
+[AC_CACHE_CHECK(whether the linker is GNU ld, rb_cv_prog_gnu_ld,
+[AS_IF([`$CC $CFLAGS $CPPFLAGS $LDFLAGS --print-prog-name=ld 2>&1` -v 2>&1 | grep "GNU ld" > /dev/null], [
+ rb_cv_prog_gnu_ld=yes
+], [
+ rb_cv_prog_gnu_ld=no
+])])
+GNU_LD=$rb_cv_prog_gnu_ld
+AC_SUBST(GNU_LD)])dnl
diff --git a/tool/m4/ruby_replace_funcs.m4 b/tool/m4/ruby_replace_funcs.m4
new file mode 100644
index 0000000000..10e85f1ac9
--- /dev/null
+++ b/tool/m4/ruby_replace_funcs.m4
@@ -0,0 +1,13 @@
+dnl -*- Autoconf -*-
+dnl RUBY_REPLACE_FUNC [func] [included]
+AC_DEFUN([RUBY_REPLACE_FUNC], [dnl
+ AC_CHECK_DECL([$1],dnl
+ [AC_DEFINE(AS_TR_CPP(HAVE_[$1]))],dnl
+ [AC_REPLACE_FUNCS($1)],dnl
+ [$2])dnl
+])dnl
+dnl
+dnl RUBY_REPLACE_FUNCS [funcs] [included]
+AC_DEFUN([RUBY_REPLACE_FUNCS] [dnl
+ m4_map_args_w([$1], [RUBY_REPLACE_FUNC(], [), [$2]])dnl
+])dnl
diff --git a/tool/m4/ruby_replace_type.m4 b/tool/m4/ruby_replace_type.m4
new file mode 100644
index 0000000000..70674b6cc7
--- /dev/null
+++ b/tool/m4/ruby_replace_type.m4
@@ -0,0 +1,58 @@
+dnl -*- Autoconf -*-
+dnl RUBY_REPLACE_TYPE [typename] [default type] [macro type] [included]
+AC_DEFUN([RUBY_REPLACE_TYPE], [dnl
+ AC_CHECK_TYPES([$1],
+ [n="patsubst([$1],["],[\\"])"],
+ [n="patsubst([$2],["],[\\"])"],
+ [$4])
+ AC_CACHE_CHECK([for convertible type of [$1]], rb_cv_[$1]_convertible, [
+ u= t=
+ AS_CASE(["$n "],
+ [*" signed "*], [ ],
+ [*" unsigned "*], [
+ u=U],
+ [RUBY_CHECK_SIGNEDNESS($n, [], [u=U], [$4])])
+ AS_IF([test x"$t" = x], [
+ for t in "long long" long int short; do
+ test -n "$u" && t="unsigned $t"
+ AC_COMPILE_IFELSE(
+ [AC_LANG_BOOL_COMPILE_TRY([AC_INCLUDES_DEFAULT([$4])]
+ [typedef $n rbcv_conftest_target_type;
+ typedef $t rbcv_conftest_replace_type;
+ extern rbcv_conftest_target_type rbcv_conftest_var;
+ extern rbcv_conftest_replace_type rbcv_conftest_var;
+ extern rbcv_conftest_target_type rbcv_conftest_func(void);
+ extern rbcv_conftest_replace_type rbcv_conftest_func(void);
+ ], [sizeof(rbcv_conftest_target_type) == sizeof(rbcv_conftest_replace_type)])],
+ [n="$t"; break])
+ done
+ ])
+ AS_CASE([" $n "],
+ [*" long long "*], [
+ t=LL],
+ [*" long "*], [
+ t=LONG],
+ [*" short "*], [
+ t=SHORT],
+ [
+ t=INT])
+ rb_cv_[$1]_convertible=${u}${t}])
+ AS_IF([test "${AS_TR_SH(ac_cv_type_[$1])}" = "yes"], [
+ n="$1"
+ ], [
+ AS_CASE(["${rb_cv_[$1]_convertible}"],
+ [*LL], [n="long long"],
+ [*LONG], [n="long"],
+ [*SHORT], [n="short"],
+ [n="int"])
+ AS_CASE(["${rb_cv_[$1]_convertible}"],
+ [U*], [n="unsigned $n"])
+ ])
+ AS_CASE("${rb_cv_[$1]_convertible}", [U*], [u=+1], [u=-1])
+ AC_DEFINE_UNQUOTED(rb_[$1], $n)
+ AC_DEFINE_UNQUOTED([SIGNEDNESS_OF_]AS_TR_CPP($1), $u)
+ AC_DEFINE_UNQUOTED([$3]2NUM[(v)], [${rb_cv_[$1]_convertible}2NUM(v)])
+ AC_DEFINE_UNQUOTED(NUM2[$3][(v)], [NUM2${rb_cv_[$1]_convertible}(v)])
+ AC_DEFINE_UNQUOTED(PRI_[$3]_PREFIX,
+ [PRI_`echo ${rb_cv_[$1]_convertible} | sed ['s/^U//']`_PREFIX])
+])dnl
diff --git a/tool/m4/ruby_rm_recursive.m4 b/tool/m4/ruby_rm_recursive.m4
new file mode 100644
index 0000000000..b97701f88e
--- /dev/null
+++ b/tool/m4/ruby_rm_recursive.m4
@@ -0,0 +1,18 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_RM_RECURSIVE], [dnl
+m4_version_prereq([2.70], [], [dnl
+# suppress error messages, rm: cannot remove 'conftest.dSYM', from
+# AC_EGREP_CPP with CFLAGS=-g on Darwin.
+AS_CASE([$build_os], [darwin*], [
+rm() {
+ rm_recursive=''
+ for arg do
+ AS_CASE("$arg",
+ [--*], [],
+ [-*r*], [break],
+ [conftest.*], [AS_IF([test -d "$arg"], [rm_recursive=-r; break])],
+ [])
+ done
+ command rm $rm_recursive "[$]@"
+}
+])])])dnl
diff --git a/tool/m4/ruby_setjmp_type.m4 b/tool/m4/ruby_setjmp_type.m4
new file mode 100644
index 0000000000..4ae26fe5cd
--- /dev/null
+++ b/tool/m4/ruby_setjmp_type.m4
@@ -0,0 +1,52 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_SETJMP_TYPE], [
+RUBY_CHECK_BUILTIN_SETJMP
+RUBY_CHECK_SETJMP(_setjmpex, [], [@%:@include <setjmpex.h>])
+RUBY_CHECK_SETJMP(_setjmp)
+RUBY_CHECK_SETJMP(sigsetjmp, [sigjmp_buf])
+AC_MSG_CHECKING(for setjmp type)
+setjmp_suffix=
+unset setjmp_sigmask
+AC_ARG_WITH(setjmp-type,
+ AS_HELP_STRING([--with-setjmp-type], [select setjmp type]),
+ [
+ AS_CASE([$withval],
+ [__builtin_setjmp], [setjmp=__builtin_setjmp],
+ [_setjmp], [ setjmp_prefix=_],
+ [sigsetjmp,*], [ setjmp_prefix=sig setjmp_sigmask=`expr "$withval" : 'sigsetjmp\(,.*\)'`],
+ [sigsetjmp], [ setjmp_prefix=sig],
+ [setjmp], [ setjmp_prefix=],
+ [setjmpex], [ setjmp_prefix= setjmp_suffix=ex],
+ [''], [ unset setjmp_prefix],
+ [ AC_MSG_ERROR(invalid setjmp type: $withval)])], [unset setjmp_prefix])
+setjmp_cast=
+AS_IF([test ${setjmp_prefix+set}], [
+ AS_IF([test "${setjmp_prefix}" && eval test '$ac_cv_func_'${setjmp_prefix}setjmp${setjmp_suffix} = no], [
+ AC_MSG_ERROR(${setjmp_prefix}setjmp${setjmp_suffix} is not available)
+ ])
+], [{ AS_CASE("$ac_cv_func___builtin_setjmp", [yes*], [true], [false]) }], [
+ setjmp_cast=`expr "$ac_cv_func___builtin_setjmp" : "yes with cast (\(.*\))"`
+ setjmp_prefix=__builtin_
+ setjmp_suffix=
+], [test "$ac_cv_header_setjmpex_h:$ac_cv_func__setjmpex" = yes:yes], [
+ setjmp_prefix=
+ setjmp_suffix=ex
+], [test "$ac_cv_func__setjmp" = yes], [
+ setjmp_prefix=_
+ setjmp_suffix=
+], [test "$ac_cv_func_sigsetjmp" = yes], [
+ AS_CASE([$target_os],[solaris*|cygwin*],[setjmp_prefix=],[setjmp_prefix=sig])
+ setjmp_suffix=
+], [
+ setjmp_prefix=
+ setjmp_suffix=
+])
+AS_IF([test x$setjmp_prefix:$setjmp_sigmask = xsig:], [
+ setjmp_sigmask=,0
+])
+AC_MSG_RESULT(${setjmp_prefix}setjmp${setjmp_suffix}${setjmp_cast:+\($setjmp_cast\)}${setjmp_sigmask})
+AC_DEFINE_UNQUOTED([RUBY_SETJMP(env)], [${setjmp_prefix}setjmp${setjmp_suffix}($setjmp_cast(env)${setjmp_sigmask})])
+AC_DEFINE_UNQUOTED([RUBY_LONGJMP(env,val)], [${setjmp_prefix}longjmp($setjmp_cast(env),val)])
+AS_IF([test "(" "$GCC" != yes ")" -o x$setjmp_prefix != x__builtin_], AC_DEFINE_UNQUOTED(RUBY_JMP_BUF, ${setjmp_sigmask+${setjmp_prefix}}jmp_buf))
+AS_IF([test x$setjmp_suffix = xex], [AC_DEFINE_UNQUOTED(RUBY_USE_SETJMPEX, 1)])
+])dnl
diff --git a/tool/m4/ruby_stack_grow_direction.m4 b/tool/m4/ruby_stack_grow_direction.m4
new file mode 100644
index 0000000000..a4d205cc3c
--- /dev/null
+++ b/tool/m4/ruby_stack_grow_direction.m4
@@ -0,0 +1,30 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_STACK_GROW_DIRECTION], [
+ AS_VAR_PUSHDEF([stack_grow_dir], [rb_cv_stack_grow_dir_$1])
+ AC_CACHE_CHECK(stack growing direction on $1, stack_grow_dir, [
+AS_CASE(["$1"],
+[m68*|x86*|x64|i?86|ppc*|sparc*|alpha*], [ $2=-1],
+[hppa*], [ $2=+1],
+[
+ AC_RUN_IFELSE([AC_LANG_SOURCE([[
+/* recurse to get rid of inlining */
+static int
+stack_growup_p(addr, n)
+ volatile int *addr, n;
+{
+ volatile int end;
+ if (n > 0)
+ return *addr = stack_growup_p(addr, n - 1);
+ else
+ return (&end > addr);
+}
+int main()
+{
+ int x;
+ return stack_growup_p(&x, 10);
+}
+]])],[$2=-1],[$2=+1],[$2=0])
+ ])
+eval stack_grow_dir=\$$2])
+eval $2=\$stack_grow_dir
+AS_VAR_POPDEF([stack_grow_dir])])dnl
diff --git a/tool/m4/ruby_thread.m4 b/tool/m4/ruby_thread.m4
new file mode 100644
index 0000000000..3831bc4c06
--- /dev/null
+++ b/tool/m4/ruby_thread.m4
@@ -0,0 +1,33 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_THREAD], [
+AC_ARG_WITH(thread,
+ AS_HELP_STRING([--with-thread=IMPLEMENTATION], [specify the thread implementation to use]),
+ [THREAD_MODEL=$withval], [
+ THREAD_MODEL=
+ AS_CASE(["$target_os"],
+ [mingw*], [
+ THREAD_MODEL=win32
+ ],
+ [
+ AS_IF([test "$rb_with_pthread" = "yes"], [
+ THREAD_MODEL=pthread
+ ])
+ ]
+ )
+])
+
+AS_CASE(["$THREAD_MODEL"],
+[pthread], [AC_CHECK_HEADERS(pthread.h)],
+[win32], [],
+[""], [AC_MSG_ERROR(thread model is missing)],
+ [AC_MSG_ERROR(unknown thread model $THREAD_MODEL)])
+
+THREAD_IMPL_H=thread_$THREAD_MODEL.h
+AS_IF([test ! -f "$srcdir/$THREAD_IMPL_H"],
+ [AC_MSG_ERROR('$srcdir/$THREAD_IMPL_H' must exist)])
+THREAD_IMPL_SRC=thread_$THREAD_MODEL.c
+AS_IF([test ! -f "$srcdir/$THREAD_IMPL_SRC"],
+ [AC_MSG_ERROR('$srcdir/$THREAD_IMPL_SRC' must exist)])
+AC_DEFINE_UNQUOTED(THREAD_IMPL_H, ["$THREAD_IMPL_H"])
+AC_DEFINE_UNQUOTED(THREAD_IMPL_SRC, ["$THREAD_IMPL_SRC"])
+])dnl
diff --git a/tool/m4/ruby_try_cflags.m4 b/tool/m4/ruby_try_cflags.m4
new file mode 100644
index 0000000000..f2c6a3094e
--- /dev/null
+++ b/tool/m4/ruby_try_cflags.m4
@@ -0,0 +1,12 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_TRY_CFLAGS], [
+ AC_MSG_CHECKING([whether ]$1[ is accepted as CFLAGS])
+ RUBY_WERROR_FLAG([
+ CFLAGS="[$]CFLAGS $1"
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[$4]], [[$5]])],
+ [$2
+ AC_MSG_RESULT(yes)],
+ [$3
+ AC_MSG_RESULT(no)])
+ ])
+])dnl
diff --git a/tool/m4/ruby_try_cxxflags.m4 b/tool/m4/ruby_try_cxxflags.m4
new file mode 100644
index 0000000000..06f645f546
--- /dev/null
+++ b/tool/m4/ruby_try_cxxflags.m4
@@ -0,0 +1,17 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_TRY_CXXFLAGS], [
+ save_CXXFLAGS="$CXXFLAGS"
+ CXXFLAGS="[$]CXXFLAGS $1"
+ AC_MSG_CHECKING([whether ]$1[ is accepted as CXXFLAGS])
+ RUBY_WERROR_FLAG([
+ AC_LANG_PUSH([C++])
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[$4]], [[$5]])],
+ [$2
+ AC_MSG_RESULT(yes)],
+ [$3
+ AC_MSG_RESULT(no)])
+ ])
+ AC_LANG_POP([C++])
+ CXXFLAGS="$save_CXXFLAGS"
+ save_CXXFLAGS=
+])dnl
diff --git a/tool/m4/ruby_try_ldflags.m4 b/tool/m4/ruby_try_ldflags.m4
new file mode 100644
index 0000000000..c3a6be0fb3
--- /dev/null
+++ b/tool/m4/ruby_try_ldflags.m4
@@ -0,0 +1,15 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_TRY_LDFLAGS], [
+ save_LDFLAGS="$LDFLAGS"
+ LDFLAGS="[$]LDFLAGS $1"
+ AC_MSG_CHECKING([whether $1 is accepted as LDFLAGS])
+ RUBY_WERROR_FLAG([
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[$4]], [[$5]])],
+ [$2
+ AC_MSG_RESULT(yes)],
+ [$3
+ AC_MSG_RESULT(no)])
+ ])
+ LDFLAGS="$save_LDFLAGS"
+ save_LDFLAGS=
+])dnl
diff --git a/tool/m4/ruby_type_attribute.m4 b/tool/m4/ruby_type_attribute.m4
new file mode 100644
index 0000000000..5ea1219c6e
--- /dev/null
+++ b/tool/m4/ruby_type_attribute.m4
@@ -0,0 +1,8 @@
+dnl -*- Autoconf -*-
+dnl RUBY_TYPE_ATTRIBUTE(attrib, macroname, cachevar, condition)
+AC_DEFUN([RUBY_TYPE_ATTRIBUTE], [dnl
+ RUBY_DECL_ATTRIBUTE([$1], [$2], [$3], [$4],
+ [type], [
+@%:@define x struct conftest_attribute_check {int i;}
+])
+])dnl
diff --git a/tool/m4/ruby_universal_arch.m4 b/tool/m4/ruby_universal_arch.m4
new file mode 100644
index 0000000000..375cdd98d2
--- /dev/null
+++ b/tool/m4/ruby_universal_arch.m4
@@ -0,0 +1,122 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_UNIVERSAL_ARCH], [
+# RUBY_UNIVERSAL_ARCH begin
+ARCH_FLAG=`expr " $CXXFLAGS " : ['.* \(-m[0-9][0-9]*\) ']`
+test ${CXXFLAGS+set} && CXXFLAGS=`echo "$CXXFLAGS" | sed [-e 's/ *-arch *[^ ]*//g' -e 's/ *-m32//g' -e 's/ *-m64//g']`
+ARCH_FLAG=`expr " $CFLAGS " : ['.* \(-m[0-9][0-9]*\) ']`
+test ${CFLAGS+set} && CFLAGS=`echo "$CFLAGS" | sed [-e 's/ *-arch *[^ ]*//g' -e 's/ *-m32//g' -e 's/ *-m64//g']`
+test ${LDFLAGS+set} && LDFLAGS=`echo "$LDFLAGS" | sed [-e 's/ *-arch *[^ ]*//g' -e 's/ *-m32//g' -e 's/ *-m64//g']`
+unset universal_binary universal_archnames
+AS_IF([test ${target_archs+set}], [
+ AC_MSG_CHECKING([target architectures])
+ target_archs=`echo $target_archs | tr , ' '`
+ # /usr/lib/arch_tool -archify_list $TARGET_ARCHS
+ for archs in $target_archs
+ do
+ AS_CASE([",$universal_binary,"],[*",$archs,"*], [],[
+ cpu=$archs
+ cpu=`echo $cpu | sed 's/-.*-.*//'`
+ universal_binary="${universal_binary+$universal_binary,}$cpu"
+ universal_archnames="${universal_archnames} ${archs}=${cpu}"
+ ARCH_FLAG="${ARCH_FLAG+$ARCH_FLAG }-arch $archs"
+ ])
+ done
+ target_archs="$universal_binary"
+ unset universal_binary
+ AS_CASE(["$target_archs"],
+ [*,*], [universal_binary=yes],
+ [unset universal_archnames])
+ AC_MSG_RESULT([$target_archs])
+
+ target=`echo $target | sed "s/^$target_cpu-/-/"`
+ target_alias=`echo $target_alias | sed "s/^$target_cpu-/-/"`
+ AS_IF([test "${universal_binary-no}" = yes], [
+ AC_SUBST(try_header,try_compile)
+ target_cpu=universal
+ real_cross_compiling=$cross_compiling
+ ], [
+ AS_IF([test x"$target_cpu" != x"${target_archs}"], [
+ echo 'int main(){return 0;}' > conftest.c
+ AS_IF([$CC $CFLAGS $ARCH_FLAG -o conftest conftest.c > /dev/null 2>&1], [
+ rm -fr conftest.*
+ ], [test -z "$ARCH_FLAG"], [
+ RUBY_DEFAULT_ARCH("$target_archs")
+ ])
+ ])
+ target_cpu=${target_archs}
+ ])
+ AS_CASE(["$target"], [-*], [ target="$target_cpu${target}"])
+ AS_CASE(["$target_alias"], [-*], [ target_alias="$target_cpu${target_alias}"])
+], [
+ AS_IF([test x"$target_alias" = x], [
+ AS_CASE(["$target_os"],
+ [darwin*], [
+ AC_MSG_CHECKING([for real target cpu])
+ target=`echo $target | sed "s/^$target_cpu-/-/"`
+ target_cpu=`$CC -E - 2>/dev/null <<EOF |
+#ifdef __x86_64__
+"processor-name=x86_64"
+#endif
+#ifdef __i386__
+"processor-name=i386"
+#endif
+#ifdef __ppc__
+"processor-name=powerpc"
+#endif
+#ifdef __ppc64__
+"processor-name=powerpc64"
+#endif
+#ifdef __arm64__
+"processor-name=arm64"
+#endif
+EOF
+ sed -n 's/^"processor-name=\(.*\)"/\1/p'`
+ target="$target_cpu${target}"
+ AC_MSG_RESULT([$target_cpu])
+ ])
+ ])
+ target_archs="$target_cpu"
+])
+AS_IF([test "${target_archs}" != "${rb_cv_target_archs-${target_archs}}"], [
+ AC_MSG_ERROR([target arch(s) has changed from ${rb_cv_target_archs-nothing} to ${target_archs}])
+], [
+ rb_cv_target_archs=${target_archs}
+])
+AS_IF([test "x${ARCH_FLAG}" != x], [
+ CFLAGS="${CFLAGS:+$CFLAGS }${ARCH_FLAG}"
+ LDFLAGS="${LDFLAGS:+$LDFLAGS }${ARCH_FLAG}"
+])
+# RUBY_UNIVERSAL_ARCH end
+])dnl
+dnl
+AC_DEFUN([RUBY_UNIVERSAL_CHECK_HEADER_COND], [ dnl
+ AC_CACHE_CHECK([for $2 when $1], [$3],
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
+ [AC_INCLUDES_DEFAULT([$6])[
+ @%:@if ]$1[
+ @%:@include <]$2[>
+ @%:@endif]], [[]])],
+ [AS_VAR_SET($3, yes)],
+ [AS_VAR_SET($3, no)]))
+ AS_VAR_IF([$3], [yes], [dnl
+ printf "@%:@if %s\n" "$1" >>confdefs.h
+ AC_DEFINE_UNQUOTED(HAVE_[]AS_TR_CPP($2), 1)dnl
+ printf "@%:@endif\n" >>confdefs.h dnl
+ $4], [$5])
+])dnl
+dnl
+# RUBY_UNIVERSAL_CHECK_HEADER(CPU-LIST, HEADER,
+# [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND],
+# [INCLUDES = DEFAULT-INCLUDES])
+AC_DEFUN([RUBY_UNIVERSAL_CHECK_HEADER], [ dnl
+ m4_if([$# dnl
+ ], [0], [], [ dnl
+ m4_foreach([rb_Header], [$1],
+ [AS_CASE([",$target_archs,"], [*,]rb_Header[,*],
+ [RUBY_UNIVERSAL_CHECK_HEADER_COND]([defined(__[]rb_Header[]__)],
+ [$2], [rb_cv_header_[]AS_TR_SH($2)_on_[]AS_TR_SH(rb_Header)],
+ [$3], [$4], [$5])
+ )
+ ])
+ ])dnl
+])dnl
diff --git a/tool/m4/ruby_werror_flag.m4 b/tool/m4/ruby_werror_flag.m4
new file mode 100644
index 0000000000..616a7f6abf
--- /dev/null
+++ b/tool/m4/ruby_werror_flag.m4
@@ -0,0 +1,18 @@
+dnl -*- Autoconf -*-
+AC_DEFUN([RUBY_WERROR_FLAG], [dnl
+save_CFLAGS="$CFLAGS"
+CFLAGS="$CFLAGS $rb_cv_warnflags"
+AS_IF([test "${ac_c_werror_flag+set}"], [
+ rb_c_werror_flag="$ac_c_werror_flag"
+], [
+ unset rb_c_werror_flag
+])
+ac_c_werror_flag=yes
+$1
+CFLAGS="$save_CFLAGS"
+save_CFLAGS=
+AS_IF([test "${rb_c_werror_flag+set}"], [
+ ac_c_werror_flag="$rb_c_werror_flag"
+], [
+ unset ac_c_werror_flag
+])])dnl
diff --git a/tool/make-snapshot b/tool/make-snapshot
new file mode 100755
index 0000000000..942e85b933
--- /dev/null
+++ b/tool/make-snapshot
@@ -0,0 +1,654 @@
+#!/usr/bin/ruby -s
+# -*- coding: us-ascii -*-
+require 'rubygems'
+require 'rubygems/package'
+require 'rubygems/package/tar_writer'
+require 'uri'
+require 'digest/sha1'
+require 'digest/sha2'
+require 'fileutils'
+require 'shellwords'
+require 'tmpdir'
+require 'pathname'
+require 'yaml'
+require 'json'
+require File.expand_path("../lib/vcs", __FILE__)
+require File.expand_path("../lib/colorize", __FILE__)
+STDOUT.sync = true
+
+$srcdir ||= nil
+$archname = nil if ($archname ||= nil) == ""
+$keep_temp ||= nil
+$patch_file ||= nil
+$packages ||= nil
+$digests ||= nil
+$tooldir = File.expand_path("..", __FILE__)
+$unicode_version = nil if ($unicode_version ||= nil) == ""
+$colorize = Colorize.new
+
+def usage
+ <<USAGE
+usage: #{File.basename $0} [option...] new-directory-to-save [version ...]
+options:
+ -srcdir=PATH source directory path
+ -archname=NAME make the basename of snapshots NAME
+ -keep_temp keep temporary working directory
+ -patch_file=PATCH apply PATCH file after export
+ -packages=PKG[,...] make PKG packages (#{PACKAGES.keys.join(", ")})
+ -digests=ALG[,...] show ALG digests (#{DIGESTS.join(", ")})
+ -unicode_version=VER Unicode version to generate encodings
+ -svn[=URL] make snapshot from SVN repository
+ (#{SVNURL})
+ -help, --help show this message
+version:
+ master, trunk, stable, branches/*, tags/*, X.Y, X.Y.Z, X.Y.Z-pL
+each versions may be followed by optional @revision.
+USAGE
+end
+
+DIGESTS = %w[SHA1 SHA256 SHA512]
+PACKAGES = {
+ "tar" => %w".tar",
+ "bzip" => %w".tar.bz2 bzip2 -c",
+ "gzip" => %w".tar.gz gzip -c",
+ "xz" => %w".tar.xz xz -c",
+ "zip" => %w".zip zip -Xqr",
+}
+DEFAULT_PACKAGES = PACKAGES.keys - ["tar"]
+if !$no7z and system("7z", out: IO::NULL)
+ PACKAGES["gzip"] = %w".tar.gz 7z a dummy -tgzip -mx -so"
+ PACKAGES["zip"] = %w".zip 7z a -tzip -mx -mtc=off" << {out: IO::NULL}
+elsif gzip = ENV.delete("GZIP")
+ PACKAGES["gzip"].concat(gzip.shellsplit)
+end
+
+if mflags = ENV["GNUMAKEFLAGS"] and /\A-(\S*)j\d*/ =~ mflags
+ mflags = mflags.gsub(/(\A|\s)(-\S*)j\d*/, '\1\2')
+ mflags.strip!
+ ENV["GNUMAKEFLAGS"] = (mflags unless mflags.empty?)
+end
+ENV["LC_ALL"] = ENV["LANG"] = "C"
+SVNURL = URI.parse("https://svn.ruby-lang.org/repos/ruby/")
+# https git clone is disabled at git.ruby-lang.org/ruby.git.
+GITURL = URI.parse("https://github.com/ruby/ruby.git")
+RUBY_VERSION_PATTERN = /^\#define\s+RUBY_VERSION\s+"([\d.]+)"/
+
+ENV["VPATH"] ||= "include/ruby"
+YACC = ENV["YACC"] ||= "bison"
+ENV["BASERUBY"] ||= "ruby"
+ENV["RUBY"] ||= "ruby"
+ENV["MV"] ||= "mv"
+ENV["RM"] ||= "rm -f"
+ENV["MINIRUBY"] ||= "ruby"
+ENV["PROGRAM"] ||= "ruby"
+ENV["AUTOCONF"] ||= "autoconf"
+ENV["BUILTIN_TRANSOBJS"] ||= "newline.o"
+ENV["TZ"] = "UTC"
+
+class String
+ # for older ruby
+ alias bytesize size unless method_defined?(:bytesize)
+end
+
+class Dir
+ def self.mktmpdir(path)
+ path = File.join(tmpdir, path+"-#{$$}-#{rand(100000)}")
+ begin
+ mkdir(path)
+ rescue Errno::EEXIST
+ path.succ!
+ retry
+ end
+ path
+ end unless respond_to?(:mktmpdir)
+end
+
+$packages &&= $packages.split(/[, ]+/).tap {|pkg|
+ if all = pkg.index("all")
+ pkg[all, 1] = DEFAULT_PACKAGES - pkg
+ end
+ pkg -= PACKAGES.keys
+ pkg.empty? or abort "#{File.basename $0}: unknown packages - #{pkg.join(", ")}"
+}
+$packages ||= DEFAULT_PACKAGES
+
+$digests &&= $digests.split(/[, ]+/).tap {|dig|
+ dig -= DIGESTS
+ dig.empty? or abort "#{File.basename $0}: unknown digests - #{dig.join(", ")}"
+}
+$digests ||= DIGESTS
+
+$patch_file &&= File.expand_path($patch_file)
+path = ENV["PATH"].split(File::PATH_SEPARATOR)
+%w[YACC BASERUBY RUBY MV MINIRUBY].each do |var|
+ cmd, = ENV[var].shellsplit
+ unless path.any? {|dir|
+ file = File.expand_path(cmd, dir)
+ File.file?(file) and File.executable?(file)
+ }
+ abort "#{File.basename $0}: #{var} command not found - #{cmd}"
+ end
+end
+
+%w[BASERUBY RUBY MINIRUBY].each do |var|
+ %x[#{ENV[var]} --disable-gem -e1 2>&1]
+ if $?.success?
+ ENV[var] += ' --disable-gem'
+ end
+end
+
+if defined?($help) or defined?($_help)
+ puts usage
+ exit
+end
+unless destdir = ARGV.shift
+ abort usage
+end
+revisions = ARGV.empty? ? [nil] : ARGV
+
+if $exported
+ abort "#{File.basename $0}: -exported option is deprecated; use -srcdir instead"
+end
+
+FileUtils.mkpath(destdir)
+destdir = File.expand_path(destdir)
+tmp = Dir.mktmpdir("ruby-snapshot")
+FileUtils.mkpath(tmp)
+at_exit {
+ Dir.chdir "/"
+ FileUtils.rm_rf(tmp)
+} unless $keep_temp
+
+def tar_create(tarball, dir)
+ header = Gem::Package::TarHeader
+ dir_type = "5"
+ uname = gname = "ruby"
+ File.open(tarball, "wb") do |f|
+ w = Gem::Package::TarWriter.new(f)
+ list = Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH)
+ list.reject! {|name| name.end_with?("/.")}
+ list.sort_by! {|name| name.split("/")}
+ list.each do |path|
+ next if File.basename(path) == "."
+ s = File.stat(path)
+ mode = 0644
+ case
+ when s.file?
+ type = nil
+ size = s.size
+ mode |= 0111 if s.executable?
+ when s.directory?
+ path += "/"
+ type = dir_type
+ size = 0
+ mode |= 0111
+ else
+ next
+ end
+ name, prefix = w.split_name(path)
+ h = header.new(name: name, prefix: prefix, typeflag: type,
+ mode: mode, size: size, mtime: s.mtime,
+ uname: uname, gname: gname)
+ f.write(h)
+ if size > 0
+ IO.copy_stream(path, f)
+ f.write("\0" * (-size % 512))
+ end
+ end
+ end
+ true
+rescue => e
+ warn e.message
+ false
+end
+
+def touch_all(time, pattern, opt, &cond)
+ Dir.glob(pattern, opt) do |n|
+ stat = File.stat(n)
+ if stat.file? or stat.directory?
+ next if cond and !yield(n, stat)
+ File.utime(time, time, n)
+ end
+ end
+rescue
+ false
+else
+ true
+end
+
+class MAKE < Struct.new(:prog, :args)
+ def initialize(vars)
+ vars = vars.map {|arg| arg.join("=")}
+ super(ENV["MAKE"] || ENV["make"] || "make", vars)
+ end
+
+ def run(target)
+ err = IO.pipe do |r, w|
+ begin
+ pid = Process.spawn(self.prog, *self.args, target, {:err => w, r => :close})
+ w.close
+ r.read
+ ensure
+ Process.wait(pid)
+ end
+ end
+ if $?.success?
+ true
+ else
+ STDERR.puts err
+ $colorize.fail("#{target} failed")
+ false
+ end
+ end
+end
+
+def measure
+ clock = Process::CLOCK_MONOTONIC
+ t0 = Process.clock_gettime(clock)
+ STDOUT.flush
+ result = yield
+ printf(" %6.3f", Process.clock_gettime(clock) - t0)
+ STDOUT.flush
+ result
+end
+
+def package(vcs, rev, destdir, tmp = nil)
+ pwd = Dir.pwd
+ patchlevel = false
+ prerelease = false
+ if rev and revision = rev[/@(\h+)\z/, 1]
+ rev = $`
+ end
+ case rev
+ when nil
+ url = nil
+ when /\A(?:master|trunk)\z/
+ url = vcs.trunk
+ when /\Abranches\//
+ url = vcs.branch($')
+ when /\Atags\//
+ url = vcs.tag($')
+ when /\Astable\z/
+ vcs.branch_list("ruby_[0-9]*") {|n| url = n[/\Aruby_\d+_\d+\z/]}
+ url &&= vcs.branch(url)
+ when /\A(.*)\.(.*)\.(.*)-(preview|rc)(\d+)/
+ prerelease = true
+ tag = "#{$4}#{$5}"
+ url = vcs.tag("v#{$1}_#{$2}_#{$3}_#{$4}#{$5}")
+ when /\A(.*)\.(.*)\.(.*)-p(\d+)/
+ patchlevel = true
+ tag = "p#{$4}"
+ url = vcs.tag("v#{$1}_#{$2}_#{$3}_#{$4}")
+ when /\A(\d+)\.(\d+)(?:\.(\d+))?\z/
+ if $3 && ($1 > "2" || $1 == "2" && $2 >= "1")
+ patchlevel = true
+ tag = ""
+ url = vcs.tag("v#{$1}_#{$2}_#{$3}")
+ else
+ url = vcs.branch("ruby_#{rev.tr('.', '_')}")
+ end
+ else
+ warn "#{$0}: unknown version - #{rev}"
+ return
+ end
+ if info = vcs.get_revisions(url)
+ modified = info[2]
+ else
+ modified = Time.now - 10
+ end
+ if !revision and info
+ revision = info
+ url ||= vcs.branch(revision[3])
+ revision = revision[1]
+ end
+ version = nil
+ unless revision
+ url = vcs.trunk
+ vcs.grep(RUBY_VERSION_PATTERN, url, "version.h") {version = $1}
+ unless rev == version
+ warn "#{$0}: #{rev} not found"
+ return
+ end
+ revision = vcs.get_revisions(url)[1]
+ end
+
+ v = "ruby"
+ puts "Exporting #{rev}@#{revision}"
+ exported = tmp ? File.join(tmp, v) : v
+ unless vcs.export(revision, url, exported, true) {|line| print line}
+ warn("Export failed")
+ return
+ end
+ if $srcdir
+ Dir.glob($srcdir + "/{tool/config.{guess,sub},gems/*.gem,.downloaded-cache/*,enc/unicode/data/**/*.txt}") do |file|
+ puts "copying #{file}" if $VERBOSE
+ dest = exported + file[$srcdir.size..-1]
+ FileUtils.mkpath(File.dirname(dest))
+ begin
+ FileUtils.cp_r(file, dest)
+ FileUtils.chmod_R("a+rwX,go-w", dest)
+ rescue SystemCallError
+ end
+ end
+ end
+
+ status = IO.read(File.dirname(__FILE__) + "/prereq.status")
+ Dir.chdir(tmp) if tmp
+
+ if !File.directory?(v)
+ v = Dir.glob("ruby-*").select(&File.method(:directory?))
+ v.size == 1 or abort "#{File.basename $0}: not exported"
+ v = v[0]
+ end
+
+ File.open("#{v}/revision.h", "wb") {|f|
+ short = vcs.short_revision(revision)
+ f.puts "#define RUBY_REVISION #{short.inspect}"
+ unless short == revision
+ f.puts "#define RUBY_FULL_REVISION #{revision.inspect}"
+ end
+ }
+ version ||= (versionhdr = IO.read("#{v}/version.h"))[RUBY_VERSION_PATTERN, 1]
+ version ||=
+ begin
+ include_ruby_versionhdr = IO.read("#{v}/include/ruby/version.h")
+ api_major_version = include_ruby_versionhdr[/^\#define\s+RUBY_API_VERSION_MAJOR\s+([\d.]+)/, 1]
+ api_minor_version = include_ruby_versionhdr[/^\#define\s+RUBY_API_VERSION_MINOR\s+([\d.]+)/, 1]
+ version_teeny = versionhdr[/^\#define\s+RUBY_VERSION_TEENY\s+(\d+)/, 1]
+ [api_major_version, api_minor_version, version_teeny].join('.')
+ end
+ version or return
+ if patchlevel
+ unless tag.empty?
+ versionhdr ||= IO.read("#{v}/version.h")
+ patchlevel = versionhdr[/^\#define\s+RUBY_PATCHLEVEL\s+(\d+)/, 1]
+ tag = (patchlevel ? "p#{patchlevel}" : vcs.revision_name(revision))
+ end
+ elsif prerelease
+ versionhdr ||= IO.read("#{v}/version.h")
+ versionhdr.sub!(/^\#define\s+RUBY_PATCHLEVEL_STR\s+"\K.+?(?=")/, tag)
+ IO.write("#{v}/version.h", versionhdr)
+ else
+ tag ||= vcs.revision_name(revision)
+ end
+
+ if $archname
+ n = $archname
+ elsif tag.empty?
+ n = "ruby-#{version}"
+ else
+ n = "ruby-#{version}-#{tag}"
+ end
+ File.directory?(n) or File.rename v, n
+ v = n
+
+ if $patch_file && !system(*%W"patch -d #{v} -p0 -i #{$patch_file}")
+ puts $colorize.fail("patching failed")
+ return
+ end
+ def (clean = []).add(n) push(n); n end
+ def clean.create(file, content = "") File.binwrite(add(file), content) end
+ Dir.chdir(v) do
+ unless File.exist?("ChangeLog")
+ vcs.export_changelog(url, nil, revision, "ChangeLog")
+ end
+
+ unless touch_all(modified, "**/*", File::FNM_DOTMATCH)
+ modified = nil
+ colors = %w[red yellow green cyan blue magenta]
+ "take a breath, and go ahead".scan(/./) do |c|
+ if c == ' '
+ print c
+ else
+ colors.push(color = colors.shift)
+ print $colorize.decorate(c, color)
+ end
+ sleep(c == "," ? 0.7 : 0.05)
+ end
+ puts
+ end
+
+ File.open(clean.add("cross.rb"), "w") do |f|
+ f.puts "Object.__send__(:remove_const, :CROSS_COMPILING) if defined?(CROSS_COMPILING)"
+ f.puts "CROSS_COMPILING=true"
+ f.puts "Object.__send__(:remove_const, :RUBY_PLATFORM)"
+ f.puts "RUBY_PLATFORM='none'"
+ f.puts "Object.__send__(:remove_const, :RUBY_VERSION)"
+ f.puts "RUBY_VERSION='#{version}'"
+ end
+ unless File.exist?("configure")
+ print "creating configure..."
+ unless system([ENV["AUTOCONF"]]*2)
+ puts $colorize.fail(" failed")
+ return
+ end
+ puts $colorize.pass(" done")
+ end
+ clean.add("autom4te.cache")
+ clean.add("enc/unicode/data")
+ print "creating prerequisites..."
+ if File.file?("common.mk") && /^prereq/ =~ commonmk = IO.read("common.mk")
+ puts
+ extout = clean.add('tmp')
+ begin
+ status = IO.read("tool/prereq.status")
+ rescue Errno::ENOENT
+ # use fallback file
+ end
+ clean.create("config.status", status)
+ clean.create("noarch-fake.rb", "require_relative 'cross'\n")
+ FileUtils.mkpath(hdrdir = "#{extout}/include/ruby")
+ File.binwrite("#{hdrdir}/config.h", "")
+ FileUtils.mkpath(defaults = "#{extout}/rubygems/defaults")
+ File.binwrite("#{defaults}/operating_system.rb", "")
+ File.binwrite("#{defaults}/ruby.rb", "")
+ miniruby = ENV['MINIRUBY'] + " -I. -I#{extout} -rcross"
+ baseruby = ENV["BASERUBY"]
+ mk = (IO.read("template/Makefile.in") rescue IO.read("Makefile.in")).
+ gsub(/^@.*\n/, '')
+ vars = {
+ "EXTOUT"=>extout,
+ "PATH_SEPARATOR"=>File::PATH_SEPARATOR,
+ "MINIRUBY"=>miniruby,
+ "RUBY"=>ENV["RUBY"],
+ "BASERUBY"=>baseruby,
+ "PWD"=>Dir.pwd,
+ "ruby_version"=>version,
+ "MAJOR"=>api_major_version,
+ "MINOR"=>api_minor_version,
+ "TEENY"=>version_teeny,
+ }
+ status.scan(/^s([%,])@([A-Za-z_][A-Za-z_0-9]*)@\1(.*?)\1g$/) do
+ vars[$2] ||= $3
+ end
+ vars.delete("UNICODE_FILES") # for stable branches
+ vars["UNICODE_VERSION"] = $unicode_version if $unicode_version
+ args = vars.dup
+ mk.gsub!(/@([A-Za-z_]\w*)@/) {args.delete($1); vars[$1] || ENV[$1]}
+ mk << commonmk.gsub(/\{\$([^(){}]*)[^{}]*\}/, "").sub(/^revision\.tmp::$/, '\& Makefile')
+ mk << <<-'APPEND'
+
+update-download:: touch-unicode-files
+prepare-package: prereq after-update
+clean-cache: $(CLEAN_CACHE)
+after-update:: extract-gems
+extract-gems: update-gems
+update-gems:
+$(UNICODE_SRC_DATA_DIR)/.unicode-tables.time:
+touch-unicode-files:
+ APPEND
+ clean.create("Makefile", mk)
+ clean.create("revision.tmp")
+ clean.create(".revision.time")
+ ENV["CACHE_SAVE"] = "no"
+ make = MAKE.new(args)
+ return unless make.run("update-download")
+ clean.push("rbconfig.rb", ".rbconfig.time", "enc.mk", "ext/ripper/y.output", ".revision.time")
+ Dir.glob("**/*") do |dest|
+ next unless File.symlink?(dest)
+ orig = File.expand_path(File.readlink(dest), File.dirname(dest))
+ File.unlink(dest)
+ FileUtils.cp_r(orig, dest)
+ end
+ File.utime(modified, modified, *Dir.glob(["tool/config.{guess,sub}", "gems/*.gem", "tool"]))
+ return unless make.run("prepare-package")
+ return unless make.run("clean-cache")
+ if modified
+ new_time = modified + 2
+ touch_all(new_time, "**/*", File::FNM_DOTMATCH) do |name, stat|
+ stat.mtime > modified unless clean.include?(name)
+ end
+ modified = new_time
+ end
+ print "prerequisites"
+ else
+ system(*%W"#{YACC} -o parse.c parse.y")
+ end
+ vcs.after_export(".") if exported
+ clean.concat(Dir.glob("ext/**/autom4te.cache"))
+ FileUtils.rm_rf(clean) unless $keep_temp
+ FileUtils.rm_rf(".downloaded-cache")
+ if File.exist?("gems/bundled_gems")
+ gems = Dir.glob("gems/*.gem")
+ gems -= File.readlines("gems/bundled_gems").map {|line|
+ next if /^\s*(?:#|$)/ =~ line
+ name, version, _ = line.split(' ')
+ "gems/#{name}-#{version}.gem"
+ }
+ FileUtils.rm_f(gems)
+ else
+ FileUtils.rm_rf("gems")
+ end
+ if modified
+ touch_all(modified, "**/*/", 0) do |name, stat|
+ stat.mtime > modified
+ end
+ File.utime(modified, modified, ".")
+ end
+ unless $?.success?
+ puts $colorize.fail(" failed")
+ return
+ end
+ puts $colorize.pass(" done")
+ end
+
+ if v == "."
+ v = File.basename(Dir.pwd)
+ Dir.chdir ".."
+ else
+ Dir.chdir(File.dirname(v))
+ v = File.basename(v)
+ end
+
+ tarball = nil
+ return $packages.collect do |mesg|
+ (ext, *cmd) = PACKAGES[mesg]
+ File.directory?(destdir) or FileUtils.mkpath(destdir)
+ file = File.join(destdir, "#{$archname||v}#{ext}")
+ case ext
+ when /\.tar/
+ if tarball
+ next if tarball.empty?
+ else
+ tarball = ext == ".tar" ? file : "#{$archname||v}.tar"
+ print "creating tarball... #{tarball}"
+ if measure {tar_create(tarball, v)}
+ puts $colorize.pass(" done")
+ File.utime(modified, modified, tarball) if modified
+ next if tarball == file
+ else
+ puts $colorize.fail(" failed")
+ tarball = ""
+ next
+ end
+ end
+ print "creating #{mesg} tarball... #{file}"
+ done = measure {system(*cmd, tarball, out: file)}
+ else
+ print "creating #{mesg} archive... #{file}"
+ if Hash === cmd.last
+ *cmd, opt = *cmd
+ cmd << file << v << opt
+ else
+ (cmd = cmd.dup) << file << v
+ end
+ done = measure {system(*cmd)}
+ end
+ if done
+ puts $colorize.pass(" done")
+ file
+ else
+ puts $colorize.fail(" failed")
+ nil
+ end
+ end.compact
+ensure
+ FileUtils.rm_rf(tmp ? File.join(tmp, v) : v) if v and !$keep_temp
+ Dir.chdir(pwd)
+end
+
+if [$srcdir, ($svn||=nil), ($git||=nil)].compact.size > 1
+ abort "#{File.basename $0}: -srcdir, -svn, and -git are exclusive"
+end
+if $srcdir
+ vcs = VCS.detect($srcdir)
+elsif $svn
+ vcs = VCS::SVN.new($svn == true ? SVNURL : URI.parse($svn))
+elsif $git
+ abort "#{File.basename $0}: use -srcdir with cloned local repository"
+else
+ begin
+ vcs = VCS.detect(File.expand_path("../..", __FILE__))
+ rescue VCS::NotFoundError
+ vcs = VCS::SVN.new(SVNURL)
+ end
+end
+
+release_date = Time.now.getutc
+info = {}
+
+success = true
+revisions.collect {|rev| package(vcs, rev, destdir, tmp)}.flatten.each do |name|
+ if !name
+ success = false
+ next
+ end
+ str = File.binread(name)
+ pathname = Pathname(name)
+ basename = pathname.basename.to_s
+ extname = pathname.extname.sub(/\A\./, '')
+ version = basename[/\Aruby-(.*)\.(?:tar|zip)/, 1]
+ key = basename[/\A(.*)\.(?:tar|zip)/, 1]
+ info[key] ||= Hash.new{|h,k|h[k]={}}
+ info[key]['version'] = version if version
+ info[key]['date'] = release_date.strftime('%Y-%m-%d')
+ if version
+ info[key]['post'] = "/en/news/#{release_date.strftime('%Y/%m/%d')}/ruby-#{version.tr('.', '-')}-released/"
+ info[key]['url'][extname] = "https://cache.ruby-lang.org/pub/ruby/#{version[/\A\d+\.\d+/]}/#{basename}"
+ else
+ info[key]['filename'][extname] = basename
+ end
+ info[key]['size'][extname] = str.bytesize
+ puts "* #{$colorize.pass(name)}"
+ puts " SIZE: #{str.bytesize} bytes"
+ $digests.each do |alg|
+ digest = Digest(alg).hexdigest(str)
+ info[key][alg.downcase][extname] = digest
+ printf " %-8s%s\n", "#{alg}:", digest
+ end
+end
+
+yaml = info.values.to_yaml
+json = info.values.to_json
+puts "#{$colorize.pass('YAML:')}"
+puts yaml
+puts "#{$colorize.pass('JSON:')}"
+puts json
+infodir = Pathname(destdir) + 'info'
+infodir.mkpath
+(infodir+'info.yml').write(yaml)
+(infodir+'info.json').write(json)
+
+exit false if !success
+
+# vim:fileencoding=US-ASCII sw=2 ts=4 noexpandtab ff=unix
diff --git a/tool/make_hgraph.rb b/tool/make_hgraph.rb
new file mode 100644
index 0000000000..0f388814dd
--- /dev/null
+++ b/tool/make_hgraph.rb
@@ -0,0 +1,95 @@
+#
+# Make dot file of internal class/module hierarchy graph.
+#
+
+require 'objspace'
+
+module ObjectSpace
+ def self.object_id_of obj
+ if obj.kind_of?(ObjectSpace::InternalObjectWrapper)
+ obj.internal_object_id
+ else
+ obj.object_id
+ end
+ end
+
+ T_ICLASS_NAME = {}
+
+ def self.class_name_of klass
+ case klass
+ when Class, Module
+ # (singleton class).name returns nil
+ klass.name || klass.inspect
+ when InternalObjectWrapper # T_ICLASS
+ if klass.type == :T_ICLASS
+ "#<I:#{class_name_of(ObjectSpace.internal_class_of(klass))}>"
+ else
+ klass.inspect
+ end
+ else
+ klass.inspect
+ end
+ end
+
+ def self.module_refenreces klass
+ h = {} # object_id -> [klass, class_of, super]
+ stack = [klass]
+ while klass = stack.pop
+ obj_id = ObjectSpace.object_id_of(klass)
+ next if h.has_key?(obj_id)
+ cls = ObjectSpace.internal_class_of(klass)
+ sup = ObjectSpace.internal_super_of(klass)
+ stack << cls if cls
+ stack << sup if sup
+ h[obj_id] = [klass, cls, sup].map{|e| ObjectSpace.class_name_of(e)}
+ end
+ h.values
+ end
+
+ def self.module_refenreces_dot klass
+ result = []
+ rank_set = {}
+
+ result << "digraph mod_h {"
+ # result << " rankdir=LR;"
+ module_refenreces(klass).each{|(m, k, s)|
+ # next if /singleton/ =~ m
+ result << "#{m.dump} -> #{s.dump} [label=\"super\"];"
+ result << "#{m.dump} -> #{k.dump} [label=\"klass\"];"
+
+ unless rank = rank_set[m]
+ rank = rank_set[m] = 0
+ end
+ unless rank_set[s]
+ rank_set[s] = rank + 1
+ end
+ unless rank_set[k]
+ rank_set[k] = rank
+ end
+ }
+
+ rs = [] # [[mods...], ...]
+ rank_set.each{|m, r|
+ rs[r] = [] unless rs[r]
+ rs[r] << m
+ }
+
+ rs.each{|ms|
+ result << "{rank = same; #{ms.map{|m| m.dump}.join(", ")}};"
+ }
+ result << "}"
+ result.join("\n")
+ end
+
+ def self.module_refenreces_image klass, file
+ dot = module_refenreces_dot(klass)
+ img = nil
+ IO.popen("dot -Tpng", 'r+'){|io|
+ #
+ io.puts dot
+ io.close_write
+ img = io.read
+ }
+ open(File.expand_path(file), 'w+'){|f| f.puts img}
+ end
+end
diff --git a/tool/mdoc2man.rb b/tool/mdoc2man.rb
new file mode 100755
index 0000000000..e005fcf19a
--- /dev/null
+++ b/tool/mdoc2man.rb
@@ -0,0 +1,505 @@
+#!/usr/bin/env ruby
+###
+### mdoc2man - mdoc to man converter
+###
+### Quick usage: mdoc2man.rb < mdoc_manpage.8 > man_manpage.8
+###
+### Ported from Perl by Akinori MUSHA.
+###
+### Copyright (c) 2001 University of Illinois Board of Trustees
+### Copyright (c) 2001 Mark D. Roth
+### Copyright (c) 2002, 2003 Akinori MUSHA
+### All rights reserved.
+###
+### Redistribution and use in source and binary forms, with or without
+### modification, are permitted provided that the following conditions
+### are met:
+### 1. Redistributions of source code must retain the above copyright
+### notice, this list of conditions and the following disclaimer.
+### 2. Redistributions in binary form must reproduce the above copyright
+### notice, this list of conditions and the following disclaimer in the
+### documentation and/or other materials provided with the distribution.
+### 3. All advertising materials mentioning features or use of this software
+### must display the following acknowledgement:
+### This product includes software developed by the University of
+### Illinois at Urbana, and their contributors.
+### 4. The University nor the names of their
+### contributors may be used to endorse or promote products derived from
+### this software without specific prior written permission.
+###
+### THIS SOFTWARE IS PROVIDED BY THE TRUSTEES AND CONTRIBUTORS ``AS IS'' AND
+### ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+### IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+### ARE DISCLAIMED. IN NO EVENT SHALL THE TRUSTEES OR CONTRIBUTORS BE LIABLE
+### FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+### DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+### OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+### HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+### LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+### OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+### SUCH DAMAGE.
+###
+### $Id$
+###
+
+class Mdoc2Man
+ ANGLE = 1
+ OPTION = 2
+ PAREN = 3
+
+ RE_PUNCT = /^[!"'),\.\/:;>\?\]`]$/
+
+ def initialize
+ @name = @date = @id = nil
+ @refauthors = @reftitle = @refissue = @refdate = @refopt = nil
+
+ @optlist = 0 ### 1 = bullet, 2 = enum, 3 = tag, 4 = item
+ @oldoptlist = 0
+ @nospace = 0 ### 0, 1, 2
+ @enum = 0
+ @synopsis = true
+ @reference = false
+ @ext = false
+ @extopt = false
+ @literal = false
+ end
+
+ def mdoc2man(i, o)
+ i.each { |line|
+ if /^\./ !~ line
+ o.print line
+ o.print ".br\n" if @literal
+ next
+ end
+
+ line.slice!(0, 1)
+
+ next if /\\"/ =~ line
+
+ line = parse_macro(line) and o.print line
+ }
+
+ initialize
+ end
+
+ def shift_arg(words)
+ case words[0]
+ when nil, RE_PUNCT
+ nil
+ when /\A"(.+)/
+ words.shift
+ word = $1
+ loop {
+ break if word.chomp!('"')
+ token = words.shift or break
+ word << ' ' << token
+ }
+ word
+ else
+ words.shift
+ end
+ end
+
+ def parse_macro(line)
+ words = line.split
+ retval = ''
+
+ quote = []
+ dl = false
+
+ while word = words.shift
+ case word
+ when RE_PUNCT
+ next retval << word if word == ':'
+ while q = quote.pop
+ case q
+ when OPTION
+ retval << ']'
+ when PAREN
+ retval << ')'
+ when ANGLE
+ retval << '>'
+ end
+ end
+ retval << word
+ next
+ when 'Li', 'Pf'
+ @nospace = 1
+ next
+ when 'Xo'
+ @ext = true
+ retval << ' ' unless retval.empty? || /[\n ]\z/ =~ retval
+ next
+ when 'Xc'
+ @ext = false
+ retval << "\n" unless @extopt
+ break
+ when 'Bd'
+ @literal = true if words[0] == '-literal'
+ retval << "\n"
+ break
+ when 'Ed'
+ @literal = false
+ break
+ when 'Ns'
+ @nospace = 1 if @nospace == 0
+ retval.chomp!(' ')
+ next
+ when 'No'
+ retval.chomp!(' ')
+ retval << words.shift
+ next
+ when 'Dq'
+ retval << '``'
+ begin
+ retval << words.shift << ' '
+ end until words.empty? || RE_PUNCT =~ words[0]
+ retval.chomp!(' ')
+ retval << '\'\''
+ @nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
+ next
+ when 'Sq', 'Ql'
+ retval << '`' << words.shift << '\''
+ @nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
+ next
+ # when 'Ic'
+ # retval << '\\fB' << words.shift << '\\fP'
+ # next
+ when 'Oo'
+ #retval << "[\\c\n"
+ @extopt = true
+ @nospace = 1 if @nospace == 0
+ retval << '['
+ next
+ when 'Oc'
+ @extopt = false
+ retval << ']'
+ next
+ when 'Ao'
+ @nospace = 1 if @nospace == 0
+ retval << '<'
+ next
+ when 'Ac'
+ retval << '>'
+ next
+ end
+
+ retval << ' ' if @nospace == 0 && !(retval.empty? || /[\n ]\z/ =~ retval)
+ @nospace = 0 if @nospace == 1
+
+ case word
+ when 'Dd'
+ @date = words.join(' ')
+ return nil
+ when 'Dt'
+ if words.size >= 2 && words[1] == '""' &&
+ /^(.*)\(([0-9])\)$/ =~ words[0]
+ words[0] = $1
+ words[1] = $2
+ end
+ @id = words.join(' ')
+ return nil
+ when 'Os'
+ retval << '.TH ' << @id << ' "' << @date << '" "' <<
+ words.join(' ') << '"'
+ break
+ when 'Sh'
+ retval << '.SH'
+ @synopsis = (words[0] == 'SYNOPSIS')
+ next
+ when 'Xr'
+ retval << '\\fB' << words.shift <<
+ '\\fP(' << words.shift << ')' << (words.shift||'')
+ break
+ when 'Rs'
+ @refauthors = []
+ @reftitle = ''
+ @refissue = ''
+ @refdate = ''
+ @refopt = ''
+ @reference = true
+ break
+ when 'Re'
+ retval << "\n"
+
+ # authors
+ while @refauthors.size > 1
+ retval << @refauthors.shift << ', '
+ end
+ retval << 'and ' unless retval.empty?
+ retval << @refauthors.shift
+
+ # title
+ retval << ', \\fI' << @reftitle << '\\fP'
+
+ # issue
+ retval << ', ' << @refissue unless @refissue.empty?
+
+ # date
+ retval << ', ' << @refdate unless @refdate.empty?
+
+ # optional info
+ retval << ', ' << @refopt unless @refopt.empty?
+
+ retval << ".\n"
+
+ @reference = false
+ break
+ when 'An'
+ next
+ when 'Dl'
+ retval << ".nf\n" << '\\& '
+ dl = true
+ next
+ when 'Ux'
+ retval << "UNIX"
+ next
+ when 'Bro'
+ retval << '{'
+ @nospace = 1 if @nospace == 0
+ next
+ when 'Brc'
+ retval.sub!(/ *\z/, '}')
+ next
+ end
+
+ if @reference
+ case word
+ when '%A'
+ @refauthors.unshift(words.join(' '))
+ break
+ when '%T'
+ @reftitle = words.join(' ')
+ @reftitle.sub!(/^"/, '')
+ @reftitle.sub!(/"$/, '')
+ break
+ when '%N'
+ @refissue = words.join(' ')
+ break
+ when '%D'
+ @refdate = words.join(' ')
+ break
+ when '%O'
+ @refopt = words.join(' ')
+ break
+ end
+ end
+
+ case word
+ when 'Nm'
+ name = words.empty? ? @name : words.shift
+ @name ||= name
+ retval << ".br\n" if @synopsis
+ retval << "\\fB" << name << "\\fP"
+ @nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
+ next
+ when 'Nd'
+ retval << '\\-'
+ next
+ when 'Fl'
+ retval << '\\fB\\-' << words.shift << '\\fP'
+ @nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
+ next
+ when 'Ar'
+ retval << '\\fI'
+ if words.empty?
+ retval << 'file ...\\fP'
+ else
+ retval << words.shift << '\\fP'
+ while words[0] == '|'
+ retval << ' ' << words.shift << ' \\fI' << words.shift << '\\fP'
+ end
+ @nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
+ next
+ end
+ when 'Cm'
+ retval << '\\fB' << words.shift << '\\fP'
+ while RE_PUNCT =~ words[0]
+ retval << words.shift
+ end
+ next
+ when 'Op'
+ quote << OPTION
+ @nospace = 1 if @nospace == 0
+ retval << '['
+ # words.push(words.pop + ']')
+ next
+ when 'Aq'
+ quote << ANGLE
+ @nospace = 1 if @nospace == 0
+ retval << '<'
+ # words.push(words.pop + '>')
+ next
+ when 'Pp'
+ retval << "\n"
+ next
+ when 'Ss'
+ retval << '.SS'
+ next
+ end
+
+ case word
+ when 'Pa'
+ if !quote.include?(OPTION)
+ retval << '\\fI'
+ retval << '\\&' if /^\./ =~ words[0]
+ retval << words.shift << '\\fP'
+ while RE_PUNCT =~ words[0]
+ retval << words.shift
+ end
+ # @nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
+ next
+ end
+ when 'Lk'
+ if !quote.include?(OPTION)
+ url = words.shift
+ if name = shift_arg(words)
+ retval << '\\fI' << name << ':\\fP '
+ end
+ retval << '\\fB'
+ retval << '\\&' if /\A\./ =~ url
+ retval << url << '\\fP'
+ next
+ end
+ end
+
+ case word
+ when 'Dv'
+ retval << '.BR'
+ next
+ when 'Em', 'Ev'
+ retval << '.IR'
+ next
+ when 'Pq'
+ retval << '('
+ @nospace = 1
+ quote << PAREN
+ next
+ when 'Sx', 'Sy'
+ retval << '.B ' << words.join(' ')
+ break
+ when 'Ic'
+ retval << '\\fB'
+ until words.empty? || RE_PUNCT =~ words[0]
+ case words[0]
+ when 'Op'
+ words.shift
+ retval << '['
+ words.push(words.pop + ']')
+ next
+ when 'Aq'
+ words.shift
+ retval << '<'
+ words.push(words.pop + '>')
+ next
+ when 'Ar'
+ words.shift
+ retval << '\\fI' << words.shift << '\\fP'
+ else
+ retval << words.shift
+ end
+
+ retval << ' ' if @nospace == 0
+ end
+
+ retval.chomp!(' ')
+ retval << '\\fP'
+ retval << words.shift unless words.empty?
+ break
+ when 'Bl'
+ @oldoptlist = @optlist
+
+ case words[0]
+ when '-bullet'
+ @optlist = 1
+ when '-enum'
+ @optlist = 2
+ @enum = 0
+ when '-tag'
+ @optlist = 3
+ when '-item'
+ @optlist = 4
+ end
+
+ break
+ when 'El'
+ @optlist = @oldoptlist
+ next
+ end
+
+ if @optlist != 0 && word == 'It'
+ case @optlist
+ when 1
+ # bullets
+ retval << '.IP \\(bu'
+ when 2
+ # enum
+ @enum += 1
+ retval << '.IP ' << @enum << '.'
+ when 3
+ # tags
+ retval << ".TP\n"
+ case words[0]
+ when 'Pa', 'Ev', 'Lk'
+ words.shift
+ retval << '.B'
+ end
+ when 4
+ # item
+ retval << ".IP\n"
+ end
+
+ next
+ end
+
+ case word
+ when 'Sm'
+ case words[0]
+ when 'off'
+ @nospace = 2
+ when 'on'
+ # retval << "\n"
+ @nospace = 0
+ end
+ words.shift
+ next
+ end
+
+ retval << word
+ end
+
+ return nil if retval == '.'
+
+ retval.sub!(/\A\.([^a-zA-Z])/, "\\1")
+ # retval.chomp!(' ')
+
+ while q = quote.pop
+ case q
+ when OPTION
+ retval << ']'
+ when PAREN
+ retval << ')'
+ when ANGLE
+ retval << '>'
+ end
+ end
+
+ # retval << ' ' unless @nospace == 0 || retval.empty? || /\n\z/ =~ retval
+
+ retval << ' ' unless !@ext || @extopt || / $/ =~ retval
+
+ retval << "\n" unless @ext || @extopt || retval.empty? || /\n\z/ =~ retval
+
+ retval << ".fi\n" if dl
+
+ return retval
+ end
+
+ def self.mdoc2man(i, o)
+ new.mdoc2man(i, o)
+ end
+end
+
+if $0 == __FILE__
+ Mdoc2Man.mdoc2man(ARGF, STDOUT)
+end
diff --git a/tool/merger.rb b/tool/merger.rb
new file mode 100755
index 0000000000..a690b47da3
--- /dev/null
+++ b/tool/merger.rb
@@ -0,0 +1,314 @@
+#!/bin/sh
+# -*- ruby -*-
+exec "${RUBY-ruby}" "-x" "$0" "$@" && [ ] if false
+#!ruby
+# This needs ruby 2.0 and Git.
+# As a Ruby committer, run this in a git repository to commit a change.
+
+require 'tempfile'
+require 'net/http'
+require 'uri'
+require 'shellwords'
+
+ENV['LC_ALL'] = 'C'
+ORIGIN = 'git@git.ruby-lang.org:ruby.git'
+GITHUB = 'git@github.com:ruby/ruby.git'
+
+class << Merger = Object.new
+ def help
+ puts <<-HELP
+\e[1msimple backport\e[0m
+ ruby #$0 1234abc
+
+\e[1mrevision increment\e[0m
+ ruby #$0 revisionup
+
+\e[1mteeny increment\e[0m
+ ruby #$0 teenyup
+
+\e[1mtagging major release\e[0m
+ ruby #$0 tag 3.2.0
+
+\e[1mtagging patch release\e[0m (for 2.1.0 or later, it means X.Y.Z (Z > 0) release)
+ ruby #$0 tag
+
+\e[1mtagging preview/RC\e[0m
+ ruby #$0 tag 3.2.0-preview1
+
+\e[1mremove tag\e[0m
+ ruby #$0 removetag 3.2.9
+
+\e[33;1m* all operations shall be applied to the working directory.\e[0m
+ HELP
+ end
+
+ def interactive(str, editfile = nil)
+ loop do
+ yield if block_given?
+ STDERR.puts "\e[1;33m#{str} ([y]es|[a]bort|[r]etry#{'|[e]dit' if editfile})\e[0m"
+ case STDIN.gets
+ when /\Aa/i then exit 1
+ when /\Ar/i then redo
+ when /\Ay/i then break
+ when /\Ae/i then system(ENV['EDITOR'], editfile)
+ else exit 1
+ end
+ end
+ end
+
+ def version_up(teeny: false)
+ now = Time.now
+ now = now.localtime(9*60*60) # server is Japan Standard Time +09:00
+ system('git', 'checkout', 'HEAD', 'version.h')
+ v, pl = version
+
+ if teeny
+ v[2].succ!
+ end
+ if pl != '-1' # trunk does not have patchlevel
+ pl.succ!
+ end
+
+ str = open('version.h', 'rb', &:read)
+ ruby_release_date = str[/RUBY_RELEASE_YEAR_STR"-"RUBY_RELEASE_MONTH_STR"-"RUBY_RELEASE_DAY_STR/] || now.strftime('"%Y-%m-%d"')
+ [%W[RUBY_VERSION "#{v.join('.')}"],
+ %W[RUBY_VERSION_CODE #{v.join('')}],
+ %W[RUBY_VERSION_MAJOR #{v[0]}],
+ %W[RUBY_VERSION_MINOR #{v[1]}],
+ %W[RUBY_VERSION_TEENY #{v[2]}],
+ %W[RUBY_RELEASE_DATE #{ruby_release_date}],
+ %W[RUBY_RELEASE_CODE #{now.strftime('%Y%m%d')}],
+ %W[RUBY_PATCHLEVEL #{pl}],
+ %W[RUBY_RELEASE_YEAR #{now.year}],
+ %W[RUBY_RELEASE_MONTH #{now.month}],
+ %W[RUBY_RELEASE_DAY #{now.day}],
+ ].each do |(k, i)|
+ str.sub!(/^(#define\s+#{k}\s+).*$/, "\\1#{i}")
+ end
+ str.sub!(/\s+\z/m, '')
+ fn = sprintf('version.h.tmp.%032b', rand(1 << 31))
+ File.rename('version.h', fn)
+ open('version.h', 'wb') do |f|
+ f.puts(str)
+ end
+ File.unlink(fn)
+ end
+
+ def tag(relname)
+ # relname:
+ # * 2.2.0-preview1
+ # * 2.2.0-rc1
+ # * 2.2.0
+ v, pl = version
+ if relname
+ abort "patchlevel is not -1 but '#{pl}' for preview or rc" if pl != '-1' && /-(?:preview|rc)/ =~ relname
+ abort "patchlevel is not 0 but '#{pl}' for the first release" if pl != '0' && relname.end_with?(".0")
+ pl = relname[/-(.*)\z/, 1]
+ curver = "#{v.join('.')}#{("-#{pl}" if pl)}"
+ if relname != curver
+ abort "given relname '#{relname}' conflicts current version '#{curver}'"
+ end
+ else
+ if pl == '-1'
+ abort 'no relname is given and not in a release branch even if this is patch release'
+ end
+ end
+ tagname = "v#{v.join('_')}#{("_#{pl}" if v[0] < "2" || (v[0] == "2" && v[1] < "1") || /^(?:preview|rc)/ =~ pl)}"
+
+ unless execute('git', 'tag', tagname)
+ abort 'specfied tag already exists. check tag name and remove it if you want to force re-tagging'
+ end
+ execute('git', 'push', ORIGIN, tagname, interactive: true)
+ end
+
+ def remove_tag(relname)
+ # relname:
+ # * 2.2.0-preview1
+ # * 2.2.0-rc1
+ # * 2.2.0
+ # * v2_2_0_preview1
+ # * v2_2_0_rc1
+ # * v2_2_0
+ unless relname
+ raise ArgumentError, 'relname is not specified'
+ end
+ if /^v/ !~ relname
+ tagname = "v#{relname.gsub(/[.-]/, '_')}"
+ else
+ tagname = relname
+ end
+
+ execute('git', 'tag', '-d', tagname)
+ execute('git', 'push', ORIGIN, ":#{tagname}", interactive: true)
+ execute('git', 'push', GITHUB, ":#{tagname}", interactive: true)
+ end
+
+ def update_revision_h
+ execute('ruby tool/file2lastrev.rb --revision.h . > revision.tmp')
+ execute('tool/ifchange', '--timestamp=.revision.time', 'revision.h', 'revision.tmp')
+ execute('rm', '-f', 'revision.tmp')
+ end
+
+ def stat
+ `git status --short`
+ end
+
+ def diff(file = nil)
+ command = %w[git diff --color HEAD]
+ IO.popen(command + [file].compact, &:read)
+ end
+
+ def commit(file)
+ current_branch = IO.popen(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], &:read).strip
+ execute('git', 'add', '.') && execute('git', 'commit', '-F', file)
+ end
+
+ def has_conflicts?
+ changes = IO.popen(%w[git status --porcelain -z]) { |io| io.readlines("\0", chomp: true) }
+ # Discover unmerged files
+ # AU: unmerged, added by us
+ # DU: unmerged, deleted by us
+ # UU: unmerged, both modified
+ # AA: unmerged, both added
+ conflict = changes.grep(/\A(?:.U|AA) /) {$'}
+ !conflict.empty?
+ end
+
+ private
+
+ # Prints the version of Ruby found in version.h
+ def version
+ v = p = nil
+ open 'version.h', 'rb' do |f|
+ f.each_line do |l|
+ case l
+ when /^#define RUBY_VERSION "(\d+)\.(\d+)\.(\d+)"$/
+ v = $~.captures
+ when /^#define RUBY_VERSION_TEENY (\d+)$/
+ (v ||= [])[2] = $1
+ when /^#define RUBY_PATCHLEVEL (-?\d+)$/
+ p = $1
+ end
+ end
+ end
+ if v and !v[0]
+ open 'include/ruby/version.h', 'rb' do |f|
+ f.each_line do |l|
+ case l
+ when /^#define RUBY_API_VERSION_MAJOR (\d+)/
+ v[0] = $1
+ when /^#define RUBY_API_VERSION_MINOR (\d+)/
+ v[1] = $1
+ end
+ end
+ end
+ end
+ return v, p
+ end
+
+ def execute(*cmd, interactive: false)
+ if interactive
+ Merger.interactive("OK?: #{cmd.shelljoin}")
+ end
+ puts "+ #{cmd.shelljoin}"
+ system(*cmd)
+ end
+end
+
+case ARGV[0]
+when "teenyup"
+ Merger.version_up(teeny: true)
+ puts Merger.diff('version.h')
+when "up", /\A(ver|version|rev|revision|lv|level|patch\s*level)\s*up\z/
+ Merger.version_up
+ puts Merger.diff('version.h')
+when "tag"
+ Merger.tag(ARGV[1])
+when /\A(?:remove|rm|del)_?tag\z/
+ Merger.remove_tag(ARGV[1])
+when nil, "-h", "--help"
+ Merger.help
+ exit
+else
+ Merger.update_revision_h
+
+ case ARGV[0]
+ when /--ticket=(.*)/
+ tickets = $1.split(/,/)
+ ARGV.shift
+ else
+ tickets = []
+ detect_ticket = true
+ end
+
+ revstr = ARGV[0].gsub(%r!https://github\.com/ruby/ruby/commit/|https://bugs\.ruby-lang\.org/projects/ruby-master/repository/git/revisions/!, '')
+ revstr = revstr.delete('^, :\-0-9a-fA-F')
+ revs = revstr.split(/[,\s]+/)
+ commit_message = ''
+
+ revs.each do |rev|
+ git_rev = nil
+ case rev
+ when /\A\h{7,40}\z/
+ git_rev = rev
+ when nil then
+ puts "#$0 revision"
+ exit
+ else
+ puts "invalid revision part '#{rev}' in '#{ARGV[0]}'"
+ exit
+ end
+
+ # Merge revision from Git patch
+ git_uri = "https://git.ruby-lang.org/ruby.git/patch/?id=#{git_rev}"
+ resp = Net::HTTP.get_response(URI(git_uri))
+ if resp.code != '200'
+ abort "'#{git_uri}' returned status '#{resp.code}':\n#{resp.body}"
+ end
+ patch = resp.body.sub(/^diff --git a\/version\.h b\/version\.h\nindex .*\n--- a\/version\.h\n\+\+\+ b\/version\.h\n@@ .* @@\n(?:[-\+ ].*\n|\n)+/, '')
+
+ if detect_ticket
+ tickets += patch.scan(/\[(?:Bug|Feature|Misc) #(\d+)\]/i).map(&:first)
+ end
+
+ message = "#{(patch[/^Subject: (.*)\n---\n /m, 1] || "Message not found for revision: #{git_rev}\n")}"
+ message.gsub!(/\G(.*)\n( .*)/, "\\1\\2")
+ message = "\n\n#{message}"
+
+ puts '+ git apply'
+ IO.popen(['git', 'apply', '--3way'], 'wb') { |f| f.write(patch) }
+
+ commit_message << message.sub(/\A-+\nr.*/, '').sub(/\n-+\n\z/, '').gsub(/^./, "\t\\&")
+ end
+
+ if Merger.diff.empty?
+ Merger.interactive('Nothing is modified, right?')
+ end
+
+ Merger.version_up
+ f = Tempfile.new 'merger.rb'
+ f.printf "merge revision(s) %s:%s", revstr, tickets.map{|num| " [Backport ##{num}]"}.join
+ f.write commit_message
+ f.flush
+ f.close
+
+ if Merger.has_conflicts?
+ Merger.interactive('conflicts resolved?', f.path) do
+ IO.popen(ENV['PAGER'] || ['less', '-R'], 'w') do |g|
+ g << Merger.stat
+ g << "\n\n"
+ f.open
+ g << f.read
+ f.close
+ g << "\n\n"
+ g << Merger.diff
+ end
+ end
+ end
+
+ unless Merger.commit(f.path)
+ puts 'commit failed; try again.'
+ end
+
+ f.close(true)
+end
diff --git a/tool/mjit_archflag.sh b/tool/mjit_archflag.sh
new file mode 100644
index 0000000000..082fb4bcd0
--- /dev/null
+++ b/tool/mjit_archflag.sh
@@ -0,0 +1,40 @@
+# -*- sh -*-
+
+quote() {
+ printf "#${indent}define $1"
+ shift
+ ${1+printf} ${1+' "%s"'$sep} ${1+"$@"}
+ echo
+}
+
+archs=""
+arch_flag=""
+
+parse_arch_flags() {
+ for arch in $1; do
+ archs="${archs:+$archs }${arch%=*}"
+ done
+
+ while shift && [ "$#" -gt 0 ]; do
+ case "$1" in
+ -arch)
+ shift
+ archs="${archs:+$archs }$1"
+ ;;
+ *)
+ arch_flag="${arch_flag:+${arch_flag} }$1"
+ ;;
+ esac
+ done
+}
+
+define_arch_flags() {
+ ${archs:+echo} ${archs:+'#if 0'}
+ for arch in $archs; do
+ echo "#elif defined __${arch}__"
+ quote "MJIT_ARCHFLAG " -arch "${arch}"
+ done
+ ${archs:+echo} ${archs:+'#else'}
+ quote "MJIT_ARCHFLAG /* ${arch_flag:-no flag} */" ${arch_flag}
+ ${archs:+echo} ${archs:+'#endif'}
+}
diff --git a/tool/mjit_tabs.rb b/tool/mjit_tabs.rb
new file mode 100644
index 0000000000..edcbf6cfcb
--- /dev/null
+++ b/tool/mjit_tabs.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+# This is a script to run a command in ARGV, expanding tabs in some files
+# included by vm.c to normalize indentation of MJIT header. You can enable
+# this feature by passing `--without-mjit-tabs` in configure.
+#
+# Note that preprocessor of GCC converts a hard tab to one spaces, where
+# we expect it to be shown as 8 spaces. To obviate this script, we need
+# to convert all tabs to spaces in these files.
+
+require 'fileutils'
+
+EXPAND_TARGETS = %w[
+ vm*.*
+ include/ruby/ruby.h
+]
+
+# These files have no hard tab indentations. Skip normalizing these files from the glob result.
+SKIPPED_FILES = %w[
+ vm_callinfo.h
+ vm_debug.h
+ vm_exec.h
+ vm_opts.h
+ vm_sync.h
+ vm_sync.c
+]
+
+srcdir = File.expand_path('..', __dir__)
+targets = EXPAND_TARGETS.flat_map { |t| Dir.glob(File.join(srcdir, t)) } - SKIPPED_FILES.map { |f| File.join(srcdir, f) }
+sources = {}
+mtimes = {}
+
+mjit_tabs, *command = ARGV
+
+targets.each do |target|
+ next if mjit_tabs != 'false'
+ unless File.writable?(target)
+ puts "tool/mjit_tabs.rb: Skipping #{target.dump} as it's not writable."
+ next
+ end
+ source = File.read(target)
+ begin
+ expanded = source.gsub(/^\t+/) { |tab| ' ' * 8 * tab.length }
+ rescue ArgumentError # invalid byte sequence in UTF-8 (Travis, RubyCI)
+ puts "tool/mjit_tabs.rb: Skipping #{target.dump} as the encoding is #{source.encoding}."
+ next
+ end
+
+ sources[target] = source
+ mtimes[target] = File.mtime(target)
+
+ if sources[target] == expanded
+ puts "#{target.dump} has no hard tab indentation. This should be ignored in tool/mjit_tabs.rb."
+ end
+ File.write(target, expanded)
+ FileUtils.touch(target, mtime: mtimes[target])
+end
+
+result = system(*command)
+
+targets.each do |target|
+ if sources.key?(target)
+ File.write(target, sources[target])
+ FileUtils.touch(target, mtime: mtimes.fetch(target))
+ end
+end
+
+exit result
diff --git a/tool/mk_builtin_loader.rb b/tool/mk_builtin_loader.rb
new file mode 100644
index 0000000000..02941735f7
--- /dev/null
+++ b/tool/mk_builtin_loader.rb
@@ -0,0 +1,370 @@
+# Parse built-in script and make rbinc file
+
+require 'ripper'
+require 'stringio'
+require_relative 'ruby_vm/helpers/c_escape'
+
+def string_literal(lit, str = [])
+ while lit
+ case lit.first
+ when :string_concat, :string_embexpr, :string_content
+ _, *lit = lit
+ lit.each {|s| string_literal(s, str)}
+ return str
+ when :string_literal
+ _, lit = lit
+ when :@tstring_content
+ str << lit[1]
+ return str
+ else
+ raise "unexpected #{lit.first}"
+ end
+ end
+end
+
+def inline_text argc, arg1
+ raise "argc (#{argc}) of inline! should be 1" unless argc == 1
+ arg1 = string_literal(arg1)
+ raise "1st argument should be string literal" unless arg1
+ arg1.join("").rstrip
+end
+
+def make_cfunc_name inlines, name, lineno
+ case name
+ when /\[\]/
+ name = '_GETTER'
+ when /\[\]=/
+ name = '_SETTER'
+ else
+ name = name.tr('!?', 'EP')
+ end
+
+ base = "builtin_inline_#{name}_#{lineno}"
+ if inlines[base]
+ 1000.times{|i|
+ name = "#{base}_#{i}"
+ return name unless inlines[name]
+ }
+ raise "too many functions in same line..."
+ else
+ base
+ end
+end
+
+def collect_locals tree
+ _type, name, (line, _cols) = tree
+ if locals = LOCALS_DB[[name, line]]
+ locals
+ else
+ if false # for debugging
+ pp LOCALS_DB
+ raise "not found: [#{name}, #{line}]"
+ end
+ end
+end
+
+def collect_builtin base, tree, name, bs, inlines, locals = nil
+ while tree
+ recv = sep = mid = args = nil
+ case tree.first
+ when :def
+ locals = collect_locals(tree[1])
+ tree = tree[3]
+ next
+ when :defs
+ locals = collect_locals(tree[3])
+ tree = tree[5]
+ next
+ when :class
+ name = 'class'
+ tree = tree[3]
+ next
+ when :sclass, :module
+ name = 'class'
+ tree = tree[2]
+ next
+ when :method_add_arg
+ _, mid, (_, (_, args)) = tree
+ case mid.first
+ when :call
+ _, recv, sep, mid = mid
+ when :fcall
+ _, mid = mid
+ else
+ mid = nil
+ end
+ when :vcall
+ _, mid = tree
+ when :command # FCALL
+ _, mid, (_, args) = tree
+ when :call, :command_call # CALL
+ _, recv, sep, mid, (_, args) = tree
+ end
+
+ if mid
+ raise "unknown sexp: #{mid.inspect}" unless %i[@ident @const].include?(mid.first)
+ _, mid, (lineno,) = mid
+ if recv
+ func_name = nil
+ case recv.first
+ when :var_ref
+ _, recv = recv
+ if recv.first == :@const and recv[1] == "Primitive"
+ func_name = mid.to_s
+ end
+ when :vcall
+ _, recv = recv
+ if recv.first == :@ident and recv[1] == "__builtin"
+ func_name = mid.to_s
+ end
+ end
+ collect_builtin(base, recv, name, bs, inlines) unless func_name
+ else
+ func_name = mid[/\A__builtin_(.+)/, 1]
+ end
+ if func_name
+ cfunc_name = func_name
+ args.pop unless (args ||= []).last
+ argc = args.size
+
+ if /(.+)[\!\?]\z/ =~ func_name
+ case $1
+ when 'attr'
+ text = inline_text(argc, args.first)
+ if text != 'inline'
+ raise "Only 'inline' is allowed to be annotated (but got: '#{text}')"
+ end
+ break
+ when 'cstmt'
+ text = inline_text argc, args.first
+
+ func_name = "_bi#{inlines.size}"
+ cfunc_name = make_cfunc_name(inlines, name, lineno)
+ inlines[cfunc_name] = [lineno, text, locals, func_name]
+ argc -= 1
+ when 'cexpr', 'cconst'
+ text = inline_text argc, args.first
+ code = "return #{text};"
+
+ func_name = "_bi#{inlines.size}"
+ cfunc_name = make_cfunc_name(inlines, name, lineno)
+
+ locals = [] if $1 == 'cconst'
+ inlines[cfunc_name] = [lineno, code, locals, func_name]
+ argc -= 1
+ when 'cinit'
+ text = inline_text argc, args.first
+ func_name = nil # required
+ inlines[inlines.size] = [lineno, text, nil, nil]
+ argc -= 1
+ when 'mandatory_only'
+ func_name = nil
+ when 'arg'
+ argc == 1 or raise "unexpected argument number #{argc}"
+ (arg = args.first)[0] == :symbol_literal or raise "symbol literal expected #{args}"
+ (arg = arg[1])[0] == :symbol or raise "symbol expected #{arg}"
+ (var = arg[1] and var = var[1]) or raise "argument name expected #{arg}"
+ func_name = nil
+ end
+ end
+
+ if bs[func_name] &&
+ bs[func_name] != [argc, cfunc_name]
+ raise "same builtin function \"#{func_name}\", but different arity (was #{bs[func_name]} but #{argc})"
+ end
+
+ bs[func_name] = [argc, cfunc_name] if func_name
+ end
+ break unless tree = args
+ end
+
+ tree.each do |t|
+ collect_builtin base, t, name, bs, inlines, locals if Array === t
+ end
+ break
+ end
+end
+
+# ruby mk_builtin_loader.rb TARGET_FILE.rb
+# #=> generate TARGET_FILE.rbinc
+#
+
+LOCALS_DB = {} # [method_name, first_line] = locals
+
+def collect_iseq iseq_ary
+ # iseq_ary.each_with_index{|e, i| p [i, e]}
+ label = iseq_ary[5]
+ first_line = iseq_ary[8]
+ type = iseq_ary[9]
+ locals = iseq_ary[10]
+ insns = iseq_ary[13]
+
+ if type == :method
+ LOCALS_DB[[label, first_line].freeze] = locals
+ end
+
+ insns.each{|insn|
+ case insn
+ when Integer
+ # ignore
+ when Array
+ # p insn.shift # insn name
+ insn.each{|op|
+ if Array === op && op[0] == "YARVInstructionSequence/SimpleDataFormat"
+ collect_iseq op
+ end
+ }
+ end
+ }
+end
+
+def generate_cexpr(ofile, lineno, line_file, body_lineno, text, locals, func_name)
+ f = StringIO.new
+ f.puts '{'
+ lineno += 1
+ locals.reverse_each.with_index{|param, i|
+ next unless Symbol === param
+ f.puts "MAYBE_UNUSED(const VALUE) #{param} = rb_vm_lvar(ec, #{-3 - i});"
+ lineno += 1
+ }
+ f.puts "#line #{body_lineno} \"#{line_file}\""
+ lineno += 1
+
+ f.puts text
+ lineno += text.count("\n") + 1
+
+ f.puts "#line #{lineno + 2} \"#{ofile}\"" # TODO: restore line number.
+ f.puts "}"
+ f.puts
+ lineno += 3
+
+ return lineno, f.string
+end
+
+def mk_builtin_header file
+ base = File.basename(file, '.rb')
+ ofile = "#{file}inc"
+
+ # bs = { func_name => argc }
+ code = File.read(file)
+ collect_iseq RubyVM::InstructionSequence.compile(code).to_a
+ collect_builtin(base, Ripper.sexp(code), 'top', bs = {}, inlines = {})
+
+ begin
+ f = open(ofile, 'w')
+ rescue Errno::EACCES
+ # Fall back to the current directory
+ f = open(File.basename(ofile), 'w')
+ end
+ begin
+ if File::ALT_SEPARATOR
+ file = file.tr(File::ALT_SEPARATOR, File::SEPARATOR)
+ ofile = ofile.tr(File::ALT_SEPARATOR, File::SEPARATOR)
+ end
+ lineno = __LINE__
+ f.puts "// -*- c -*-"
+ f.puts "// DO NOT MODIFY THIS FILE DIRECTLY."
+ f.puts "// auto-generated file"
+ f.puts "// by #{__FILE__}"
+ f.puts "// with #{file}"
+ f.puts '#include "internal/compilers.h" /* for MAYBE_UNUSED */'
+ f.puts '#include "internal/warnings.h" /* for COMPILER_WARNING_PUSH */'
+ f.puts '#include "ruby/ruby.h" /* for VALUE */'
+ f.puts '#include "builtin.h" /* for RB_BUILTIN_FUNCTION */'
+ f.puts 'struct rb_execution_context_struct; /* in vm_core.h */'
+ f.puts
+ lineno = __LINE__ - lineno - 1
+ line_file = file
+
+ inlines.each{|cfunc_name, (body_lineno, text, locals, func_name)|
+ if String === cfunc_name
+ f.puts "static VALUE #{cfunc_name}(struct rb_execution_context_struct *ec, const VALUE self)"
+ lineno += 1
+ lineno, str = generate_cexpr(ofile, lineno, line_file, body_lineno, text, locals, func_name)
+ f.write str
+ else
+ # cinit!
+ f.puts "#line #{body_lineno} \"#{line_file}\""
+ lineno += 1
+ f.puts text
+ lineno += text.count("\n") + 1
+ f.puts "#line #{lineno + 2} \"#{ofile}\"" # TODO: restore line number.
+ lineno += 1
+ end
+ }
+
+ bs.each_pair{|func, (argc, cfunc_name)|
+ decl = ', VALUE' * argc
+ argv = argc \
+ . times \
+ . map {|i|", argv[#{i}]"} \
+ . join('')
+ f.puts %'static void'
+ f.puts %'mjit_compile_invokebuiltin_for_#{func}(FILE *f, long index, unsigned stack_size, bool inlinable_p)'
+ f.puts %'{'
+ f.puts %' fprintf(f, " VALUE self = GET_SELF();\\n");'
+ f.puts %' fprintf(f, " typedef VALUE (*func)(rb_execution_context_t *, VALUE#{decl});\\n");'
+ if inlines.has_key? cfunc_name
+ body_lineno, text, locals, func_name = inlines[cfunc_name]
+ lineno, str = generate_cexpr(ofile, lineno, line_file, body_lineno, text, locals, func_name)
+ f.puts %' if (inlinable_p) {'
+ str.gsub(/^(?!#)/, ' ').each_line {|i|
+ j = RubyVM::CEscape.rstring2cstr(i).dup
+ j.sub!(/^ return\b/ , ' val =')
+ f.printf(%' fprintf(f, "%%s", %s);\n', j)
+ }
+ f.puts(%' return;')
+ f.puts(%' }')
+ end
+ if argc > 0
+ f.puts %' if (index == -1) {'
+ f.puts %' fprintf(f, " const VALUE *argv = &stack[%d];\\n", stack_size - #{argc});'
+ f.puts %' }'
+ f.puts %' else {'
+ f.puts %' fprintf(f, " const unsigned int lnum = GET_ISEQ()->body->local_table_size;\\n");'
+ f.puts %' fprintf(f, " const VALUE *argv = GET_EP() - lnum - VM_ENV_DATA_SIZE + 1 + %ld;\\n", index);'
+ f.puts %' }'
+ end
+ f.puts %' fprintf(f, " func f = (func)%"PRIuVALUE"; /* == #{cfunc_name} */\\n", (VALUE)#{cfunc_name});'
+ f.puts %' fprintf(f, " val = f(ec, self#{argv});\\n");'
+ f.puts %'}'
+ f.puts
+ }
+
+ f.puts "void Init_builtin_#{base}(void)"
+ f.puts "{"
+
+ table = "#{base}_table"
+ f.puts " // table definition"
+ f.puts " static const struct rb_builtin_function #{table}[] = {"
+ bs.each.with_index{|(func, (argc, cfunc_name)), i|
+ f.puts " RB_BUILTIN_FUNCTION(#{i}, #{func}, #{cfunc_name}, #{argc}, mjit_compile_invokebuiltin_for_#{func}),"
+ }
+ f.puts " RB_BUILTIN_FUNCTION(-1, NULL, NULL, 0, 0),"
+ f.puts " };"
+
+ f.puts
+ f.puts " // arity_check"
+ f.puts "COMPILER_WARNING_PUSH"
+ f.puts "#if GCC_VERSION_SINCE(5, 1, 0) || defined __clang__"
+ f.puts "COMPILER_WARNING_ERROR(-Wincompatible-pointer-types)"
+ f.puts "#endif"
+ bs.each{|func, (argc, cfunc_name)|
+ f.puts " if (0) rb_builtin_function_check_arity#{argc}(#{cfunc_name});"
+ }
+ f.puts "COMPILER_WARNING_POP"
+
+ f.puts
+ f.puts " // load"
+ f.puts " rb_load_with_builtin_functions(#{base.dump}, #{table});"
+
+ f.puts "}"
+ ensure
+ f.close
+ end
+end
+
+ARGV.each{|file|
+ # feature.rb => load_feature.inc
+ mk_builtin_header file
+}
diff --git a/tool/mkconfig.rb b/tool/mkconfig.rb
new file mode 100755
index 0000000000..41bee02247
--- /dev/null
+++ b/tool/mkconfig.rb
@@ -0,0 +1,392 @@
+#!./miniruby -s
+
+# This script, which is run when ruby is built, generates rbconfig.rb by
+# parsing information from config.status. rbconfig.rb contains build
+# information for ruby (compiler flags, paths, etc.) and is used e.g. by
+# mkmf to build compatible native extensions.
+
+# avoid warnings with -d.
+$install_name ||= nil
+$so_name ||= nil
+$unicode_version ||= nil
+$unicode_emoji_version ||= nil
+arch = $arch or raise "missing -arch"
+version = $version or raise "missing -version"
+
+srcdir = File.expand_path('../..', __FILE__)
+$:.unshift(".")
+
+mkconfig = File.basename($0)
+
+fast = {'prefix'=>true, 'ruby_install_name'=>true, 'INSTALL'=>true, 'EXEEXT'=>true}
+
+win32 = /mswin/ =~ arch
+universal = /universal.*darwin/ =~ arch
+v_fast = []
+v_others = []
+vars = {}
+continued_name = nil
+continued_line = nil
+install_name = nil
+so_name = nil
+platform = nil
+File.foreach "config.status" do |line|
+ next if /^#/ =~ line
+ name = nil
+ case line
+ when /^s([%,])@(\w+)@\1(?:\|\#_!!_\#\|)?(.*)\1/
+ name = $2
+ val = $3.gsub(/\\(?=,)/, '')
+ when /^S\["(\w+)"\]\s*=\s*"(.*)"\s*(\\)?$/
+ name = $1
+ val = $2
+ if $3
+ continued_line = [val]
+ continued_name = name
+ next
+ end
+ when /^"(.*)"\s*(\\)?$/
+ next if !continued_line
+ continued_line << $1
+ next if $2
+ continued_line.each {|s| s.sub!(/\\n\z/, "\n")}
+ val = continued_line.join
+ name = continued_name
+ continued_line = nil
+ when /^(?:ac_given_)?INSTALL=(.*)/
+ v_fast << " CONFIG[\"INSTALL\"] = " + $1 + "\n"
+ end
+
+ if name
+ case name
+ when /^(?:ac_.*|configure_input|(?:top_)?srcdir|\w+OBJS)$/; next
+ when /^(?:X|(?:MINI|RUN|(?:HAVE_)?BASE|BOOTSTRAP|BTEST)RUBY(?:_COMMAND)?$)/; next
+ when /^INSTALLDOC|TARGET$/; next
+ when /^DTRACE/; next
+ when /^MJIT_(CC|SUPPORT)$/; # pass
+ when /^MJIT_/; next
+ when /^(?:MAJOR|MINOR|TEENY)$/; vars[name] = val; next
+ when /^LIBRUBY_D?LD/; next
+ when /^RUBY_INSTALL_NAME$/; next vars[name] = (install_name = val).dup if $install_name
+ when /^RUBY_SO_NAME$/; next vars[name] = (so_name = val).dup if $so_name
+ when /^arch$/; if val.empty? then val = arch else arch = val end
+ when /^sitearch$/; val = '$(arch)' if val.empty?
+ when /^DESTDIR$/; next
+ when /RUBYGEMS/; next
+ end
+ case val
+ when /^\$\(ac_\w+\)$/; next
+ when /^\$\{ac_\w+\}$/; next
+ when /^\$ac_\w+$/; next
+ end
+ if /^program_transform_name$/ =~ name
+ val.sub!(/\As(\\?\W)(?:\^|\${1,2})\1\1(;|\z)/, '')
+ if val.empty?
+ $install_name ||= "ruby"
+ next
+ end
+ unless $install_name
+ $install_name = "ruby"
+ val.gsub!(/\$\$/, '$')
+ val.scan(%r[\G[\s;]*(/(?:\\.|[^/])*+/)?([sy])(\\?\W)((?:(?!\3)(?:\\.|.))*+)\3((?:(?!\3)(?:\\.|.))*+)\3([gi]*)]) do
+ |addr, cmd, sep, pat, rep, opt|
+ if addr
+ Regexp.new(addr[/\A\/(.*)\/\z/, 1]) =~ $install_name or next
+ end
+ case cmd
+ when 's'
+ pat = Regexp.new(pat, opt.include?('i'))
+ if opt.include?('g')
+ $install_name.gsub!(pat, rep)
+ else
+ $install_name.sub!(pat, rep)
+ end
+ when 'y'
+ $install_name.tr!(Regexp.quote(pat), rep)
+ end
+ end
+ end
+ end
+ eq = win32 && vars[name] ? '<< "\n"' : '='
+ vars[name] = val
+ if name == "configure_args"
+ val.gsub!(/--with-out-ext/, "--without-ext")
+ end
+ val = val.gsub(/\$(?:\$|\{?(\w+)\}?)/) {$1 ? "$(#{$1})" : $&}.dump
+ case name
+ when /^prefix$/
+ val = "(TOPDIR || DESTDIR + #{val})"
+ when /^ARCH_FLAG$/
+ val = "arch_flag || #{val}" if universal
+ when /^UNIVERSAL_ARCHNAMES$/
+ universal, val = val, 'universal' if universal
+ when /^arch$/
+ if universal
+ platform = val.sub(/universal/, %q[#{arch && universal[/(?:\A|\s)#{Regexp.quote(arch)}=(\S+)/, 1] || RUBY_PLATFORM[/\A[^-]*/]}])
+ end
+ when /^includedir$/
+ val = '"$(SDKROOT)"'+val if /darwin/ =~ arch
+ end
+ v = " CONFIG[\"#{name}\"] #{eq} #{val}\n"
+ if fast[name]
+ v_fast << v
+ else
+ v_others << v
+ end
+ #case name
+ #when "RUBY_PROGRAM_VERSION"
+ # version = val[/\A"(.*)"\z/, 1]
+ #end
+ end
+# break if /^CEOF/
+end
+
+drive = File::PATH_SEPARATOR == ';'
+
+def vars.expand(val, config = self)
+ newval = val.gsub(/\$\$|\$\(([^()]+)\)|\$\{([^{}]+)\}/) {
+ var = $&
+ if !(v = $1 || $2)
+ '$'
+ elsif key = config[v = v[/\A[^:]+(?=(?::(.*?)=(.*))?\z)/]]
+ pat, sub = $1, $2
+ config[v] = false
+ config[v] = expand(key, config)
+ key = key.gsub(/#{Regexp.quote(pat)}(?=\s|\z)/n) {sub} if pat
+ key
+ else
+ var
+ end
+ }
+ val.replace(newval) unless newval == val
+ val
+end
+prefix = vars.expand(vars["prefix"] ||= "")
+rubyarchdir = vars.expand(vars["rubyarchdir"] ||= "")
+relative_archdir = rubyarchdir.rindex(prefix, 0) ? rubyarchdir[prefix.size..-1] : rubyarchdir
+
+puts %[\
+# encoding: ascii-8bit
+# frozen-string-literal: false
+#
+# The module storing Ruby interpreter configurations on building.
+#
+# This file was created by #{mkconfig} when ruby was built. It contains
+# build information for ruby which is used e.g. by mkmf to build
+# compatible native extensions. Any changes made to this file will be
+# lost the next time ruby is built.
+
+module RbConfig
+ RUBY_VERSION.start_with?("#{version[/^[0-9]+\.[0-9]+\./] || version}") or
+ raise "ruby lib version (#{version}) doesn't match executable version (\#{RUBY_VERSION})"
+
+]
+print " # Ruby installed directory.\n"
+print " TOPDIR = File.dirname(__FILE__).chomp!(#{relative_archdir.dump})\n"
+print " # DESTDIR on make install.\n"
+print " DESTDIR = ", (drive ? "TOPDIR && TOPDIR[/\\A[a-z]:/i] || " : ""), "'' unless defined? DESTDIR\n"
+print <<'ARCH' if universal
+ arch_flag = ENV['ARCHFLAGS'] || ((e = ENV['RC_ARCHS']) && e.split.uniq.map {|a| "-arch #{a}"}.join(' '))
+ arch = arch_flag && arch_flag[/\A\s*-arch\s+(\S+)\s*\z/, 1]
+ARCH
+print " universal = #{universal}\n" if universal
+print " # The hash configurations stored.\n"
+print " CONFIG = {}\n"
+print " CONFIG[\"DESTDIR\"] = DESTDIR\n"
+
+versions = {}
+IO.foreach(File.join(srcdir, "version.h")) do |l|
+ m = /^\s*#\s*define\s+RUBY_(PATCHLEVEL)\s+(-?\d+)/.match(l)
+ if m
+ versions[m[1]] = m[2]
+ break if versions.size == 4
+ next
+ end
+ m = /^\s*#\s*define\s+RUBY_VERSION_(\w+)\s+(-?\d+)/.match(l)
+ if m
+ versions[m[1]] = m[2]
+ break if versions.size == 4
+ next
+ end
+ m = /^\s*#\s*define\s+RUBY_VERSION\s+\W?([.\d]+)/.match(l)
+ if m
+ versions['MAJOR'], versions['MINOR'], versions['TEENY'] = m[1].split('.')
+ break if versions.size == 4
+ next
+ end
+end
+if versions.size != 4
+ IO.foreach(File.join(srcdir, "include/ruby/version.h")) do |l|
+ m = /^\s*#\s*define\s+RUBY_API_VERSION_(\w+)\s+(-?\d+)/.match(l)
+ if m
+ versions[m[1]] ||= m[2]
+ break if versions.size == 4
+ next
+ end
+ end
+end
+%w[MAJOR MINOR TEENY PATCHLEVEL].each do |v|
+ print " CONFIG[#{v.dump}] = #{(versions[v]||vars[v]).dump}\n"
+end
+
+dest = drive ? %r'= "(?!\$[\(\{])(?i:[a-z]:)' : %r'= "(?!\$[\(\{])'
+v_disabled = {}
+v_others.collect! do |x|
+ if /^\s*CONFIG\["((?!abs_|old)[a-z]+(?:_prefix|dir))"\]/ === x
+ name = $1
+ if /= "no"$/ =~ x
+ v_disabled[name] = true
+ v_others.delete(name)
+ next
+ end
+ x.sub(dest, '= "$(DESTDIR)')
+ else
+ x
+ end
+end
+v_others.compact!
+
+if $install_name
+ if install_name and vars.expand("$(RUBY_INSTALL_NAME)") == $install_name
+ $install_name = install_name
+ end
+ v_fast << " CONFIG[\"ruby_install_name\"] = \"" + $install_name + "\"\n"
+ v_fast << " CONFIG[\"RUBY_INSTALL_NAME\"] = \"" + $install_name + "\"\n"
+end
+if $so_name
+ if so_name and vars.expand("$(RUBY_SO_NAME)") == $so_name
+ $so_name = so_name
+ end
+ v_fast << " CONFIG[\"RUBY_SO_NAME\"] = \"" + $so_name + "\"\n"
+end
+
+print(*v_fast)
+print(*v_others)
+print <<EOS if $unicode_version
+ CONFIG["UNICODE_VERSION"] = #{$unicode_version.dump}
+EOS
+print <<EOS if $unicode_emoji_version
+ CONFIG["UNICODE_EMOJI_VERSION"] = #{$unicode_emoji_version.dump}
+EOS
+print prefix.start_with?("/System/") ? <<EOS : <<EOS if /darwin/ =~ arch
+ if sdkroot = ENV["SDKROOT"]
+ sdkroot = sdkroot.dup
+ elsif File.exist?(File.join(CONFIG["prefix"], "include")) ||
+ !(sdkroot = (IO.popen(%w[/usr/bin/xcrun --sdk macosx --show-sdk-path], in: IO::NULL, err: IO::NULL, &:read) rescue nil))
+ sdkroot = +""
+ else
+ sdkroot.chomp!
+ end
+ CONFIG["SDKROOT"] = sdkroot
+EOS
+ CONFIG["SDKROOT"] = ""
+EOS
+print <<EOS
+ CONFIG["platform"] = #{platform || '"$(arch)"'}
+ CONFIG["archdir"] = "$(rubyarchdir)"
+ CONFIG["topdir"] = File.dirname(__FILE__)
+ # Almost same with CONFIG. MAKEFILE_CONFIG has other variable
+ # reference like below.
+ #
+ # MAKEFILE_CONFIG["bindir"] = "$(exec_prefix)/bin"
+ #
+ # The values of this constant is used for creating Makefile.
+ #
+ # require 'rbconfig'
+ #
+ # print <<-END_OF_MAKEFILE
+ # prefix = \#{RbConfig::MAKEFILE_CONFIG['prefix']}
+ # exec_prefix = \#{RbConfig::MAKEFILE_CONFIG['exec_prefix']}
+ # bindir = \#{RbConfig::MAKEFILE_CONFIG['bindir']}
+ # END_OF_MAKEFILE
+ #
+ # => prefix = /usr/local
+ # exec_prefix = $(prefix)
+ # bindir = $(exec_prefix)/bin MAKEFILE_CONFIG = {}
+ #
+ # RbConfig.expand is used for resolving references like above in rbconfig.
+ #
+ # require 'rbconfig'
+ # p RbConfig.expand(RbConfig::MAKEFILE_CONFIG["bindir"])
+ # # => "/usr/local/bin"
+ MAKEFILE_CONFIG = {}
+ CONFIG.each{|k,v| MAKEFILE_CONFIG[k] = v.dup}
+
+ # call-seq:
+ #
+ # RbConfig.expand(val) -> string
+ # RbConfig.expand(val, config) -> string
+ #
+ # expands variable with given +val+ value.
+ #
+ # RbConfig.expand("$(bindir)") # => /home/foobar/all-ruby/ruby19x/bin
+ def RbConfig::expand(val, config = CONFIG)
+ newval = val.gsub(/\\$\\$|\\$\\(([^()]+)\\)|\\$\\{([^{}]+)\\}/) {
+ var = $&
+ if !(v = $1 || $2)
+ '$'
+ elsif key = config[v = v[/\\A[^:]+(?=(?::(.*?)=(.*))?\\z)/]]
+ pat, sub = $1, $2
+ config[v] = false
+ config[v] = RbConfig::expand(key, config)
+ key = key.gsub(/\#{Regexp.quote(pat)}(?=\\s|\\z)/n) {sub} if pat
+ key
+ else
+ var
+ end
+ }
+ val.replace(newval) unless newval == val
+ val
+ end
+ CONFIG.each_value do |val|
+ RbConfig::expand(val)
+ end
+
+ # :nodoc:
+ # call-seq:
+ #
+ # RbConfig.fire_update!(key, val) -> array
+ # RbConfig.fire_update!(key, val, mkconf, conf) -> array
+ #
+ # updates +key+ in +mkconf+ with +val+, and all values depending on
+ # the +key+ in +mkconf+.
+ #
+ # RbConfig::MAKEFILE_CONFIG.values_at("CC", "LDSHARED") # => ["gcc", "$(CC) -shared"]
+ # RbConfig::CONFIG.values_at("CC", "LDSHARED") # => ["gcc", "gcc -shared"]
+ # RbConfig.fire_update!("CC", "gcc-8") # => ["CC", "LDSHARED"]
+ # RbConfig::MAKEFILE_CONFIG.values_at("CC", "LDSHARED") # => ["gcc-8", "$(CC) -shared"]
+ # RbConfig::CONFIG.values_at("CC", "LDSHARED") # => ["gcc-8", "gcc-8 -shared"]
+ #
+ # returns updated keys list, or +nil+ if nothing changed.
+ def RbConfig.fire_update!(key, val, mkconf = MAKEFILE_CONFIG, conf = CONFIG)
+ return if mkconf[key] == val
+ mkconf[key] = val
+ keys = [key]
+ deps = []
+ begin
+ re = Regexp.new("\\\\$\\\\((?:%1$s)\\\\)|\\\\$\\\\{(?:%1$s)\\\\}" % keys.join('|'))
+ deps |= keys
+ keys.clear
+ mkconf.each {|k,v| keys << k if re =~ v}
+ end until keys.empty?
+ deps.each {|k| conf[k] = mkconf[k].dup}
+ deps.each {|k| expand(conf[k])}
+ deps
+ end
+
+ # call-seq:
+ #
+ # RbConfig.ruby -> path
+ #
+ # returns the absolute pathname of the ruby command.
+ def RbConfig.ruby
+ File.join(
+ RbConfig::CONFIG["bindir"],
+ RbConfig::CONFIG["ruby_install_name"] + RbConfig::CONFIG["EXEEXT"]
+ )
+ end
+end
+CROSS_COMPILING = nil unless defined? CROSS_COMPILING
+EOS
+
+# vi:set sw=2:
diff --git a/tool/mkrunnable.rb b/tool/mkrunnable.rb
new file mode 100755
index 0000000000..3b71b0751b
--- /dev/null
+++ b/tool/mkrunnable.rb
@@ -0,0 +1,149 @@
+#!./miniruby
+# -*- coding: us-ascii -*-
+
+# Used by "make runnable" target, to make symbolic links from a build
+# directory.
+
+require './rbconfig'
+require 'fileutils'
+
+case ARGV[0]
+when "-n"
+ ARGV.shift
+ include FileUtils::DryRun
+when "-v"
+ ARGV.shift
+ include FileUtils::Verbose
+else
+ include FileUtils
+end
+
+module Mswin
+ def ln_safe(src, dest, *opt)
+ cmd = ["mklink", dest.tr("/", "\\"), src.tr("/", "\\")]
+ cmd[1, 0] = opt
+ return if system("cmd", "/c", *cmd)
+ # TODO: use RUNAS or something
+ puts cmd.join(" ")
+ end
+
+ def ln_dir_safe(src, dest)
+ ln_safe(src, dest, "/d")
+ end
+end
+
+def clean_link(src, dest)
+ begin
+ link = File.readlink(dest)
+ rescue
+ else
+ return if link == src
+ File.unlink(dest)
+ end
+ yield src, dest
+end
+
+def ln_safe(src, dest)
+ ln_sf(src, dest)
+rescue Errno::ENOENT
+ # Windows disallows to create broken symboic links, probably because
+ # it is a kind of reparse points.
+ raise if File.exist?(src)
+end
+
+alias ln_dir_safe ln_safe
+
+case RUBY_PLATFORM
+when /linux|darwin|solaris/
+ def ln_exe(src, dest)
+ ln(src, dest, force: true)
+ end
+else
+ alias ln_exe ln_safe
+end
+
+if !File.respond_to?(:symlink) && /mingw|mswin/ =~ (CROSS_COMPILING || RUBY_PLATFORM)
+ extend Mswin
+end
+
+def clean_path(path)
+ path = "#{path}/".gsub(/(\A|\/)(?:\.\/)+/, '\1').tr_s('/', '/')
+ nil while path.sub!(/[^\/]+\/\.\.\//, '')
+ path
+end
+
+def relative_path_from(path, base)
+ path = clean_path(path)
+ base = clean_path(base)
+ path, base = [path, base].map{|s|s.split("/")}
+ until path.empty? or base.empty? or path[0] != base[0]
+ path.shift
+ base.shift
+ end
+ path, base = [path, base].map{|s|s.join("/")}
+ if /(\A|\/)\.\.\// =~ base
+ File.expand_path(path)
+ else
+ base.gsub!(/[^\/]+/, '..')
+ File.join(base, path)
+ end
+end
+
+def ln_relative(src, dest, executable = false)
+ return if File.identical?(src, dest)
+ parent = File.dirname(dest)
+ File.directory?(parent) or mkdir_p(parent)
+ return ln_exe(src, dest) if executable
+ clean_link(relative_path_from(src, parent), dest) {|s, d| ln_safe(s, d)}
+end
+
+def ln_dir_relative(src, dest)
+ return if File.identical?(src, dest)
+ parent = File.dirname(dest)
+ File.directory?(parent) or mkdir_p(parent)
+ clean_link(relative_path_from(src, parent), dest) {|s, d| ln_dir_safe(s, d)}
+end
+
+config = RbConfig::MAKEFILE_CONFIG.merge("prefix" => ".", "exec_prefix" => ".")
+config.each_value {|s| RbConfig.expand(s, config)}
+srcdir = config["srcdir"] ||= File.dirname(__FILE__)
+top_srcdir = config["top_srcdir"] ||= File.dirname(srcdir)
+extout = ARGV[0] || config["EXTOUT"]
+arch = config["arch"]
+bindir = config["bindir"]
+libdirname = config["libdirname"]
+libdir = config[libdirname || "libdir"]
+vendordir = config["vendordir"]
+rubylibdir = config["rubylibdir"]
+rubyarchdir = config["rubyarchdir"]
+archdir = "#{extout}/#{arch}"
+[bindir, libdir, archdir].uniq.each do |dir|
+ File.directory?(dir) or mkdir_p(dir)
+end
+
+exeext = config["EXEEXT"]
+ruby_install_name = config["ruby_install_name"]
+rubyw_install_name = config["rubyw_install_name"]
+goruby_install_name = "go" + ruby_install_name
+[ruby_install_name, rubyw_install_name, goruby_install_name].map do |ruby|
+ if ruby and !ruby.empty?
+ ruby += exeext
+ ln_relative(ruby, "#{bindir}/#{ruby}", true)
+ end
+end
+so = config["LIBRUBY_SO"]
+libruby = [config["LIBRUBY_A"]]
+if /\.dll\z/i =~ so
+ ln_relative(so, "#{bindir}/#{so}")
+else
+ libruby << so
+end
+libruby.concat(config["LIBRUBY_ALIASES"].split)
+libruby.each {|lib|ln_relative(lib, "#{libdir}/#{lib}")}
+ln_dir_relative("#{extout}/common", rubylibdir)
+rubyarchdir.sub!(rubylibdir, "#{extout}/common")
+vendordir.sub!(rubylibdir, "#{extout}/common")
+ln_dir_relative(archdir, rubyarchdir)
+vendordir.sub!(rubyarchdir, archdir)
+ln_dir_relative("#{top_srcdir}/lib", vendordir)
+ln_relative("rbconfig.rb", "#{archdir}/rbconfig.rb")
diff --git a/tool/node_name.rb b/tool/node_name.rb
new file mode 100755
index 0000000000..dc0584e821
--- /dev/null
+++ b/tool/node_name.rb
@@ -0,0 +1,8 @@
+#! ./miniruby -n
+
+# Used when making Ruby to generate node_name.inc.
+# See common.mk for details.
+
+if (t ||= /^enum node_type \{/ =~ $_) and (t = /^\};/ !~ $_)
+ /(NODE_.+),/ =~ $_ and puts(" case #{$1}:\n\treturn \"#{$1}\";")
+end
diff --git a/tool/parse.rb b/tool/parse.rb
new file mode 100644
index 0000000000..93ae3e43cb
--- /dev/null
+++ b/tool/parse.rb
@@ -0,0 +1,16 @@
+# Used as part of the "make parse" Makefile target.
+# See common.mk for details.
+
+$file = ARGV[0]
+$str = ARGF.read.sub(/^__END__.*\z/m, '')
+puts '# ' + '-' * 70
+puts "# target program: "
+puts '# ' + '-' * 70
+puts $str
+puts '# ' + '-' * 70
+
+$parsed = RubyVM::InstructionSequence.compile_file($file)
+puts "# disasm result: "
+puts '# ' + '-' * 70
+puts $parsed.disasm
+puts '# ' + '-' * 70
diff --git a/tool/prereq.status b/tool/prereq.status
new file mode 100644
index 0000000000..6de00c8a92
--- /dev/null
+++ b/tool/prereq.status
@@ -0,0 +1,44 @@
+s,@EXTOUT@,tmp,g
+s,@ruby_version@,0.0.0,g
+s,@NULLCMD@,:,g
+s,@ARCH_FLAG@,,g
+s,@ASMEXT@,S,g
+s,@BASERUBY@,ruby,g
+s,@BOOTSTRAPRUBY@,$(BASERUBY),g
+s,@CC@,false,g
+s,@CFLAGS@,,g
+s,@CHDIR@,cd,g
+s,@CONFIGURE@,configure,g
+s,@CP@,cp,g
+s,@CPPFLAGS@,,g
+s,@CXXFLAGS@,,g
+s,@DLDFLAGS@,,g
+s,@DTRACE_EXT@,dmyh,g
+s,@EXEEXT@,,g
+s,@HAVE_BASERUBY@,yes,g
+s,@IFCHANGE@,tool/ifchange,g
+s,@LDFLAGS@,,g
+s,@LIBEXT@,a,g
+s,@LIBRUBY@,libruby.a,g
+s,@LIBRUBY_A@,libruby.a,g
+s,@MINIRUBY@,$(BASERUBY),g
+s,@MKDIR_P@,mkdir -p,g
+s,@OBJEXT@,o,g
+s,@PATH_SEPARATOR@,:,g
+s,@PWD@,.,g
+s,@RM@,rm -f,g
+s,@RMALL@,rm -fr,g
+s,@RMDIR@,rmdir,g
+s,@RMDIRS@,$(RMDIR) -p,g
+s,@RUBY@,$(BASERUBY),g
+s,@RUNRUBY@,$(MINIRUBY),g
+s,@arch@,noarch,g
+s,@bindir@,,g
+s,@configure_args@,,g
+s,@ruby_install_name@,,g
+s,@rubyarchdir@,,g
+s,@rubylibprefix@,,g
+s,@srcdir@,.,g
+
+s/@[A-Za-z][A-Za-z0-9_]*@//g
+s/{\$([A-Za-z]*)}//g
diff --git a/tool/probes_to_wiki.rb b/tool/probes_to_wiki.rb
new file mode 100644
index 0000000000..ba8204c188
--- /dev/null
+++ b/tool/probes_to_wiki.rb
@@ -0,0 +1,16 @@
+###
+# Converts the probes.d file to redmine wiki format. Usage:
+#
+# ruby tool/probes_to_wiki.rb probes.d
+
+File.read(ARGV[0]).scan(/\/\*.*?\*\//m).grep(/ruby/) do |comment|
+ comment.gsub!(/^(\/\*|[ ]*)|\*\/$/, '').strip!
+ puts
+ comment.each_line.with_index do |line, i|
+ if i == 0
+ puts "=== #{line.chomp}"
+ else
+ puts line.gsub(/`([^`]*)`/, '(({\1}))')
+ end
+ end
+end
diff --git a/tool/pure_parser.rb b/tool/pure_parser.rb
new file mode 100755
index 0000000000..21c87cc5d6
--- /dev/null
+++ b/tool/pure_parser.rb
@@ -0,0 +1,24 @@
+#!/usr/bin/ruby -pi.bak
+BEGIN {
+ # pathological setting
+ ENV['LANG'] = ENV['LC_MESSAGES'] = ENV['LC_ALL'] = 'C'
+
+ require_relative 'lib/colorize'
+
+ colorize = Colorize.new
+ file = ARGV.shift
+ begin
+ version = IO.popen(ARGV+%w[--version], "rb", &:read)
+ rescue Errno::ENOENT
+ abort "Failed to run `#{colorize.fail ARGV.join(' ')}'; You may have to install it."
+ end
+ unless /\Abison .* (\d+)\.\d+/ =~ version
+ puts colorize.fail("not bison")
+ exit
+ end
+ exit if $1.to_i >= 3
+ ARGV.clear
+ ARGV.push(file)
+}
+$_.sub!(/^%define\s+api\.pure/, '%pure-parser')
+$_.sub!(/^%define\s+.*/, '')
diff --git a/tool/rbinstall.rb b/tool/rbinstall.rb
new file mode 100755
index 0000000000..ec232fb896
--- /dev/null
+++ b/tool/rbinstall.rb
@@ -0,0 +1,1142 @@
+#!./miniruby
+
+# Used by the "make install" target to install Ruby.
+# See common.mk for more details.
+
+ENV["SDKROOT"] ||= "" if /darwin/ =~ RUBY_PLATFORM
+
+begin
+ load "./rbconfig.rb"
+rescue LoadError
+ CONFIG = Hash.new {""}
+else
+ include RbConfig
+ $".unshift File.expand_path("./rbconfig.rb")
+end
+
+srcdir = File.expand_path('../..', __FILE__)
+unless defined?(CROSS_COMPILING) and CROSS_COMPILING
+ $:.replace([srcdir+"/lib", Dir.pwd])
+end
+require 'fileutils'
+require 'shellwords'
+require 'optparse'
+require 'optparse/shellwords'
+require 'rubygems'
+begin
+ require "zlib"
+rescue LoadError
+ $" << "zlib.rb"
+end
+
+INDENT = " "*36
+STDOUT.sync = true
+File.umask(022)
+
+def parse_args(argv = ARGV)
+ $mantype = 'doc'
+ $destdir = nil
+ $extout = nil
+ $make = 'make'
+ $mflags = []
+ $install = []
+ $installed = {}
+ $installed_list = nil
+ $exclude = []
+ $dryrun = false
+ $rdocdir = nil
+ $htmldir = nil
+ $data_mode = 0644
+ $prog_mode = 0755
+ $dir_mode = nil
+ $script_mode = nil
+ $strip = false
+ $debug_symbols = nil
+ $cmdtype = (if File::ALT_SEPARATOR == '\\'
+ File.exist?("rubystub.exe") ? 'exe' : 'cmd'
+ end)
+ mflags = []
+ gnumake = false
+ opt = OptionParser.new
+ opt.on('-n', '--dry-run') {$dryrun = true}
+ opt.on('--dest-dir=DIR') {|dir| $destdir = dir}
+ opt.on('--extout=DIR') {|dir| $extout = (dir unless dir.empty?)}
+ opt.on('--ext-build-dir=DIR') {|v| $ext_build_dir = v }
+ opt.on('--make=COMMAND') {|make| $make = make}
+ opt.on('--mantype=MAN') {|man| $mantype = man}
+ opt.on('--make-flags=FLAGS', '--mflags', Shellwords) do |v|
+ if arg = v.first
+ arg.insert(0, '-') if /\A[^-][^=]*\Z/ =~ arg
+ end
+ $mflags.concat(v)
+ end
+ opt.on('-i', '--install=TYPE', $install_procs.keys) do |ins|
+ $install << ins
+ end
+ opt.on('-x', '--exclude=TYPE', $install_procs.keys) do |exc|
+ $exclude << exc
+ end
+ opt.on('--data-mode=OCTAL-MODE', OptionParser::OctalInteger) do |mode|
+ $data_mode = mode
+ end
+ opt.on('--prog-mode=OCTAL-MODE', OptionParser::OctalInteger) do |mode|
+ $prog_mode = mode
+ end
+ opt.on('--dir-mode=OCTAL-MODE', OptionParser::OctalInteger) do |mode|
+ $dir_mode = mode
+ end
+ opt.on('--script-mode=OCTAL-MODE', OptionParser::OctalInteger) do |mode|
+ $script_mode = mode
+ end
+ opt.on('--installed-list [FILENAME]') {|name| $installed_list = name}
+ opt.on('--rdoc-output [DIR]') {|dir| $rdocdir = dir}
+ opt.on('--html-output [DIR]') {|dir| $htmldir = dir}
+ opt.on('--cmd-type=TYPE', %w[cmd plain]) {|cmd| $cmdtype = (cmd unless cmd == 'plain')}
+ opt.on('--[no-]strip') {|strip| $strip = strip}
+ opt.on('--gnumake') {gnumake = true}
+ opt.on('--debug-symbols=SUFFIX', /\w+/) {|name| $debug_symbols = ".#{name}"}
+
+ opt.order!(argv) do |v|
+ case v
+ when /\AINSTALL[-_]([-\w]+)=(.*)/
+ argv.unshift("--#{$1.tr('_', '-')}=#{$2}")
+ when /\A\w[-\w]*=/
+ mflags << v
+ when /\A\w[-\w+]*\z/
+ $install << v.intern
+ else
+ raise OptionParser::InvalidArgument, v
+ end
+ end rescue abort "#{$!.message}\n#{opt.help}"
+
+ unless defined?(RbConfig)
+ puts opt.help
+ exit
+ end
+
+ $make, *rest = Shellwords.shellwords($make)
+ $mflags.unshift(*rest) unless rest.empty?
+ $mflags.unshift(*mflags)
+ $mflags.reject! {|v| /\A-[OW]/ =~ v} if gnumake
+
+ def $mflags.set?(flag)
+ grep(/\A-(?!-).*#{flag.chr}/i) { return true }
+ false
+ end
+ def $mflags.defined?(var)
+ grep(/\A#{var}=(.*)/) {return block_given? ? yield($1) : $1}
+ false
+ end
+
+ if $mflags.set?(?n)
+ $dryrun = true
+ else
+ $mflags << '-n' if $dryrun
+ end
+
+ $destdir ||= $mflags.defined?("DESTDIR")
+ if $extout ||= $mflags.defined?("EXTOUT")
+ RbConfig.expand($extout)
+ end
+
+ $continue = $mflags.set?(?k)
+
+ if $installed_list ||= $mflags.defined?('INSTALLED_LIST')
+ RbConfig.expand($installed_list, RbConfig::CONFIG)
+ $installed_list = open($installed_list, "ab")
+ $installed_list.sync = true
+ end
+
+ $rdocdir ||= $mflags.defined?('RDOCOUT')
+ $htmldir ||= $mflags.defined?('HTMLOUT')
+
+ $dir_mode ||= $prog_mode | 0700
+ $script_mode ||= $prog_mode
+ if $ext_build_dir.nil?
+ raise OptionParser::MissingArgument.new("--ext-build-dir=DIR")
+ end
+end
+
+$install_procs = Hash.new {[]}
+def install?(*types, &block)
+ unless types.delete(:nodefault)
+ $install_procs[:all] <<= block
+ end
+ types.each do |type|
+ $install_procs[type] <<= block
+ end
+end
+
+def strip_file(files)
+ if !defined?($strip_command) and (cmd = CONFIG["STRIP"])
+ case cmd
+ when "", "true", ":" then return
+ else $strip_command = Shellwords.shellwords(cmd)
+ end
+ elsif !$strip_command
+ return
+ end
+ system(*($strip_command + [files].flatten))
+end
+
+def install(src, dest, options = {})
+ options = options.clone
+ strip = options.delete(:strip)
+ options[:preserve] = true
+ srcs = Array(src).select {|s| !$installed[$made_dirs[dest] ? File.join(dest, s) : dest]}
+ return if srcs.empty?
+ src = srcs if Array === src
+ d = with_destdir(dest)
+ super(src, d, **options)
+ srcs.each {|s| $installed[$made_dirs[dest] ? File.join(dest, s) : dest] = true}
+ if strip
+ d = srcs.map {|s| File.join(d, File.basename(s))} if $made_dirs[dest]
+ strip_file(d)
+ end
+ if $installed_list
+ dest = srcs.map {|s| File.join(dest, File.basename(s))} if $made_dirs[dest]
+ $installed_list.puts dest
+ end
+end
+
+def ln_sf(src, dest)
+ super(src, with_destdir(dest))
+ $installed_list.puts dest if $installed_list
+end
+
+$made_dirs = {}
+def makedirs(dirs)
+ dirs = fu_list(dirs)
+ dirs.collect! do |dir|
+ realdir = with_destdir(dir)
+ realdir unless $made_dirs.fetch(dir) do
+ $made_dirs[dir] = true
+ $installed_list.puts(File.join(dir, "")) if $installed_list
+ File.directory?(realdir)
+ end
+ end.compact!
+ super(dirs, :mode => $dir_mode) unless dirs.empty?
+end
+
+FalseProc = proc {false}
+def path_matcher(pat)
+ if pat and !pat.empty?
+ proc {|f| pat.any? {|n| File.fnmatch?(n, f)}}
+ else
+ FalseProc
+ end
+end
+
+def install_recursive(srcdir, dest, options = {})
+ opts = options.clone
+ noinst = opts.delete(:no_install)
+ glob = opts.delete(:glob) || "*"
+ maxdepth = opts.delete(:maxdepth)
+ subpath = (srcdir.size+1)..-1
+ prune = []
+ skip = []
+ if noinst
+ if Array === noinst
+ prune = noinst.grep(/#{File::SEPARATOR}/o).map!{|f| f.chomp(File::SEPARATOR)}
+ skip = noinst.grep(/\A[^#{File::SEPARATOR}]*\z/o)
+ else
+ if noinst.index(File::SEPARATOR)
+ prune = [noinst]
+ else
+ skip = [noinst]
+ end
+ end
+ end
+ skip |= %w"#*# *~ *.old *.bak *.orig *.rej *.diff *.patch *.core"
+ prune = path_matcher(prune)
+ skip = path_matcher(skip)
+ File.directory?(srcdir) or return rescue return
+ paths = [[srcdir, dest, 0]]
+ found = []
+ while file = paths.shift
+ found << file
+ file, d, dir = *file
+ if dir
+ depth = dir + 1
+ next if maxdepth and maxdepth < depth
+ files = []
+ Dir.foreach(file) do |f|
+ src = File.join(file, f)
+ d = File.join(dest, dir = src[subpath])
+ stat = File.lstat(src) rescue next
+ if stat.directory?
+ files << [src, d, depth] if maxdepth != depth and /\A\./ !~ f and !prune[dir]
+ elsif stat.symlink?
+ # skip
+ else
+ files << [src, d, false] if File.fnmatch?(glob, f, File::FNM_EXTGLOB) and !skip[f]
+ end
+ end
+ paths.insert(0, *files)
+ end
+ end
+ for src, d, dir in found
+ if dir
+ next
+ # makedirs(d)
+ else
+ makedirs(d[/.*(?=\/)/m])
+ if block_given?
+ yield src, d, opts
+ else
+ install src, d, opts
+ end
+ end
+ end
+end
+
+def open_for_install(path, mode)
+ data = open(realpath = with_destdir(path), "rb") {|f| f.read} rescue nil
+ newdata = yield
+ unless $dryrun
+ unless newdata == data
+ open(realpath, "wb", mode) {|f| f.write newdata}
+ end
+ File.chmod(mode, realpath)
+ end
+ $installed_list.puts path if $installed_list
+end
+
+def with_destdir(dir)
+ return dir if !$destdir or $destdir.empty?
+ dir = dir.sub(/\A\w:/, '') if File::PATH_SEPARATOR == ';'
+ $destdir + dir
+end
+
+def without_destdir(dir)
+ return dir if !$destdir or $destdir.empty?
+ dir.start_with?($destdir) ? dir[$destdir.size..-1] : dir
+end
+
+def prepare(mesg, basedir, subdirs=nil)
+ return unless basedir
+ case
+ when !subdirs
+ dirs = basedir
+ when subdirs.size == 0
+ subdirs = nil
+ when subdirs.size == 1
+ dirs = [basedir = File.join(basedir, subdirs)]
+ subdirs = nil
+ else
+ dirs = [basedir, *subdirs.collect {|dir| File.join(basedir, dir)}]
+ end
+ printf("%-*s%s%s\n", INDENT.size, "installing #{mesg}:", basedir,
+ (subdirs ? " (#{subdirs.join(', ')})" : ""))
+ makedirs(dirs)
+end
+
+def CONFIG.[](name, mandatory = false)
+ value = super(name)
+ if mandatory
+ raise "CONFIG['#{name}'] must be set" if !value or value.empty?
+ end
+ value
+end
+
+exeext = CONFIG["EXEEXT"]
+
+ruby_install_name = CONFIG["ruby_install_name", true]
+rubyw_install_name = CONFIG["rubyw_install_name"]
+goruby_install_name = "go" + ruby_install_name
+
+bindir = CONFIG["bindir", true]
+libdir = CONFIG[CONFIG.fetch("libdirname", "libdir"), true]
+rubyhdrdir = CONFIG["rubyhdrdir", true]
+archhdrdir = CONFIG["rubyarchhdrdir"] || (rubyhdrdir + "/" + CONFIG['arch'])
+rubylibdir = CONFIG["rubylibdir", true]
+archlibdir = CONFIG["rubyarchdir", true]
+if CONFIG["sitedir"]
+ sitelibdir = CONFIG["sitelibdir"]
+ sitearchlibdir = CONFIG["sitearchdir"]
+end
+if CONFIG["vendordir"]
+ vendorlibdir = CONFIG["vendorlibdir"]
+ vendorarchlibdir = CONFIG["vendorarchdir"]
+end
+mandir = CONFIG["mandir", true]
+docdir = CONFIG["docdir", true]
+enable_shared = CONFIG["ENABLE_SHARED"] == 'yes'
+dll = CONFIG["LIBRUBY_SO", enable_shared]
+lib = CONFIG["LIBRUBY", true]
+arc = CONFIG["LIBRUBY_A", true]
+load_relative = CONFIG["LIBRUBY_RELATIVE"] == 'yes'
+
+rdoc_noinst = %w[created.rid]
+
+install?(:local, :arch, :bin, :'bin-arch') do
+ prepare "binary commands", bindir
+
+ install ruby_install_name+exeext, bindir, :mode => $prog_mode, :strip => $strip
+ if rubyw_install_name and !rubyw_install_name.empty?
+ install rubyw_install_name+exeext, bindir, :mode => $prog_mode, :strip => $strip
+ end
+ if File.exist? goruby_install_name+exeext
+ install goruby_install_name+exeext, bindir, :mode => $prog_mode, :strip => $strip
+ end
+ if enable_shared and dll != lib
+ install dll, bindir, :mode => $prog_mode, :strip => $strip
+ end
+end
+
+install?(:local, :arch, :lib, :'lib-arch') do
+ prepare "base libraries", libdir
+
+ install lib, libdir, :mode => $prog_mode, :strip => $strip unless lib == arc
+ install arc, libdir, :mode => $data_mode unless CONFIG["INSTALL_STATIC_LIBRARY"] == "no"
+ if dll == lib and dll != arc
+ for link in CONFIG["LIBRUBY_ALIASES"].split - [File.basename(dll)]
+ ln_sf(dll, File.join(libdir, link))
+ end
+ end
+
+ prepare "arch files", archlibdir
+ install "rbconfig.rb", archlibdir, :mode => $data_mode
+ if CONFIG["ARCHFILE"]
+ for file in CONFIG["ARCHFILE"].split
+ install file, archlibdir, :mode => $data_mode
+ end
+ end
+end
+
+install?(:local, :arch, :data) do
+ pc = CONFIG["ruby_pc"]
+ if pc and File.file?(pc) and File.size?(pc)
+ prepare "pkgconfig data", pkgconfigdir = File.join(libdir, "pkgconfig")
+ install pc, pkgconfigdir, :mode => $data_mode
+ end
+end
+
+install?(:ext, :arch, :'ext-arch') do
+ prepare "extension objects", archlibdir
+ noinst = %w[-* -*/] | (CONFIG["no_install_files"] || "").split
+ install_recursive("#{$extout}/#{CONFIG['arch']}", archlibdir, :no_install => noinst, :mode => $prog_mode, :strip => $strip)
+ prepare "extension objects", sitearchlibdir
+ prepare "extension objects", vendorarchlibdir
+ if extso = File.read("exts.mk")[/^EXTSO[ \t]*=[ \t]*((?:.*\\\n)*.*)/, 1] and
+ !(extso = extso.gsub(/\\\n/, '').split).empty?
+ libpathenv = CONFIG["LIBPATHENV"]
+ dest = CONFIG[!libpathenv || libpathenv == "PATH" ? "bindir" : "libdir"]
+ prepare "external libraries", dest
+ for file in extso
+ install file, dest, :mode => $prog_mode
+ end
+ end
+end
+install?(:ext, :arch, :hdr, :'arch-hdr', :'hdr-arch') do
+ prepare "extension headers", archhdrdir
+ install_recursive("#{$extout}/include/#{CONFIG['arch']}", archhdrdir, :glob => "*.h", :mode => $data_mode)
+ install_recursive("#{$extout}/include/#{CONFIG['arch']}", archhdrdir, :glob => "rb_mjit_header-*.obj", :mode => $data_mode)
+ install_recursive("#{$extout}/include/#{CONFIG['arch']}", archhdrdir, :glob => "rb_mjit_header-*.pch", :mode => $data_mode)
+end
+install?(:ext, :comm, :'ext-comm') do
+ prepare "extension scripts", rubylibdir
+ install_recursive("#{$extout}/common", rubylibdir, :mode => $data_mode)
+ prepare "extension scripts", sitelibdir
+ prepare "extension scripts", vendorlibdir
+end
+install?(:ext, :comm, :hdr, :'comm-hdr', :'hdr-comm') do
+ hdrdir = rubyhdrdir + "/ruby"
+ prepare "extension headers", hdrdir
+ install_recursive("#{$extout}/include/ruby", hdrdir, :glob => "*.h", :mode => $data_mode)
+end
+
+install?(:doc, :rdoc) do
+ if $rdocdir
+ ridatadir = File.join(CONFIG['ridir'], CONFIG['ruby_version'], "system")
+ prepare "rdoc", ridatadir
+ install_recursive($rdocdir, ridatadir, :no_install => rdoc_noinst, :mode => $data_mode)
+ end
+end
+install?(:doc, :html) do
+ if $htmldir
+ prepare "html-docs", docdir
+ install_recursive($htmldir, docdir+"/html", :no_install => rdoc_noinst, :mode => $data_mode)
+ end
+end
+install?(:doc, :capi) do
+ prepare "capi-docs", docdir
+ install_recursive "doc/capi", docdir+"/capi", :mode => $data_mode
+end
+
+prolog_script = <<EOS
+bindir="#{load_relative ? '${0%/*}' : bindir.gsub(/\"/, '\\\\"')}"
+EOS
+if CONFIG["LIBRUBY_RELATIVE"] != 'yes' and libpathenv = CONFIG["LIBPATHENV"]
+ pathsep = File::PATH_SEPARATOR
+ prolog_script << <<EOS
+libdir="#{load_relative ? '$\{bindir%/bin\}/lib' : libdir.gsub(/\"/, '\\\\"')}"
+export #{libpathenv}="$libdir${#{libpathenv}:+#{pathsep}$#{libpathenv}}"
+EOS
+end
+prolog_script << %Q[exec "$bindir/#{ruby_install_name}" "-x" "$0" "$@"\n]
+PROLOG_SCRIPT = {}
+PROLOG_SCRIPT["exe"] = "#!#{bindir}/#{ruby_install_name}"
+PROLOG_SCRIPT["cmd"] = <<EOS
+:""||{ ""=> %q<-*- ruby -*-
+@"%~dp0#{ruby_install_name}" -x "%~f0" %*
+@exit /b %ERRORLEVEL%
+};{ #\n#{prolog_script.gsub(/(?=\n)/, ' #')}>,\n}
+EOS
+PROLOG_SCRIPT.default = (load_relative || /\s/ =~ bindir) ?
+ <<EOS : PROLOG_SCRIPT["exe"]
+#!/bin/sh
+# -*- ruby -*-
+_=_\\
+=begin
+#{prolog_script.chomp}
+=end
+EOS
+
+installer = Struct.new(:ruby_shebang, :ruby_bin, :ruby_install_name, :stub, :trans) do
+ def transform(name)
+ RbConfig.expand(trans[name])
+ end
+end
+
+$script_installer = Class.new(installer) do
+ ruby_shebang = File.join(bindir, ruby_install_name)
+ if File::ALT_SEPARATOR
+ ruby_bin = ruby_shebang.tr(File::SEPARATOR, File::ALT_SEPARATOR)
+ end
+ if trans = CONFIG["program_transform_name"]
+ exp = []
+ trans.gsub!(/\$\$/, '$')
+ trans.scan(%r[\G[\s;]*(/(?:\\.|[^/])*+/)?([sy])(\\?\W)((?:(?!\3)(?:\\.|.))*+)\3((?:(?!\3)(?:\\.|.))*+)\3([gi]*)]) do
+ |addr, cmd, sep, pat, rep, opt|
+ addr &&= Regexp.new(addr[/\A\/(.*)\/\z/, 1])
+ case cmd
+ when 's'
+ next if pat == '^' and rep.empty?
+ exp << [addr, (opt.include?('g') ? :gsub! : :sub!),
+ Regexp.new(pat, opt.include?('i')), rep.gsub(/&/){'\&'}]
+ when 'y'
+ exp << [addr, :tr!, Regexp.quote(pat), rep]
+ end
+ end
+ trans = proc do |base|
+ exp.each {|addr, opt, pat, rep| base.__send__(opt, pat, rep) if !addr or addr =~ base}
+ base
+ end
+ elsif /ruby/ =~ ruby_install_name
+ trans = proc {|base| ruby_install_name.sub(/ruby/, base)}
+ else
+ trans = proc {|base| base}
+ end
+
+ def prolog(shebang)
+ shebang.sub!(/\r$/, '')
+ script = PROLOG_SCRIPT[$cmdtype]
+ shebang.sub!(/\A(\#!.*?ruby\b)?/) do
+ if script.end_with?("\n")
+ script + ($1 || "#!ruby\n")
+ else
+ $1 ? script : "#{script}\n"
+ end
+ end
+ shebang
+ end
+
+ def install(src, cmd)
+ cmd = cmd.sub(/[^\/]*\z/m) {|n| transform(n)}
+
+ shebang, body = open(src, "rb") do |f|
+ next f.gets, f.read
+ end
+ shebang or raise "empty file - #{src}"
+ shebang = prolog(shebang)
+ body.gsub!(/\r$/, '')
+
+ cmd << ".#{$cmdtype}" if $cmdtype
+ open_for_install(cmd, $script_mode) do
+ case $cmdtype
+ when "exe"
+ stub + shebang + body
+ else
+ shebang + body
+ end
+ end
+ end
+
+ def self.get_rubystub
+ stubfile = "rubystub.exe"
+ stub = File.open(stubfile, "rb") {|f| f.read} << "\n"
+ rescue => e
+ abort "No #{stubfile}: #{e}"
+ else
+ stub
+ end
+
+ def stub
+ super or self.stub = self.class.get_rubystub
+ end
+
+ break new(ruby_shebang, ruby_bin, ruby_install_name, nil, trans)
+end
+
+install?(:local, :comm, :bin, :'bin-comm') do
+ prepare "command scripts", bindir
+
+ install_recursive(File.join(srcdir, "bin"), bindir, :maxdepth => 1) do |src, cmd|
+ $script_installer.install(src, cmd)
+ end
+end
+
+install?(:local, :comm, :lib) do
+ prepare "library scripts", rubylibdir
+ noinst = %w[*.txt *.rdoc *.gemspec]
+ install_recursive(File.join(srcdir, "lib"), rubylibdir, :no_install => noinst, :mode => $data_mode)
+end
+
+install?(:local, :comm, :hdr, :'comm-hdr') do
+ prepare "common headers", rubyhdrdir
+
+ noinst = []
+ unless RUBY_PLATFORM =~ /mswin|mingw|bccwin/
+ noinst << "win32.h"
+ end
+ noinst = nil if noinst.empty?
+ install_recursive(File.join(srcdir, "include"), rubyhdrdir, :no_install => noinst, :glob => "*.{h,hpp}", :mode => $data_mode)
+end
+
+install?(:local, :comm, :man) do
+ mdocs = Dir["#{srcdir}/man/*.[1-9]"]
+ prepare "manpages", mandir, ([] | mdocs.collect {|mdoc| mdoc[/\d+$/]}).sort.collect {|sec| "man#{sec}"}
+
+ case $mantype
+ when /\.(?:(gz)|bz2)\z/
+ compress = $1 ? "gzip" : "bzip2"
+ suffix = $&
+ end
+ mandir = File.join(mandir, "man")
+ has_goruby = File.exist?(goruby_install_name+exeext)
+ require File.join(srcdir, "tool/mdoc2man.rb") if /\Adoc\b/ !~ $mantype
+ mdocs.each do |mdoc|
+ next unless File.file?(mdoc) and open(mdoc){|fh| fh.read(1) == '.'}
+ base = File.basename(mdoc)
+ if base == "goruby.1"
+ next unless has_goruby
+ end
+
+ destdir = mandir + (section = mdoc[/\d+$/])
+ destname = ruby_install_name.sub(/ruby/, base.chomp(".#{section}"))
+ destfile = File.join(destdir, "#{destname}.#{section}")
+
+ if /\Adoc\b/ =~ $mantype
+ if compress
+ w = open(mdoc) {|f|
+ stdin = STDIN.dup
+ STDIN.reopen(f)
+ begin
+ destfile << suffix
+ IO.popen(compress, &:read)
+ ensure
+ STDIN.reopen(stdin)
+ stdin.close
+ end
+ }
+ open_for_install(destfile, $data_mode) {w}
+ else
+ install mdoc, destfile, :mode => $data_mode
+ end
+ else
+ class << (w = [])
+ alias print push
+ end
+ if File.basename(mdoc).start_with?('bundle') ||
+ File.basename(mdoc).start_with?('gemfile')
+ w = File.read(mdoc)
+ else
+ open(mdoc) {|r| Mdoc2Man.mdoc2man(r, w)}
+ w = w.join("")
+ end
+ if compress
+ require 'tmpdir'
+ Dir.mktmpdir("man") {|d|
+ dest = File.join(d, File.basename(destfile))
+ File.open(dest, "wb") {|f| f.write w}
+ if system(compress, dest)
+ w = File.open(dest+suffix, "rb") {|f| f.read}
+ destfile << suffix
+ end
+ }
+ end
+ open_for_install(destfile, $data_mode) {w}
+ end
+ end
+end
+
+install?(:dbg, :nodefault) do
+ prepare "debugger commands", bindir
+ prepare "debugger scripts", rubylibdir
+ conf = RbConfig::MAKEFILE_CONFIG.merge({"prefix"=>"${prefix#/}"})
+ Dir.glob(File.join(srcdir, "template/ruby-*db.in")) do |src|
+ cmd = $script_installer.transform(File.basename(src, ".in"))
+ open_for_install(File.join(bindir, cmd), $script_mode) {
+ RbConfig.expand(File.read(src), conf)
+ }
+ end
+ install File.join(srcdir, "misc/lldb_cruby.py"), File.join(rubylibdir, "lldb_cruby.py")
+ install File.join(srcdir, ".gdbinit"), File.join(rubylibdir, "gdbinit")
+ if $debug_symbols
+ {
+ ruby_install_name => bindir,
+ rubyw_install_name => bindir,
+ goruby_install_name => bindir,
+ dll => libdir,
+ }.each do |src, dest|
+ next if src.empty?
+ src += $debug_symbols
+ if File.directory?(src)
+ install_recursive src, File.join(dest, src)
+ end
+ end
+ end
+end
+
+module RbInstall
+ def self.no_write(options = nil)
+ u = File.umask(0022)
+ if $dryrun
+ fu = ::Object.class_eval do
+ fu = remove_const(:FileUtils)
+ const_set(:FileUtils, fu::NoWrite)
+ fu
+ end
+ dir_mode = options.delete(:dir_mode) if options
+ end
+ yield
+ ensure
+ options[:dir_mode] = dir_mode if dir_mode
+ if fu
+ ::Object.class_eval do
+ remove_const(:FileUtils)
+ const_set(:FileUtils, fu)
+ end
+ end
+ File.umask(u)
+ end
+
+ module Specs
+ class FileCollector
+ def initialize(gemspec)
+ @gemspec = gemspec
+ @base_dir = File.dirname(gemspec)
+ end
+
+ def collect
+ (ruby_libraries + built_libraries).sort
+ end
+
+ def skip_install?(files)
+ case type
+ when "ext"
+ # install ext only when it's configured
+ !File.exist?("#{$ext_build_dir}/#{relative_base}/Makefile")
+ when "lib"
+ files.empty?
+ end
+ end
+
+ private
+ def type
+ /\/(ext|lib)?\/.*?\z/ =~ @base_dir
+ $1
+ end
+
+ def ruby_libraries
+ case type
+ when "ext"
+ prefix = "#{$extout}/common/"
+ base = "#{prefix}#{relative_base}"
+ when "lib"
+ base = @base_dir
+ prefix = base.sub(/lib\/.*?\z/, "")
+ # for lib/net/net-smtp.gemspec
+ if m = File.basename(@gemspec, ".gemspec").match(/.*\-(.*)\z/)
+ base = "#{@base_dir}/#{m[1]}" unless remove_prefix(prefix, @base_dir).include?(m[1])
+ end
+ end
+
+ files = if base
+ Dir.glob("#{base}{.rb,/**/*.rb}").collect do |ruby_source|
+ remove_prefix(prefix, ruby_source)
+ end
+ else
+ [File.basename(@gemspec, '.gemspec') + '.rb']
+ end
+
+ case File.basename(@gemspec, ".gemspec")
+ when "net-http"
+ files << "lib/net/https.rb"
+ when "optparse"
+ files << "lib/optionparser.rb"
+ end
+
+ files
+ end
+
+ def built_libraries
+ case type
+ when "ext"
+ prefix = "#{$extout}/#{CONFIG['arch']}/"
+ base = "#{prefix}#{relative_base}"
+ dlext = CONFIG['DLEXT']
+ Dir.glob("#{base}{.#{dlext},/**/*.#{dlext}}").collect do |built_library|
+ remove_prefix(prefix, built_library)
+ end
+ when "lib"
+ []
+ else
+ []
+ end
+ end
+
+ def relative_base
+ /\/#{Regexp.escape(type)}\/(.*?)\z/ =~ @base_dir
+ $1
+ end
+
+ def remove_prefix(prefix, string)
+ string.sub(/\A#{Regexp.escape(prefix)}/, "")
+ end
+ end
+ end
+
+ class DirPackage
+ attr_reader :spec
+
+ attr_accessor :dir_mode
+ attr_accessor :prog_mode
+ attr_accessor :data_mode
+
+ def initialize(spec, dir_map = nil)
+ @spec = spec
+ @src_dir = File.dirname(@spec.loaded_from)
+ @dir_map = dir_map
+ end
+
+ def extract_files(destination_dir, pattern = "*")
+ return if @src_dir == destination_dir
+ File.chmod(0700, destination_dir) unless $dryrun
+ mode = pattern == File.join(spec.bindir, '*') ? prog_mode : data_mode
+ destdir = without_destdir(destination_dir)
+ if @dir_map
+ (dir_map = @dir_map.map {|k, v| Regexp.quote(k) unless k == v}).compact!
+ dir_map = %r{\A(?:#{dir_map.join('|')})(?=/)}
+ end
+ spec.files.each do |f|
+ next unless File.fnmatch(pattern, f)
+ src = File.join(@src_dir, dir_map =~ f ? "#{@dir_map[$&]}#{$'}" : f)
+ dest = File.join(destdir, f)
+ makedirs(dest[/.*(?=\/)/m])
+ install src, dest, :mode => mode
+ end
+ File.chmod(dir_mode, destination_dir) unless $dryrun
+ end
+ end
+
+ class GemInstaller < Gem::Installer
+ end
+
+ class UnpackedInstaller < GemInstaller
+ def write_cache_file
+ end
+
+ def shebang(bin_file_name)
+ path = File.join(gem_dir, spec.bindir, bin_file_name)
+ first_line = File.open(path, "rb") {|file| file.gets}
+ $script_installer.prolog(first_line).chomp
+ end
+
+ def app_script_text(bin_file_name)
+ # move shell script part after comments generated by RubyGems.
+ super.sub(/\A
+ (\#!\/bin\/sh\n\#.*-\*-\s*ruby\s*-\*-.*\n)
+ ((?:.*\n)*?\#!.*ruby.*\n)
+ \#\n
+ ((?:\#.*\n)+)/x, '\1\3\2')
+ end
+
+ def check_executable_overwrite(filename)
+ return if @wrappers and same_bin_script?(filename, @bin_dir)
+ super
+ end
+
+ def generate_bin_script(filename, bindir)
+ return if same_bin_script?(filename, bindir)
+ super
+ end
+
+ def same_bin_script?(filename, bindir)
+ path = File.join(bindir, formatted_program_filename(filename))
+ begin
+ return true if File.binread(path) == app_script_text(filename)
+ rescue
+ end
+ false
+ end
+
+ def write_spec
+ super unless $dryrun
+ $installed_list.puts(without_destdir(spec_file)) if $installed_list
+ end
+
+ def write_default_spec
+ super unless $dryrun
+ $installed_list.puts(without_destdir(default_spec_file)) if $installed_list
+ end
+ end
+
+ class GemInstaller
+ def install
+ spec.post_install_message = nil
+ RbInstall.no_write(options) {super}
+ end
+
+ if RbConfig::CONFIG["LIBRUBY_RELATIVE"] == "yes" || RbConfig::CONFIG["CROSS_COMPILING"] == "yes" || ENV["DESTDIR"]
+ # TODO: always build extensions in bundled gems by build-ext and
+ # install the built binaries.
+ def build_extensions
+ end
+ end
+
+ def generate_bin_script(filename, bindir)
+ name = formatted_program_filename(filename)
+ unless $dryrun
+ super
+ File.chmod($script_mode, File.join(bindir, name))
+ end
+ $installed_list.puts(File.join(without_destdir(bindir), name)) if $installed_list
+ end
+
+ def verify_gem_home # :nodoc:
+ end
+
+ def ensure_writable_dir(dir)
+ $made_dirs.fetch(d = without_destdir(dir)) do
+ $made_dirs[d] = true
+ super unless $dryrun
+ $installed_list.puts(d+"/") if $installed_list
+ end
+ end
+ end
+end
+
+# :startdoc:
+
+install?(:ext, :comm, :gem, :'default-gems', :'default-gems-comm') do
+ install_default_gem('lib', srcdir, bindir)
+end
+install?(:ext, :arch, :gem, :'default-gems', :'default-gems-arch') do
+ install_default_gem('ext', srcdir, bindir)
+end
+
+def load_gemspec(file, base = nil)
+ file = File.realpath(file)
+ code = File.read(file, encoding: "utf-8:-")
+ code.gsub!(/(?:`git[^\`]*`|%x\[git[^\]]*\])\.split\([^\)]*\)/m) do
+ files = []
+ if base
+ Dir.glob("**/*", File::FNM_DOTMATCH, base: base) do |n|
+ case File.basename(n); when ".", ".."; next; end
+ next if File.directory?(File.join(base, n))
+ files << n.dump
+ end
+ end
+ "[" + files.join(", ") + "]"
+ end
+ spec = eval(code, binding, file)
+ unless Gem::Specification === spec
+ raise TypeError, "[#{file}] isn't a Gem::Specification (#{spec.class} instead)."
+ end
+ spec.loaded_from = base ? File.join(base, File.basename(file)) : file
+ spec.files.reject! {|n| n.end_with?(".gemspec") or n.start_with?(".git")}
+
+ spec
+end
+
+def install_default_gem(dir, srcdir, bindir)
+ gem_dir = Gem.default_dir
+ install_dir = with_destdir(gem_dir)
+ prepare "default gems from #{dir}", gem_dir
+ RbInstall.no_write do
+ makedirs(Gem.ensure_default_gem_subdirectories(install_dir, $dir_mode).map {|d| File.join(gem_dir, d)})
+ end
+
+ options = {
+ :install_dir => with_destdir(gem_dir),
+ :bin_dir => with_destdir(bindir),
+ :ignore_dependencies => true,
+ :dir_mode => $dir_mode,
+ :data_mode => $data_mode,
+ :prog_mode => $script_mode,
+ :wrappers => true,
+ :format_executable => true,
+ :install_as_default => true,
+ }
+ default_spec_dir = Gem.default_specifications_dir
+
+ gems = Dir.glob("#{srcdir}/#{dir}/**/*.gemspec").map {|src|
+ spec = load_gemspec(src)
+ file_collector = RbInstall::Specs::FileCollector.new(src)
+ files = file_collector.collect
+ if file_collector.skip_install?(files)
+ next
+ end
+ spec.files = files
+ spec
+ }
+ gems.compact.sort_by(&:name).each do |gemspec|
+ old_gemspecs = Dir[File.join(with_destdir(default_spec_dir), "#{gemspec.name}-*.gemspec")]
+ if old_gemspecs.size > 0
+ old_gemspecs.each {|spec| rm spec }
+ end
+
+ full_name = "#{gemspec.name}-#{gemspec.version}"
+
+ gemspec.loaded_from = File.join srcdir, gemspec.spec_name
+
+ package = RbInstall::DirPackage.new gemspec, {gemspec.bindir => 'libexec'}
+ ins = RbInstall::UnpackedInstaller.new(package, options)
+ puts "#{INDENT}#{gemspec.name} #{gemspec.version}"
+ ins.install
+ end
+end
+
+install?(:ext, :comm, :gem, :'bundled-gems') do
+ gem_dir = Gem.default_dir
+ install_dir = with_destdir(gem_dir)
+ prepare "bundled gems", gem_dir
+ RbInstall.no_write do
+ makedirs(Gem.ensure_gem_subdirectories(install_dir, $dir_mode).map {|d| File.join(gem_dir, d)})
+ end
+
+ installed_gems = {}
+ skipped = {}
+ options = {
+ :install_dir => install_dir,
+ :bin_dir => with_destdir(bindir),
+ :domain => :local,
+ :ignore_dependencies => true,
+ :dir_mode => $dir_mode,
+ :data_mode => $data_mode,
+ :prog_mode => $script_mode,
+ :wrappers => true,
+ :format_executable => true,
+ }
+
+ extensions_dir = Gem::StubSpecification.gemspec_stub("", gem_dir, gem_dir).extensions_dir
+ specifications_dir = File.join(gem_dir, "specifications")
+ build_dir = Gem::StubSpecification.gemspec_stub("", ".bundle", ".bundle").extensions_dir
+
+ # We are about to build extensions, and want to configure extensions with the
+ # newly installed ruby.
+ Gem.instance_variable_set(:@ruby, with_destdir(File.join(bindir, ruby_install_name)))
+ # Prevent fake.rb propagation. It conflicts with the natural mkmf configs of
+ # the newly installed ruby.
+ ENV.delete('RUBYOPT')
+
+ File.foreach("#{srcdir}/gems/bundled_gems") do |name|
+ next if /^\s*(?:#|$)/ =~ name
+ next unless /^(\S+)\s+(\S+).*/ =~ name
+ gem = $1
+ gem_name = "#$1-#$2"
+ # Try to find the original gemspec file
+ path = "#{srcdir}/.bundle/gems/#{gem_name}/#{gem}.gemspec"
+ unless File.exist?(path)
+ # Try to find the gemspec file for C ext gems
+ # ex .bundle/gems/debug-1.7.1/debug-1.7.1.gemspec
+ # This gemspec keep the original dependencies
+ path = "#{srcdir}/.bundle/gems/#{gem_name}/#{gem_name}.gemspec"
+ unless File.exist?(path)
+ # Try to find the gemspec file for gems that hasn't own gemspec
+ path = "#{srcdir}/.bundle/specifications/#{gem_name}.gemspec"
+ unless File.exist?(path)
+ skipped[gem_name] = "gemspec not found"
+ next
+ end
+ end
+ end
+ spec = load_gemspec(path, "#{srcdir}/.bundle/gems/#{gem_name}")
+ unless spec.platform == Gem::Platform::RUBY
+ skipped[gem_name] = "not ruby platform (#{spec.platform})"
+ next
+ end
+ unless spec.full_name == gem_name
+ skipped[gem_name] = "full name unmatch #{spec.full_name}"
+ next
+ end
+ spec.extension_dir = "#{extensions_dir}/#{spec.full_name}"
+ package = RbInstall::DirPackage.new spec
+ ins = RbInstall::UnpackedInstaller.new(package, options)
+ puts "#{INDENT}#{spec.name} #{spec.version}"
+ ins.install
+ install_recursive("#{build_dir}/#{gem_name}", "#{extensions_dir}/#{gem_name}") do |src, dest|
+ # puts "#{INDENT} #{dest[extensions_dir.size+gem_name.size+2..-1]}"
+ install src, dest, :mode => (File.executable?(src) ? $prog_mode : $data_mode)
+ end
+ installed_gems[spec.full_name] = true
+ end
+ installed_gems, gems = Dir.glob(srcdir+'/gems/*.gem').partition {|gem| installed_gems.key?(File.basename(gem, '.gem'))}
+ unless installed_gems.empty?
+ prepare "bundled gem cache", gem_dir+"/cache"
+ install installed_gems, gem_dir+"/cache"
+ end
+ next if gems.empty?
+ if defined?(Zlib)
+ silent = Gem::SilentUI.new
+ gems.each do |gem|
+ package = Gem::Package.new(gem)
+ inst = RbInstall::GemInstaller.new(package, options)
+ inst.spec.extension_dir = "#{extensions_dir}/#{inst.spec.full_name}"
+ begin
+ Gem::DefaultUserInteraction.use_ui(silent) {inst.install}
+ rescue Gem::InstallError
+ next
+ end
+ gemname = File.basename(gem)
+ puts "#{INDENT}#{gemname}"
+ end
+ # fix directory permissions
+ # TODO: Gem.install should accept :dir_mode option or something
+ File.chmod($dir_mode, *Dir.glob(install_dir+"/**/"))
+ # fix .gemspec permissions
+ File.chmod($data_mode, *Dir.glob(install_dir+"/specifications/*.gemspec"))
+ else
+ puts "skip installing bundled gems because of lacking zlib"
+ end
+end
+
+parse_args()
+
+include FileUtils
+include FileUtils::NoWrite if $dryrun
+@fileutils_output = STDOUT
+@fileutils_label = ''
+
+$install << :all if $install.empty?
+installs = $install.map do |inst|
+ if !(procs = $install_procs[inst]) || procs.empty?
+ next warn("unknown install target - #{inst}")
+ end
+ procs
+end
+installs.flatten!
+installs -= $exclude.map {|exc| $install_procs[exc]}.flatten
+puts "Installing to #$destdir" unless installs.empty?
+installs.each do |block|
+ dir = Dir.pwd
+ begin
+ block.call
+ ensure
+ Dir.chdir(dir)
+ end
+end
+
+# vi:set sw=2:
diff --git a/tool/rbuninstall.rb b/tool/rbuninstall.rb
new file mode 100755
index 0000000000..f0c286012c
--- /dev/null
+++ b/tool/rbuninstall.rb
@@ -0,0 +1,73 @@
+#! /usr/bin/ruby -nl
+
+# Used by the "make uninstall" target to uninstall Ruby.
+# See common.mk for more details.
+
+BEGIN {
+ $dryrun = false
+ $tty = STDOUT.tty?
+ until ARGV.empty?
+ case ARGV[0]
+ when /\A--destdir=(.*)/
+ $destdir = $1
+ when /\A-n\z/
+ $dryrun = true
+ when /\A--(?:no-)?tty\z/
+ $tty = !$1
+ else
+ break
+ end
+ ARGV.shift
+ end
+ $dirs = []
+ $files = []
+}
+list = ($_.chomp!('/') ? $dirs : $files)
+list << $_
+END {
+ status = true
+ $\ = nil
+ ors = (!$dryrun and $tty) ? "\e[K\r" : "\n"
+ $files.each do |file|
+ print "rm #{file}#{ors}"
+ unless $dryrun
+ file = File.join($destdir, file) if $destdir
+ begin
+ File.unlink(file)
+ rescue Errno::ENOENT
+ rescue
+ status = false
+ puts $!
+ end
+ end
+ end
+ unlink = {}
+ $dirs.each do |dir|
+ unlink[dir] = true
+ end
+ while dir = $dirs.pop
+ dir = File.dirname(dir) while File.basename(dir) == '.'
+ print "rmdir #{dir}#{ors}"
+ unless $dryrun
+ realdir = $destdir ? File.join($destdir, dir) : dir
+ begin
+ begin
+ unlink.delete(dir)
+ Dir.rmdir(realdir)
+ rescue Errno::ENOTDIR
+ raise unless File.symlink?(realdir)
+ File.unlink(realdir)
+ end
+ rescue Errno::ENOENT, Errno::ENOTEMPTY
+ rescue
+ status = false
+ puts $!
+ else
+ parent = File.dirname(dir)
+ $dirs.push(parent) unless parent == dir or unlink[parent]
+ end
+ end
+ end
+ print ors.chomp
+ exit(status)
+}
diff --git a/tool/redmine-backporter.rb b/tool/redmine-backporter.rb
new file mode 100755
index 0000000000..843132ab3a
--- /dev/null
+++ b/tool/redmine-backporter.rb
@@ -0,0 +1,507 @@
+#!/usr/bin/env ruby
+require 'open-uri'
+require 'openssl'
+require 'net/http'
+require 'json'
+require 'io/console'
+require 'stringio'
+require 'strscan'
+require 'optparse'
+require 'abbrev'
+require 'pp'
+require 'shellwords'
+require 'reline'
+
+opts = OptionParser.new
+target_version = nil
+repo_path = nil
+api_key = nil
+ssl_verify = true
+opts.on('-k REDMINE_API_KEY', '--key=REDMINE_API_KEY', 'specify your REDMINE_API_KEY') {|v| api_key = v}
+opts.on('-t TARGET_VERSION', '--target=TARGET_VARSION', /\A\d(?:\.\d)+\z/, 'specify target version (ex: 3.1)') {|v| target_version = v}
+opts.on('-r RUBY_REPO_PATH', '--repository=RUBY_REPO_PATH', 'specify repository path') {|v| repo_path = v}
+opts.on('--[no-]ssl-verify', TrueClass, 'use / not use SSL verify') {|v| ssl_verify = v}
+opts.parse!(ARGV)
+
+http_options = {use_ssl: true}
+http_options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE unless ssl_verify
+$openuri_options = {}
+$openuri_options[:ssl_verify_mode] = OpenSSL::SSL::VERIFY_NONE unless ssl_verify
+
+TARGET_VERSION = target_version || ENV['TARGET_VERSION'] || (puts opts.help; raise 'need to specify TARGET_VERSION')
+RUBY_REPO_PATH = repo_path || ENV['RUBY_REPO_PATH']
+BACKPORT_CF_KEY = 'cf_5'
+STATUS_CLOSE = 5
+REDMINE_API_KEY = api_key || ENV['REDMINE_API_KEY'] || (puts opts.help; raise 'need to specify REDMINE_API_KEY')
+REDMINE_BASE = 'https://bugs.ruby-lang.org'
+
+@query = {
+ 'f[]' => BACKPORT_CF_KEY,
+ "op[#{BACKPORT_CF_KEY}]" => '~',
+ "v[#{BACKPORT_CF_KEY}][]" => "\"#{TARGET_VERSION}: REQUIRED\"",
+ 'limit' => 40,
+ 'status_id' => STATUS_CLOSE,
+ 'sort' => 'updated_on'
+}
+
+PRIORITIES = {
+ 'Low' => [:white, :blue],
+ 'Normal' => [],
+ 'High' => [:red],
+ 'Urgent' => [:red, :white],
+ 'Immediate' => [:red, :white, {underscore: true}],
+}
+COLORS = {
+ black: 30,
+ red: 31,
+ green: 32,
+ yellow: 33,
+ blue: 34,
+ magenta: 35,
+ cyan: 36,
+ white: 37,
+}
+
+class String
+ def color(fore=nil, back=nil, opts={}, bold: false, underscore: false)
+ seq = ""
+ if bold || opts[:bold]
+ seq = seq + "\e[1m"
+ end
+ if underscore || opts[:underscore]
+ seq = seq + "\e[2m"
+ end
+ if fore
+ c = COLORS[fore]
+ raise "unknown foreground color #{fore}" unless c
+ seq = seq + "\e[#{c}m"
+ end
+ if back
+ c = COLORS[back]
+ raise "unknown background color #{back}" unless c
+ seq = seq + "\e[#{c + 10}m"
+ end
+ if seq.empty?
+ self
+ else
+ seq = seq + self + "\e[0m"
+ end
+ end
+end
+
+class StringScanner
+ # lx: limit of x (columns of screen)
+ # ly: limit of y (rows of screen)
+ def getrows(lx, ly)
+ cp1 = charpos
+ x = 0
+ y = 0
+ until eos?
+ case c = getch
+ when "\r"
+ x = 0
+ when "\n"
+ x = 0
+ y += 1
+ when "\t"
+ x += 8
+ when /[\x00-\x7f]/
+ # halfwidth
+ x += 1
+ else
+ # fullwidth
+ x += 2
+ end
+
+ if x > lx
+ x = 0
+ y += 1
+ unscan
+ end
+ if y >= ly
+ return string[cp1...charpos]
+ end
+ end
+ string[cp1..-1]
+ end
+end
+
+def more(sio)
+ console = IO.console
+ ly, lx = console.winsize
+ ly -= 1
+ str = sio.string
+ cls = "\r" + (" " * lx) + "\r"
+
+ ss = StringScanner.new(str)
+
+ rows = ss.getrows(lx, ly)
+ puts rows
+ until ss.eos?
+ print ":"
+ case c = console.getch
+ when ' '
+ rows = ss.getrows(lx, ly)
+ puts cls + rows
+ when 'j', "\r"
+ rows = ss.getrows(lx, 1)
+ puts cls + rows
+ when "q"
+ print cls
+ break
+ else
+ print "\b"
+ end
+ end
+end
+
+def find_git_log(pattern)
+ `git #{RUBY_REPO_PATH ? "-C #{RUBY_REPO_PATH.shellescape}" : ""} log --grep="#{pattern}"`
+end
+
+def has_commit(commit, branch)
+ base = RUBY_REPO_PATH ? ["-C", RUBY_REPO_PATH.shellescape] : nil
+ system("git", *base, "merge-base", "--is-ancestor", commit, branch)
+end
+
+def show_last_journal(http, uri)
+ res = http.get("#{uri.path}?include=journals")
+ res.value
+ h = JSON(res.body)
+ x = h["issue"]
+ raise "no issue" unless x
+ x = x["journals"]
+ raise "no journals" unless x
+ x = x.last
+ puts "== #{x["user"]["name"]} (#{x["created_on"]})"
+ x["details"].each do |y|
+ puts JSON(y)
+ end
+ puts x["notes"]
+end
+
+def merger_path
+ RUBY_PLATFORM =~ /mswin|mingw/ ? 'merger' : File.expand_path('../merger.rb', __FILE__)
+end
+
+def backport_command_string
+ unless @changesets.respond_to?(:validated)
+ @changesets = @changesets.select do |c|
+ next false if c.match(/\A\d{1,6}\z/) # skip SVN revision
+
+ # check if the Git revision is included in master
+ has_commit(c, "master")
+ end.sort_by do |changeset|
+ Integer(IO.popen(%W[git show -s --format=%ct #{changeset}], &:read))
+ end
+ @changesets.define_singleton_method(:validated){true}
+ end
+ "#{merger_path} --ticket=#{@issue} #{@changesets.join(',')}"
+end
+
+def status_char(obj)
+ case obj["name"]
+ when "Closed"
+ "C".color(bold: true)
+ else
+ obj["name"][0]
+ end
+end
+
+console = IO.console
+row, = console.winsize
+@query['limit'] = row - 2
+puts "Redmine Backporter".color(bold: true) + " for Ruby #{TARGET_VERSION}"
+
+class CommandSyntaxError < RuntimeError; end
+commands = {
+ "ls" => proc{|args|
+ raise CommandSyntaxError unless /\A(\d+)?\z/ =~ args
+ uri = URI(REDMINE_BASE+'/projects/ruby-master/issues.json?'+URI.encode_www_form(@query.dup.merge('page' => ($1 ? $1.to_i : 1))))
+ # puts uri
+ res = JSON(uri.read($openuri_options))
+ @issues = issues = res["issues"]
+ from = res["offset"] + 1
+ total = res["total_count"]
+ closed = issues.count { |x, _| x["status"]["name"] == "Closed" }
+ to = from + issues.size - 1
+ puts "#{from}-#{to} / #{total} (closed: #{closed})"
+ issues.each_with_index do |x, i|
+ id = "##{x["id"]}".color(*PRIORITIES[x["priority"]["name"]], bold: x["status"]["name"] == "Closed")
+ puts "#{'%2d' % i} #{id} #{x["priority"]["name"][0]} #{status_char(x["status"])} #{x["subject"][0,80]}"
+ end
+ },
+
+ "show" => proc{|args|
+ if /\A(\d+)\z/ =~ args
+ id = $1.to_i
+ id = @issues[id]["id"] if @issues && id < @issues.size
+ @issue = id
+ elsif @issue
+ id = @issue
+ else
+ raise CommandSyntaxError
+ end
+ uri = "#{REDMINE_BASE}/issues/#{id}"
+ uri = URI(uri+".json?include=children,attachments,relations,changesets,journals")
+ res = JSON(uri.read($openuri_options))
+ i = res["issue"]
+ unless i["changesets"]
+ abort "You don't have view_changesets permission"
+ end
+ unless i["custom_fields"]
+ puts "The specified ticket \##{@issue} seems to be a feature ticket"
+ @issue = nil
+ next
+ end
+ id = "##{i["id"]}".color(*PRIORITIES[i["priority"]["name"]])
+ sio = StringIO.new
+ sio.set_encoding("utf-8")
+ sio.puts <<eom
+#{i["subject"].color(bold: true, underscore: true)}
+#{i["project"]["name"]} [#{i["tracker"]["name"]} #{id}] #{i["status"]["name"]} (#{i["created_on"]})
+author: #{i["author"]["name"]}
+assigned: #{i["assigned_to"].to_h["name"]}
+eom
+ i["custom_fields"].each do |x|
+ sio.puts "%-10s: %s" % [x["name"], x["value"]]
+ end
+ #res["attachments"].each do |x|
+ #end
+ sio.puts i["description"]
+ sio.puts
+ sio.puts "= changesets".color(bold: true, underscore: true)
+ @changesets = []
+ i["changesets"].each do |x|
+ @changesets << x["revision"]
+ sio.puts "== #{x["revision"]} #{x["committed_on"]} #{x["user"]["name"] rescue nil}".color(bold: true, underscore: true)
+ sio.puts x["comments"]
+ end
+ @changesets = @changesets.sort.uniq
+ if i["journals"] && !i["journals"].empty?
+ sio.puts "= journals".color(bold: true, underscore: true)
+ i["journals"].each do |x|
+ sio.puts "== #{x["user"]["name"]} (#{x["created_on"]})".color(bold: true, underscore: true)
+ x["details"].each do |y|
+ sio.puts JSON(y)
+ end
+ sio.puts x["notes"]
+ end
+ end
+ more(sio)
+ },
+
+ "rel" => proc{|args|
+ # this feature requires custom redmine which allows add_related_issue API
+ case args
+ when /\A\h{7,40}\z/ # Git
+ rev = args
+ uri = URI("#{REDMINE_BASE}/projects/ruby-master/repository/git/revisions/#{rev}/issues.json")
+ else
+ raise CommandSyntaxError
+ end
+ unless @issue
+ puts "ticket not selected"
+ next
+ end
+
+ Net::HTTP.start(uri.host, uri.port, http_options) do |http|
+ res = http.post(uri.path, "issue_id=#@issue",
+ 'X-Redmine-API-Key' => REDMINE_API_KEY)
+ begin
+ res.value
+ rescue
+ if $!.respond_to?(:response) && $!.response.is_a?(Net::HTTPConflict)
+ $stderr.puts "the revision has already related to the ticket"
+ else
+ $stderr.puts "#{$!.class}: #{$!.message}\n\ndeployed redmine doesn't have https://github.com/ruby/bugs.ruby-lang.org/commit/01fbba60d68cb916ddbccc8a8710e68c5217171d\nask naruse or hsbt"
+ end
+ next
+ end
+ puts res.body
+ @changesets << rev
+ class << @changesets
+ remove_method(:validated) rescue nil
+ end
+ end
+ },
+
+ "backport" => proc{|args|
+ # this feature implies backport command which wraps tool/merger.rb
+ raise CommandSyntaxError unless args.empty?
+ unless @issue
+ puts "ticket not selected"
+ next
+ end
+ puts backport_command_string
+ },
+
+ "done" => proc{|args|
+ raise CommandSyntaxError unless /\A(\d+)?(?: *by (\h+))?(?:\s*-- +(.*))?\z/ =~ args
+ notes = $3
+ notes.strip! if notes
+ rev = $2
+ if $1
+ i = $1.to_i
+ i = @issues[i]["id"] if @issues && i < @issues.size
+ @issue = i
+ end
+ unless @issue
+ puts "ticket not selected"
+ next
+ end
+
+ if rev.nil? && log = find_git_log("##@issue]")
+ /^commit (?<rev>\h{40})$/ =~ log
+ end
+ if log && rev
+ str = log[/merge revision\(s\) ([^:]+)(?=:)/]
+ if str
+ str.sub!(/\Amerge/, 'merged')
+ str.gsub!(/\h{40}/, 'commit:\0')
+ str = "ruby_#{TARGET_VERSION.tr('.','_')} commit:#{rev} #{str}."
+ else
+ str = "ruby_#{TARGET_VERSION.tr('.','_')} commit:#{rev}."
+ end
+ if notes
+ str << "\n"
+ str << notes
+ end
+ notes = str
+ elsif rev && has_commit(rev, "ruby_#{TARGET_VERSION.tr('.','_')}")
+ # Backport commit's log doesn't have the issue number.
+ # Instead of that manually it's provided.
+ notes = "ruby_#{TARGET_VERSION.tr('.','_')} commit:#{rev}."
+ else
+ puts "no commit is found whose log include ##@issue"
+ next
+ end
+ puts notes
+
+ uri = URI("#{REDMINE_BASE}/issues/#{@issue}.json")
+ Net::HTTP.start(uri.host, uri.port, http_options) do |http|
+ res = http.get(uri.path)
+ data = JSON(res.body)
+ h = data["issue"]["custom_fields"].find{|x|x["id"]==5}
+ if h and val = h["value"] and val != ""
+ case val[/(?:\A|, )#{Regexp.quote TARGET_VERSION}: ([^,]+)/, 1]
+ when 'REQUIRED', 'UNKNOWN', 'DONTNEED', 'WONTFIX'
+ val[$~.offset(1)[0]...$~.offset(1)[1]] = 'DONE'
+ when 'DONE' # , /\A\d+\z/
+ puts 'already backport is done'
+ next # already done
+ when nil
+ val << ", #{TARGET_VERSION}: DONE"
+ else
+ raise "unknown status '#$1'"
+ end
+ else
+ val = "#{TARGET_VERSION}: DONE"
+ end
+
+ data = { "issue" => { "custom_fields" => [ {"id"=>5, "value" => val} ] } }
+ data['issue']['notes'] = notes if notes
+ res = http.put(uri.path, JSON(data),
+ 'X-Redmine-API-Key' => REDMINE_API_KEY,
+ 'Content-Type' => 'application/json')
+ res.value
+
+ show_last_journal(http, uri)
+ end
+ },
+
+ "close" => proc{|args|
+ raise CommandSyntaxError unless /\A(\d+)?\z/ =~ args
+ if $1
+ i = $1.to_i
+ i = @issues[i]["id"] if @issues && i < @issues.size
+ @issue = i
+ end
+ unless @issue
+ puts "ticket not selected"
+ next
+ end
+
+ uri = URI("#{REDMINE_BASE}/issues/#{@issue}.json")
+ Net::HTTP.start(uri.host, uri.port, http_options) do |http|
+ data = { "issue" => { "status_id" => STATUS_CLOSE } }
+ res = http.put(uri.path, JSON(data),
+ 'X-Redmine-API-Key' => REDMINE_API_KEY,
+ 'Content-Type' => 'application/json')
+ res.value
+
+ show_last_journal(http, uri)
+ end
+ },
+
+ "last" => proc{|args|
+ raise CommandSyntaxError unless /\A(\d+)?\z/ =~ args
+ if $1
+ i = $1.to_i
+ i = @issues[i]["id"] if @issues && i < @issues.size
+ @issue = i
+ end
+ unless @issue
+ puts "ticket not selected"
+ next
+ end
+
+ uri = URI("#{REDMINE_BASE}/issues/#{@issue}.json")
+ Net::HTTP.start(uri.host, uri.port, http_options) do |http|
+ show_last_journal(http, uri)
+ end
+ },
+
+ "!" => proc{|args|
+ system(args.strip)
+ },
+
+ "quit" => proc{|args|
+ raise CommandSyntaxError unless args.empty?
+ exit
+ },
+ "exit" => "quit",
+
+ "help" => proc{|args|
+ puts 'ls [PAGE] '.color(bold: true) + ' show all required tickets'
+ puts '[show] TICKET '.color(bold: true) + ' show the detail of the TICKET, and select it'
+ puts 'backport '.color(bold: true) + ' show the option of selected ticket for merger.rb'
+ puts 'rel REVISION '.color(bold: true) + ' add the selected ticket as related to the REVISION'
+ puts 'done [TICKET] [-- NOTE]'.color(bold: true) + ' set Backport field of the TICKET to DONE'
+ puts 'close [TICKET] '.color(bold: true) + ' close the TICKET'
+ puts 'last [TICKET] '.color(bold: true) + ' show the last journal of the TICKET'
+ puts '! COMMAND '.color(bold: true) + ' execute COMMAND'
+ }
+}
+list = Abbrev.abbrev(commands.keys)
+
+@issues = nil
+@issue = nil
+@changesets = nil
+while true
+ begin
+ l = Reline.readline "#{('#' + @issue.to_s).color(bold: true) if @issue}> "
+ rescue Interrupt
+ break
+ end
+ break unless l
+ cmd, args = l.strip.split(/\s+|\b/, 2)
+ next unless cmd
+ if (!args || args.empty?) && /\A\d+\z/ =~ cmd
+ args = cmd
+ cmd = "show"
+ end
+ cmd = list[cmd]
+ if commands[cmd].is_a? String
+ cmd = list[commands[cmd]]
+ end
+ begin
+ if cmd
+ commands[cmd].call(args)
+ else
+ raise CommandSyntaxError
+ end
+ rescue CommandSyntaxError
+ puts "error #{l.inspect}"
+ end
+end
diff --git a/tool/release.sh b/tool/release.sh
new file mode 100755
index 0000000000..0988dc2a67
--- /dev/null
+++ b/tool/release.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+# Bash version 3.2+ is required for regexp
+
+EXTS='.tar.gz .tar.bz2 .tar.xz .zip'
+
+ver=$1
+if [[ $ver =~ ^([1-9]\.[0-9])\.([0-9]|[1-9][0-9]|0-(preview[1-9]|rc[1-9]))$ ]]; then
+ :
+else
+ echo $ver is not valid release version
+ exit 1
+fi
+
+short=${BASH_REMATCH[1]}
+echo $ver
+echo $short
+for ext in $EXTS; do
+ aws --profile ruby s3 cp s3://ftp.r-l.o/pub/tmp/ruby-$ver-draft$ext s3://ftp.r-l.o/pub/ruby/$short/ruby-$ver$ext
+done
diff --git a/tool/releng/gen-mail.rb b/tool/releng/gen-mail.rb
new file mode 100755
index 0000000000..b958a64e65
--- /dev/null
+++ b/tool/releng/gen-mail.rb
@@ -0,0 +1,50 @@
+#!/usr/bin/env ruby
+require "open-uri"
+require "yaml"
+
+lang = ARGV.shift
+unless lang
+ abort "usage: #$1 {en,ja} | pbcopy"
+end
+
+# Confirm current directory is www.ruby-lang.org's working directory
+def confirm_w_r_l_o_wd
+ File.foreach('.git/config') do |line|
+ return true if line.include?('git@github.com:ruby/www.ruby-lang.org.git')
+ end
+ abort "Run this script in www.ruby-lang.org's working directory"
+end
+confirm_w_r_l_o_wd
+
+releases = YAML.load_file('_data/releases.yml')
+
+url = "https://hackmd.io/@naruse/ruby-relnote-#{lang}/download"
+src = URI(url).read
+src.gsub!(/[ \t]+$/, "")
+src.sub!(/(?<!\n)\z/, "\n")
+src.sub!(/^breaks: false\n/, '')
+
+if /^\{% assign release = site.data.releases \| where: "version", "([^"]+)" \| first %\}/ =~ src
+ version = $1
+else
+ abort %[#{url} doesn't include `{% assign release = site.data.releases | where: "version", "<version>" | first %}`]
+end
+
+release = releases.find{|rel|rel['version'] == version}
+unless release
+ abort "#{version} is not found in '_data/releases.yml'"
+end
+
+src.gsub!(/^{% assign .*\n/, '')
+src.gsub!(/\{\{(.*?)\}\}/) do
+ var = $1.strip
+ case var
+ when /\Arelease\.(.*)/
+ val = release.dig(*$1.split('.'))
+ raise "invalid variable '#{var}'" unless val
+ else
+ raise "unknown variable '#{var}'"
+ end
+ val
+end
+puts src
diff --git a/tool/releng/gen-release-note.rb b/tool/releng/gen-release-note.rb
new file mode 100755
index 0000000000..5afd11b796
--- /dev/null
+++ b/tool/releng/gen-release-note.rb
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+require 'open-uri'
+require 'time'
+require 'yaml'
+
+# Confirm current directory is www.ruby-lang.org's working directory
+def confirm_w_r_l_o_wd
+ File.foreach('.git/config') do |line|
+ return true if line.include?('git@github.com:ruby/www.ruby-lang.org.git')
+ end
+ abort "Run this script in www.ruby-lang.org's working directory"
+end
+confirm_w_r_l_o_wd
+
+%w[
+ https://hackmd.io/@naruse/ruby-relnote-en/download
+ https://hackmd.io/@naruse/ruby-relnote-ja/download
+].each do |url|
+ src = URI(url).read
+ src.gsub!(/[ \t]+$/, "")
+ src.sub!(/\s+\z/, "\n")
+ src.sub!(/^breaks: false\n/, '')
+ if /^\{% assign release = site.data.releases \| where: "version", "([^"]+)" \| first %\}/ =~ src
+ version = $1
+ else
+ abort %[#{url} doesn't include `{% assign release = site.data.releases | where: "version", "<version>" | first %}`]
+ end
+ puts "#{url} -> #{version}"
+
+
+ # Write release note article
+ path = Time.parse(src[/^date: (.*)/, 1]).
+ strftime("./#{src[/^lang: (\w+)/, 1]}/news/_posts/%Y-%m-%d-ruby-#{version.tr('.', '-')}-released.md")
+ puts path
+ File.write(path, src)
+end
diff --git a/tool/releng/update-www-meta.rb b/tool/releng/update-www-meta.rb
new file mode 100755
index 0000000000..8a5651dcd0
--- /dev/null
+++ b/tool/releng/update-www-meta.rb
@@ -0,0 +1,213 @@
+#!/usr/bin/env ruby
+require "open-uri"
+require "yaml"
+
+class Tarball
+ attr_reader :version, :size, :sha1, :sha256, :sha512
+
+ def initialize(version, url, size, sha1, sha256, sha512)
+ @url = url
+ @size = size
+ @sha1 = sha1
+ @sha256 = sha256
+ @sha512 = sha512
+ @version = version
+ @xy = version[/\A\d+\.\d+/]
+ end
+
+ def gz?; @url.end_with?('.gz'); end
+ def zip?; @url.end_with?('.zip'); end
+ def xz?; @url.end_with?('.xz'); end
+
+ def ext; @url[/(?:zip|tar\.(?:gz|xz))\z/]; end
+
+ def to_md
+ <<eom
+* <https://cache.ruby-lang.org/pub/ruby/#{@xy}/ruby-#{@version}.#{ext}>
+
+ SIZE: #{@size} bytes
+ SHA1: #{@sha1}
+ SHA256: #{@sha256}
+ SHA512: #{@sha512}
+eom
+ end
+
+ # * /home/naruse/obj/ruby-trunk/tmp/ruby-2.6.0-preview3.tar.gz
+ # SIZE: 17116009 bytes
+ # SHA1: 21f62c369661a2ab1b521fd2fa8191a4273e12a1
+ # SHA256: 97cea8aa63dfa250ba6902b658a7aa066daf817b22f82b7ee28f44aec7c2e394
+ # SHA512: 1e2042324821bb4e110af7067f52891606dcfc71e640c194ab1c117f0b941550e0b3ac36ad3511214ac80c536b9e5cfaf8789eec74cf56971a832ea8fc4e6d94
+ def self.parse(wwwdir, version)
+ unless /\A(\d+)\.(\d+)\.(\d+)(?:-(?:preview|rc)\d+)?\z/ =~ version
+ raise "unexpected version string '#{version}'"
+ end
+ x = $1.to_i
+ y = $2.to_i
+ z = $3.to_i
+ # previous tag for git diff --shortstat
+ # It's only for x.y.0 release
+ if z != 0
+ prev_tag = nil
+ elsif y != 0
+ prev_tag = "v#{x}_#{y-1}_0"
+ prev_ver = "#{x}.#{y-1}.0"
+ elsif x == 3 && y == 0 && z == 0
+ prev_tag = "v2_7_0"
+ prev_ver = "2.7.0"
+ else
+ raise "unexpected version for prev_ver '#{version}'"
+ end
+
+ uri = "https://cache.ruby-lang.org/pub/tmp/ruby-info-#{version}-draft.yml"
+ info = YAML.load(URI(uri).read)
+ if info.size != 1
+ raise "unexpected info.yml '#{uri}'"
+ end
+ tarballs = []
+ info[0]["size"].each_key do |ext|
+ url = info[0]["url"][ext]
+ size = info[0]["size"][ext]
+ sha1 = info[0]["sha1"][ext]
+ sha256 = info[0]["sha256"][ext]
+ sha512 = info[0]["sha512"][ext]
+ tarball = Tarball.new(version, url, size, sha1, sha256, sha512)
+ tarballs << tarball
+ end
+
+ if prev_tag
+ # show diff shortstat
+ tag = "v#{version.gsub(/[.\-]/, '_')}"
+ rubydir = File.expand_path(File.join(__FILE__, '../../../'))
+ puts %`git -C #{rubydir} diff --shortstat #{prev_tag}..#{tag}`
+ stat = `git -C #{rubydir} diff --shortstat #{prev_tag}..#{tag}`
+ files_changed, insertions, deletions = stat.scan(/\d+/)
+ end
+
+ xy = version[/\A\d+\.\d+/]
+ #puts "## Download\n\n"
+ #tarballs.each do |tarball|
+ # puts tarball.to_md
+ #end
+ update_branches_yml(version, xy, wwwdir)
+ update_downloads_yml(version, xy, wwwdir)
+ update_releases_yml(version, xy, tarballs, wwwdir, files_changed, insertions, deletions)
+ end
+
+ def self.update_branches_yml(ver, xy, wwwdir)
+ filename = "_data/branches.yml"
+ data = File.read(File.join(wwwdir, filename))
+ if data.include?("\n- name: #{xy}\n")
+ data.sub!(/\n- name: #{Regexp.escape(xy)}\n(?: .*\n)*/) do |node|
+ unless ver.include?("-")
+ # assume this is X.Y.0 release
+ node.sub!(/^ status: preview\n/, " status: normal maintenance\n")
+ node.sub!(/^ date:\n/, " date: #{Time.now.year}-12-25\n")
+ end
+ node
+ end
+ else
+ if ver.include?("-")
+ status = "preview"
+ year = nil
+ else
+ status = "normal maintenance"
+ year = Time.now.year
+ end
+ entry = <<eom
+- name: #{xy}
+ status: #{status}
+ date:#{ year && " #{year}-12-25" }
+ eol_date:
+
+eom
+ data.sub!(/(?=^- name)/, entry)
+ end
+ File.write(File.join(wwwdir, filename), data)
+ end
+
+ def self.update_downloads_yml(ver, xy, wwwdir)
+ filename = "_data/downloads.yml"
+ data = File.read(File.join(wwwdir, filename))
+
+ if /^preview:\n\n(?: .*\n)* - #{Regexp.escape(xy)}\./ =~ data
+ if ver.include?("-")
+ data.sub!(/^ - #{Regexp.escape(xy)}\..*/, " - #{ver}")
+ else
+ data.sub!(/^ - #{Regexp.escape(xy)}\..*\n/, "")
+ data.sub!(/(?<=^stable:\n\n)/, " - #{ver}\n")
+ end
+ else
+ unless data.sub!(/^ - #{Regexp.escape(xy)}\..*/, " - #{ver}")
+ if ver.include?("-")
+ data.sub!(/(?<=^preview:\n\n)/, " - #{ver}\n")
+ else
+ data.sub!(/(?<=^stable:\n\n)/, " - #{ver}\n")
+ end
+ end
+ end
+ File.write(File.join(wwwdir, filename), data)
+ end
+
+ def self.update_releases_yml(ver, xy, ary, wwwdir, files_changed, insertions, deletions)
+ filename = "_data/releases.yml"
+ data = File.read(File.join(wwwdir, filename))
+
+ date = Time.now.utc # use utc to use previous day in midnight
+ entry = <<eom
+- version: #{ver}
+ tag: v#{ver.tr('-.', '_')}
+ date: #{date.strftime("%Y-%m-%d")}
+ post: /en/news/#{date.strftime("%Y/%m/%d")}/ruby-#{ver.tr('.', '-')}-released/
+ stats:
+ files_changed: #{files_changed}
+ insertions: #{insertions}
+ deletions: #{deletions}
+ url:
+ gz: https://cache.ruby-lang.org/pub/ruby/#{xy}/ruby-#{ver}.tar.gz
+ zip: https://cache.ruby-lang.org/pub/ruby/#{xy}/ruby-#{ver}.zip
+ xz: https://cache.ruby-lang.org/pub/ruby/#{xy}/ruby-#{ver}.tar.xz
+ size:
+ gz: #{ary.find{|x|x.gz? }.size}
+ zip: #{ary.find{|x|x.zip?}.size}
+ xz: #{ary.find{|x|x.xz? }.size}
+ sha1:
+ gz: #{ary.find{|x|x.gz? }.sha1}
+ zip: #{ary.find{|x|x.zip?}.sha1}
+ xz: #{ary.find{|x|x.xz? }.sha1}
+ sha256:
+ gz: #{ary.find{|x|x.gz? }.sha256}
+ zip: #{ary.find{|x|x.zip?}.sha256}
+ xz: #{ary.find{|x|x.xz? }.sha256}
+ sha512:
+ gz: #{ary.find{|x|x.gz? }.sha512}
+ zip: #{ary.find{|x|x.zip?}.sha512}
+ xz: #{ary.find{|x|x.xz? }.sha512}
+eom
+
+ if data.include?("\n- version: #{ver}\n")
+ elsif data.sub!(/\n# #{Regexp.escape(xy)} series\n/, "\\&\n#{entry}")
+ else
+ data.sub!(/^$/, "\n# #{xy} series\n\n#{entry}")
+ end
+ File.write(File.join(wwwdir, filename), data)
+ end
+end
+
+# Confirm current directory is www.ruby-lang.org's working directory
+def confirm_w_r_l_o_wd
+ File.foreach('.git/config') do |line|
+ return true if line.include?('git@github.com:ruby/www.ruby-lang.org.git')
+ end
+ abort "Run this script in www.ruby-lang.org's working directory"
+end
+
+def main
+ if ARGV.size != 1
+ abort "usage: #$1 <version>"
+ end
+ confirm_w_r_l_o_wd
+ version = ARGV.shift
+ Tarball.parse(Dir.pwd, version)
+end
+
+main
diff --git a/tool/rmdirs b/tool/rmdirs
new file mode 100755
index 0000000000..76c4a39cb1
--- /dev/null
+++ b/tool/rmdirs
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+# Script used by configure to delete directories recursively.
+
+for dir do
+ while rmdir "$dir" >/dev/null 2>&1 &&
+ parent=`expr "$dir" : '\(.*\)/[^/][^/]*'`; do
+ case "$parent" in
+ . | .. | "$dir") break;;
+ *) dir="$parent";;
+ esac
+ done
+done
+true
diff --git a/tool/ruby_vm/controllers/application_controller.rb b/tool/ruby_vm/controllers/application_controller.rb
new file mode 100644
index 0000000000..25c10947ed
--- /dev/null
+++ b/tool/ruby_vm/controllers/application_controller.rb
@@ -0,0 +1,25 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/dumper'
+require_relative '../models/instructions'
+require_relative '../models/typemap'
+require_relative '../loaders/vm_opts_h'
+
+class ApplicationController
+ def generate i, destdir
+ path = Pathname.new i
+ dst = destdir ? Pathname.new(destdir).join(i) : Pathname.new(i)
+ dumper = RubyVM::Dumper.new dst
+ return [path, dumper]
+ end
+end
diff --git a/tool/ruby_vm/helpers/c_escape.rb b/tool/ruby_vm/helpers/c_escape.rb
new file mode 100644
index 0000000000..34fafd1e34
--- /dev/null
+++ b/tool/ruby_vm/helpers/c_escape.rb
@@ -0,0 +1,128 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require 'securerandom'
+
+module RubyVM::CEscape
+ module_function
+
+ # generate comment, with escaps.
+ def commentify str
+ return "/* #{str.b.gsub('*/', '*\\/').gsub('/*', '/\\*')} */"
+ end
+
+ # Mimic gensym of CL.
+ def gensym prefix = 'gensym_'
+ return as_tr_cpp "#{prefix}#{SecureRandom.uuid}"
+ end
+
+ # Mimic AS_TR_CPP() of autoconf.
+ def as_tr_cpp name
+ q = name.b
+ q.gsub! %r/[^a-zA-Z0-9_]/m, '_'
+ q.gsub! %r/_+/, '_'
+ return q
+ end
+
+ # Section 6.10.4 of ISO/IEC 9899:1999 specifies that the file name used for
+ # #line directive shall be a "character string literal". So this is needed.
+ #
+ # I'm not sure how many chars are allowed here, though. The standard
+ # specifies 4095 chars at most, after string concatenation (section 5.2.4.1).
+ # But it is easy to have a path that is longer than that.
+ #
+ # Here we ignore the standard. Just create single string literal of any
+ # needed length.
+ def rstring2cstr str
+ # I believe this is the fastest implementation done in pure-ruby.
+ # Constants cached, gsub skips block evaluation, string literal optimized.
+ buf = str.b
+ buf.gsub! %r/./nm, RString2CStr
+ return %'"#{buf}"'
+ end
+
+ RString2CStr = {
+ "\x00"=> "\\0", "\x01"=> "\\x1", "\x02"=> "\\x2", "\x03"=> "\\x3",
+ "\x04"=> "\\x4", "\x05"=> "\\x5", "\x06"=> "\\x6", "\a"=> "\\a",
+ "\b"=> "\\b", "\t"=> "\\t", "\n"=> "\\n", "\v"=> "\\v",
+ "\f"=> "\\f", "\r"=> "\\r", "\x0E"=> "\\xe", "\x0F"=> "\\xf",
+ "\x10"=>"\\x10", "\x11"=>"\\x11", "\x12"=>"\\x12", "\x13"=>"\\x13",
+ "\x14"=>"\\x14", "\x15"=>"\\x15", "\x16"=>"\\x16", "\x17"=>"\\x17",
+ "\x18"=>"\\x18", "\x19"=>"\\x19", "\x1A"=>"\\x1a", "\e"=>"\\x1b",
+ "\x1C"=>"\\x1c", "\x1D"=>"\\x1d", "\x1E"=>"\\x1e", "\x1F"=>"\\x1f",
+ " "=> " ", "!"=> "!", "\""=> "\\\"", "#"=> "#",
+ "$"=> "$", "%"=> "%", "&"=> "&", "'"=> "'",
+ "("=> "(", ")"=> ")", "*"=> "*", "+"=> "+",
+ ","=> ",", "-"=> "-", "."=> ".", "/"=> "/",
+ "0"=> "0", "1"=> "1", "2"=> "2", "3"=> "3",
+ "4"=> "4", "5"=> "5", "6"=> "6", "7"=> "7",
+ "8"=> "8", "9"=> "9", ":"=> ":", ";"=> ";",
+ "<"=> "<", "="=> "=", ">"=> ">", "?"=> "?",
+ "@"=> "@", "A"=> "A", "B"=> "B", "C"=> "C",
+ "D"=> "D", "E"=> "E", "F"=> "F", "G"=> "G",
+ "H"=> "H", "I"=> "I", "J"=> "J", "K"=> "K",
+ "L"=> "L", "M"=> "M", "N"=> "N", "O"=> "O",
+ "P"=> "P", "Q"=> "Q", "R"=> "R", "S"=> "S",
+ "T"=> "T", "U"=> "U", "V"=> "V", "W"=> "W",
+ "X"=> "X", "Y"=> "Y", "Z"=> "Z", "["=> "[",
+ "\\"=> "\\\\", "]"=> "]", "^"=> "^", "_"=> "_",
+ "`"=> "`", "a"=> "a", "b"=> "b", "c"=> "c",
+ "d"=> "d", "e"=> "e", "f"=> "f", "g"=> "g",
+ "h"=> "h", "i"=> "i", "j"=> "j", "k"=> "k",
+ "l"=> "l", "m"=> "m", "n"=> "n", "o"=> "o",
+ "p"=> "p", "q"=> "q", "r"=> "r", "s"=> "s",
+ "t"=> "t", "u"=> "u", "v"=> "v", "w"=> "w",
+ "x"=> "x", "y"=> "y", "z"=> "z", "{"=> "{",
+ "|"=> "|", "}"=> "}", "~"=> "~", "\x7F"=>"\\x7f",
+ "\x80"=>"\\x80", "\x81"=>"\\x81", "\x82"=>"\\x82", "\x83"=>"\\x83",
+ "\x84"=>"\\x84", "\x85"=>"\\x85", "\x86"=>"\\x86", "\x87"=>"\\x87",
+ "\x88"=>"\\x88", "\x89"=>"\\x89", "\x8A"=>"\\x8a", "\x8B"=>"\\x8b",
+ "\x8C"=>"\\x8c", "\x8D"=>"\\x8d", "\x8E"=>"\\x8e", "\x8F"=>"\\x8f",
+ "\x90"=>"\\x90", "\x91"=>"\\x91", "\x92"=>"\\x92", "\x93"=>"\\x93",
+ "\x94"=>"\\x94", "\x95"=>"\\x95", "\x96"=>"\\x96", "\x97"=>"\\x97",
+ "\x98"=>"\\x98", "\x99"=>"\\x99", "\x9A"=>"\\x9a", "\x9B"=>"\\x9b",
+ "\x9C"=>"\\x9c", "\x9D"=>"\\x9d", "\x9E"=>"\\x9e", "\x9F"=>"\\x9f",
+ "\xA0"=>"\\xa0", "\xA1"=>"\\xa1", "\xA2"=>"\\xa2", "\xA3"=>"\\xa3",
+ "\xA4"=>"\\xa4", "\xA5"=>"\\xa5", "\xA6"=>"\\xa6", "\xA7"=>"\\xa7",
+ "\xA8"=>"\\xa8", "\xA9"=>"\\xa9", "\xAA"=>"\\xaa", "\xAB"=>"\\xab",
+ "\xAC"=>"\\xac", "\xAD"=>"\\xad", "\xAE"=>"\\xae", "\xAF"=>"\\xaf",
+ "\xB0"=>"\\xb0", "\xB1"=>"\\xb1", "\xB2"=>"\\xb2", "\xB3"=>"\\xb3",
+ "\xB4"=>"\\xb4", "\xB5"=>"\\xb5", "\xB6"=>"\\xb6", "\xB7"=>"\\xb7",
+ "\xB8"=>"\\xb8", "\xB9"=>"\\xb9", "\xBA"=>"\\xba", "\xBB"=>"\\xbb",
+ "\xBC"=>"\\xbc", "\xBD"=>"\\xbd", "\xBE"=>"\\xbe", "\xBF"=>"\\xbf",
+ "\xC0"=>"\\xc0", "\xC1"=>"\\xc1", "\xC2"=>"\\xc2", "\xC3"=>"\\xc3",
+ "\xC4"=>"\\xc4", "\xC5"=>"\\xc5", "\xC6"=>"\\xc6", "\xC7"=>"\\xc7",
+ "\xC8"=>"\\xc8", "\xC9"=>"\\xc9", "\xCA"=>"\\xca", "\xCB"=>"\\xcb",
+ "\xCC"=>"\\xcc", "\xCD"=>"\\xcd", "\xCE"=>"\\xce", "\xCF"=>"\\xcf",
+ "\xD0"=>"\\xd0", "\xD1"=>"\\xd1", "\xD2"=>"\\xd2", "\xD3"=>"\\xd3",
+ "\xD4"=>"\\xd4", "\xD5"=>"\\xd5", "\xD6"=>"\\xd6", "\xD7"=>"\\xd7",
+ "\xD8"=>"\\xd8", "\xD9"=>"\\xd9", "\xDA"=>"\\xda", "\xDB"=>"\\xdb",
+ "\xDC"=>"\\xdc", "\xDD"=>"\\xdd", "\xDE"=>"\\xde", "\xDF"=>"\\xdf",
+ "\xE0"=>"\\xe0", "\xE1"=>"\\xe1", "\xE2"=>"\\xe2", "\xE3"=>"\\xe3",
+ "\xE4"=>"\\xe4", "\xE5"=>"\\xe5", "\xE6"=>"\\xe6", "\xE7"=>"\\xe7",
+ "\xE8"=>"\\xe8", "\xE9"=>"\\xe9", "\xEA"=>"\\xea", "\xEB"=>"\\xeb",
+ "\xEC"=>"\\xec", "\xED"=>"\\xed", "\xEE"=>"\\xee", "\xEF"=>"\\xef",
+ "\xF0"=>"\\xf0", "\xF1"=>"\\xf1", "\xF2"=>"\\xf2", "\xF3"=>"\\xf3",
+ "\xF4"=>"\\xf4", "\xF5"=>"\\xf5", "\xF6"=>"\\xf6", "\xF7"=>"\\xf7",
+ "\xF8"=>"\\xf8", "\xF9"=>"\\xf9", "\xFA"=>"\\xfa", "\xFB"=>"\\xfb",
+ "\xFC"=>"\\xfc", "\xFD"=>"\\xfd", "\xFE"=>"\\xfe", "\xFF"=>"\\xff",
+ }.freeze
+ private_constant :RString2CStr
+end
+
+unless defined? ''.b
+ class String
+ def b
+ return dup.force_encoding 'binary'
+ end
+ end
+end
diff --git a/tool/ruby_vm/helpers/dumper.rb b/tool/ruby_vm/helpers/dumper.rb
new file mode 100644
index 0000000000..98104f4b92
--- /dev/null
+++ b/tool/ruby_vm/helpers/dumper.rb
@@ -0,0 +1,113 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require 'pathname'
+require 'erb'
+require_relative 'c_escape'
+
+class RubyVM::Dumper
+ include RubyVM::CEscape
+ private
+
+ def new_binding
+ # This `eval 'binding'` does not return the current binding
+ # but creates one on top of it.
+ return eval 'binding'
+ end
+
+ def new_erb spec
+ path = Pathname.new(__FILE__)
+ path = (path.relative_path_from(Pathname.pwd) rescue path).dirname
+ path += '../views'
+ path += spec
+ src = path.read mode: 'rt:utf-8:utf-8'
+ rescue Errno::ENOENT
+ raise "don't know how to generate #{path}"
+ else
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
+ erb = ERB.new(src, trim_mode: '%-')
+ else
+ erb = ERB.new(src, nil, '%-')
+ end
+ erb.filename = path.to_path
+ return erb
+ end
+
+ def finderb spec
+ return @erb.fetch spec do |k|
+ erb = new_erb k
+ @erb[k] = erb
+ end
+ end
+
+ def replace_pragma_line str, lineno
+ if /#(\s*)pragma RubyVM reset source\n/ =~ str then
+ return "##{$1}line #{lineno + 2} #{@file}\n"
+ else
+ return str
+ end
+ end
+
+ def local_variable_set bnd, var, val
+ eval '__locals__ ||= {}', bnd
+ locals = eval '__locals__', bnd
+ locals[var] = val
+ eval "#{var} = __locals__[:#{var}]", bnd
+ test = eval "#{var}", bnd
+ raise unless test == val
+ end
+
+ public
+
+ def do_render source, locals
+ erb = finderb source
+ bnd = @empty.dup
+ locals.each_pair do |k, v|
+ local_variable_set bnd, k, v
+ end
+ return erb.result bnd
+ end
+
+ def replace_pragma str
+ return str \
+ . each_line \
+ . with_index \
+ . map {|i, j| replace_pragma_line i, j } \
+ . join
+ end
+
+ def initialize dst
+ @erb = {}
+ @empty = new_binding
+ @file = cstr dst.to_path
+ end
+
+ def render partial, opts = { :locals => {} }
+ return do_render "_#{partial}.erb", opts[:locals]
+ end
+
+ def generate template
+ str = do_render "#{template}.erb", {}
+ return replace_pragma str
+ end
+
+ private
+
+ # view helpers
+
+ alias cstr rstring2cstr
+ alias comm commentify
+
+ def render_c_expr expr
+ render 'c_expr', locals: { expr: expr, }
+ end
+end
diff --git a/tool/ruby_vm/helpers/scanner.rb b/tool/ruby_vm/helpers/scanner.rb
new file mode 100644
index 0000000000..ef6de8120e
--- /dev/null
+++ b/tool/ruby_vm/helpers/scanner.rb
@@ -0,0 +1,53 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require 'pathname'
+
+# Poor man's StringScanner.
+# Sadly https://bugs.ruby-lang.org/issues/8343 is not backported to 2.0. We
+# have to do it by hand.
+class RubyVM::Scanner
+ attr_reader :__FILE__
+ attr_reader :__LINE__
+
+ def initialize path
+ src = Pathname.new(__FILE__)
+ src = (src.relative_path_from(Pathname.pwd) rescue src).dirname
+ src += path
+ @__LINE__ = 1
+ @__FILE__ = src.to_path
+ @str = src.read mode: 'rt:utf-8:utf-8'
+ @pos = 0
+ end
+
+ def eos?
+ return @pos >= @str.length
+ end
+
+ def scan re
+ ret = @__LINE__
+ @last_match = @str.match re, @pos
+ return unless @last_match
+ @__LINE__ += @last_match.to_s.count "\n"
+ @pos = @last_match.end 0
+ return ret
+ end
+
+ def scan! re
+ scan re or raise sprintf "parse error at %s:%d near:\n %s...", \
+ @__FILE__, @__LINE__, @str[@pos, 32]
+ end
+
+ def [] key
+ return @last_match[key]
+ end
+end
diff --git a/tool/ruby_vm/loaders/insns_def.rb b/tool/ruby_vm/loaders/insns_def.rb
new file mode 100644
index 0000000000..034905f74e
--- /dev/null
+++ b/tool/ruby_vm/loaders/insns_def.rb
@@ -0,0 +1,100 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/scanner'
+require_relative './vm_opts_h'
+
+json = []
+scanner = RubyVM::Scanner.new '../../../insns.def'
+path = scanner.__FILE__
+grammar = %r'
+ (?<comment> /[*] [^*]* [*]+ (?: [^*/] [^*]* [*]+ )* / ){0}
+ (?<keyword> typedef | extern | static | auto | register |
+ struct | union | enum ){0}
+ (?<C> (?: \g<block> | [^{}]+ )* ){0}
+ (?<block> \{ \g<ws>* \g<C> \g<ws>* \} ){0}
+ (?<ws> \g<comment> | \s ){0}
+ (?<ident> [_a-zA-Z] [0-9_a-zA-Z]* ){0}
+ (?<type> (?: \g<keyword> \g<ws>+ )* \g<ident> ){0}
+ (?<arg> \g<type> \g<ws>+ \g<ident> ){0}
+ (?<argv> (?# empty ) |
+ void |
+ (?: \.\.\. | \g<arg>) (?: \g<ws>* , \g<ws>* \g<arg> \g<ws>* )* ){0}
+ (?<pragma> \g<ws>* // \s* attr \g<ws>+
+ (?<pragma:type> \g<type> ) \g<ws>+
+ (?<pragma:name> \g<ident> ) \g<ws>*
+ = \g<ws>*
+ (?<pragma:expr> .+?; ) \g<ws>* ){0}
+ (?<insn> DEFINE_INSN(_IF\((?<insn:if>\w+)\))? \g<ws>+
+ (?<insn:name> \g<ident> ) \g<ws>*
+ [(] \g<ws>* (?<insn:opes> \g<argv> ) \g<ws>* [)] \g<ws>*
+ [(] \g<ws>* (?<insn:pops> \g<argv> ) \g<ws>* [)] \g<ws>*
+ [(] \g<ws>* (?<insn:rets> \g<argv> ) \g<ws>* [)] \g<ws>* ){0}
+'x
+
+until scanner.eos? do
+ next if scanner.scan(/\G#{grammar}\g<ws>+/o)
+ split = lambda {|v|
+ case v when /\Avoid\z/ then
+ []
+ else
+ v.split(/, */)
+ end
+ }
+
+ l1 = scanner.scan!(/\G#{grammar}\g<insn>/o)
+ name = scanner["insn:name"]
+ opt = scanner["insn:if"]
+ ope = split.(scanner["insn:opes"])
+ pop = split.(scanner["insn:pops"])
+ ret = split.(scanner["insn:rets"])
+ if ope.include?("...")
+ raise sprintf("parse error at %s:%d:%s: operands cannot be variadic",
+ scanner.__FILE__, scanner.__LINE__, name)
+ end
+
+ attrs = []
+ while l2 = scanner.scan(/\G#{grammar}\g<pragma>/o) do
+ attrs << {
+ location: [path, l2],
+ name: scanner["pragma:name"],
+ type: scanner["pragma:type"],
+ expr: scanner["pragma:expr"],
+ }
+ end
+
+ l3 = scanner.scan!(/\G#{grammar}\g<block>/o)
+ if opt.nil? || RubyVM::VmOptsH[opt]
+ json << {
+ name: name,
+ location: [path, l1],
+ signature: {
+ name: name,
+ ope: ope,
+ pop: pop,
+ ret: ret,
+ },
+ attributes: attrs,
+ expr: {
+ location: [path, l3],
+ expr: scanner["block"],
+ },
+ }
+ end
+end
+
+RubyVM::InsnsDef = json
+
+if __FILE__ == $0 then
+ require 'json'
+ JSON.dump RubyVM::InsnsDef, STDOUT
+end
diff --git a/tool/ruby_vm/loaders/opt_insn_unif_def.rb b/tool/ruby_vm/loaders/opt_insn_unif_def.rb
new file mode 100644
index 0000000000..aa6fd79e79
--- /dev/null
+++ b/tool/ruby_vm/loaders/opt_insn_unif_def.rb
@@ -0,0 +1,34 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/scanner'
+
+json = []
+scanner = RubyVM::Scanner.new '../../../defs/opt_insn_unif.def'
+path = scanner.__FILE__
+until scanner.eos? do
+ next if scanner.scan(/\G ^ (?: \#.* )? \n /x)
+ break if scanner.scan(/\G ^ __END__ $ /x)
+
+ pos = scanner.scan!(/\G (?<series> (?: [\ \t]* \w+ )+ ) \n /mx)
+ json << {
+ location: [path, pos],
+ signature: scanner["series"].strip.split
+ }
+end
+
+RubyVM::OptInsnUnifDef = json
+
+if __FILE__ == $0 then
+ require 'json'
+ JSON.dump RubyVM::OptInsnUnifDef, STDOUT
+end
diff --git a/tool/ruby_vm/loaders/opt_operand_def.rb b/tool/ruby_vm/loaders/opt_operand_def.rb
new file mode 100644
index 0000000000..29aef8a325
--- /dev/null
+++ b/tool/ruby_vm/loaders/opt_operand_def.rb
@@ -0,0 +1,56 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/scanner'
+
+json = []
+scanner = RubyVM::Scanner.new '../../../defs/opt_operand.def'
+path = scanner.__FILE__
+grammar = %r/
+ (?<comment> \# .+? \n ){0}
+ (?<ws> \g<comment> | \s ){0}
+ (?<insn> \w+ ){0}
+ (?<paren> \( (?: \g<paren> | [^()]+)* \) ){0}
+ (?<expr> (?: \g<paren> | [^(),\ \n] )+ ){0}
+ (?<remain> \g<expr> ){0}
+ (?<arg> \g<expr> ){0}
+ (?<extra> , \g<ws>* \g<remain> ){0}
+ (?<args> \g<arg> \g<extra>* ){0}
+ (?<decl> \g<insn> \g<ws>+ \g<args> \n ){0}
+/mx
+
+until scanner.eos? do
+ break if scanner.scan(/\G ^ __END__ $ /x)
+ next if scanner.scan(/\G#{grammar} \g<ws>+ /ox)
+
+ line = scanner.scan!(/\G#{grammar} \g<decl> /mox)
+ insn = scanner["insn"]
+ args = scanner["args"]
+ ary = []
+ until args.strip.empty? do
+ md = /\G#{grammar} \g<args> /mox.match(args)
+ ary << md["arg"]
+ args = md["remain"]
+ break unless args
+ end
+ json << {
+ location: [path, line],
+ signature: [insn, ary]
+ }
+end
+
+RubyVM::OptOperandDef = json
+
+if __FILE__ == $0 then
+ require 'json'
+ JSON.dump RubyVM::OptOperandDef, STDOUT
+end
diff --git a/tool/ruby_vm/loaders/vm_opts_h.rb b/tool/ruby_vm/loaders/vm_opts_h.rb
new file mode 100644
index 0000000000..3f05c270ee
--- /dev/null
+++ b/tool/ruby_vm/loaders/vm_opts_h.rb
@@ -0,0 +1,37 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/scanner'
+
+json = {}
+scanner = RubyVM::Scanner.new '../../../vm_opts.h'
+grammar = %r/
+ (?<ws> \u0020 ){0}
+ (?<key> \w+ ){0}
+ (?<value> 0|1 ){0}
+ (?<define> \G \#define \g<ws>+ OPT_\g<key> \g<ws>+ \g<value> \g<ws>*\n )
+/mx
+
+until scanner.eos? do
+ if scanner.scan grammar then
+ json[scanner['key']] = ! scanner['value'].to_i.zero? # not nonzero?
+ else
+ scanner.scan(/\G.*\n/)
+ end
+end
+
+RubyVM::VmOptsH = json
+
+if __FILE__ == $0 then
+ require 'json'
+ JSON.dump RubyVM::VmOptsH, STDOUT
+end
diff --git a/tool/ruby_vm/models/attribute.rb b/tool/ruby_vm/models/attribute.rb
new file mode 100644
index 0000000000..de35e7234a
--- /dev/null
+++ b/tool/ruby_vm/models/attribute.rb
@@ -0,0 +1,59 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative 'c_expr'
+
+class RubyVM::Attribute
+ include RubyVM::CEscape
+ attr_reader :insn, :key, :type, :expr
+
+ def initialize opts = {}
+ @insn = opts[:insn]
+ @key = opts[:name]
+ @expr = RubyVM::CExpr.new location: opts[:location], expr: opts[:expr]
+ @type = opts[:type]
+ @ope_decls = @insn.opes.map do |operand|
+ decl = operand[:decl]
+ if @key == 'comptime_sp_inc' && operand[:type] == 'CALL_DATA'
+ decl = decl.gsub('CALL_DATA', 'CALL_INFO').gsub('cd', 'ci')
+ end
+ decl
+ end
+ end
+
+ def name
+ as_tr_cpp "attr #{@key} @ #{@insn.name}"
+ end
+
+ def pretty_name
+ "attr #{type} #{key} @ #{insn.pretty_name}"
+ end
+
+ def declaration
+ if @ope_decls.empty?
+ argv = "void"
+ else
+ argv = @ope_decls.join(', ')
+ end
+ sprintf '%s %s(%s)', @type, name, argv
+ end
+
+ def definition
+ if @ope_decls.empty?
+ argv = "void"
+ else
+ argv = @ope_decls.map {|decl| "MAYBE_UNUSED(#{decl})" }.join(",\n ")
+ argv = "\n #{argv}\n" if @ope_decls.size > 1
+ end
+ sprintf "%s\n%s(%s)", @type, name, argv
+ end
+end
diff --git a/tool/ruby_vm/models/bare_instructions.rb b/tool/ruby_vm/models/bare_instructions.rb
new file mode 100755
index 0000000000..6b5f1f6cf8
--- /dev/null
+++ b/tool/ruby_vm/models/bare_instructions.rb
@@ -0,0 +1,240 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../loaders/insns_def'
+require_relative 'c_expr'
+require_relative 'typemap'
+require_relative 'attribute'
+
+class RubyVM::BareInstructions
+ attr_reader :template, :name, :opes, :pops, :rets, :decls, :expr
+
+ def initialize opts = {}
+ @template = opts[:template]
+ @name = opts[:name]
+ @loc = opts[:location]
+ @sig = opts[:signature]
+ @expr = RubyVM::CExpr.new opts[:expr]
+ @opes = typesplit @sig[:ope]
+ @pops = typesplit @sig[:pop].reject {|i| i == '...' }
+ @rets = typesplit @sig[:ret].reject {|i| i == '...' }
+ @attrs = opts[:attributes].map {|i|
+ RubyVM::Attribute.new i.merge(:insn => self)
+ }.each_with_object({}) {|a, h|
+ h[a.key] = a
+ }
+ @attrs_orig = @attrs.dup
+ check_attribute_consistency
+ predefine_attributes
+ end
+
+ def pretty_name
+ n = @sig[:name]
+ o = @sig[:ope].map{|i| i[/\S+$/] }.join ', '
+ p = @sig[:pop].map{|i| i[/\S+$/] }.join ', '
+ r = @sig[:ret].map{|i| i[/\S+$/] }.join ', '
+ return sprintf "%s(%s)(%s)(%s)", n, o, p, r
+ end
+
+ def bin
+ return "BIN(#{name})"
+ end
+
+ def call_attribute name
+ return sprintf 'attr_%s_%s(%s)', name, @name, \
+ @opes.map {|i| i[:name] }.compact.join(', ')
+ end
+
+ def has_attribute? k
+ @attrs_orig.has_key? k
+ end
+
+ def attributes
+ return @attrs \
+ . sort_by {|k, _| k } \
+ . map {|_, v| v }
+ end
+
+ def width
+ return 1 + opes.size
+ end
+
+ def declarations
+ return @variables \
+ . values \
+ . group_by {|h| h[:type] } \
+ . sort_by {|t, v| t } \
+ . map {|t, v| [t, v.map {|i| i[:name] }.sort ] } \
+ . map {|t, v|
+ sprintf("MAYBE_UNUSED(%s) %s", t, v.join(', '))
+ }
+ end
+
+ def preamble
+ # preamble makes sense for operand unifications
+ return []
+ end
+
+ def sc?
+ # sc stands for stack caching.
+ return false
+ end
+
+ def cast_to_VALUE var, expr = var[:name]
+ RubyVM::Typemap.typecast_to_VALUE var[:type], expr
+ end
+
+ def cast_from_VALUE var, expr = var[:name]
+ RubyVM::Typemap.typecast_from_VALUE var[:type], expr
+ end
+
+ def operands_info
+ opes.map {|o|
+ c, _ = RubyVM::Typemap.fetch o[:type]
+ next c
+ }.join
+ end
+
+ def handles_sp?
+ /\b(false|0)\b/ !~ @attrs.fetch('handles_sp').expr.expr
+ end
+
+ def always_leaf?
+ @attrs.fetch('leaf').expr.expr == 'true;'
+ end
+
+ def leaf_without_check_ints?
+ @attrs.fetch('leaf').expr.expr == 'leafness_of_check_ints;'
+ end
+
+ def handle_canary stmt
+ # Stack canary is basically a good thing that we want to add, however:
+ #
+ # - When the instruction returns variadic number of return values,
+ # it is not easy to tell where is the stack top. We can't but
+ # skip it.
+ #
+ # - When the instruction body is empty (like putobject), we can
+ # say for 100% sure that canary is a waste of time.
+ #
+ # So we skip canary for those cases.
+ return '' if @sig[:ret].any? {|i| i == '...' }
+ return '' if @expr.blank?
+ return " #{stmt};\n"
+ end
+
+ def inspect
+ sprintf "#<%s %s@%s:%d>", self.class.name, @name, @loc[0], @loc[1]
+ end
+
+ def has_ope? var
+ return @opes.any? {|i| i[:name] == var[:name] }
+ end
+
+ def has_pop? var
+ return @pops.any? {|i| i[:name] == var[:name] }
+ end
+
+ def use_call_data?
+ @use_call_data ||=
+ @variables.find { |_, var_info| var_info[:type] == 'CALL_DATA' }
+ end
+
+ private
+
+ def check_attribute_consistency
+ if has_attribute?('sp_inc') \
+ && use_call_data? \
+ && !has_attribute?('comptime_sp_inc')
+ # As the call cache caches information that can only be obtained at
+ # runtime, we do not need it when compiling from AST to bytecode. This
+ # attribute defines an expression that computes the stack pointer
+ # increase based on just the call info to avoid reserving space for the
+ # call cache at compile time. In the expression, all call data operands
+ # are mapped to their call info counterpart. Additionally, all mentions
+ # of `cd` in the operand name are replaced with `ci`.
+ raise "Please define attribute `comptime_sp_inc` for `#{@name}`"
+ end
+ end
+
+ def generate_attribute t, k, v
+ @attrs[k] ||= RubyVM::Attribute.new \
+ insn: self, \
+ name: k, \
+ type: t, \
+ location: [], \
+ expr: v.to_s + ';'
+ return @attrs[k] ||= attr
+ end
+
+ def predefine_attributes
+ # Beware: order matters here because some attribute depends another.
+ generate_attribute 'const char*', 'name', "insn_name(#{bin})"
+ generate_attribute 'enum ruby_vminsn_type', 'bin', bin
+ generate_attribute 'rb_num_t', 'open', opes.size
+ generate_attribute 'rb_num_t', 'popn', pops.size
+ generate_attribute 'rb_num_t', 'retn', rets.size
+ generate_attribute 'rb_num_t', 'width', width
+ generate_attribute 'rb_snum_t', 'sp_inc', rets.size - pops.size
+ generate_attribute 'bool', 'handles_sp', default_definition_of_handles_sp
+ generate_attribute 'bool', 'leaf', default_definition_of_leaf
+ end
+
+ def default_definition_of_handles_sp
+ # Insn with ISEQ should yield it; can handle sp.
+ return opes.any? {|o| o[:type] == 'ISEQ' }
+ end
+
+ def default_definition_of_leaf
+ # Insn that handles SP can never be a leaf.
+ if not has_attribute? 'handles_sp' then
+ return ! default_definition_of_handles_sp
+ elsif handles_sp? then
+ return "! #{call_attribute 'handles_sp'}"
+ else
+ return true
+ end
+ end
+
+ def typesplit a
+ @variables ||= {}
+ a.map do |decl|
+ md = %r'
+ (?<comment> /[*] [^*]* [*]+ (?: [^*/] [^*]* [*]+ )* / ){0}
+ (?<ws> \g<comment> | \s ){0}
+ (?<ident> [_a-zA-Z] [0-9_a-zA-Z]* ){0}
+ (?<type> (?: \g<ident> \g<ws>+ )* \g<ident> ){0}
+ (?<var> \g<ident> ){0}
+ \G \g<ws>* \g<type> \g<ws>+ \g<var>
+ 'x.match(decl)
+ @variables[md['var']] ||= {
+ decl: decl,
+ type: md['type'],
+ name: md['var'],
+ }
+ end
+ end
+
+ @instances = RubyVM::InsnsDef.map {|h|
+ new h.merge(:template => h)
+ }
+
+ def self.fetch name
+ @instances.find do |insn|
+ insn.name == name
+ end or raise IndexError, "instruction not found: #{name}"
+ end
+
+ def self.to_a
+ @instances
+ end
+end
diff --git a/tool/ruby_vm/models/c_expr.rb b/tool/ruby_vm/models/c_expr.rb
new file mode 100644
index 0000000000..073112f545
--- /dev/null
+++ b/tool/ruby_vm/models/c_expr.rb
@@ -0,0 +1,41 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/c_escape.rb'
+
+class RubyVM::CExpr
+ include RubyVM::CEscape
+
+ attr_reader :__FILE__, :__LINE__, :expr
+
+ def initialize opts = {}
+ @__FILE__ = opts[:location][0]
+ @__LINE__ = opts[:location][1]
+ @expr = opts[:expr]
+ end
+
+ # blank, in sense of C program.
+ RE = %r'\A{\g<s>*}\z|\A(?<s>\s|/[*][^*]*[*]+([^*/][^*]*[*]+)*/)*\z'
+ if RUBY_VERSION > '2.4' then
+ def blank?
+ RE.match? @expr
+ end
+ else
+ def blank?
+ RE =~ @expr
+ end
+ end
+
+ def inspect
+ sprintf "#<%s:%d %s>", @__FILE__, @__LINE__, @expr
+ end
+end
diff --git a/tool/ruby_vm/models/instructions.rb b/tool/ruby_vm/models/instructions.rb
new file mode 100644
index 0000000000..1198c7a4a6
--- /dev/null
+++ b/tool/ruby_vm/models/instructions.rb
@@ -0,0 +1,22 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative 'bare_instructions'
+require_relative 'operands_unifications'
+require_relative 'instructions_unifications'
+
+RubyVM::Instructions = RubyVM::BareInstructions.to_a + \
+ RubyVM::OperandsUnifications.to_a + \
+ RubyVM::InstructionsUnifications.to_a
+
+require_relative 'trace_instructions'
+RubyVM::Instructions.freeze
diff --git a/tool/ruby_vm/models/instructions_unifications.rb b/tool/ruby_vm/models/instructions_unifications.rb
new file mode 100644
index 0000000000..214ba5fcc2
--- /dev/null
+++ b/tool/ruby_vm/models/instructions_unifications.rb
@@ -0,0 +1,43 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/c_escape'
+require_relative '../loaders/opt_insn_unif_def'
+require_relative 'bare_instructions'
+
+class RubyVM::InstructionsUnifications
+ include RubyVM::CEscape
+
+ attr_reader :name
+
+ def initialize opts = {}
+ @location = opts[:location]
+ @name = namegen opts[:signature]
+ @series = opts[:signature].map do |i|
+ RubyVM::BareInstructions.fetch i # Misshit is fatal
+ end
+ end
+
+ private
+
+ def namegen signature
+ as_tr_cpp ['UNIFIED', *signature].join('_')
+ end
+
+ @instances = RubyVM::OptInsnUnifDef.map do |h|
+ new h
+ end
+
+ def self.to_a
+ @instances
+ end
+end
diff --git a/tool/ruby_vm/models/operands_unifications.rb b/tool/ruby_vm/models/operands_unifications.rb
new file mode 100644
index 0000000000..ee4e3a695d
--- /dev/null
+++ b/tool/ruby_vm/models/operands_unifications.rb
@@ -0,0 +1,142 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/c_escape'
+require_relative '../loaders/opt_operand_def'
+require_relative 'bare_instructions'
+
+class RubyVM::OperandsUnifications < RubyVM::BareInstructions
+ include RubyVM::CEscape
+
+ attr_reader :preamble, :original, :spec
+
+ def initialize opts = {}
+ name = opts[:signature][0]
+ @original = RubyVM::BareInstructions.fetch name
+ template = @original.template
+ parts = compose opts[:location], opts[:signature], template[:signature]
+ json = template.dup
+ json[:location] = opts[:location]
+ json[:signature] = parts[:signature]
+ json[:name] = parts[:name]
+ @preamble = parts[:preamble]
+ @spec = parts[:spec]
+ super json.merge(:template => template)
+ @konsts = parts[:vars]
+ @konsts.each do |v|
+ @variables[v[:name]] ||= v
+ end
+ end
+
+ def operand_shift_of var
+ before = @original.opes.find_index var
+ after = @opes.find_index var
+ raise "no #{var} for #{@name}" unless before and after
+ return before - after
+ end
+
+ def condition ptr
+ # :FIXME: I'm not sure if this method should be in model?
+ exprs = @spec.each_with_index.map do |(var, val), i|
+ case val when '*' then
+ next nil
+ else
+ type = @original.opes[i][:type]
+ expr = RubyVM::Typemap.typecast_to_VALUE type, val
+ next "#{ptr}[#{i}] == #{expr}"
+ end
+ end
+ exprs.compact!
+ if exprs.size == 1 then
+ return exprs[0]
+ else
+ exprs.map! {|i| "(#{i})" }
+ return exprs.join ' && '
+ end
+ end
+
+ def has_ope? var
+ super or @konsts.any? {|i| i[:name] == var[:name] }
+ end
+
+ private
+
+ def namegen signature
+ insn, argv = *signature
+ wcary = argv.map do |i|
+ case i when '*' then
+ 'WC'
+ else
+ i
+ end
+ end
+ as_tr_cpp [insn, *wcary].join(', ')
+ end
+
+ def compose location, spec, template
+ name = namegen spec
+ *, argv = *spec
+ opes = @original.opes
+ if opes.size != argv.size
+ raise sprintf("operand size mismatch for %s (%s's: %d, given: %d)",
+ name, template[:name], opes.size, argv.size)
+ else
+ src = []
+ mod = []
+ spec = []
+ vars = []
+ argv.each_index do |i|
+ j = argv[i]
+ k = opes[i]
+ spec[i] = [k, j]
+ case j when '*' then
+ # operand is from iseq
+ mod << k[:decl]
+ else
+ # operand is inside C
+ vars << k
+ src << {
+ location: location,
+ expr: " const #{k[:decl]} = #{j};"
+ }
+ end
+ end
+ src.map! {|i| RubyVM::CExpr.new i }
+ return {
+ name: name,
+ signature: {
+ name: name,
+ ope: mod,
+ pop: template[:pop],
+ ret: template[:ret],
+ },
+ preamble: src,
+ vars: vars,
+ spec: spec
+ }
+ end
+ end
+
+ @instances = RubyVM::OptOperandDef.map do |h|
+ new h
+ end
+
+ def self.to_a
+ @instances
+ end
+
+ def self.each_group
+ to_a.group_by(&:original).each_pair do |k, v|
+ yield k, v
+ end
+ end
+end
diff --git a/tool/ruby_vm/models/trace_instructions.rb b/tool/ruby_vm/models/trace_instructions.rb
new file mode 100644
index 0000000000..4ed4c8cb42
--- /dev/null
+++ b/tool/ruby_vm/models/trace_instructions.rb
@@ -0,0 +1,71 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require_relative '../helpers/c_escape'
+require_relative 'bare_instructions'
+
+class RubyVM::TraceInstructions
+ include RubyVM::CEscape
+
+ attr_reader :name
+
+ def initialize orig
+ @orig = orig
+ @name = as_tr_cpp "trace @ #{@orig.name}"
+ end
+
+ def pretty_name
+ return sprintf "%s(...)(...)(...)", @name
+ end
+
+ def jump_destination
+ return @orig.name
+ end
+
+ def bin
+ return sprintf "BIN(%s)", @name
+ end
+
+ def width
+ return @orig.width
+ end
+
+ def operands_info
+ return @orig.operands_info
+ end
+
+ def rets
+ return ['...']
+ end
+
+ def pops
+ return ['...']
+ end
+
+ def attributes
+ return []
+ end
+
+ def has_attribute? *;
+ return false
+ end
+
+ private
+
+ @instances = RubyVM::Instructions.map {|i| new i }
+
+ def self.to_a
+ @instances
+ end
+
+ RubyVM::Instructions.push(*to_a)
+end
diff --git a/tool/ruby_vm/models/typemap.rb b/tool/ruby_vm/models/typemap.rb
new file mode 100644
index 0000000000..ed3aea7d2e
--- /dev/null
+++ b/tool/ruby_vm/models/typemap.rb
@@ -0,0 +1,62 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+RubyVM::Typemap = {
+ "..." => %w[. TS_VARIABLE],
+ "CALL_DATA" => %w[C TS_CALLDATA],
+ "CDHASH" => %w[H TS_CDHASH],
+ "IC" => %w[K TS_IC],
+ "IVC" => %w[A TS_IVC],
+ "ID" => %w[I TS_ID],
+ "ISE" => %w[T TS_ISE],
+ "ISEQ" => %w[S TS_ISEQ],
+ "OFFSET" => %w[O TS_OFFSET],
+ "VALUE" => %w[V TS_VALUE],
+ "lindex_t" => %w[L TS_LINDEX],
+ "rb_insn_func_t" => %w[F TS_FUNCPTR],
+ "rb_num_t" => %w[N TS_NUM],
+ "RB_BUILTIN" => %w[R TS_BUILTIN],
+}
+
+# :FIXME: should this method be here?
+class << RubyVM::Typemap
+ def typecast_from_VALUE type, val
+ # see also iseq_set_sequence()
+ case type
+ when '...'
+ raise "cast not possible: #{val}"
+ when 'VALUE' then
+ return val
+ when 'rb_num_t', 'lindex_t' then
+ return "NUM2LONG(#{val})"
+ when 'ID' then
+ return "SYM2ID(#{val})"
+ else
+ return "(#{type})(#{val})"
+ end
+ end
+
+ def typecast_to_VALUE type, val
+ case type
+ when 'VALUE' then
+ return val
+ when 'ISEQ', 'rb_insn_func_t' then
+ return "(VALUE)(#{val})"
+ when 'rb_num_t', 'lindex_t'
+ "LONG2NUM(#{val})"
+ when 'ID' then
+ return "ID2SYM(#{val})"
+ else
+ raise ":FIXME: TBW for #{type}"
+ end
+ end
+end
diff --git a/tool/ruby_vm/scripts/converter.rb b/tool/ruby_vm/scripts/converter.rb
new file mode 100644
index 0000000000..4e7c28d67b
--- /dev/null
+++ b/tool/ruby_vm/scripts/converter.rb
@@ -0,0 +1,29 @@
+# This script was needed only once when I converted the old insns.def.
+# Consider historical.
+#
+# ruby converter.rb insns.def | sponge insns.def
+
+BEGIN { $str = ARGF.read }
+END { puts $str }
+
+# deal with spaces
+$str.gsub! %r/\r\n|\r|\n|\z/, "\n"
+$str.gsub! %r/([^\t\n]*)\t/ do
+ x = $1
+ y = 8 - x.length % 8
+ next x + ' ' * y
+end
+$str.gsub! %r/\s+$/, "\n"
+
+# deal with comments
+$str.gsub! %r/@c.*?@e/m, ''
+$str.gsub! %r/@j.*?\*\//m, '*/'
+$str.gsub! %r/\n(\s*\n)+/, "\n\n"
+$str.gsub! %r/\/\*\*?\s*\n\s*/, "/* "
+$str.gsub! %r/\n\s+\*\//, " */"
+$str.gsub! %r/^(?!.*\/\*.+\*\/$)(.+?)\s*\*\//, "\\1\n */"
+
+# deal with sp_inc
+$str.gsub! %r/ \/\/ inc -= (.*)/, ' // inc += -\\1'
+$str.gsub! %r/\s+\/\/ inc \+= (.*)/, "\n// attr rb_snum_t sp_inc = \\1;"
+$str.gsub! %r/;;$/, ";"
diff --git a/tool/ruby_vm/scripts/insns2vm.rb b/tool/ruby_vm/scripts/insns2vm.rb
new file mode 100644
index 0000000000..8325dd364f
--- /dev/null
+++ b/tool/ruby_vm/scripts/insns2vm.rb
@@ -0,0 +1,93 @@
+#! /your/favourite/path/to/ruby
+# -*- Ruby -*-
+# -*- frozen_string_literal: true; -*-
+# -*- warn_indent: true; -*-
+#
+# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+#
+# This file is a part of the programming language Ruby. Permission is hereby
+# granted, to either redistribute and/or modify this file, provided that the
+# conditions mentioned in the file COPYING are met. Consult the file for
+# details.
+
+require 'optparse'
+require_relative '../controllers/application_controller.rb'
+
+module RubyVM::Insns2VM
+ def self.router argv
+ options = { destdir: nil }
+ targets = generate_parser(options).parse argv
+ return targets.map do |i|
+ next ApplicationController.new.generate i, options[:destdir]
+ end
+ end
+
+ def self.generate_parser(options)
+ OptionParser.new do |this|
+ this.on "-I", "--srcdir=DIR", <<-'end'
+ Historically this option has been passed to the script. This is
+ supposedly because at the beginning the script was placed
+ outside of the ruby source tree. Decades passed since the merge
+ of YARV, now I can safely assume this feature is obsolescent.
+ Just ignore the passed value here.
+ end
+
+ this.on "-L", "--vpath=SPEC", <<-'end'
+ Likewise, this option is no longer supported.
+ end
+
+ this.on "--path-separator=SEP", /\A(?:\W\z|\.(\W).+)/, <<-'end'
+ Old script says this option is a "separator for vpath". I am
+ confident we no longer need this option.
+ end
+
+ this.on "-Dname", "--enable=name[,name...]", Array, <<-'end'
+ This option was used to override VM option that is defined in
+ vm_opts.h. Now it is officially unsupported because vm_opts.h to
+ remain mismatched with this option must break things. Just edit
+ vm_opts.h directly.
+ end
+
+ this.on "-Uname", "--disable=name[,name...]", Array, <<-'end'
+ This option was used to override VM option that is defined in
+ vm_opts.h. Now it is officially unsupported because vm_opts.h to
+ remain mismatched with this option must break things. Just edit
+ vm_opts.h directly.
+ end
+
+ this.on "-i", "--insnsdef=FILE", "--instructions-def", <<-'end'
+ This option was used to specify alternative path to insns.def. For
+ the same reason to ignore -I, we no longer support this.
+ end
+
+ this.on "-o", "--opt-operanddef=FILE", "--opt-operand-def", <<-'end'
+ This option was used to specify alternative path to opt_operand.def.
+ For the same reason to ignore -I, we no longer support this.
+ end
+
+ this.on "-u", "--opt-insnunifdef=FILE", "--opt-insn-unif-def", <<-'end'
+ This option was used to specify alternative path to
+ opt_insn_unif.def. For the same reason to ignore -I, we no
+ longer support this.
+ end
+
+ this.on "-C", "--[no-]use-const", <<-'end'
+ We use const whenever possible now so this option is ignored.
+ The author believes that C compilers can constant-fold.
+ end
+
+ this.on "-d", "--destdir", "--output-directory=DIR", <<-'begin' do |dir|
+ THIS IS THE ONLY OPTION THAT WORKS today. Change destination
+ directory from the current working directory to the given path.
+ begin
+ raise "directory was not found in '#{dir}'" unless Dir.exist?(dir)
+ options[:destdir] = dir
+ end
+
+ this.on "-V", "--[no-]verbose", <<-'end'
+ Please let us ignore this and be modest.
+ end
+ end
+ end
+ private_class_method :generate_parser
+end
diff --git a/tool/ruby_vm/tests/.gitkeep b/tool/ruby_vm/tests/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tool/ruby_vm/tests/.gitkeep
diff --git a/tool/ruby_vm/views/_attributes.erb b/tool/ruby_vm/views/_attributes.erb
new file mode 100644
index 0000000000..89a89817af
--- /dev/null
+++ b/tool/ruby_vm/views/_attributes.erb
@@ -0,0 +1,35 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%#
+#ifndef RUBY_VM_EXEC_H
+/* can't #include "vm_exec.h" here... */
+typedef long OFFSET;
+typedef unsigned long lindex_t;
+typedef VALUE GENTRY;
+typedef rb_iseq_t *ISEQ;
+#endif
+
+% attrs = RubyVM::Instructions.map(&:attributes).flatten
+%
+% attrs.each do |a|
+PUREFUNC(MAYBE_UNUSED(static <%= a.declaration %>));
+% end
+%
+% attrs.each do |a|
+
+/* <%= a.pretty_name %> */
+<%= a.definition %>
+{
+% str = render_c_expr a.expr
+% case str when /\A#/ then
+ return
+<%= str -%>
+% else
+ return <%= str -%>
+% end
+}
+% end
diff --git a/tool/ruby_vm/views/_c_expr.erb b/tool/ruby_vm/views/_c_expr.erb
new file mode 100644
index 0000000000..4e1b0ec883
--- /dev/null
+++ b/tool/ruby_vm/views/_c_expr.erb
@@ -0,0 +1,17 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%;
+% if expr.blank?
+% # empty
+% elsif ! expr.__LINE__
+<%= expr.expr %>
+% else
+#line <%= expr.__LINE__ %> <%=cstr expr.__FILE__ %>
+<%= expr.expr %>
+#pragma RubyVM reset source
+% end
diff --git a/tool/ruby_vm/views/_comptime_insn_stack_increase.erb b/tool/ruby_vm/views/_comptime_insn_stack_increase.erb
new file mode 100644
index 0000000000..b633ab4d32
--- /dev/null
+++ b/tool/ruby_vm/views/_comptime_insn_stack_increase.erb
@@ -0,0 +1,62 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%#
+PUREFUNC(MAYBE_UNUSED(static int comptime_insn_stack_increase(int depth, int insn, const VALUE *opes)));
+PUREFUNC(static rb_snum_t comptime_insn_stack_increase_dispatch(enum ruby_vminsn_type insn, const VALUE *opes));
+
+rb_snum_t
+comptime_insn_stack_increase_dispatch(enum ruby_vminsn_type insn, const VALUE *opes)
+{
+ static const signed char t[] = {
+% RubyVM::Instructions.each_slice 8 do |a|
+ <%= a.map { |i|
+ if i.has_attribute?('sp_inc')
+ '-127'
+ else
+ sprintf("%4d", i.rets.size - i.pops.size)
+ end
+ }.join(', ') -%>,
+% end
+ };
+ signed char c = t[insn];
+
+ ASSERT_VM_INSTRUCTION_SIZE(t);
+ if (c != -127) {
+ return c;
+ }
+ else switch(insn) {
+ default:
+ UNREACHABLE;
+% RubyVM::Instructions.each do |i|
+% next unless i.has_attribute?('sp_inc')
+% attr_function =
+% if i.has_attribute?('comptime_sp_inc')
+% "attr_comptime_sp_inc_#{i.name}"
+% else
+% "attr_sp_inc_#{i.name}"
+% end
+ case <%= i.bin %>:
+ return <%= attr_function %>(<%=
+ i.opes.map.with_index do |v, j|
+ if v[:type] == 'CALL_DATA' && i.has_attribute?('comptime_sp_inc')
+ v = v.dup
+ v[:type] = 'CALL_INFO'
+ end
+ i.cast_from_VALUE v, "opes[#{j}]"
+ end.join(", ")
+ %>);
+% end
+ }
+}
+
+int
+comptime_insn_stack_increase(int depth, int insn, const VALUE *opes)
+{
+ enum ruby_vminsn_type itype = (enum ruby_vminsn_type)insn;
+ return depth + (int)comptime_insn_stack_increase_dispatch(itype, opes);
+}
diff --git a/tool/ruby_vm/views/_copyright.erb b/tool/ruby_vm/views/_copyright.erb
new file mode 100644
index 0000000000..2154146f0d
--- /dev/null
+++ b/tool/ruby_vm/views/_copyright.erb
@@ -0,0 +1,31 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%;
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%;
+%;
+%# Below is the licensing term for the generated output, not this erb file.
+/* This is an auto-generated file and is a part of the programming language
+ * Ruby. The person who created a program to generate this file (``I''
+ * hereafter) would like to refrain from defining licensing of this generated
+ * source code.
+ *
+ * This file consists of many small parts of codes copyrighted by each author,
+ * not only the ``I'' person. Those original authors agree with some
+ * open-source license. I believe that the license we agree is the condition
+ * mentioned in the file COPYING. It states "4. You may modify and include
+ * the part of the software into any other software ...". But the problem is,
+ * the license never makes it clear if such modified parts still remain in the
+ * same license, or not. The fact that we agree with the source code's
+ * licensing terms does not automatically define that of generated ones. This
+ * is the reason why this file is under an unclear situation. All what I know
+ * is that above provision guarantees this file to exist.
+ *
+ * Please let me hesitate to declare something about this nuanced contract. I
+ * am not in the position to take over other authors' license to merge into my
+ * one. Changing them to (say) GPLv3 is not doable by myself. Perhaps someday
+ * it might turn out to be okay to say this file is under a license. I wish
+ * the situation would become more clear in the future. */
diff --git a/tool/ruby_vm/views/_insn_entry.erb b/tool/ruby_vm/views/_insn_entry.erb
new file mode 100644
index 0000000000..f34afddb1f
--- /dev/null
+++ b/tool/ruby_vm/views/_insn_entry.erb
@@ -0,0 +1,76 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%;
+% body = render_c_expr(insn.expr).gsub(/^#/, '# ')
+
+/* insn <%= insn.pretty_name %> */
+INSN_ENTRY(<%= insn.name %>)
+{
+ /* ### Declare that we have just entered into an instruction. ### */
+ START_OF_ORIGINAL_INSN(<%= insn.name %>);
+ DEBUG_ENTER_INSN(<%=cstr insn.name %>);
+
+ /* ### Declare and assign variables. ### */
+% insn.preamble.each do |konst|
+<%= render_c_expr konst -%>
+% end
+%
+% insn.opes.each_with_index do |ope, i|
+ <%= ope[:decl] %> = (<%= ope[:type] %>)GET_OPERAND(<%= i + 1 %>);
+% end
+# define INSN_ATTR(x) <%= insn.call_attribute(' ## x ## ') %>
+ const bool leaf = INSN_ATTR(leaf);
+% insn.pops.reverse_each.with_index.reverse_each do |pop, i|
+ <%= pop[:decl] %> = <%= insn.cast_from_VALUE pop, "TOPN(#{i})"%>;
+% end
+%
+% insn.rets.each do |ret|
+% next if insn.has_ope?(ret) or insn.has_pop?(ret)
+ <%= ret[:decl] %>;
+% end
+
+ /* ### Instruction preambles. ### */
+ if (! leaf) ADD_PC(INSN_ATTR(width));
+% if insn.handles_sp?
+ POPN(INSN_ATTR(popn));
+% end
+<%= insn.handle_canary "SETUP_CANARY(leaf)" -%>
+ COLLECT_USAGE_INSN(INSN_ATTR(bin));
+% insn.opes.each_with_index do |ope, i|
+ COLLECT_USAGE_OPERAND(INSN_ATTR(bin), <%= i %>, <%= ope[:name] %>);
+% end
+% unless body.empty?
+
+ /* ### Here we do the instruction body. ### */
+%# NAME_OF_CURRENT_INSN is used in vm_exec.h
+# define NAME_OF_CURRENT_INSN <%= insn.name %>
+<%= body -%>
+# undef NAME_OF_CURRENT_INSN
+% end
+
+ /* ### Instruction trailers. ### */
+ CHECK_VM_STACK_OVERFLOW_FOR_INSN(VM_REG_CFP, INSN_ATTR(retn));
+<%= insn.handle_canary "CHECK_CANARY(leaf, INSN_ATTR(bin))" -%>
+% if insn.handles_sp?
+% insn.rets.reverse_each do |ret|
+ PUSH(<%= insn.cast_to_VALUE ret %>);
+% end
+% else
+ INC_SP(INSN_ATTR(sp_inc));
+% insn.rets.reverse_each.with_index do |ret, i|
+ TOPN(<%= i %>) = <%= insn.cast_to_VALUE ret %>;
+ VM_ASSERT(!RB_TYPE_P(TOPN(<%= i %>), T_NONE));
+ VM_ASSERT(!RB_TYPE_P(TOPN(<%= i %>), T_MOVED));
+% end
+% end
+ if (leaf) ADD_PC(INSN_ATTR(width));
+# undef INSN_ATTR
+
+ /* ### Leave the instruction. ### */
+ END_INSN(<%= insn.name %>);
+}
diff --git a/tool/ruby_vm/views/_insn_len_info.erb b/tool/ruby_vm/views/_insn_len_info.erb
new file mode 100644
index 0000000000..569dca5845
--- /dev/null
+++ b/tool/ruby_vm/views/_insn_len_info.erb
@@ -0,0 +1,28 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+CONSTFUNC(MAYBE_UNUSED(static int insn_len(VALUE insn)));
+
+RUBY_SYMBOL_EXPORT_BEGIN /* for debuggers */
+extern const uint8_t rb_vm_insn_len_info[VM_INSTRUCTION_SIZE];
+RUBY_SYMBOL_EXPORT_END
+
+#ifdef RUBY_VM_INSNS_INFO
+const uint8_t rb_vm_insn_len_info[] = {
+% RubyVM::Instructions.each_slice 23 do |a|
+ <%= a.map(&:width).join(', ') -%>,
+% end
+};
+
+ASSERT_VM_INSTRUCTION_SIZE(rb_vm_insn_len_info);
+#endif
+
+int
+insn_len(VALUE i)
+{
+ return rb_vm_insn_len_info[i];
+}
diff --git a/tool/ruby_vm/views/_insn_name_info.erb b/tool/ruby_vm/views/_insn_name_info.erb
new file mode 100644
index 0000000000..e7ded75e65
--- /dev/null
+++ b/tool/ruby_vm/views/_insn_name_info.erb
@@ -0,0 +1,44 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%
+% a = RubyVM::Instructions.map {|i| i.name }
+% b = (0...a.size)
+% c = a.inject([0]) {|r, i| r << (r[-1] + i.length + 1) }
+% c.pop
+%
+CONSTFUNC(MAYBE_UNUSED(static const char *insn_name(VALUE insn)));
+
+RUBY_SYMBOL_EXPORT_BEGIN /* for debuggers */
+extern const int rb_vm_max_insn_name_size;
+extern const char rb_vm_insn_name_base[];
+extern const unsigned short rb_vm_insn_name_offset[VM_INSTRUCTION_SIZE];
+RUBY_SYMBOL_EXPORT_END
+
+#ifdef RUBY_VM_INSNS_INFO
+const int rb_vm_max_insn_name_size = <%= a.map(&:size).max %>;
+
+const char rb_vm_insn_name_base[] =
+% a.each do |i|
+ <%=cstr i%> "\0"
+% end
+ ;
+
+const unsigned short rb_vm_insn_name_offset[] = {
+% c.each_slice 12 do |d|
+ <%= d.map {|i| sprintf("%4d", i) }.join(', ') %>,
+% end
+};
+
+ASSERT_VM_INSTRUCTION_SIZE(rb_vm_insn_name_offset);
+#endif
+
+const char *
+insn_name(VALUE i)
+{
+ return &rb_vm_insn_name_base[rb_vm_insn_name_offset[i]];
+}
diff --git a/tool/ruby_vm/views/_insn_operand_info.erb b/tool/ruby_vm/views/_insn_operand_info.erb
new file mode 100644
index 0000000000..996c33e960
--- /dev/null
+++ b/tool/ruby_vm/views/_insn_operand_info.erb
@@ -0,0 +1,53 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%
+% a = RubyVM::Instructions.map {|i| i.operands_info }
+% b = (0...a.size)
+% c = a.inject([0]) {|r, i| r << (r[-1] + i.length + 1) }
+% c.pop
+%
+CONSTFUNC(MAYBE_UNUSED(static const char *insn_op_types(VALUE insn)));
+CONSTFUNC(MAYBE_UNUSED(static int insn_op_type(VALUE insn, long pos)));
+
+RUBY_SYMBOL_EXPORT_BEGIN /* for debuggers */
+extern const char rb_vm_insn_op_base[];
+extern const unsigned short rb_vm_insn_op_offset[VM_INSTRUCTION_SIZE];
+RUBY_SYMBOL_EXPORT_END
+
+#ifdef RUBY_VM_INSNS_INFO
+const char rb_vm_insn_op_base[] =
+% a.each_slice 5 do |d|
+ <%= d.map {|i| sprintf("%-6s", cstr(i)) }.join(' "\0" ') %> "\0"
+% end
+ ;
+
+const unsigned short rb_vm_insn_op_offset[] = {
+% c.each_slice 12 do |d|
+ <%= d.map {|i| sprintf("%3d", i) }.join(', ') %>,
+% end
+};
+
+ASSERT_VM_INSTRUCTION_SIZE(rb_vm_insn_op_offset);
+#endif
+
+const char *
+insn_op_types(VALUE i)
+{
+ return &rb_vm_insn_op_base[rb_vm_insn_op_offset[i]];
+}
+
+int
+insn_op_type(VALUE i, long j)
+{
+ if (j >= insn_len(i)) {
+ return 0;
+ }
+ else {
+ return insn_op_types(i)[j];
+ }
+}
diff --git a/tool/ruby_vm/views/_insn_sp_pc_dependency.erb b/tool/ruby_vm/views/_insn_sp_pc_dependency.erb
new file mode 100644
index 0000000000..95528fbbf4
--- /dev/null
+++ b/tool/ruby_vm/views/_insn_sp_pc_dependency.erb
@@ -0,0 +1,27 @@
+%# -*- C -*-
+%# Copyright (c) 2019 Takashi Kokubun. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%#
+PUREFUNC(MAYBE_UNUSED(static bool insn_may_depend_on_sp_or_pc(int insn, const VALUE *opes)));
+
+static bool
+insn_may_depend_on_sp_or_pc(int insn, const VALUE *opes)
+{
+ switch (insn) {
+% RubyVM::Instructions.each do |insn|
+% # handles_sp?: If true, it requires to move sp in JIT
+% # always_leaf?: If false, it may call an arbitrary method. pc should be moved
+% # before the call, and the method may refer to caller's pc (lineno).
+% unless !insn.is_a?(RubyVM::TraceInstructions) && !insn.handles_sp? && insn.always_leaf?
+ case <%= insn.bin %>:
+% end
+% end
+ return true;
+ default:
+ return false;
+ }
+}
diff --git a/tool/ruby_vm/views/_insn_type_chars.erb b/tool/ruby_vm/views/_insn_type_chars.erb
new file mode 100644
index 0000000000..4e1f63e660
--- /dev/null
+++ b/tool/ruby_vm/views/_insn_type_chars.erb
@@ -0,0 +1,13 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%
+enum ruby_insn_type_chars {
+% RubyVM::Typemap.each_value do |(c, t)|
+ <%= t %> = '<%= c %>',
+% end
+};
diff --git a/tool/ruby_vm/views/_leaf_helpers.erb b/tool/ruby_vm/views/_leaf_helpers.erb
new file mode 100644
index 0000000000..1735db2196
--- /dev/null
+++ b/tool/ruby_vm/views/_leaf_helpers.erb
@@ -0,0 +1,54 @@
+%# -*- C -*-
+%# Copyright (c) 2018 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%;
+#line <%= __LINE__ + 1 %> <%=cstr __FILE__ %>
+
+#include "iseq.h"
+
+// This is used to tell MJIT that this insn would be leaf if CHECK_INTS didn't exist.
+// It should be used only when RUBY_VM_CHECK_INTS is directly written in insns.def.
+static bool leafness_of_check_ints = false;
+
+static bool
+leafness_of_defined(rb_num_t op_type)
+{
+ /* see also: vm_insnhelper.c:vm_defined() */
+ switch (op_type) {
+ case DEFINED_IVAR:
+ case DEFINED_GVAR:
+ case DEFINED_CVAR:
+ case DEFINED_YIELD:
+ case DEFINED_REF:
+ case DEFINED_ZSUPER:
+ return false;
+ case DEFINED_CONST:
+ case DEFINED_CONST_FROM:
+ /* has rb_autoload_load(); */
+ return false;
+ case DEFINED_FUNC:
+ case DEFINED_METHOD:
+ /* calls #respond_to_missing? */
+ return false;
+ default:
+ rb_bug("unknown operand %ld: blame @shyouhei.", op_type);
+ }
+}
+
+static bool
+leafness_of_checkmatch(rb_num_t flag)
+{
+ /* see also: vm_insnhelper.c:check_match() */
+ if (flag == VM_CHECKMATCH_TYPE_WHEN) {
+ return true;
+ }
+ else {
+ /* has rb_funcallv() */
+ return false;
+ }
+}
+#pragma RubyVM reset source
diff --git a/tool/ruby_vm/views/_mjit_compile_getinlinecache.erb b/tool/ruby_vm/views/_mjit_compile_getinlinecache.erb
new file mode 100644
index 0000000000..d4eb4977a4
--- /dev/null
+++ b/tool/ruby_vm/views/_mjit_compile_getinlinecache.erb
@@ -0,0 +1,31 @@
+% # -*- C -*-
+% # Copyright (c) 2020 Takashi Kokubun. All rights reserved.
+% #
+% # This file is a part of the programming language Ruby. Permission is hereby
+% # granted, to either redistribute and/or modify this file, provided that the
+% # conditions mentioned in the file COPYING are met. Consult the file for
+% # details.
+%
+% # compiler: Declare dst and ic
+% insn.opes.each_with_index do |ope, i|
+ <%= ope.fetch(:decl) %> = (<%= ope.fetch(:type) %>)operands[<%= i %>];
+% end
+
+% # compiler: Capture IC values, locking getinlinecache
+ struct iseq_inline_constant_cache_entry *ice = ic->entry;
+ if (ice != NULL && GET_IC_SERIAL(ice) && !status->compile_info->disable_const_cache) {
+% # JIT: Inline everything in IC, and cancel the slow path
+ fprintf(f, " if (vm_inlined_ic_hit_p(0x%"PRIxVALUE", 0x%"PRIxVALUE", (const rb_cref_t *)0x%"PRIxVALUE", %"PRI_SERIALT_PREFIX"u, reg_cfp->ep)) {", ice->flags, ice->value, (VALUE)ice->ic_cref, GET_IC_SERIAL(ice));
+ fprintf(f, " stack[%d] = 0x%"PRIxVALUE";\n", b->stack_size, ice->value);
+ fprintf(f, " goto label_%d;\n", pos + insn_len(insn) + (int)dst);
+ fprintf(f, " }");
+ fprintf(f, " else {");
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size);
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", pos);
+ fprintf(f, " goto const_cancel;\n");
+ fprintf(f, " }");
+
+% # compiler: Move JIT compiler's internal stack pointer
+ b->stack_size += <%= insn.call_attribute('sp_inc') %>;
+ break;
+ }
diff --git a/tool/ruby_vm/views/_mjit_compile_insn.erb b/tool/ruby_vm/views/_mjit_compile_insn.erb
new file mode 100644
index 0000000000..f54d1b0e0e
--- /dev/null
+++ b/tool/ruby_vm/views/_mjit_compile_insn.erb
@@ -0,0 +1,92 @@
+% # -*- C -*-
+% # Copyright (c) 2018 Takashi Kokubun. All rights reserved.
+% #
+% # This file is a part of the programming language Ruby. Permission is hereby
+% # granted, to either redistribute and/or modify this file, provided that the
+% # conditions mentioned in the file COPYING are met. Consult the file for
+% # details.
+ fprintf(f, "{\n");
+ {
+% # compiler: Prepare operands which may be used by `insn.call_attribute`
+% insn.opes.each_with_index do |ope, i|
+ MAYBE_UNUSED(<%= ope.fetch(:decl) %>) = (<%= ope.fetch(:type) %>)operands[<%= i %>];
+% end
+%
+% # JIT: Declare stack_size to be used in some macro of _mjit_compile_insn_body.erb
+ if (status->local_stack_p) {
+ fprintf(f, " MAYBE_UNUSED(unsigned int) stack_size = %u;\n", b->stack_size);
+ }
+%
+% # JIT: Declare variables for operands, popped values and return values
+% insn.declarations.each do |decl|
+ fprintf(f, " <%= decl %>;\n");
+% end
+
+% # JIT: Set const expressions for `RubyVM::OperandsUnifications` insn
+% insn.preamble.each do |amble|
+ fprintf(f, "<%= amble.expr.sub(/const \S+\s+/, '') %>\n");
+% end
+%
+% # JIT: Initialize operands
+% insn.opes.each_with_index do |ope, i|
+ fprintf(f, " <%= ope.fetch(:name) %> = (<%= ope.fetch(:type) %>)0x%"PRIxVALUE";", operands[<%= i %>]);
+% case ope.fetch(:type)
+% when 'ID'
+ comment_id(f, (ID)operands[<%= i %>]);
+% when 'CALL_DATA'
+ comment_id(f, vm_ci_mid(((CALL_DATA)operands[<%= i %>])->ci));
+% when 'VALUE'
+ if (SYMBOL_P((VALUE)operands[<%= i %>])) comment_id(f, SYM2ID((VALUE)operands[<%= i %>]));
+% end
+ fprintf(f, "\n");
+% end
+%
+% # JIT: Initialize popped values
+% insn.pops.reverse_each.with_index.reverse_each do |pop, i|
+ fprintf(f, " <%= pop.fetch(:name) %> = stack[%d];\n", b->stack_size - <%= i + 1 %>);
+% end
+%
+% # JIT: move sp and pc if necessary
+<%= render 'mjit_compile_pc_and_sp', locals: { insn: insn } -%>
+%
+% # JIT: Print insn body in insns.def
+<%= render 'mjit_compile_insn_body', locals: { insn: insn } -%>
+%
+% # JIT: Set return values
+% insn.rets.reverse_each.with_index do |ret, i|
+% # TOPN(n) = ...
+ fprintf(f, " stack[%d] = <%= ret.fetch(:name) %>;\n", b->stack_size + (int)<%= insn.call_attribute('sp_inc') %> - <%= i + 1 %>);
+% end
+%
+% # JIT: We should evaluate ISeq modified for TracePoint if it's enabled. Note: This is slow.
+% # leaf insn may not cancel JIT. leaf_without_check_ints is covered in RUBY_VM_CHECK_INTS of _mjit_compile_insn_body.erb.
+% unless insn.always_leaf? || insn.leaf_without_check_ints?
+ fprintf(f, " if (UNLIKELY(!mjit_call_p)) {\n");
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size + (int)<%= insn.call_attribute('sp_inc') %>);
+ if (!pc_moved_p) {
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", next_pos);
+ }
+ fprintf(f, " RB_DEBUG_COUNTER_INC(mjit_cancel_invalidate_all);\n");
+ fprintf(f, " goto cancel;\n");
+ fprintf(f, " }\n");
+% end
+%
+% # compiler: Move JIT compiler's internal stack pointer
+ b->stack_size += <%= insn.call_attribute('sp_inc') %>;
+ }
+ fprintf(f, "}\n");
+%
+% # compiler: If insn has conditional JUMP, the code should go to the branch not targeted by JUMP next.
+% if insn.expr.expr =~ /if\s+\([^{}]+\)\s+\{[^{}]+JUMP\([^)]+\);[^{}]+\}/
+ if (ALREADY_COMPILED_P(status, pos + insn_len(insn))) {
+ fprintf(f, "goto label_%d;\n", pos + insn_len(insn));
+ }
+ else {
+ compile_insns(f, body, b->stack_size, pos + insn_len(insn), status);
+ }
+% end
+%
+% # compiler: If insn returns (leave) or does longjmp (throw), the branch should no longer be compiled. TODO: create attr for it?
+% if insn.expr.expr =~ /\sTHROW_EXCEPTION\([^)]+\);/ || insn.expr.expr =~ /\bvm_pop_frame\(/
+ b->finish_p = TRUE;
+% end
diff --git a/tool/ruby_vm/views/_mjit_compile_insn_body.erb b/tool/ruby_vm/views/_mjit_compile_insn_body.erb
new file mode 100644
index 0000000000..187e043837
--- /dev/null
+++ b/tool/ruby_vm/views/_mjit_compile_insn_body.erb
@@ -0,0 +1,129 @@
+% # -*- C -*-
+% # Copyright (c) 2018 Takashi Kokubun. All rights reserved.
+% #
+% # This file is a part of the programming language Ruby. Permission is hereby
+% # granted, to either redistribute and/or modify this file, provided that the
+% # conditions mentioned in the file COPYING are met. Consult the file for
+% # details.
+%
+% to_cstr = lambda do |line|
+% normalized = line.gsub(/\t/, ' ' * 8)
+% indented = normalized.sub(/\A(?!#)/, ' ') # avoid indenting preprocessor
+% rstring2cstr(indented.rstrip).sub(/"\z/, '\\n"')
+% end
+%
+% #
+% # Expand simple macro, which doesn't require dynamic C code.
+% #
+% expand_simple_macros = lambda do |arg_expr|
+% arg_expr.dup.tap do |expr|
+% # For `leave`. We can't proceed next ISeq in the same JIT function.
+% expr.gsub!(/^(?<indent>\s*)RESTORE_REGS\(\);\n/) do
+% indent = Regexp.last_match[:indent]
+% <<-end.gsub(/^ +/, '')
+% #if OPT_CALL_THREADED_CODE
+% #{indent}rb_ec_thread_ptr(ec)->retval = val;
+% #{indent}return 0;
+% #else
+% #{indent}return val;
+% #endif
+% end
+% end
+% expr.gsub!(/^(?<indent>\s*)NEXT_INSN\(\);\n/) do
+% indent = Regexp.last_match[:indent]
+% <<-end.gsub(/^ +/, '')
+% #{indent}UNREACHABLE_RETURN(Qundef);
+% end
+% end
+% end
+% end
+%
+% #
+% # Print a body of insn, but with macro expansion.
+% #
+% expand_simple_macros.call(insn.expr.expr).each_line do |line|
+% #
+% # Expand dynamic macro here (only JUMP for now)
+% #
+% # TODO: support combination of following macros in the same line
+% case line
+% when /\A\s+RUBY_VM_CHECK_INTS\(ec\);\s+\z/
+% if insn.leaf_without_check_ints? # lazily move PC and optionalize mjit_call_p here
+ fprintf(f, " if (UNLIKELY(RUBY_VM_INTERRUPTED_ANY(ec))) {\n");
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", next_pos); /* ADD_PC(INSN_ATTR(width)); */
+ fprintf(f, " rb_threadptr_execute_interrupts(rb_ec_thread_ptr(ec), 0);\n");
+ fprintf(f, " if (UNLIKELY(!mjit_call_p)) {\n");
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size);
+ fprintf(f, " RB_DEBUG_COUNTER_INC(mjit_cancel_invalidate_all);\n");
+ fprintf(f, " goto cancel;\n");
+ fprintf(f, " }\n");
+ fprintf(f, " }\n");
+% else
+ fprintf(f, <%= to_cstr.call(line) %>);
+% end
+% when /\A\s+JUMP\((?<dest>[^)]+)\);\s+\z/
+% dest = Regexp.last_match[:dest]
+%
+% if insn.name == 'opt_case_dispatch' # special case... TODO: use another macro to avoid checking name
+ {
+ struct case_dispatch_var arg;
+ arg.f = f;
+ arg.base_pos = pos + insn_len(insn);
+ arg.last_value = Qundef;
+
+ fprintf(f, " switch (<%= dest %>) {\n");
+ st_foreach(RHASH_TBL_RAW(hash), compile_case_dispatch_each, (VALUE)&arg);
+ fprintf(f, " case %lu:\n", else_offset);
+ fprintf(f, " goto label_%lu;\n", arg.base_pos + else_offset);
+ fprintf(f, " }\n");
+ }
+% else
+% # Before we `goto` next insn, we need to set return values, especially for getinlinecache
+% insn.rets.reverse_each.with_index do |ret, i|
+% # TOPN(n) = ...
+ fprintf(f, " stack[%d] = <%= ret.fetch(:name) %>;\n", b->stack_size + (int)<%= insn.call_attribute('sp_inc') %> - <%= i + 1 %>);
+% end
+%
+ next_pos = pos + insn_len(insn) + (unsigned int)<%= dest %>;
+ fprintf(f, " goto label_%d;\n", next_pos);
+% end
+% when /\A\s+CALL_SIMPLE_METHOD\(\);\s+\z/
+% # For `opt_xxx`'s fallbacks.
+ if (status->local_stack_p) {
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size);
+ }
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", pos);
+ fprintf(f, " RB_DEBUG_COUNTER_INC(mjit_cancel_opt_insn);\n");
+ fprintf(f, " goto cancel;\n");
+% when /\A(?<prefix>.+\b)INSN_LABEL\((?<name>[^)]+)\)(?<suffix>.+)\z/m
+% prefix, name, suffix = Regexp.last_match[:prefix], Regexp.last_match[:name], Regexp.last_match[:suffix]
+ fprintf(f, " <%= prefix.gsub(/\t/, ' ' * 8) %>INSN_LABEL(<%= name %>_%d)<%= suffix.sub(/\n/, '\n') %>", pos);
+% else
+% if insn.handles_sp?
+% # If insn.handles_sp? is true, cfp->sp might be changed inside insns (like vm_caller_setup_arg_block)
+% # and thus we need to use cfp->sp, even when local_stack_p is TRUE. When insn.handles_sp? is true,
+% # cfp->sp should be available too because _mjit_compile_pc_and_sp.erb sets it.
+ fprintf(f, <%= to_cstr.call(line) %>);
+% else
+% # If local_stack_p is TRUE and insn.handles_sp? is false, stack values are only available in local variables
+% # for stack. So we need to replace those macros if local_stack_p is TRUE here.
+% case line
+% when /\bGET_SP\(\)/
+% # reg_cfp->sp
+ fprintf(f, <%= to_cstr.call(line.sub(/\bGET_SP\(\)/, '%s')) %>, (status->local_stack_p ? "(stack + stack_size)" : "GET_SP()"));
+% when /\bSTACK_ADDR_FROM_TOP\((?<num>[^)]+)\)/
+% # #define STACK_ADDR_FROM_TOP(n) (GET_SP()-(n))
+% num = Regexp.last_match[:num]
+ fprintf(f, <%= to_cstr.call(line.sub(/\bSTACK_ADDR_FROM_TOP\(([^)]+)\)/, '%s')) %>,
+ (status->local_stack_p ? "(stack + (stack_size - (<%= num %>)))" : "STACK_ADDR_FROM_TOP(<%= num %>)"));
+% when /\bTOPN\((?<num>[^)]+)\)/
+% # #define TOPN(n) (*(GET_SP()-(n)-1))
+% num = Regexp.last_match[:num]
+ fprintf(f, <%= to_cstr.call(line.sub(/\bTOPN\(([^)]+)\)/, '%s')) %>,
+ (status->local_stack_p ? "*(stack + (stack_size - (<%= num %>) - 1))" : "TOPN(<%= num %>)"));
+% else
+ fprintf(f, <%= to_cstr.call(line) %>);
+% end
+% end
+% end
+% end
diff --git a/tool/ruby_vm/views/_mjit_compile_invokebuiltin.erb b/tool/ruby_vm/views/_mjit_compile_invokebuiltin.erb
new file mode 100644
index 0000000000..a3796ffc5e
--- /dev/null
+++ b/tool/ruby_vm/views/_mjit_compile_invokebuiltin.erb
@@ -0,0 +1,29 @@
+% # -*- C -*-
+% # Copyright (c) 2020 Urabe, Shyouhei. All rights reserved.
+% #
+% # This file is a part of the programming language Ruby. Permission is hereby
+% # granted, to either redistribute and/or modify this file, provided that the
+% # conditions mentioned in the file COPYING are met. Consult the file for
+% # details.
+%
+% insn.opes.each_with_index do |ope, i|
+ <%= ope.fetch(:decl) %> = (<%= ope.fetch(:type) %>)operands[<%= i %>];
+% end
+ rb_snum_t sp_inc = <%= insn.call_attribute('sp_inc') %>;
+ unsigned sp = b->stack_size + (unsigned)sp_inc;
+ VM_ASSERT(b->stack_size > -sp_inc);
+ VM_ASSERT(sp_inc < UINT_MAX - b->stack_size);
+
+ if (bf->compiler) {
+ fprintf(f, "{\n");
+ fprintf(f, " VALUE val;\n");
+ bf->compiler(f, <%=
+ insn.name == 'invokebuiltin' ? '-1' : '(rb_num_t)operands[1]'
+ %>, b->stack_size, body->builtin_inline_p);
+ fprintf(f, " stack[%u] = val;\n", sp - 1);
+ fprintf(f, "}\n");
+% if insn.name != 'opt_invokebuiltin_delegate_leave'
+ b->stack_size = sp;
+ break;
+% end
+ }
diff --git a/tool/ruby_vm/views/_mjit_compile_ivar.erb b/tool/ruby_vm/views/_mjit_compile_ivar.erb
new file mode 100644
index 0000000000..5105584ba3
--- /dev/null
+++ b/tool/ruby_vm/views/_mjit_compile_ivar.erb
@@ -0,0 +1,101 @@
+% # -*- C -*-
+% # Copyright (c) 2018 Takashi Kokubun. All rights reserved.
+% #
+% # This file is a part of the programming language Ruby. Permission is hereby
+% # granted, to either redistribute and/or modify this file, provided that the
+% # conditions mentioned in the file COPYING are met. Consult the file for
+% # details.
+%
+% # Optimized case of get_instancevariable instruction.
+#if OPT_IC_FOR_IVAR
+{
+% # compiler: Prepare operands which may be used by `insn.call_attribute`
+% insn.opes.each_with_index do |ope, i|
+ MAYBE_UNUSED(<%= ope.fetch(:decl) %>) = (<%= ope.fetch(:type) %>)operands[<%= i %>];
+% end
+% # compiler: Use copied IVC to avoid race condition
+ IVC ic_copy = &(status->is_entries + ((union iseq_inline_storage_entry *)ic - body->is_entries))->iv_cache;
+%
+ if (!status->compile_info->disable_ivar_cache && ic_copy->entry) { // Only ic_copy is enabled.
+% # JIT: optimize away motion of sp and pc. This path does not call rb_warning() and so it's always leaf and not `handles_sp`.
+% # <%= render 'mjit_compile_pc_and_sp', locals: { insn: insn } -%>
+%
+% # JIT: prepare vm_getivar/vm_setivar arguments and variables
+ fprintf(f, "{\n");
+ fprintf(f, " VALUE obj = GET_SELF();\n");
+ fprintf(f, " const uint32_t index = %u;\n", (ic_copy->entry->index));
+ if (status->merge_ivar_guards_p) {
+% # JIT: Access ivar without checking these VM_ASSERTed prerequisites as we checked them in the beginning of `mjit_compile_body`
+ fprintf(f, " VM_ASSERT(RB_TYPE_P(obj, T_OBJECT));\n");
+ fprintf(f, " VM_ASSERT((rb_serial_t)%"PRI_SERIALT_PREFIX"u == RCLASS_SERIAL(RBASIC(obj)->klass));\n", ic_copy->entry->class_serial);
+ fprintf(f, " VM_ASSERT(index < ROBJECT_NUMIV(obj));\n");
+% if insn.name == 'setinstancevariable'
+ fprintf(f, " if (LIKELY(!RB_OBJ_FROZEN_RAW(obj) && %s)) {\n", status->max_ivar_index >= ROBJECT_EMBED_LEN_MAX ? "true" : "RB_FL_ANY_RAW(obj, ROBJECT_EMBED)");
+ fprintf(f, " RB_OBJ_WRITE(obj, &ROBJECT(obj)->as.%s, stack[%d]);\n",
+ status->max_ivar_index >= ROBJECT_EMBED_LEN_MAX ? "heap.ivptr[index]" : "ary[index]", b->stack_size - 1);
+ fprintf(f, " }\n");
+% else
+ fprintf(f, " VALUE val;\n");
+ fprintf(f, " if (LIKELY(%s && (val = ROBJECT(obj)->as.%s) != Qundef)) {\n",
+ status->max_ivar_index >= ROBJECT_EMBED_LEN_MAX ? "true" : "RB_FL_ANY_RAW(obj, ROBJECT_EMBED)",
+ status->max_ivar_index >= ROBJECT_EMBED_LEN_MAX ? "heap.ivptr[index]" : "ary[index]");
+ fprintf(f, " stack[%d] = val;\n", b->stack_size);
+ fprintf(f, " }\n");
+%end
+ }
+ else {
+ fprintf(f, " const rb_serial_t ic_serial = (rb_serial_t)%"PRI_SERIALT_PREFIX"u;\n", ic_copy->entry->class_serial);
+% # JIT: cache hit path of vm_getivar/vm_setivar, or cancel JIT (recompile it with exivar)
+% if insn.name == 'setinstancevariable'
+ fprintf(f, " if (LIKELY(RB_TYPE_P(obj, T_OBJECT) && ic_serial == RCLASS_SERIAL(RBASIC(obj)->klass) && index < ROBJECT_NUMIV(obj) && !RB_OBJ_FROZEN_RAW(obj))) {\n");
+ fprintf(f, " VALUE *ptr = ROBJECT_IVPTR(obj);\n");
+ fprintf(f, " RB_OBJ_WRITE(obj, &ptr[index], stack[%d]);\n", b->stack_size - 1);
+ fprintf(f, " }\n");
+% else
+ fprintf(f, " VALUE val;\n");
+ fprintf(f, " if (LIKELY(RB_TYPE_P(obj, T_OBJECT) && ic_serial == RCLASS_SERIAL(RBASIC(obj)->klass) && index < ROBJECT_NUMIV(obj) && (val = ROBJECT_IVPTR(obj)[index]) != Qundef)) {\n");
+ fprintf(f, " stack[%d] = val;\n", b->stack_size);
+ fprintf(f, " }\n");
+% end
+ }
+ fprintf(f, " else {\n");
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", pos);
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size);
+ fprintf(f, " goto ivar_cancel;\n");
+ fprintf(f, " }\n");
+
+% # compiler: Move JIT compiler's internal stack pointer
+ b->stack_size += <%= insn.call_attribute('sp_inc') %>;
+ fprintf(f, "}\n");
+ break;
+ }
+% if insn.name == 'getinstancevariable'
+ else if (!status->compile_info->disable_exivar_cache && ic_copy->entry) {
+% # JIT: optimize away motion of sp and pc. This path does not call rb_warning() and so it's always leaf and not `handles_sp`.
+% # <%= render 'mjit_compile_pc_and_sp', locals: { insn: insn } -%>
+%
+% # JIT: prepare vm_getivar's arguments and variables
+ fprintf(f, "{\n");
+ fprintf(f, " VALUE obj = GET_SELF();\n");
+ fprintf(f, " const rb_serial_t ic_serial = (rb_serial_t)%"PRI_SERIALT_PREFIX"u;\n", ic_copy->entry->class_serial);
+ fprintf(f, " const uint32_t index = %u;\n", ic_copy->entry->index);
+% # JIT: cache hit path of vm_getivar, or cancel JIT (recompile it without any ivar optimization)
+ fprintf(f, " struct gen_ivtbl *ivtbl;\n");
+ fprintf(f, " VALUE val;\n");
+ fprintf(f, " if (LIKELY(FL_TEST_RAW(obj, FL_EXIVAR) && ic_serial == RCLASS_SERIAL(RBASIC(obj)->klass) && rb_ivar_generic_ivtbl_lookup(obj, &ivtbl) && index < ivtbl->numiv && (val = ivtbl->ivptr[index]) != Qundef)) {\n");
+ fprintf(f, " stack[%d] = val;\n", b->stack_size);
+ fprintf(f, " }\n");
+ fprintf(f, " else {\n");
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", pos);
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size);
+ fprintf(f, " goto exivar_cancel;\n");
+ fprintf(f, " }\n");
+
+% # compiler: Move JIT compiler's internal stack pointer
+ b->stack_size += <%= insn.call_attribute('sp_inc') %>;
+ fprintf(f, "}\n");
+ break;
+ }
+% end
+}
+#endif // OPT_IC_FOR_IVAR
diff --git a/tool/ruby_vm/views/_mjit_compile_pc_and_sp.erb b/tool/ruby_vm/views/_mjit_compile_pc_and_sp.erb
new file mode 100644
index 0000000000..390b3ce525
--- /dev/null
+++ b/tool/ruby_vm/views/_mjit_compile_pc_and_sp.erb
@@ -0,0 +1,38 @@
+% # Copyright (c) 2018 Takashi Kokubun. All rights reserved.
+% #
+% # This file is a part of the programming language Ruby. Permission is hereby
+% # granted, to either redistribute and/or modify this file, provided that the
+% # conditions mentioned in the file COPYING are met. Consult the file for
+% # details.
+%
+% # JIT: When an insn is leaf, we don't need to Move pc for a catch table on catch_except_p, #caller_locations,
+% # and rb_profile_frames. For check_ints, we lazily move PC when we have interruptions.
+ MAYBE_UNUSED(bool pc_moved_p) = false;
+ if (<%= !(insn.always_leaf? || insn.leaf_without_check_ints?) %>) {
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", next_pos); /* ADD_PC(INSN_ATTR(width)); */
+ pc_moved_p = true;
+ }
+%
+% # JIT: move sp to use or preserve stack variables
+ if (status->local_stack_p) {
+% # sp motion is optimized away for `handles_sp? #=> false` case.
+% # Thus sp should be set properly before `goto cancel`.
+% if insn.handles_sp?
+% # JIT-only behavior (pushing JIT's local variables to VM's stack):
+ {
+ rb_snum_t i, push_size;
+ push_size = -<%= insn.call_attribute('sp_inc') %> + <%= insn.rets.size %> - <%= insn.pops.size %>;
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %ld;\n", push_size); /* POPN(INSN_ATTR(popn)); */
+ for (i = 0; i < push_size; i++) {
+ fprintf(f, " *(reg_cfp->sp + %ld) = stack[%ld];\n", i - push_size, (rb_snum_t)b->stack_size - push_size + i);
+ }
+ }
+% end
+ }
+ else {
+% if insn.handles_sp?
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size - <%= insn.pops.size %>); /* POPN(INSN_ATTR(popn)); */
+% else
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size);
+% end
+ }
diff --git a/tool/ruby_vm/views/_mjit_compile_send.erb b/tool/ruby_vm/views/_mjit_compile_send.erb
new file mode 100644
index 0000000000..28e316a1ef
--- /dev/null
+++ b/tool/ruby_vm/views/_mjit_compile_send.erb
@@ -0,0 +1,119 @@
+% # -*- C -*-
+% # Copyright (c) 2018 Takashi Kokubun. All rights reserved.
+% #
+% # This file is a part of the programming language Ruby. Permission is hereby
+% # granted, to either redistribute and/or modify this file, provided that the
+% # conditions mentioned in the file COPYING are met. Consult the file for
+% # details.
+%
+% # Optimized case of send / opt_send_without_block instructions.
+{
+% # compiler: Prepare operands which may be used by `insn.call_attribute`
+% insn.opes.each_with_index do |ope, i|
+ MAYBE_UNUSED(<%= ope.fetch(:decl) %>) = (<%= ope.fetch(:type) %>)operands[<%= i %>];
+% end
+% # compiler: Use captured cc to avoid race condition
+ size_t cd_index = call_data_index(cd, body);
+ const struct rb_callcache **cc_entries = captured_cc_entries(status);
+ const struct rb_callcache *captured_cc = cc_entries[cd_index];
+%
+% # compiler: Inline send insn where some supported fastpath is used.
+ const rb_iseq_t *iseq = NULL;
+ const CALL_INFO ci = cd->ci;
+ int kw_splat = IS_ARGS_KW_SPLAT(ci) > 0;
+ extern bool rb_splat_or_kwargs_p(const struct rb_callinfo *restrict ci);
+ if (!status->compile_info->disable_send_cache && has_valid_method_type(captured_cc) && (
+% # `CC_SET_FASTPATH(cd->cc, vm_call_cfunc_with_frame, ...)` in `vm_call_cfunc`
+ (vm_cc_cme(captured_cc)->def->type == VM_METHOD_TYPE_CFUNC
+ && !rb_splat_or_kwargs_p(ci) && !kw_splat)
+% # `CC_SET_FASTPATH(cc, vm_call_iseq_setup_func(...), vm_call_iseq_optimizable_p(...))` in `vm_callee_setup_arg`,
+% # and support only non-VM_CALL_TAILCALL path inside it
+ || (vm_cc_cme(captured_cc)->def->type == VM_METHOD_TYPE_ISEQ
+ && fastpath_applied_iseq_p(ci, captured_cc, iseq = def_iseq_ptr(vm_cc_cme(captured_cc)->def))
+ && !(vm_ci_flag(ci) & VM_CALL_TAILCALL))
+ )) {
+ const bool cfunc_debug = false; // Set true when you want to see inlined cfunc
+ if (cfunc_debug && vm_cc_cme(captured_cc)->def->type == VM_METHOD_TYPE_CFUNC)
+ fprintf(stderr, " * %s\n", rb_id2name(vm_ci_mid(ci)));
+
+ int sp_inc = (int)sp_inc_of_sendish(ci);
+ fprintf(f, "{\n");
+
+% # JIT: Invalidate call cache if it requires vm_search_method. This allows to inline some of following things.
+ bool opt_class_of = !maybe_special_const_class_p(captured_cc->klass); // If true, use RBASIC_CLASS instead of CLASS_OF to reduce code size
+ fprintf(f, " const struct rb_callcache *cc = (const struct rb_callcache *)0x%"PRIxVALUE";\n", (VALUE)captured_cc);
+ fprintf(f, " const rb_callable_method_entry_t *cc_cme = (const rb_callable_method_entry_t *)0x%"PRIxVALUE";\n", (VALUE)vm_cc_cme(captured_cc));
+ fprintf(f, " const VALUE recv = stack[%d];\n", b->stack_size + sp_inc - 1);
+ fprintf(f, " if (UNLIKELY(%s || !vm_cc_valid_p(cc, cc_cme, %s(recv)))) {\n", opt_class_of ? "RB_SPECIAL_CONST_P(recv)" : "false", opt_class_of ? "RBASIC_CLASS" : "CLASS_OF");
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", pos);
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size);
+ fprintf(f, " goto send_cancel;\n");
+ fprintf(f, " }\n");
+
+% # JIT: move sp and pc if necessary
+<%= render 'mjit_compile_pc_and_sp', locals: { insn: insn } -%>
+
+% # JIT: If ISeq is inlinable, call the inlined method without pushing a frame.
+ if (iseq && status->inlined_iseqs != NULL && iseq->body == status->inlined_iseqs[pos]) {
+ fprintf(f, " {\n");
+ fprintf(f, " VALUE orig_self = reg_cfp->self;\n");
+ fprintf(f, " reg_cfp->self = stack[%d];\n", b->stack_size + sp_inc - 1);
+ fprintf(f, " stack[%d] = _mjit%d_inlined_%d(ec, reg_cfp, orig_self, original_iseq);\n", b->stack_size + sp_inc - 1, status->compiled_id, pos);
+ fprintf(f, " reg_cfp->self = orig_self;\n");
+ fprintf(f, " }\n");
+ }
+ else {
+% # JIT: Forked `vm_sendish` (except method_explorer = vm_search_method_wrap) to inline various things
+ fprintf(f, " {\n");
+ fprintf(f, " VALUE val;\n");
+ fprintf(f, " struct rb_calling_info calling;\n");
+% if insn.name == 'send'
+ fprintf(f, " calling.block_handler = vm_caller_setup_arg_block(ec, reg_cfp, (const struct rb_callinfo *)0x%"PRIxVALUE", (rb_iseq_t *)0x%"PRIxVALUE", FALSE);\n", (VALUE)ci, (VALUE)blockiseq);
+% else
+ fprintf(f, " calling.block_handler = VM_BLOCK_HANDLER_NONE;\n");
+% end
+ fprintf(f, " calling.kw_splat = %d;\n", kw_splat);
+ fprintf(f, " calling.recv = stack[%d];\n", b->stack_size + sp_inc - 1);
+ fprintf(f, " calling.argc = %d;\n", vm_ci_argc(ci));
+
+ if (vm_cc_cme(captured_cc)->def->type == VM_METHOD_TYPE_CFUNC) {
+% # TODO: optimize this more
+ fprintf(f, " calling.ci = (CALL_INFO)0x%"PRIxVALUE";\n", (VALUE)ci); // creating local cd here because operand's cd->cc may not be the same as inlined cc.
+ fprintf(f, " calling.cc = cc;");
+ fprintf(f, " val = vm_call_cfunc_with_frame(ec, reg_cfp, &calling);\n");
+ }
+ else { // VM_METHOD_TYPE_ISEQ
+% # fastpath_applied_iseq_p checks rb_simple_iseq_p, which ensures has_opt == FALSE
+ fprintf(f, " vm_call_iseq_setup_normal(ec, reg_cfp, &calling, cc_cme, 0, %d, %d);\n", iseq->body->param.size, iseq->body->local_table_size);
+ if (iseq->body->catch_except_p) {
+ fprintf(f, " VM_ENV_FLAGS_SET(ec->cfp->ep, VM_FRAME_FLAG_FINISH);\n");
+ fprintf(f, " val = vm_exec(ec, true);\n");
+ }
+ else {
+ fprintf(f, " if ((val = mjit_exec(ec)) == Qundef) {\n");
+ fprintf(f, " VM_ENV_FLAGS_SET(ec->cfp->ep, VM_FRAME_FLAG_FINISH);\n"); // This is vm_call0_body's code after vm_call_iseq_setup
+ fprintf(f, " val = vm_exec(ec, false);\n");
+ fprintf(f, " }\n");
+ }
+ }
+ fprintf(f, " stack[%d] = val;\n", b->stack_size + sp_inc - 1);
+ fprintf(f, " }\n");
+
+% # JIT: We should evaluate ISeq modified for TracePoint if it's enabled. Note: This is slow.
+ fprintf(f, " if (UNLIKELY(!mjit_call_p)) {\n");
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size + (int)<%= insn.call_attribute('sp_inc') %>);
+ if (!pc_moved_p) {
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", next_pos);
+ }
+ fprintf(f, " RB_DEBUG_COUNTER_INC(mjit_cancel_invalidate_all);\n");
+ fprintf(f, " goto cancel;\n");
+ fprintf(f, " }\n");
+ }
+
+% # compiler: Move JIT compiler's internal stack pointer
+ b->stack_size += <%= insn.call_attribute('sp_inc') %>;
+
+ fprintf(f, "}\n");
+ break;
+ }
+}
diff --git a/tool/ruby_vm/views/_notice.erb b/tool/ruby_vm/views/_notice.erb
new file mode 100644
index 0000000000..d17e019727
--- /dev/null
+++ b/tool/ruby_vm/views/_notice.erb
@@ -0,0 +1,22 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%;
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%;
+%;
+/*******************************************************************/
+/*******************************************************************/
+/*******************************************************************/
+/**
+ This file <%= this_file %>.
+
+ ----
+ This file is auto generated by insns2vm.rb
+ DO NOT TOUCH!
+
+ If you want to fix something, you must edit <%= cstr edit %>
+ or tool/insns2vm.rb
+ */
diff --git a/tool/ruby_vm/views/_sp_inc_helpers.erb b/tool/ruby_vm/views/_sp_inc_helpers.erb
new file mode 100644
index 0000000000..d0b0bd79ef
--- /dev/null
+++ b/tool/ruby_vm/views/_sp_inc_helpers.erb
@@ -0,0 +1,37 @@
+%# -*- C -*-
+%# Copyright (c) 2018 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%;
+#line <%= __LINE__ + 1 %> <%=cstr __FILE__ %>
+
+static rb_snum_t
+sp_inc_of_sendish(const struct rb_callinfo *ci)
+{
+ /* Send-ish instructions will:
+ *
+ * 1. Pop block argument, if any.
+ * 2. Pop ordinal arguments.
+ * 3. Pop receiver.
+ * 4. Push return value.
+ */
+ const int argb = (vm_ci_flag(ci) & VM_CALL_ARGS_BLOCKARG) ? 1 : 0;
+ const int argc = vm_ci_argc(ci);
+ const int recv = 1;
+ const int retn = 1;
+
+ /* 1. 2. 3. 4. */
+ return 0 - argb - argc - recv + retn;
+}
+
+static rb_snum_t
+sp_inc_of_invokeblock(const struct rb_callinfo *ci)
+{
+ /* sp_inc of invokeblock is almost identical to that of sendish
+ * instructions, except that it does not pop receiver. */
+ return sp_inc_of_sendish(ci) + 1;
+}
+#pragma RubyVM reset source
diff --git a/tool/ruby_vm/views/_trace_instruction.erb b/tool/ruby_vm/views/_trace_instruction.erb
new file mode 100644
index 0000000000..3588207d39
--- /dev/null
+++ b/tool/ruby_vm/views/_trace_instruction.erb
@@ -0,0 +1,21 @@
+%# -*- C -*-
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+%;
+
+/* insn <%= insn.pretty_name %> */
+INSN_ENTRY(<%= insn.name %>)
+{
+ vm_trace(ec, GET_CFP());
+% if insn.name =~
+% /\Atrace_opt_(plus|minus|mult|div|mod|eq|neq|lt|le|gt|ge|ltlt|and|or|aref|aset|length|size|empty_p|nil_p|succ|not|regexpmatch2)\z/
+% jump_dest = "opt_send_without_block"
+% end
+ <%= 'ADD_PC(1);' if insn.name == 'trace_opt_neq' %>
+ DISPATCH_ORIGINAL_INSN(<%= jump_dest || insn.jump_destination %>);
+ END_INSN(<%= insn.name %>);
+}
diff --git a/tool/ruby_vm/views/insns.inc.erb b/tool/ruby_vm/views/insns.inc.erb
new file mode 100644
index 0000000000..29981a8a2d
--- /dev/null
+++ b/tool/ruby_vm/views/insns.inc.erb
@@ -0,0 +1,26 @@
+/* -*- C -*- */
+
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+<%= render 'copyright' %>
+<%= render 'notice', locals: {
+ this_file: 'contains YARV instruction list',
+ edit: __FILE__,
+} -%>
+
+/* BIN : Basic Instruction Name */
+#define BIN(n) YARVINSN_##n
+
+enum ruby_vminsn_type {
+% RubyVM::Instructions.each do |i|
+ <%= i.bin %>,
+% end
+ VM_INSTRUCTION_SIZE
+};
+
+#define ASSERT_VM_INSTRUCTION_SIZE(array) \
+ STATIC_ASSERT(numberof_##array, numberof(array) == VM_INSTRUCTION_SIZE)
diff --git a/tool/ruby_vm/views/insns_info.inc.erb b/tool/ruby_vm/views/insns_info.inc.erb
new file mode 100644
index 0000000000..2ca5aca7cf
--- /dev/null
+++ b/tool/ruby_vm/views/insns_info.inc.erb
@@ -0,0 +1,22 @@
+/* -*- C -*- */
+
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+<%= render 'copyright' %>
+<%= render 'notice', locals: {
+ this_file: 'contains instruction information for yarv instruction sequence.',
+ edit: __FILE__,
+} %>
+<%= render 'insn_type_chars' %>
+<%= render 'insn_name_info' %>
+<%= render 'insn_len_info' %>
+<%= render 'insn_operand_info' %>
+<%= render 'leaf_helpers' %>
+<%= render 'sp_inc_helpers' %>
+<%= render 'attributes' %>
+<%= render 'comptime_insn_stack_increase' %>
+<%= render 'insn_sp_pc_dependency' %>
diff --git a/tool/ruby_vm/views/mjit_compile.inc.erb b/tool/ruby_vm/views/mjit_compile.inc.erb
new file mode 100644
index 0000000000..5820f81770
--- /dev/null
+++ b/tool/ruby_vm/views/mjit_compile.inc.erb
@@ -0,0 +1,110 @@
+/* -*- C -*- */
+
+% # Copyright (c) 2018 Takashi Kokubun. All rights reserved.
+% #
+% # This file is a part of the programming language Ruby. Permission is hereby
+% # granted, to either redistribute and/or modify this file, provided that the
+% # conditions mentioned in the file COPYING are met. Consult the file for
+% # details.
+<%= render 'copyright' %>
+%
+% # This is an ERB template that generates Ruby code that generates C code that
+% # generates JIT-ed C code.
+<%= render 'notice', locals: {
+ this_file: 'is the main part of compile_insn() in mjit_compile.c',
+ edit: __FILE__,
+} -%>
+%
+% unsupported_insns = [
+% 'defineclass', # low priority
+% ]
+%
+% opt_send_without_block = RubyVM::Instructions.find { |i| i.name == 'opt_send_without_block' }
+% if opt_send_without_block.nil?
+% raise 'opt_send_without_block not found'
+% end
+%
+% send_compatible_opt_insns = RubyVM::BareInstructions.to_a.select do |insn|
+% insn.name.start_with?('opt_') && opt_send_without_block.opes == insn.opes &&
+% insn.expr.expr.lines.any? { |l| l.match(/\A\s+CALL_SIMPLE_METHOD\(\);\s+\z/) }
+% end.map(&:name)
+%
+% # Available variables and macros in JIT-ed function:
+% # ec: the first argument of _mjitXXX
+% # reg_cfp: the second argument of _mjitXXX
+% # GET_CFP(): refers to `reg_cfp`
+% # GET_EP(): refers to `reg_cfp->ep`
+% # GET_SP(): refers to `reg_cfp->sp`, or `(stack + stack_size)` if local_stack_p
+% # GET_SELF(): refers to `cfp_self`
+% # GET_LEP(): refers to `VM_EP_LEP(reg_cfp->ep)`
+% # EXEC_EC_CFP(): refers to `val = vm_exec(ec, true)` with frame setup
+% # CALL_METHOD(): using `GET_CFP()` and `EXEC_EC_CFP()`
+% # TOPN(): refers to `reg_cfp->sp`, or `*(stack + (stack_size - num - 1))` if local_stack_p
+% # STACK_ADDR_FROM_TOP(): refers to `reg_cfp->sp`, or `stack + (stack_size - num)` if local_stack_p
+% # DISPATCH_ORIGINAL_INSN(): expanded in _mjit_compile_insn.erb
+% # THROW_EXCEPTION(): specially defined for JIT
+% # RESTORE_REGS(): specially defined for `leave`
+
+switch (insn) {
+% (RubyVM::BareInstructions.to_a + RubyVM::OperandsUnifications.to_a).each do |insn|
+% next if unsupported_insns.include?(insn.name)
+ case BIN(<%= insn.name %>): {
+% # Instruction-specific behavior in JIT
+% case insn.name
+% when 'opt_send_without_block', 'send'
+<%= render 'mjit_compile_send', locals: { insn: insn } -%>
+% when *send_compatible_opt_insns
+% # To avoid cancel, just emit `opt_send_without_block` instead of `opt_*` insn if call cache is populated.
+% cd_index = insn.opes.index { |o| o.fetch(:type) == 'CALL_DATA' }
+ if (has_cache_for_send(captured_cc_entries(status)[call_data_index((CALL_DATA)operands[<%= cd_index %>], body)], BIN(<%= insn.name %>))) {
+<%= render 'mjit_compile_send', locals: { insn: opt_send_without_block } -%>
+<%= render 'mjit_compile_insn', locals: { insn: opt_send_without_block } -%>
+ break;
+ }
+% when 'getinstancevariable', 'setinstancevariable'
+<%= render 'mjit_compile_ivar', locals: { insn: insn } -%>
+% when 'invokebuiltin', 'opt_invokebuiltin_delegate'
+<%= render 'mjit_compile_invokebuiltin', locals: { insn: insn } -%>
+% when 'opt_getinlinecache'
+<%= render 'mjit_compile_getinlinecache', locals: { insn: insn } -%>
+% when 'leave', 'opt_invokebuiltin_delegate_leave'
+% # opt_invokebuiltin_delegate_leave also implements leave insn. We need to handle it here for inlining.
+% if insn.name == 'opt_invokebuiltin_delegate_leave'
+<%= render 'mjit_compile_invokebuiltin', locals: { insn: insn } -%>
+% else
+ if (b->stack_size != 1) {
+ if (mjit_opts.warnings || mjit_opts.verbose)
+ fprintf(stderr, "MJIT warning: Unexpected JIT stack_size on leave: %d\n", b->stack_size);
+ status->success = false;
+ }
+% end
+% # Skip vm_pop_frame for inlined call
+ if (status->inlined_iseqs != NULL) { // the current ISeq is NOT being inlined
+% # Cancel on interrupts to make leave insn leaf
+ fprintf(f, " if (UNLIKELY(RUBY_VM_INTERRUPTED_ANY(ec))) {\n");
+ fprintf(f, " reg_cfp->sp = vm_base_ptr(reg_cfp) + %d;\n", b->stack_size);
+ fprintf(f, " reg_cfp->pc = original_body_iseq + %d;\n", pos);
+ fprintf(f, " rb_threadptr_execute_interrupts(rb_ec_thread_ptr(ec), 0);\n");
+ fprintf(f, " }\n");
+ fprintf(f, " ec->cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(reg_cfp);\n"); // vm_pop_frame
+ }
+ fprintf(f, " return stack[0];\n");
+ b->stack_size += <%= insn.call_attribute('sp_inc') %>;
+ b->finish_p = TRUE;
+ break;
+% end
+%
+% # Main insn implementation generated by insns.def
+<%= render 'mjit_compile_insn', locals: { insn: insn } -%>
+ break;
+ }
+% end
+%
+% # We don't support InstructionsUnifications yet because it's not used for now.
+% # We don't support TraceInstructions yet. There is no blocker for it but it's just not implemented.
+ default:
+ if (mjit_opts.warnings || mjit_opts.verbose)
+ fprintf(stderr, "MJIT warning: Skipped to compile unsupported instruction: %s\n", insn_name(insn));
+ status->success = false;
+ break;
+}
diff --git a/tool/ruby_vm/views/opt_sc.inc.erb b/tool/ruby_vm/views/opt_sc.inc.erb
new file mode 100644
index 0000000000..e58c81989f
--- /dev/null
+++ b/tool/ruby_vm/views/opt_sc.inc.erb
@@ -0,0 +1,40 @@
+/* -*- C -*- */
+
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+% raise ':FIXME:TBW' if RubyVM::VmOptsH['STACK_CACHING']
+<%= render 'copyright' %>
+<%= render 'notice', locals: {
+ this_file: 'is for threaded code',
+ edit: __FILE__,
+} -%>
+
+#define SC_STATE_SIZE 6
+
+#define SCS_XX 1
+#define SCS_AX 2
+#define SCS_BX 3
+#define SCS_AB 4
+#define SCS_BA 5
+
+#define SC_ERROR 0xffffffff
+
+static const VALUE sc_insn_info[][SC_STATE_SIZE] = {
+#define NO_SC { SC_ERROR, SC_ERROR, SC_ERROR, SC_ERROR, SC_ERROR, SC_ERROR }
+% RubyVM::Instructions.each_slice 8 do |a|
+ <%= a.map{|i| 'NO_SC' }.join(', ') %>,
+% end
+#undef NO_SC
+};
+
+static const VALUE sc_insn_next[] = {
+% RubyVM::Instructions.each_slice 8 do |a|
+ <%= a.map{|i| 'SCS_XX' }.join(', ') %>,
+% end
+};
+
+ASSERT_VM_INSTRUCTION_SIZE(sc_insn_next);
diff --git a/tool/ruby_vm/views/optinsn.inc.erb b/tool/ruby_vm/views/optinsn.inc.erb
new file mode 100644
index 0000000000..676f1edaba
--- /dev/null
+++ b/tool/ruby_vm/views/optinsn.inc.erb
@@ -0,0 +1,71 @@
+/* -*- C -*- */
+
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+<%= render 'copyright' -%>
+<%= render 'notice', locals: {
+ this_file: 'is for threaded code',
+ edit: __FILE__,
+} -%>
+
+static INSN *
+insn_operands_unification(INSN *iobj)
+{
+#ifdef OPT_OPERANDS_UNIFICATION
+ VALUE *op = iobj->operands;
+
+ switch (iobj->insn_id) {
+ default:
+ /* do nothing */;
+ break;
+
+% RubyVM::OperandsUnifications.each_group do |orig, unifs|
+ case <%= orig.bin %>:
+% unifs.each do |insn|
+
+ /* <%= insn.pretty_name %> */
+ if ( <%= insn.condition('op') %> ) {
+% insn.opes.each_with_index do |o, x|
+% n = insn.operand_shift_of(o)
+% if n != 0 then
+ op[<%= x %>] = op[<%= x + n %>];
+% end
+% end
+ iobj->insn_id = <%= insn.bin %>;
+ iobj->operand_size = <%= insn.opes.size %>;
+ break;
+ }
+% end
+
+ break;
+% end
+ }
+#endif
+ return iobj;
+}
+
+int
+rb_insn_unified_local_var_level(VALUE insn)
+{
+#ifdef OPT_OPERANDS_UNIFICATION
+ /* optimize rule */
+ switch (insn) {
+ default:
+ return -1; /* do nothing */;
+% RubyVM::OperandsUnifications.each_group do |orig, unifs|
+% unifs.each do|insn|
+ case <%= insn.bin %>:
+% insn.spec.map{|(var,val)|val}.reject{|i| i == '*' }.each do |val|
+ return <%= val %>;
+% break
+% end
+% end
+% end
+ }
+#endif
+ return -1;
+}
diff --git a/tool/ruby_vm/views/optunifs.inc.erb b/tool/ruby_vm/views/optunifs.inc.erb
new file mode 100644
index 0000000000..e92a95beff
--- /dev/null
+++ b/tool/ruby_vm/views/optunifs.inc.erb
@@ -0,0 +1,21 @@
+/* -*- C -*- */
+
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+% raise ':FIXME:TBW' if RubyVM::VmOptsH['INSTRUCTIONS_UNIFICATION']
+% n = RubyVM::Instructions.size
+<%= render 'copyright' %>
+<%= render 'notice', locals: {
+ this_file: 'is for threaded code',
+ edit: __FILE__,
+} -%>
+
+/* Let .bss section automatically initialize this variable */
+/* cf. Section 6.7.8 of ISO/IEC 9899:1999 */
+static const int *const *const unified_insns_data[<%= n %>];
+
+ASSERT_VM_INSTRUCTION_SIZE(unified_insns_data);
diff --git a/tool/ruby_vm/views/vm.inc.erb b/tool/ruby_vm/views/vm.inc.erb
new file mode 100644
index 0000000000..c1a3faf60a
--- /dev/null
+++ b/tool/ruby_vm/views/vm.inc.erb
@@ -0,0 +1,30 @@
+/* -*- C -*- */
+
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+<%= render 'copyright' %>
+<%= render 'notice', locals: {
+ this_file: 'is VM main loop',
+ edit: __FILE__,
+} -%>
+
+#include "vm_insnhelper.h"
+% RubyVM::BareInstructions.to_a.each do |insn|
+<%= render 'insn_entry', locals: { insn: insn } -%>
+% end
+%
+% RubyVM::OperandsUnifications.to_a.each do |insn|
+<%= render 'insn_entry', locals: { insn: insn } -%>
+% end
+%
+% RubyVM::InstructionsUnifications.to_a.each do |insn|
+<%= render 'insn_entry', locals: { insn: insn } -%>
+% end
+%
+% RubyVM::TraceInstructions.to_a.each do |insn|
+<%= render 'trace_instruction', locals: { insn: insn } -%>
+% end
diff --git a/tool/ruby_vm/views/vmtc.inc.erb b/tool/ruby_vm/views/vmtc.inc.erb
new file mode 100644
index 0000000000..99cbd92614
--- /dev/null
+++ b/tool/ruby_vm/views/vmtc.inc.erb
@@ -0,0 +1,21 @@
+/* -*- C -*- */
+
+%# Copyright (c) 2017 Urabe, Shyouhei. All rights reserved.
+%#
+%# This file is a part of the programming language Ruby. Permission is hereby
+%# granted, to either redistribute and/or modify this file, provided that the
+%# conditions mentioned in the file COPYING are met. Consult the file for
+%# details.
+<%= render 'copyright' -%>
+<%= render 'notice', locals: {
+ this_file: 'is for threaded code',
+ edit: __FILE__,
+} -%>
+
+static const void *const insns_address_table[] = {
+% RubyVM::Instructions.each do |i|
+ LABEL_PTR(<%= i.name %>),
+% end
+};
+
+ASSERT_VM_INSTRUCTION_SIZE(insns_address_table);
diff --git a/tool/run-gcov.rb b/tool/run-gcov.rb
new file mode 100644
index 0000000000..5df7622aa3
--- /dev/null
+++ b/tool/run-gcov.rb
@@ -0,0 +1,54 @@
+#!ruby
+require "pathname"
+require "open3"
+
+Pathname.glob("**/*.gcda").sort.each do |gcda|
+ if gcda.fnmatch("ext/*")
+ cwd, gcda = gcda.split.map {|s| s.to_s }
+ objdir = "."
+ elsif gcda.fnmatch("rubyspec_temp/*")
+ next
+ else
+ cwd, objdir, gcda = ".", gcda.dirname.to_s, gcda.to_s
+ end
+ puts "$ gcov -lpbc -o #{ objdir } #{ gcda }"
+ out, err, _status = Open3.capture3("gcov", "-lpbc", "-o", objdir, gcda, chdir: cwd)
+ puts out
+ puts err
+
+ # a black list of source files that contains wrong #line directives
+ if err !~ %r(
+ \A(
+ Cannot\ open\ source\ file\ (
+ defs/keywords
+ |zonetab\.list
+ |enc/jis/props\.kwd
+ |parser\.c
+ |parser\.rl
+ )\n
+ )*\z
+ )x
+ raise "Unexpected gcov output"
+ end
+
+ if out !~ %r(
+ \A(
+ File\ .*\nLines\ executed:.*\n
+ (
+ Branches\ executed:.*\n
+ Taken\ at\ least\ once:.*\n
+ |
+ No\ branches\n
+ )?
+ (
+ Calls\ executed:.*\n
+ |
+ No\ calls\n
+ )?
+ Creating\ .*\n
+ \n
+ )+\z
+ )x
+ raise "Unexpected gcov output"
+ end
+end
diff --git a/tool/run-lcov.rb b/tool/run-lcov.rb
new file mode 100644
index 0000000000..f27578200a
--- /dev/null
+++ b/tool/run-lcov.rb
@@ -0,0 +1,164 @@
+#!ruby
+require "pathname"
+require "open3"
+require "tmpdir"
+
+def backup_gcda_files(gcda_files)
+ gcda_files = gcda_files.map do |gcda|
+ [gcda, gcda.sub_ext(".bak")]
+ end
+ begin
+ gcda_files.each do |before, after|
+ before.rename(after)
+ end
+ yield
+ ensure
+ gcda_files.each do |before, after|
+ after.rename(before)
+ end
+ end
+end
+
+def run_lcov(*args)
+ system("lcov", "--rc", "lcov_branch_coverage=1", *args)
+end
+
+$info_files = []
+def run_lcov_capture(dir, info)
+ $info_files << info
+ run_lcov("--capture", "-d", dir, "-o", info)
+end
+
+def run_lcov_merge(files, info)
+ run_lcov(*files.flat_map {|f| ["--add-tracefile", f] }, "-o", info)
+end
+
+def run_lcov_remove(info_src, info_out)
+ dirs = %w(/usr/*)
+ dirs << File.join(Dir.tmpdir, "*")
+ %w(
+ test/*
+ ext/-test-/*
+ ext/nkf/nkf-utf8/nkf.c
+ ).each {|f| dirs << File.join(File.dirname(__dir__), f) }
+ run_lcov("--remove", info_src, *dirs, "-o", info_out)
+end
+
+def run_genhtml(info, out)
+ system("genhtml", "--branch-coverage", "--ignore-errors", "source", info, "-o", out)
+end
+
+def gen_rb_lcov(file)
+ res = Marshal.load(File.binread(file))
+
+ open("lcov-rb-all.info", "w") do |f|
+ f.puts "TN:" # no test name
+ base_dir = File.dirname(__dir__)
+ res.each do |path, cov|
+ next unless path.start_with?(base_dir)
+ next if path.start_with?(File.join(base_dir, "test"))
+ f.puts "SF:#{ path }"
+
+ total = covered = 0
+ cov.each_with_index do |count, lineno|
+ next unless count
+ f.puts "DA:#{ lineno + 1 },#{ count }"
+ total += 1
+ covered += 1 if count > 0
+ end
+ f.puts "LF:#{ total }"
+ f.puts "LH:#{ covered }"
+
+ f.puts "end_of_record"
+ end
+ end
+end
+
+def gen_rb_lcov(file)
+ res = Marshal.load(File.binread(file))
+
+ open("lcov-rb-all.info", "w") do |f|
+ f.puts "TN:" # no test name
+ base_dir = File.dirname(File.dirname(__dir__))
+ res.each do |path, cov|
+ next unless path.start_with?(base_dir)
+ next if path.start_with?(File.join(base_dir, "test"))
+ f.puts "SF:#{ path }"
+
+ # function coverage
+ total = covered = 0
+ cov[:methods].each do |(klass, name, lineno), count|
+ f.puts "FN:#{ lineno },#{ klass }##{ name }"
+ total += 1
+ covered += 1 if count > 0
+ end
+ f.puts "FNF:#{ total }"
+ f.puts "FNF:#{ covered }"
+ cov[:methods].each do |(klass, name, _), count|
+ f.puts "FNDA:#{ count },#{ klass }##{ name }"
+ end
+
+ # line coverage
+ total = covered = 0
+ cov[:lines].each_with_index do |count, lineno|
+ next unless count
+ f.puts "DA:#{ lineno + 1 },#{ count }"
+ total += 1
+ covered += 1 if count > 0
+ end
+ f.puts "LF:#{ total }"
+ f.puts "LH:#{ covered }"
+
+ # branch coverage
+ total = covered = 0
+ id = 0
+ cov[:branches].each do |(_base_type, _, base_lineno), targets|
+ i = 0
+ targets.each do |(_target_type, _target_lineno), count|
+ f.puts "BRDA:#{ base_lineno },#{ id },#{ i },#{ count }"
+ total += 1
+ covered += 1 if count > 0
+ i += 1
+ end
+ id += 1
+ end
+ f.puts "BRF:#{ total }"
+ f.puts "BRH:#{ covered }"
+ f.puts "end_of_record"
+ end
+ end
+end
+
+gcda_files = Pathname.glob("**/*.gcda")
+ext_gcda_files = gcda_files.select {|f| f.fnmatch("ext/*") }
+rubyspec_temp_gcda_files = gcda_files.select {|f| f.fnmatch("rubyspec_temp/*") }
+
+backup_gcda_files(rubyspec_temp_gcda_files) do
+ if ext_gcda_files != []
+ backup_gcda_files(ext_gcda_files) do
+ info = "lcov-root.info"
+ run_lcov_capture(".", info)
+ end
+ end
+ ext_gcda_files.group_by {|f| f.descend.to_a[1] }.each do |key, files|
+ info = "lcov-#{ key.to_s.gsub(File::Separator, "-") }.info"
+ run_lcov_capture(key.to_s, info)
+ end
+end
+if $info_files != []
+ run_lcov_merge($info_files, "lcov-c-all.info")
+ run_lcov_remove("lcov-c-all.info", "lcov-c-all-filtered.info")
+ run_genhtml("lcov-c-all-filtered.info", "lcov-c-out")
+end
+
+if File.readable?("test-coverage.dat")
+ gen_rb_lcov("test-coverage.dat")
+ run_lcov_remove("lcov-rb-all.info", "lcov-rb-all-filtered.info")
+ run_genhtml("lcov-rb-all-filtered.info", "lcov-rb-out")
+end
+
+if File.readable?("lcov-c-all.info") && File.readable?("lcov-rb-all.info")
+ run_lcov_merge(%w(lcov-c-all.info lcov-rb-all.info), "lcov-all.info")
+ run_lcov_remove("lcov-all.info", "lcov-all-filtered.info")
+ run_genhtml("lcov-all-filtered.info", "lcov-out")
+end
diff --git a/tool/runruby.rb b/tool/runruby.rb
new file mode 100755
index 0000000000..1efe38fd13
--- /dev/null
+++ b/tool/runruby.rb
@@ -0,0 +1,178 @@
+#!./miniruby
+
+# Used by "make runruby", configure, and by hand to run a locally-built Ruby
+# with correct environment variables and arguments.
+
+show = false
+precommand = []
+srcdir = File.realpath('..', File.dirname(__FILE__))
+case
+when ENV['RUNRUBY_USE_GDB'] == 'true'
+ debugger = :gdb
+when ENV['RUNRUBY_USE_LLDB'] == 'true'
+ debugger = :lldb
+when ENV['RUNRUBY_YJIT_STATS']
+ use_yjit_stat = true
+end
+while arg = ARGV[0]
+ break ARGV.shift if arg == '--'
+ case arg
+ when '-C', /\A-C(.+)/m
+ ARGV.shift
+ Dir.chdir($1 || ARGV.shift)
+ next
+ end
+ /\A--([-\w]+)(?:=(.*))?\z/ =~ arg or break
+ arg, value = $1, $2
+ re = Regexp.new('\A'+arg.gsub(/\w+\b/, '\&\\w*')+'\z', "i")
+ case
+ when re =~ "srcdir"
+ srcdir = value
+ when re =~ "archdir"
+ archdir = value
+ when re =~ "cpu"
+ precommand << "arch" << "-arch" << value
+ when re =~ "extout"
+ extout = value
+ when re =~ "pure"
+ # obsolete switch do nothing
+ when re =~ "debugger"
+ require 'shellwords'
+ case value
+ when nil
+ debugger = :gdb
+ when "lldb"
+ debugger = :lldb
+ when "no"
+ else
+ debugger = Shellwords.shellwords(value)
+ end and precommand |= [:debugger]
+ when re =~ "precommand"
+ require 'shellwords'
+ precommand.concat(Shellwords.shellwords(value))
+ when re =~ "show"
+ show = true
+ when re =~ "chdir"
+ Dir.chdir(value)
+ else
+ break
+ end
+ ARGV.shift
+end
+
+unless defined?(File.realpath)
+ def File.realpath(*args)
+ path = expand_path(*args)
+ if File.stat(path).directory?
+ Dir.chdir(path) {Dir.pwd}
+ else
+ dir, base = File.split(path)
+ File.join(Dir.chdir(dir) {Dir.pwd}, base)
+ end
+ end
+end
+
+begin
+ conffile = File.realpath('rbconfig.rb', archdir)
+rescue Errno::ENOENT => e
+ # retry if !archdir and ARGV[0] and File.directory?(archdir = ARGV.shift)
+ abort "#$0: rbconfig.rb not found, use --archdir option"
+end
+
+abs_archdir = File.dirname(conffile)
+archdir ||= abs_archdir
+$:.unshift(abs_archdir)
+
+config = File.read(conffile)
+config.sub!(/^(\s*)RUBY_VERSION\b.*(\sor\s*)\n.*\n/, '')
+config = Module.new {module_eval(config, conffile)}::RbConfig::CONFIG
+
+install_name = config["RUBY_INSTALL_NAME"]+config['EXEEXT']
+ruby = File.join(archdir, install_name)
+unless File.exist?(ruby)
+ abort "#{ruby} is not found.\nTry `make' first, then `make test', please.\n"
+end
+
+libs = [abs_archdir]
+extout ||= config["EXTOUT"]
+if extout
+ abs_extout = File.expand_path(extout, abs_archdir)
+ libs << File.expand_path("common", abs_extout) << File.expand_path(config['arch'], abs_extout)
+end
+libs << File.expand_path("lib", srcdir)
+config["bindir"] = abs_archdir
+
+env = {
+ # Test with the smallest possible machine stack sizes.
+ # These values are clamped to machine-dependent minimum values in vm_core.h
+ 'RUBY_THREAD_MACHINE_STACK_SIZE' => '1',
+ 'RUBY_FIBER_MACHINE_STACK_SIZE' => '1',
+}
+
+runner = File.join(abs_archdir, "exe/#{install_name}")
+runner = nil unless File.exist?(runner)
+abs_ruby = runner || File.expand_path(ruby)
+env["RUBY"] = abs_ruby
+env["GEM_PATH"] = env["GEM_HOME"] = File.expand_path(".bundle", srcdir)
+env["GEM_COMMAND"] = "#{abs_ruby} -rrubygems #{srcdir}/bin/gem --backtrace"
+env["PATH"] = [File.dirname(abs_ruby), abs_archdir, ENV["PATH"]].compact.join(File::PATH_SEPARATOR)
+
+if e = ENV["RUBYLIB"]
+ libs |= e.split(File::PATH_SEPARATOR)
+end
+env["RUBYLIB"] = $:.replace(libs).join(File::PATH_SEPARATOR)
+
+gem_path = [abs_archdir, srcdir].map {|d| File.realdirpath(".bundle", d)}
+if e = ENV["GEM_PATH"]
+ gem_path |= e.split(File::PATH_SEPARATOR)
+end
+env["GEM_PATH"] = gem_path.join(File::PATH_SEPARATOR)
+
+libruby_so = File.join(abs_archdir, config['LIBRUBY_SO'])
+if File.file?(libruby_so)
+ if e = config['LIBPATHENV'] and !e.empty?
+ env[e] = [abs_archdir, ENV[e]].compact.join(File::PATH_SEPARATOR)
+ end
+end
+
+ENV.update env
+
+if debugger
+ case debugger
+ when :gdb, nil
+ debugger = %W'gdb -x #{srcdir}/.gdbinit'
+ if File.exist?(gdb = 'run.gdb') or
+ File.exist?(gdb = File.join(abs_archdir, 'run.gdb'))
+ debugger.push('-x', gdb)
+ end
+ debugger << '--args'
+ when :lldb
+ debugger = ['lldb', '-O', "command script import #{srcdir}/misc/lldb_cruby.py"]
+ if File.exist?(lldb = 'run.lldb') or
+ File.exist?(lldb = File.join(abs_archdir, 'run.lldb'))
+ debugger.push('-s', lldb)
+ end
+ debugger << '--'
+ end
+
+ if idx = precommand.index(:debugger)
+ precommand[idx, 1] = debugger
+ else
+ precommand.concat(debugger)
+ end
+end
+
+cmd = [runner || ruby]
+if use_yjit_stat
+ cmd << '--yjit-stats'
+end
+cmd.concat(ARGV)
+cmd.unshift(*precommand) unless precommand.empty?
+
+if show
+ require 'shellwords'
+ env.each {|k,v| puts "#{k}=#{v}"}
+ puts Shellwords.join(cmd)
+end
+
+exec(*cmd, close_others: false)
diff --git a/tool/search-cgvars.rb b/tool/search-cgvars.rb
new file mode 100644
index 0000000000..c62641a3ff
--- /dev/null
+++ b/tool/search-cgvars.rb
@@ -0,0 +1,55 @@
+#
+# Listing C's global variables in .so or .o, or .bundle on Mac OS using "objdump -t" (elf64-x86-64)
+# to check ractor-safety.
+#
+# Usage: ruby search-cgvars.rb foo.so bar.o .ext/x86_64-darwin18/psych.bundle
+#
+MAC_OS = RbConfig::CONFIG['host_os'].match? /darwin|mac os/
+
+def gvars file
+ # '0000000000031ac8 g O .bss 0000000000000008 rb_cSockIfaddr'
+ # On mac, with .bundle files:
+ # '0000000000004258 l O __DATA,__bss _passwd_blocking'
+
+ strs = `objdump -t #{file}`
+ found = {}
+ strs.each_line{|line|
+ if /[\da-f]{16} / =~ line
+ addr = line[0...16]
+ flags = line[17...24].tr(' ', '').split(//).sort.uniq
+ rest = line[25..]
+ if MAC_OS
+ seg, name = rest.split(/\s+/)
+ else
+ seg, size, name = rest.split(/\s+/)
+ end
+ if flags.include?('O')
+ # p [addr, flags, seg, size, name]
+ found[name] = [flags, seg, *size]
+ end
+ end
+ }
+ puts "## #{file}:"
+ found.sort_by{|name, (flags, *)|
+ [flags, name]
+ }.each{|name, rest|
+ flags, seg, size = *rest
+ next if (size.to_i == 0 && !MAC_OS) && seg != '*UND*'
+ case seg
+ when ".rodata", ".data.rel.ro", ".got.plt", ".eh_frame", ".fini_array"
+ next
+ when /,__const$/ # Mac OS
+ next
+ end
+ case name
+ when /^id_/, /^rbimpl_id/, /^sym_/, /^rb_[cme]/, /\Acompleted\.\d+\z/
+ next
+ when /^_id_/, /\.rbimpl_id(\.\d+)?$/ # Mac OS
+ next
+ end
+ puts " %40s %s" % [name, rest.inspect]
+ }
+end
+ARGV.each{|file|
+ gvars file
+}
diff --git a/tool/strip-rdoc.rb b/tool/strip-rdoc.rb
new file mode 100755
index 0000000000..d8e311cdbf
--- /dev/null
+++ b/tool/strip-rdoc.rb
@@ -0,0 +1,14 @@
+#!ruby
+# frozen_string_literal: true
+
+# Filter for preventing Doxygen from processing RDoc comments.
+# Used by the Doxygen template.
+
+print ARGF.binmode.read.tap {|src|
+ src.gsub!(%r|(/\*[!*])(?:(?!\*/).)+?^\s*\*\s?\-\-\s*$(.+?\*/)|m) {
+ marker = $1
+ comment = $2
+ comment.sub!(%r|^\s*\*\s?\+\+\s*$.+?(\s*\*/)\z|m, '\\1')
+ marker + comment
+ }
+}
diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb
new file mode 100755
index 0000000000..564877a26b
--- /dev/null
+++ b/tool/sync_default_gems.rb
@@ -0,0 +1,638 @@
+#!/usr/bin/env ruby
+# sync upstream github repositories to ruby repository
+
+require 'fileutils'
+include FileUtils
+
+REPOSITORIES = {
+ rubygems: 'rubygems/rubygems',
+ rdoc: 'ruby/rdoc',
+ reline: 'ruby/reline',
+ json: 'flori/json',
+ psych: 'ruby/psych',
+ fileutils: 'ruby/fileutils',
+ fiddle: 'ruby/fiddle',
+ stringio: 'ruby/stringio',
+ "io-console": 'ruby/io-console',
+ "io-nonblock": 'ruby/io-nonblock',
+ "io-wait": 'ruby/io-wait',
+ csv: 'ruby/csv',
+ etc: 'ruby/etc',
+ date: 'ruby/date',
+ zlib: 'ruby/zlib',
+ fcntl: 'ruby/fcntl',
+ strscan: 'ruby/strscan',
+ ipaddr: 'ruby/ipaddr',
+ logger: 'ruby/logger',
+ ostruct: 'ruby/ostruct',
+ irb: 'ruby/irb',
+ forwardable: "ruby/forwardable",
+ mutex_m: "ruby/mutex_m",
+ racc: "ruby/racc",
+ singleton: "ruby/singleton",
+ open3: "ruby/open3",
+ getoptlong: "ruby/getoptlong",
+ pstore: "ruby/pstore",
+ delegate: "ruby/delegate",
+ benchmark: "ruby/benchmark",
+ cgi: "ruby/cgi",
+ readline: "ruby/readline",
+ "readline-ext": "ruby/readline-ext",
+ observer: "ruby/observer",
+ timeout: "ruby/timeout",
+ yaml: "ruby/yaml",
+ uri: "ruby/uri",
+ openssl: "ruby/openssl",
+ did_you_mean: "ruby/did_you_mean",
+ weakref: "ruby/weakref",
+ tempfile: "ruby/tempfile",
+ tmpdir: "ruby/tmpdir",
+ English: "ruby/English",
+ "net-protocol": "ruby/net-protocol",
+ "net-http": "ruby/net-http",
+ bigdecimal: "ruby/bigdecimal",
+ optparse: "ruby/optparse",
+ set: "ruby/set",
+ find: "ruby/find",
+ rinda: "ruby/rinda",
+ erb: "ruby/erb",
+ nkf: "ruby/nkf",
+ tsort: "ruby/tsort",
+ abbrev: "ruby/abbrev",
+ shellwords: "ruby/shellwords",
+ base64: "ruby/base64",
+ syslog: "ruby/syslog",
+ "open-uri": "ruby/open-uri",
+ securerandom: "ruby/securerandom",
+ resolv: "ruby/resolv",
+ "resolv-replace": "ruby/resolv-replace",
+ time: "ruby/time",
+ pp: "ruby/pp",
+ prettyprint: "ruby/prettyprint",
+ drb: "ruby/drb",
+ pathname: "ruby/pathname",
+ digest: "ruby/digest",
+ error_highlight: "ruby/error_highlight",
+ un: "ruby/un",
+ win32ole: "ruby/win32ole",
+}
+
+# We usually don't use this. Please consider using #sync_default_gems_with_commits instead.
+def sync_default_gems(gem)
+ repo = REPOSITORIES[gem.to_sym]
+ puts "Sync #{repo}"
+
+ upstream = File.join("..", "..", repo)
+
+ case gem
+ when "rubygems"
+ rm_rf(%w[lib/rubygems lib/rubygems.rb test/rubygems])
+ cp_r(Dir.glob("#{upstream}/lib/rubygems*"), "lib")
+ cp_r("#{upstream}/test/rubygems", "test")
+ rm_rf(%w[lib/bundler lib/bundler.rb libexec/bundler libexec/bundle spec/bundler tool/bundler/*])
+ cp_r(Dir.glob("#{upstream}/bundler/lib/bundler*"), "lib")
+ cp_r(Dir.glob("#{upstream}/bundler/exe/bundle*"), "libexec")
+
+ gemspec_content = File.readlines("#{upstream}/bundler/bundler.gemspec").map do |line|
+ next if line =~ /LICENSE\.md/
+
+ line.gsub("bundler.gemspec", "lib/bundler/bundler.gemspec").gsub('"exe"', '"libexec"')
+ end.compact.join
+ File.write("lib/bundler/bundler.gemspec", gemspec_content)
+
+ cp_r("#{upstream}/bundler/spec", "spec/bundler")
+ cp_r(Dir.glob("#{upstream}/bundler/tool/bundler/dev_gems*"), "tool/bundler")
+ cp_r(Dir.glob("#{upstream}/bundler/tool/bundler/test_gems*"), "tool/bundler")
+ cp_r(Dir.glob("#{upstream}/bundler/tool/bundler/rubocop_gems*"), "tool/bundler")
+ cp_r(Dir.glob("#{upstream}/bundler/tool/bundler/standard_gems*"), "tool/bundler")
+ rm_rf(%w[spec/bundler/support/artifice/vcr_cassettes])
+ license_files = %w[
+ lib/bundler/vendor/thor/LICENSE.md
+ lib/rubygems/resolver/molinillo/LICENSE
+ lib/bundler/vendor/molinillo/LICENSE
+ lib/bundler/vendor/connection_pool/LICENSE
+ lib/bundler/vendor/net-http-persistent/README.rdoc
+ lib/bundler/vendor/fileutils/LICENSE.txt
+ lib/bundler/vendor/tsort/LICENSE.txt
+ lib/bundler/vendor/uri/LICENSE.txt
+ lib/rubygems/optparse/COPYING
+ lib/rubygems/tsort/LICENSE.txt
+ ]
+ rm_rf license_files
+ when "rdoc"
+ rm_rf(%w[lib/rdoc lib/rdoc.rb test/rdoc libexec/rdoc libexec/ri])
+ cp_r(Dir.glob("#{upstream}/lib/rdoc*"), "lib")
+ cp_r("#{upstream}/test/rdoc", "test")
+ cp_r("#{upstream}/rdoc.gemspec", "lib/rdoc")
+ cp_r("#{upstream}/Gemfile", "lib/rdoc")
+ cp_r("#{upstream}/Rakefile", "lib/rdoc")
+ cp_r("#{upstream}/exe/rdoc", "libexec")
+ cp_r("#{upstream}/exe/ri", "libexec")
+ parser_files = {
+ 'lib/rdoc/markdown.kpeg' => 'lib/rdoc/markdown.rb',
+ 'lib/rdoc/markdown/literals.kpeg' => 'lib/rdoc/markdown/literals.rb',
+ 'lib/rdoc/rd/block_parser.ry' => 'lib/rdoc/rd/block_parser.rb',
+ 'lib/rdoc/rd/inline_parser.ry' => 'lib/rdoc/rd/inline_parser.rb'
+ }
+ Dir.chdir(upstream) do
+ `bundle install`
+ parser_files.each_value do |dst|
+ `bundle exec rake #{dst}`
+ end
+ end
+ parser_files.each_pair do |src, dst|
+ rm_rf(src)
+ cp_r("#{upstream}/#{dst}", dst)
+ end
+ `git checkout lib/rdoc/.document`
+ rm_rf(%w[lib/rdoc/Gemfile lib/rdoc/Rakefile])
+ when "reline"
+ rm_rf(%w[lib/reline lib/reline.rb test/reline])
+ cp_r(Dir.glob("#{upstream}/lib/reline*"), "lib")
+ cp_r("#{upstream}/test/reline", "test")
+ cp_r("#{upstream}/reline.gemspec", "lib/reline")
+ when "irb"
+ rm_rf(%w[lib/irb lib/irb.rb test/irb])
+ cp_r(Dir.glob("#{upstream}/lib/irb*"), "lib")
+ cp_r("#{upstream}/test/irb", "test")
+ cp_r("#{upstream}/irb.gemspec", "lib/irb")
+ cp_r("#{upstream}/man/irb.1", "man/irb.1")
+ cp_r("#{upstream}/doc/irb", "doc")
+ when "json"
+ rm_rf(%w[ext/json test/json])
+ cp_r("#{upstream}/ext/json/ext", "ext/json")
+ cp_r("#{upstream}/tests", "test/json")
+ rm_rf("test/json/lib")
+ cp_r("#{upstream}/lib", "ext/json")
+ cp_r("#{upstream}/json.gemspec", "ext/json")
+ cp_r("#{upstream}/VERSION", "ext/json")
+ rm_rf(%w[ext/json/lib/json/ext ext/json/lib/json/pure.rb ext/json/lib/json/pure])
+ `git checkout ext/json/extconf.rb ext/json/parser/prereq.mk ext/json/generator/depend ext/json/parser/depend ext/json/depend`
+ when "psych"
+ rm_rf(%w[ext/psych test/psych])
+ cp_r("#{upstream}/ext/psych", "ext")
+ cp_r("#{upstream}/lib", "ext/psych")
+ cp_r("#{upstream}/test/psych", "test")
+ rm_rf(%w[ext/psych/lib/org ext/psych/lib/psych.jar ext/psych/lib/psych_jars.rb])
+ rm_rf(%w[ext/psych/lib/psych.{bundle,so} ext/psych/lib/2.*])
+ rm_rf(["ext/psych/yaml/LICENSE"])
+ cp_r("#{upstream}/psych.gemspec", "ext/psych")
+ `git checkout ext/psych/depend`
+ when "fiddle"
+ rm_rf(%w[ext/fiddle test/fiddle])
+ cp_r("#{upstream}/ext/fiddle", "ext")
+ cp_r("#{upstream}/lib", "ext/fiddle")
+ cp_r("#{upstream}/test/fiddle", "test")
+ cp_r("#{upstream}/fiddle.gemspec", "ext/fiddle")
+ `git checkout ext/fiddle/depend`
+ rm_rf(%w[ext/fiddle/lib/fiddle.{bundle,so}])
+ when "stringio"
+ rm_rf(%w[ext/stringio test/stringio])
+ cp_r("#{upstream}/ext/stringio", "ext")
+ cp_r("#{upstream}/test/stringio", "test")
+ cp_r("#{upstream}/stringio.gemspec", "ext/stringio")
+ `git checkout ext/stringio/depend ext/stringio/README.md`
+ when "io-console"
+ rm_rf(%w[ext/io/console test/io/console])
+ cp_r("#{upstream}/ext/io/console", "ext/io")
+ cp_r("#{upstream}/test/io/console", "test/io")
+ mkdir_p("ext/io/console/lib")
+ cp_r("#{upstream}/lib/io/console", "ext/io/console/lib")
+ rm_rf("ext/io/console/lib/console/ffi")
+ cp_r("#{upstream}/io-console.gemspec", "ext/io/console")
+ `git checkout ext/io/console/depend`
+ when "io-nonblock"
+ rm_rf(%w[ext/io/nonblock test/io/nonblock])
+ cp_r("#{upstream}/ext/io/nonblock", "ext/io")
+ cp_r("#{upstream}/test/io/nonblock", "test/io")
+ cp_r("#{upstream}/io-nonblock.gemspec", "ext/io/nonblock")
+ `git checkout ext/io/nonblock/depend`
+ when "io-wait"
+ rm_rf(%w[ext/io/wait test/io/wait])
+ cp_r("#{upstream}/ext/io/wait", "ext/io")
+ cp_r("#{upstream}/test/io/wait", "test/io")
+ cp_r("#{upstream}/io-wait.gemspec", "ext/io/wait")
+ `git checkout ext/io/wait/depend`
+ when "etc"
+ rm_rf(%w[ext/etc test/etc])
+ cp_r("#{upstream}/ext/etc", "ext")
+ cp_r("#{upstream}/test/etc", "test")
+ cp_r("#{upstream}/etc.gemspec", "ext/etc")
+ `git checkout ext/etc/depend`
+ when "date"
+ rm_rf(%w[ext/date test/date])
+ cp_r("#{upstream}/ext/date", "ext")
+ cp_r("#{upstream}/lib", "ext/date")
+ cp_r("#{upstream}/test/date", "test")
+ cp_r("#{upstream}/date.gemspec", "ext/date")
+ `git checkout ext/date/depend`
+ rm_rf(["ext/date/lib/date_core.bundle"])
+ when "zlib"
+ rm_rf(%w[ext/zlib test/zlib])
+ cp_r("#{upstream}/ext/zlib", "ext")
+ cp_r("#{upstream}/test/zlib", "test")
+ cp_r("#{upstream}/zlib.gemspec", "ext/zlib")
+ `git checkout ext/zlib/depend`
+ when "fcntl"
+ rm_rf(%w[ext/fcntl])
+ cp_r("#{upstream}/ext/fcntl", "ext")
+ cp_r("#{upstream}/fcntl.gemspec", "ext/fcntl")
+ `git checkout ext/fcntl/depend`
+ when "strscan"
+ rm_rf(%w[ext/strscan test/strscan])
+ cp_r("#{upstream}/ext/strscan", "ext")
+ cp_r("#{upstream}/test/strscan", "test")
+ cp_r("#{upstream}/strscan.gemspec", "ext/strscan")
+ rm_rf(%w["ext/strscan/regenc.h ext/strscan/regint.h"])
+ `git checkout ext/strscan/depend`
+ when "racc"
+ rm_rf(%w[lib/racc lib/racc.rb ext/racc test/racc])
+ cp_r(Dir.glob("#{upstream}/lib/racc*"), "lib")
+ mkdir_p("ext/racc/cparse")
+ cp_r(Dir.glob("#{upstream}/ext/racc/cparse/*"), "ext/racc/cparse")
+ cp_r("#{upstream}/test", "test/racc")
+ cp_r("#{upstream}/racc.gemspec", "lib/racc")
+ rm_rf("test/racc/lib")
+ rm_rf("lib/racc/cparse-jruby.jar")
+ `git checkout ext/racc/cparse/README ext/racc/cparse/depend`
+ when "cgi"
+ rm_rf(%w[lib/cgi.rb lib/cgi ext/cgi test/cgi])
+ cp_r("#{upstream}/ext/cgi", "ext")
+ cp_r("#{upstream}/lib", ".")
+ cp_r("#{upstream}/test/cgi", "test")
+ cp_r("#{upstream}/cgi.gemspec", "lib/cgi")
+ `git checkout ext/cgi/escape/depend`
+ when "openssl"
+ rm_rf(%w[ext/openssl test/openssl])
+ cp_r("#{upstream}/ext/openssl", "ext")
+ cp_r("#{upstream}/lib", "ext/openssl")
+ cp_r("#{upstream}/test/openssl", "test")
+ rm_rf("test/openssl/envutil.rb")
+ cp_r("#{upstream}/openssl.gemspec", "ext/openssl")
+ cp_r("#{upstream}/History.md", "ext/openssl")
+ `git checkout ext/openssl/depend`
+ when "net-protocol"
+ rm_rf(%w[lib/net/protocol.rb lib/net/net-protocol.gemspec test/net/protocol])
+ cp_r("#{upstream}/lib/net/protocol.rb", "lib/net")
+ cp_r("#{upstream}/test/net/protocol", "test/net")
+ cp_r("#{upstream}/net-protocol.gemspec", "lib/net")
+ when "net-http"
+ rm_rf(%w[lib/net/http.rb lib/net/http test/net/http])
+ cp_r("#{upstream}/lib/net/http.rb", "lib/net")
+ cp_r("#{upstream}/lib/net/http", "lib/net")
+ cp_r("#{upstream}/test/net/http", "test/net")
+ cp_r("#{upstream}/net-http.gemspec", "lib/net/http")
+ when "readline-ext"
+ rm_rf(%w[ext/readline test/readline])
+ cp_r("#{upstream}/ext/readline", "ext")
+ cp_r("#{upstream}/test/readline", "test")
+ cp_r("#{upstream}/readline-ext.gemspec", "ext/readline")
+ `git checkout ext/readline/depend`
+ when "did_you_mean"
+ rm_rf(%w[lib/did_you_mean lib/did_you_mean.rb test/did_you_mean])
+ cp_r(Dir.glob("#{upstream}/lib/did_you_mean*"), "lib")
+ cp_r("#{upstream}/did_you_mean.gemspec", "lib/did_you_mean")
+ cp_r("#{upstream}/test", "test/did_you_mean")
+ rm_rf(%w[test/did_you_mean/tree_spell/test_explore.rb])
+ when "erb"
+ rm_rf(%w[lib/erb* test/erb libexec/erb])
+ cp_r("#{upstream}/lib/erb.rb", "lib")
+ cp_r("#{upstream}/test/erb", "test")
+ cp_r("#{upstream}/erb.gemspec", "lib")
+ cp_r("#{upstream}/libexec/erb", "libexec")
+ when "nkf"
+ rm_rf(%w[ext/nkf test/nkf])
+ cp_r("#{upstream}/ext/nkf", "ext")
+ cp_r("#{upstream}/lib", "ext/nkf")
+ cp_r("#{upstream}/test/nkf", "test")
+ cp_r("#{upstream}/nkf.gemspec", "ext/nkf")
+ `git checkout ext/nkf/depend`
+ when "syslog"
+ rm_rf(%w[ext/syslog test/syslog test/test_syslog.rb])
+ cp_r("#{upstream}/ext/syslog", "ext")
+ cp_r("#{upstream}/lib", "ext/syslog")
+ cp_r("#{upstream}/test/syslog", "test")
+ cp_r("#{upstream}/test/test_syslog.rb", "test")
+ cp_r("#{upstream}/syslog.gemspec", "ext/syslog")
+ `git checkout ext/syslog/depend`
+ when "bigdecimal"
+ rm_rf(%w[ext/bigdecimal test/bigdecimal])
+ cp_r("#{upstream}/ext/bigdecimal", "ext")
+ cp_r("#{upstream}/sample", "ext/bigdecimal")
+ cp_r("#{upstream}/lib", "ext/bigdecimal")
+ cp_r("#{upstream}/test/bigdecimal", "test")
+ cp_r("#{upstream}/bigdecimal.gemspec", "ext/bigdecimal")
+ `git checkout ext/bigdecimal/depend`
+ when "pathname"
+ rm_rf(%w[ext/pathname test/pathname])
+ cp_r("#{upstream}/ext/pathname", "ext")
+ cp_r("#{upstream}/test/pathname", "test")
+ cp_r("#{upstream}/lib", "ext/pathname")
+ cp_r("#{upstream}/pathname.gemspec", "ext/pathname")
+ `git checkout ext/pathname/depend`
+ when "digest"
+ rm_rf(%w[ext/digest test/digest])
+ cp_r("#{upstream}/ext/digest", "ext")
+ mkdir_p("ext/digest/lib/digest")
+ cp_r("#{upstream}/lib/digest.rb", "ext/digest/lib/")
+ cp_r("#{upstream}/lib/digest/version.rb", "ext/digest/lib/digest/")
+ mkdir_p("ext/digest/sha2/lib")
+ cp_r("#{upstream}/lib/digest/sha2.rb", "ext/digest/sha2/lib")
+ move("ext/digest/lib/digest/sha2", "ext/digest/sha2/lib")
+ cp_r("#{upstream}/test/digest", "test")
+ cp_r("#{upstream}/digest.gemspec", "ext/digest")
+ `git checkout ext/digest/depend ext/digest/*/depend`
+ when "set"
+ sync_lib gem, upstream
+ cp_r("#{upstream}/test", ".")
+ when "optparse"
+ sync_lib gem, upstream
+ rm_rf(%w[doc/optparse])
+ mkdir_p("doc/optparse")
+ cp_r("#{upstream}/doc/optparse", "doc")
+ when "error_highlight"
+ rm_rf(%w[lib/error_highlight lib/error_highlight.rb test/error_highlight])
+ cp_r(Dir.glob("#{upstream}/lib/error_highlight*"), "lib")
+ cp_r("#{upstream}/error_highlight.gemspec", "lib/error_highlight")
+ cp_r("#{upstream}/test", "test/error_highlight")
+ when "win32ole"
+ sync_lib gem, upstream
+ rm_rf(%w[ext/win32ole/lib])
+ Dir.mkdir(*%w[ext/win32ole/lib])
+ move("lib/win32ole/win32ole.gemspec", "ext/win32ole")
+ move(Dir.glob("lib/win32ole*"), "ext/win32ole/lib")
+ when "open3"
+ sync_lib gem, upstream
+ rm_rf("lib/open3/jruby_windows.rb")
+ else
+ sync_lib gem, upstream
+ end
+end
+
+IGNORE_FILE_PATTERN =
+ /\A(?:[A-Z]\w*\.(?:md|txt)
+ |[^\/]+\.yml
+ |\.git.*
+ |[A-Z]\w+file
+ |COPYING
+ |rakelib\/
+ )\z/x
+
+def message_filter(repo, sha)
+ log = STDIN.read
+ log.delete!("\r")
+ url = "https://github.com/#{repo}"
+ print "[#{repo}] ", log.gsub(/\b(?i:fix) +\K#(?=\d+\b)|\(\K#(?=\d+\))|\bGH-(?=\d+\b)/) {
+ "#{url}/pull/"
+ }.gsub(%r{(?<![-\[\](){}\w@/])(?:(\w+(?:-\w+)*/\w+(?:-\w+)*)@)?(\h{10,40})\b}) {|c|
+ "https://github.com/#{$1 || repo}/commit/#{$2[0,12]}"
+ }.sub(/\s*(?=(?i:\nCo-authored-by:.*)*\Z)/) {
+ "\n\n" "#{url}/commit/#{sha[0,10]}\n"
+ }
+end
+
+# NOTE: This method is also used by ruby-commit-hook/bin/update-default-gem.sh
+# @param gem [String] A gem name, also used as a git remote name. REPOSITORIES converts it to the appropriate GitHub repository.
+# @param ranges [Array<String>] "before..after". Note that it will NOT sync "before" (but commits after that).
+# @param edit [TrueClass] Set true if you want to resolve conflicts. Obviously, update-default-gem.sh doesn't use this.
+def sync_default_gems_with_commits(gem, ranges, edit: nil)
+ repo = REPOSITORIES[gem.to_sym]
+ puts "Sync #{repo} with commit history."
+
+ IO.popen(%W"git remote") do |f|
+ unless f.read.split.include?(gem)
+ `git remote add #{gem} git@github.com:#{repo}.git`
+ end
+ end
+ system(*%W"git fetch --no-tags #{gem}")
+
+ if ranges == true
+ pattern = "https://github\.com/#{Regexp.quote(repo)}/commit/([0-9a-f]+)$"
+ log = IO.popen(%W"git log -E --grep=#{pattern} -n1 --format=%B", &:read)
+ ranges = ["#{log[%r[#{pattern}\n\s*(?i:co-authored-by:.*)*\s*\Z], 1]}..#{gem}/master"]
+ end
+
+ commits = ranges.flat_map do |range|
+ unless range.include?("..")
+ range = "#{range}~1..#{range}"
+ end
+
+ IO.popen(%W"git log --format=%H,%s #{range} --") do |f|
+ f.read.split("\n").reverse.map{|commit| commit.split(',', 2)}
+ end
+ end
+
+ # Ignore Merge commit and insufficiency commit for ruby core repository.
+ commits.delete_if do |sha, subject|
+ files = IO.popen(%W"git diff-tree --no-commit-id --name-only -r #{sha}", &:readlines)
+ subject =~ /^Merge/ || subject =~ /^Auto Merge/ || files.all?{|file| file =~ IGNORE_FILE_PATTERN}
+ end
+
+ if commits.empty?
+ puts "No commits to pick"
+ return true
+ end
+
+ puts "Try to pick these commits:"
+ puts commits.map{|commit| commit.join(": ")}
+ puts "----"
+
+ failed_commits = []
+
+ ENV["FILTER_BRANCH_SQUELCH_WARNING"] = "1"
+
+ require 'shellwords'
+ filter = [
+ ENV.fetch('RUBY', 'ruby').shellescape,
+ File.realpath(__FILE__).shellescape,
+ "--message-filter",
+ ]
+ commits.each do |sha, subject|
+ puts "Pick #{sha} from #{repo}."
+
+ skipped = false
+ result = IO.popen(%W"git cherry-pick #{sha}", &:read)
+ if result =~ /nothing\ to\ commit/
+ `git reset`
+ skipped = true
+ puts "Skip empty commit #{sha}"
+ end
+ next if skipped
+
+ if result.empty?
+ skipped = true
+ elsif /^CONFLICT/ =~ result
+ result = IO.popen(%W"git status --porcelain", &:readlines).each(&:chomp!)
+ result.map! {|line| line[/^.U (.*)/, 1]}
+ result.compact!
+ ignore, conflict = result.partition {|name| IGNORE_FILE_PATTERN =~ name}
+ unless ignore.empty?
+ system(*%W"git reset HEAD --", *ignore)
+ File.unlink(*ignore)
+ ignore = IO.popen(%W"git status --porcelain" + ignore, &:readlines).map! {|line| line[/^.. (.*)/, 1]}
+ system(*%W"git checkout HEAD --", *ignore) unless ignore.empty?
+ end
+ unless conflict.empty?
+ if edit
+ case
+ when (editor = ENV["GIT_EDITOR"] and !editor.empty?)
+ when (editor = `git config core.editor` and (editor.chomp!; !editor.empty?))
+ end
+ if editor
+ system([editor, conflict].join(' '))
+ end
+ end
+ end
+ skipped = !system({"GIT_EDITOR"=>"true"}, *%W"git cherry-pick --no-edit --continue")
+ end
+
+ if skipped
+ failed_commits << sha
+ `git reset` && `git checkout .` && `git clean -fd`
+ puts "Failed to pick #{sha}"
+ next
+ end
+
+ puts "Update commit message: #{sha}"
+
+ IO.popen(%W[git filter-branch -f --msg-filter #{[filter, repo, sha].join(' ')} -- HEAD~1..HEAD], &:read)
+ unless $?.success?
+ puts "Failed to modify commit message of #{sha}"
+ break
+ end
+ end
+
+ unless failed_commits.empty?
+ puts "---- failed commits ----"
+ puts failed_commits
+ return false
+ end
+ return true
+end
+
+def sync_lib(repo, upstream = nil)
+ unless upstream and File.directory?(upstream) or File.directory?(upstream = "../#{repo}")
+ abort %[Expected '#{upstream}' \(#{File.expand_path("#{upstream}")}\) to be a directory, but it wasn't.]
+ end
+ rm_rf(["lib/#{repo}.rb", "lib/#{repo}/*", "test/test_#{repo}.rb"])
+ cp_r(Dir.glob("#{upstream}/lib/*"), "lib")
+ tests = if File.directory?("test/#{repo}")
+ "test/#{repo}"
+ else
+ "test/test_#{repo}.rb"
+ end
+ cp_r("#{upstream}/#{tests}", "test") if File.exist?("#{upstream}/#{tests}")
+ gemspec = if File.directory?("lib/#{repo}")
+ "lib/#{repo}/#{repo}.gemspec"
+ else
+ "lib/#{repo}.gemspec"
+ end
+ cp_r("#{upstream}/#{repo}.gemspec", "#{gemspec}")
+end
+
+def update_default_gems(gem, release: false)
+
+ author, repository = REPOSITORIES[gem.to_sym].split('/')
+
+ puts "Update #{author}/#{repository}"
+
+ unless File.exist?("../../#{author}/#{repository}")
+ mkdir_p("../../#{author}")
+ `git clone git@github.com:#{author}/#{repository}.git ../../#{author}/#{repository}`
+ end
+
+ Dir.chdir("../../#{author}/#{repository}") do
+ unless `git remote`.match(/ruby\-core/)
+ `git remote add ruby-core git@github.com:ruby/ruby.git`
+ end
+ `git fetch ruby-core master --no-tags`
+ unless `git branch`.match(/ruby\-core/)
+ `git checkout ruby-core/master`
+ `git branch ruby-core`
+ end
+ `git checkout ruby-core`
+ `git rebase ruby-core/master`
+ `git fetch origin --tags`
+
+ if release
+ last_release = `git tag`.chomp.split.delete_if{|v| v =~ /pre|beta/ }.last
+ `git checkout #{last_release}`
+ else
+ `git checkout master`
+ `git rebase origin/master`
+ end
+ end
+end
+
+case ARGV[0]
+when "up"
+ if ARGV[1]
+ update_default_gems(ARGV[1])
+ else
+ REPOSITORIES.keys.each{|gem| update_default_gems(gem.to_s)}
+ end
+when "all"
+ if ARGV[1] == "release"
+ REPOSITORIES.keys.each do |gem|
+ update_default_gems(gem.to_s, release: true)
+ sync_default_gems(gem.to_s)
+ end
+ else
+ REPOSITORIES.keys.each{|gem| sync_default_gems(gem.to_s)}
+ end
+when "list"
+ ARGV.shift
+ pattern = Regexp.new(ARGV.join('|'))
+ REPOSITORIES.each_pair do |name, gem|
+ next unless pattern =~ name or pattern =~ gem
+ printf "%-15s https://github.com/%s\n", name, gem
+ end
+when "--message-filter"
+ ARGV.shift
+ abort unless ARGV.size == 2
+ message_filter(*ARGV)
+ exit
+when nil, "-h", "--help"
+ puts <<-HELP
+\e[1mSync with upstream code of default libraries\e[0m
+
+\e[1mImport a default library through `git clone` and `cp -rf` (git commits are lost)\e[0m
+ ruby #$0 rubygems
+
+\e[1mPick a single commit from the upstream repository\e[0m
+ ruby #$0 rubygems 97e9768612
+
+\e[1mPick a commit range from the upstream repository\e[0m
+ ruby #$0 rubygems 97e9768612..9e53702832
+
+\e[1mList known libraries\e[0m
+ ruby #$0 list
+
+\e[1mList known libraries matching with patterns\e[0m
+ ruby #$0 list read
+ HELP
+
+ exit
+else
+ while /\A-/ =~ ARGV[0]
+ case ARGV[0]
+ when "-e"
+ edit = true
+ ARGV.shift
+ when "-a"
+ auto = true
+ ARGV.shift
+ else
+ $stderr.puts "Unknown command line option: #{ARGV[0]}"
+ exit 1
+ end
+ end
+ gem = ARGV.shift
+ if ARGV[0]
+ exit sync_default_gems_with_commits(gem, ARGV, edit: edit)
+ elsif auto
+ exit sync_default_gems_with_commits(gem, true, edit: edit)
+ else
+ sync_default_gems(gem)
+ end
+end
diff --git a/tool/test-bundled-gems.rb b/tool/test-bundled-gems.rb
new file mode 100644
index 0000000000..79c6b61493
--- /dev/null
+++ b/tool/test-bundled-gems.rb
@@ -0,0 +1,116 @@
+require 'rbconfig'
+require 'timeout'
+require 'fileutils'
+
+ENV.delete("GNUMAKEFLAGS")
+
+github_actions = ENV["GITHUB_ACTIONS"] == "true"
+
+allowed_failures = ENV['TEST_BUNDLED_GEMS_ALLOW_FAILURES'] || ''
+allowed_failures = allowed_failures.split(',').reject(&:empty?)
+
+ENV["GEM_PATH"] = [File.realpath('.bundle'), File.realpath('../.bundle', __dir__)].join(File::PATH_SEPARATOR)
+
+rake = File.realpath("../../.bundle/bin/rake", __FILE__)
+gem_dir = File.realpath('../../gems', __FILE__)
+rubylib = [gem_dir+'/lib', ENV["RUBYLIB"]].compact.join(File::PATH_SEPARATOR)
+exit_code = 0
+ruby = ENV['RUBY'] || RbConfig.ruby
+failed = []
+File.foreach("#{gem_dir}/bundled_gems") do |line|
+ next if /^\s*(?:#|$)/ =~ line
+ gem = line.split.first
+ next if ARGV.any? {|pat| !File.fnmatch?(pat, gem)}
+ puts "#{github_actions ? "##[group]" : "\n"}Testing the #{gem} gem"
+
+ test_command = "#{ruby} -C #{gem_dir}/src/#{gem} #{rake} test"
+ envs = {}
+ first_timeout = 600 # 10min
+
+ toplib = gem
+ case gem
+ when "typeprof"
+
+ when "rbs"
+ # TODO: We should skip test file instead of test class/methods
+ skip_test_files = %w[
+ test/stdlib/Prime_test.rb
+ ]
+
+ skip_test_files.each do |file|
+ path = "#{gem_dir}/src/#{gem}/#{file}"
+ File.unlink(path) if File.exist?(path)
+ end
+
+ test_command << " stdlib_test validate"
+ first_timeout *= 3
+
+ when "minitest"
+ # Tentatively exclude some tests that conflict with error_highlight
+ # https://github.com/seattlerb/minitest/pull/880
+ test_command << " 'TESTOPTS=-e /test_stub_value_block_args_5__break_if_not_passed|test_no_method_error_on_unexpected_methods/'"
+
+ when "debug"
+ # Since debug gem requires debug.so in child processes without
+ # acitvating the gem, we preset necessary paths in RUBYLIB
+ # environment variable.
+ load_path = true
+
+ # disable remote test in debug.gem on macOS
+ if /darwin/ =~ RUBY_PLATFORM
+ envs["RUBY_DEBUG_TEST_NO_REMOTE"] = "1"
+ end
+
+ when "test-unit"
+ test_command = "#{ruby} -C #{gem_dir}/src/#{gem} test/run-test.rb"
+
+ when /\Anet-/
+ toplib = gem.tr("-", "/")
+
+ end
+
+ if load_path
+ libs = IO.popen([ruby, "-e", "old = $:.dup; require '#{toplib}'; puts $:-old"], &:read)
+ next unless $?.success?
+ puts libs
+ ENV["RUBYLIB"] = [libs.split("\n"), rubylib].join(File::PATH_SEPARATOR)
+ else
+ ENV["RUBYLIB"] = rubylib
+ end
+
+ print "[command]" if github_actions
+ puts test_command
+ pid = Process.spawn(envs, test_command, "#{/mingw|mswin/ =~ RUBY_PLATFORM ? 'new_' : ''}pgroup": true)
+ {nil => first_timeout, INT: 30, TERM: 10, KILL: nil}.each do |sig, sec|
+ if sig
+ puts "Sending #{sig} signal"
+ Process.kill("-#{sig}", pid)
+ end
+ begin
+ break Timeout.timeout(sec) {Process.wait(pid)}
+ rescue Timeout::Error
+ end
+ rescue Interrupt
+ exit_code = Signal.list["INT"]
+ Process.kill("-KILL", pid)
+ Process.wait(pid)
+ break
+ end
+
+ unless $?.success?
+
+ puts "Tests failed " +
+ ($?.signaled? ? "by SIG#{Signal.signame($?.termsig)}" :
+ "with exit code #{$?.exitstatus}")
+ if allowed_failures.include?(gem)
+ puts "Ignoring test failures for #{gem} due to \$TEST_BUNDLED_GEMS_ALLOW_FAILURES"
+ else
+ failed << gem
+ exit_code = $?.exitstatus if $?.exitstatus
+ end
+ end
+ print "##[endgroup]\n" if github_actions
+end
+
+puts "Failed gems: #{failed.join(', ')}" unless failed.empty?
+exit exit_code
diff --git a/tool/test-coverage.rb b/tool/test-coverage.rb
new file mode 100644
index 0000000000..4950bc65de
--- /dev/null
+++ b/tool/test-coverage.rb
@@ -0,0 +1,118 @@
+require "coverage"
+
+Coverage.start(lines: true, branches: true, methods: true)
+
+TEST_COVERAGE_DATA_FILE = "test-coverage.dat"
+
+def merge_coverage_data(res1, res2)
+ res1.each do |path, cov1|
+ cov2 = res2[path]
+ if cov2
+ cov1[:lines].each_with_index do |count1, i|
+ next unless count1
+ add_count(cov2[:lines], i, count1)
+ end
+ cov1[:branches].each do |base_key, targets1|
+ if cov2[:branches][base_key]
+ targets1.each do |target_key, count1|
+ add_count(cov2[:branches][base_key], target_key, count1)
+ end
+ else
+ cov2[:branches][base_key] = targets1
+ end
+ end
+ cov1[:methods].each do |key, count1|
+ add_count(cov2[:methods], key, count1)
+ end
+ else
+ res2[path] = cov1
+ end
+ end
+ res2
+end
+
+def add_count(h, key, count)
+ if h[key]
+ h[key] += count
+ else
+ h[key] = count
+ end
+end
+
+def save_coverage_data(res1)
+ res1.each do |_path, cov|
+ if cov[:methods]
+ h = {}
+ cov[:methods].each do |(klass, *key), count|
+ h[[klass.name, *key]] = count
+ end
+ cov[:methods].replace h
+ end
+ end
+ File.open(TEST_COVERAGE_DATA_FILE, File::RDWR | File::CREAT | File::BINARY) do |f|
+ f.flock(File::LOCK_EX)
+ s = f.read
+ res2 = s.size > 0 ? Marshal.load(s) : {}
+ res1 = merge_coverage_data(res1, res2)
+ f.rewind
+ f << Marshal.dump(res2)
+ f.flush
+ f.truncate(f.pos)
+ end
+end
+
+def invoke_simplecov_formatter
+ %w[doclie simplecov-html simplecov].each do |f|
+ $LOAD_PATH.unshift "#{__dir__}/../coverage/#{f}/lib"
+ end
+
+ require "simplecov"
+ res = Marshal.load(File.binread(TEST_COVERAGE_DATA_FILE))
+ simplecov_result = {}
+ base_dir = File.dirname(__dir__)
+ cur_dir = Dir.pwd
+
+ res.each do |path, cov|
+ next unless path.start_with?(base_dir) || path.start_with?(cur_dir)
+ next if path.start_with?(File.join(base_dir, "test"))
+ simplecov_result[path] = cov[:lines]
+ end
+
+ a, b = base_dir, cur_dir
+ until a == b
+ if a.size > b.size
+ a = File.dirname(a)
+ else
+ b = File.dirname(b)
+ end
+ end
+ root_dir = a
+
+ SimpleCov.configure do
+ root(root_dir)
+ coverage_dir(File.join(cur_dir, "coverage"))
+ end
+ res = SimpleCov::Result.new(simplecov_result)
+ res.command_name = "Ruby's `make test-all`"
+ SimpleCov::Formatter::HTMLFormatter.new.format(res)
+end
+
+pid = $$
+pwd = Dir.pwd
+
+at_exit do
+ exit_exc = $!
+
+ Dir.chdir(pwd) do
+ save_coverage_data(Coverage.result)
+ if pid == $$
+ begin
+ nil while Process.waitpid(-1)
+ rescue Errno::ECHILD
+ invoke_simplecov_formatter
+ end
+ end
+ end
+
+ raise exit_exc if exit_exc
+end
diff --git a/tool/test/runner.rb b/tool/test/runner.rb
new file mode 100644
index 0000000000..c629943090
--- /dev/null
+++ b/tool/test/runner.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+require 'rbconfig'
+
+$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
+
+require 'test/unit'
+
+require "profile_test_all" if ENV.key?('RUBY_TEST_ALL_PROFILE')
+require "tracepointchecker"
+require "zombie_hunter"
+require "iseq_loader_checker"
+require "gc_checker"
+require_relative "../test-coverage.rb" if ENV.key?('COVERAGE')
+
+case $0
+when __FILE__
+ dir = __dir__
+when "-e"
+ # No default directory
+else
+ dir = File.expand_path("..", $0)
+end
+exit Test::Unit::AutoRunner.run(true, dir)
diff --git a/tool/test/test_jisx0208.rb b/tool/test/test_jisx0208.rb
new file mode 100644
index 0000000000..98f216ff4f
--- /dev/null
+++ b/tool/test/test_jisx0208.rb
@@ -0,0 +1,40 @@
+require 'test/unit'
+
+require_relative '../lib/jisx0208'
+
+class Test_JISX0208_Char < Test::Unit::TestCase
+ def test_create_with_row_cell
+ assert_equal JISX0208::Char.new(0x2121), JISX0208::Char.new(1, 1)
+ end
+
+ def test_succ
+ assert_equal JISX0208::Char.new(0x2221), JISX0208::Char.new(0x217e).succ
+ assert_equal JISX0208::Char.new(2, 1), JISX0208::Char.new(1, 94).succ
+ assert_equal JISX0208::Char.new(0x7f21), JISX0208::Char.new(0x7e7e).succ
+ end
+
+ def test_to_shift_jis
+ assert_equal 0x895C, JISX0208::Char.new(0x313D).to_sjis
+ assert_equal 0x895C, JISX0208::Char.from_sjis(0x895C).to_sjis
+ assert_equal 0xF3DE, JISX0208::Char.from_sjis(0xF3DE).to_sjis
+ assert_equal 0xFC40, JISX0208::Char.from_sjis(0xFC40).to_sjis
+ end
+
+ def test_from_sjis
+ assert_raise(ArgumentError) { JISX0208::Char.from_sjis(-1) }
+ assert_raise(ArgumentError) { JISX0208::Char.from_sjis(0x10000) }
+ assert_nothing_raised { JISX0208::Char.from_sjis(0x8140) }
+ assert_nothing_raised { JISX0208::Char.from_sjis(0xFCFC) }
+ assert_equal JISX0208::Char.new(0x313D), JISX0208::Char.from_sjis(0x895C)
+ end
+
+ def test_row
+ assert_equal 1, JISX0208::Char.new(0x2121).row
+ assert_equal 94, JISX0208::Char.new(0x7E7E).row
+ end
+
+ def test_cell
+ assert_equal 1, JISX0208::Char.new(0x2121).cell
+ assert_equal 94, JISX0208::Char.new(0x7E7E).cell
+ end
+end
diff --git a/tool/test/testunit/metametameta.rb b/tool/test/testunit/metametameta.rb
new file mode 100644
index 0000000000..e494038939
--- /dev/null
+++ b/tool/test/testunit/metametameta.rb
@@ -0,0 +1,70 @@
+# encoding: utf-8
+# frozen_string_literal: false
+
+require 'tempfile'
+require 'stringio'
+
+class Test::Unit::TestCase
+ def clean s
+ s.gsub(/^ {6}/, '')
+ end
+end
+
+class MetaMetaMetaTestCase < Test::Unit::TestCase
+ def assert_report expected, flags = %w[--seed 42]
+ header = clean <<-EOM
+ Run options: #{flags.map { |s| s =~ /\|/ ? s.inspect : s }.join " "}
+
+ # Running tests:
+
+ EOM
+
+ with_output do
+ @tu.run flags
+ end
+
+ output = @output.string.dup
+ output.sub!(/Finished tests in .*/, "Finished tests in 0.00")
+ output.sub!(/Loaded suite .*/, 'Loaded suite blah')
+
+ output.gsub!(/ = \d+.\d\d s = /, ' = 0.00 s = ')
+ output.gsub!(/0x[A-Fa-f0-9]+/, '0xXXX')
+
+ if windows? then
+ output.gsub!(/\[(?:[A-Za-z]:)?[^\]:]+:\d+\]/, '[FILE:LINE]')
+ output.gsub!(/^(\s+)(?:[A-Za-z]:)?[^:]+:\d+:in/, '\1FILE:LINE:in')
+ else
+ output.gsub!(/\[[^\]:]+:\d+\]/, '[FILE:LINE]')
+ output.gsub!(/^(\s+)[^:]+:\d+:in/, '\1FILE:LINE:in')
+ end
+
+ assert_equal header + expected, output
+ end
+
+ def setup
+ super
+ srand 42
+ Test::Unit::TestCase.reset
+ @tu = Test::Unit::Runner.new
+
+ Test::Unit::Runner.runner = nil # protect the outer runner from the inner tests
+ end
+
+ def teardown
+ super
+ end
+
+ def with_output
+ synchronize do
+ begin
+ save = Test::Unit::Runner.output
+ @output = StringIO.new("")
+ Test::Unit::Runner.output = @output
+
+ yield
+ ensure
+ Test::Unit::Runner.output = save
+ end
+ end
+ end
+end
diff --git a/tool/test/testunit/test4test_hideskip.rb b/tool/test/testunit/test4test_hideskip.rb
new file mode 100644
index 0000000000..410bffc13c
--- /dev/null
+++ b/tool/test/testunit/test4test_hideskip.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: false
+$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../../lib"
+
+require 'test/unit'
+
+class TestForTestHideSkip < Test::Unit::TestCase
+ def test_skip
+ skip "do nothing"
+ end
+end
diff --git a/tool/test/testunit/test4test_redefinition.rb b/tool/test/testunit/test4test_redefinition.rb
new file mode 100644
index 0000000000..ad3c5e7113
--- /dev/null
+++ b/tool/test/testunit/test4test_redefinition.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: false
+$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../../lib"
+
+require 'test/unit'
+
+class TestForTestRedefinition < Test::Unit::TestCase
+ def test_redefinition
+ skip "do nothing (1)"
+ end
+
+ def test_redefinition
+ skip "do nothing (2)"
+ end
+end
diff --git a/tool/test/testunit/test4test_sorting.rb b/tool/test/testunit/test4test_sorting.rb
new file mode 100644
index 0000000000..698c875b79
--- /dev/null
+++ b/tool/test/testunit/test4test_sorting.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: false
+$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../../lib"
+
+require 'test/unit'
+
+class TestForTestHideSkip < Test::Unit::TestCase
+ def test_c
+ skip "do nothing"
+ end
+
+ def test_b
+ assert_equal true, false
+ end
+
+ def test_a
+ raise
+ end
+end
diff --git a/tool/test/testunit/test_assertion.rb b/tool/test/testunit/test_assertion.rb
new file mode 100644
index 0000000000..8c83b447a7
--- /dev/null
+++ b/tool/test/testunit/test_assertion.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: false
+require 'test/unit'
+class TestAssertion < Test::Unit::TestCase
+ def test_wrong_assertion
+ error, line = assert_raise(ArgumentError) {assert(true, true)}, __LINE__
+ assert_match(/assertion message must be String or Proc, but TrueClass was given/, error.message)
+ assert_match(/\A#{Regexp.quote(__FILE__)}:#{line}:/, error.backtrace[0])
+ end
+
+ def test_timeout_separately
+ assert_raise(Timeout::Error) do
+ assert_separately([], <<~"end;", timeout: 0.1)
+ sleep
+ end;
+ end
+ end
+
+ def return_in_assert_raise
+ assert_raise(RuntimeError) do
+ return
+ end
+ end
+
+ def test_assert_raise
+ assert_raise(Test::Unit::AssertionFailedError) do
+ return_in_assert_raise
+ end
+ end
+end
diff --git a/tool/test/testunit/test_hideskip.rb b/tool/test/testunit/test_hideskip.rb
new file mode 100644
index 0000000000..13d887189e
--- /dev/null
+++ b/tool/test/testunit/test_hideskip.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: false
+require 'test/unit'
+
+class TestHideSkip < Test::Unit::TestCase
+ def test_hideskip
+ assert_not_match(/^ *1\) Skipped/, hideskip)
+ assert_match(/^ *1\) Skipped/, hideskip("--show-skip"))
+ output = hideskip("--hide-skip")
+ output.gsub!(/Successful MJIT finish\n/, '') if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled?
+ assert_match(/assertions\/s.\n+1 tests, 0 assertions, 0 failures, 0 errors, 1 skips/, output)
+ end
+
+ private
+
+ def hideskip(*args)
+ IO.popen([*@options[:ruby], "#{File.dirname(__FILE__)}/test4test_hideskip.rb",
+ "--verbose", *args], err: [:child, :out]) {|f|
+ f.read
+ }
+ end
+end
diff --git a/tool/test/testunit/test_minitest_unit.rb b/tool/test/testunit/test_minitest_unit.rb
new file mode 100644
index 0000000000..5941392fa0
--- /dev/null
+++ b/tool/test/testunit/test_minitest_unit.rb
@@ -0,0 +1,1474 @@
+# encoding: utf-8
+# frozen_string_literal: false
+
+require 'pathname'
+require_relative 'metametameta'
+
+module MyModule; end
+class AnError < StandardError; include MyModule; end
+class ImmutableString < String; def inspect; super.freeze; end; end
+
+class TestMiniTestUnit < MetaMetaMetaTestCase
+ pwd = Pathname.new File.expand_path Dir.pwd
+ basedir = Pathname.new(File.expand_path "lib/test")
+ basedir = basedir.relative_path_from(pwd).to_s
+ MINITEST_BASE_DIR = basedir[/\A\./] ? basedir : "./#{basedir}"
+ BT_MIDDLE = ["#{MINITEST_BASE_DIR}/test.rb:161:in `each'",
+ "#{MINITEST_BASE_DIR}/test.rb:158:in `each'",
+ "#{MINITEST_BASE_DIR}/test.rb:139:in `run'",
+ "#{MINITEST_BASE_DIR}/test.rb:106:in `run'"]
+
+ def test_class_puke_with_assertion_failed
+ exception = Test::Unit::AssertionFailedError.new "Oh no!"
+ exception.set_backtrace ["unhappy"]
+ assert_equal 'F', @tu.puke('SomeClass', 'method_name', exception)
+ assert_equal 1, @tu.failures
+ assert_match(/^Failure.*Oh no!/m, @tu.report.first)
+ assert_match("SomeClass#method_name [unhappy]", @tu.report.first)
+ end
+
+ def test_class_puke_with_assertion_failed_and_long_backtrace
+ bt = (["test/test_some_class.rb:615:in `method_name'",
+ "#{MINITEST_BASE_DIR}/unit.rb:140:in `assert_raise'",
+ "test/test_some_class.rb:615:in `each'",
+ "test/test_some_class.rb:614:in `test_method_name'",
+ "#{MINITEST_BASE_DIR}/test.rb:165:in `__send__'"] +
+ BT_MIDDLE +
+ ["#{MINITEST_BASE_DIR}/test.rb:29"])
+ bt = util_expand_bt bt
+
+ ex_location = util_expand_bt(["test/test_some_class.rb:615"]).first
+
+ exception = Test::Unit::AssertionFailedError.new "Oh no!"
+ exception.set_backtrace bt
+ assert_equal 'F', @tu.puke('TestSomeClass', 'test_method_name', exception)
+ assert_equal 1, @tu.failures
+ assert_match(/^Failure.*Oh no!/m, @tu.report.first)
+ assert_match("TestSomeClass#test_method_name [#{ex_location}]", @tu.report.first)
+ end
+
+ def test_class_puke_with_assertion_failed_and_user_defined_assertions
+ bt = (["lib/test/my/util.rb:16:in `another_method_name'",
+ "#{MINITEST_BASE_DIR}/unit.rb:140:in `assert_raise'",
+ "lib/test/my/util.rb:15:in `block in assert_something'",
+ "lib/test/my/util.rb:14:in `each'",
+ "lib/test/my/util.rb:14:in `assert_something'",
+ "test/test_some_class.rb:615:in `each'",
+ "test/test_some_class.rb:614:in `test_method_name'",
+ "#{MINITEST_BASE_DIR}/test.rb:165:in `__send__'"] +
+ BT_MIDDLE +
+ ["#{MINITEST_BASE_DIR}/test.rb:29"])
+ bt = util_expand_bt bt
+
+ ex_location = util_expand_bt(["test/test_some_class.rb:615"]).first
+
+ exception = Test::Unit::AssertionFailedError.new "Oh no!"
+ exception.set_backtrace bt
+ assert_equal 'F', @tu.puke('TestSomeClass', 'test_method_name', exception)
+ assert_equal 1, @tu.failures
+ assert_match(/^Failure.*Oh no!/m, @tu.report.first)
+ assert_match("TestSomeClass#test_method_name [#{ex_location}]", @tu.report.first)
+ end
+
+ def test_class_puke_with_failure_and_flunk_in_backtrace
+ exception = begin
+ Test::Unit::TestCase.new('fake tc').flunk
+ rescue Test::Unit::AssertionFailedError => failure
+ failure
+ end
+ assert_equal 'F', @tu.puke('SomeClass', 'method_name', exception)
+ refute @tu.report.any?{|line| line =~ /in .flunk/}
+ end
+
+ def test_class_puke_with_flunk_and_user_defined_assertions
+ bt = (["lib/test/my/util.rb:16:in `flunk'",
+ "#{MINITEST_BASE_DIR}/unit.rb:140:in `assert_raise'",
+ "lib/test/my/util.rb:15:in `block in assert_something'",
+ "lib/test/my/util.rb:14:in `each'",
+ "lib/test/my/util.rb:14:in `assert_something'",
+ "test/test_some_class.rb:615:in `each'",
+ "test/test_some_class.rb:614:in `test_method_name'",
+ "#{MINITEST_BASE_DIR}/test.rb:165:in `__send__'"] +
+ BT_MIDDLE +
+ ["#{MINITEST_BASE_DIR}/test.rb:29"])
+ bt = util_expand_bt bt
+
+ ex_location = util_expand_bt(["test/test_some_class.rb:615"]).first
+
+ exception = Test::Unit::AssertionFailedError.new "Oh no!"
+ exception.set_backtrace bt
+ assert_equal 'F', @tu.puke('TestSomeClass', 'test_method_name', exception)
+ assert_equal 1, @tu.failures
+ assert_match(/^Failure.*Oh no!/m, @tu.report.first)
+ assert_match("TestSomeClass#test_method_name [#{ex_location}]", @tu.report.first)
+ end
+
+ def test_class_puke_with_non_failure_exception
+ exception = Exception.new("Oh no again!")
+ assert_equal 'E', @tu.puke('SomeClass', 'method_name', exception)
+ assert_equal 1, @tu.errors
+ assert_match(/^Exception.*Oh no again!/m, @tu.report.first)
+ end
+
+ def test_filter_backtrace
+ # this is a semi-lame mix of relative paths.
+ # I cheated by making the autotest parts not have ./
+ bt = (["lib/autotest.rb:571:in `add_exception'",
+ "test/test_autotest.rb:62:in `test_add_exception'",
+ "#{MINITEST_BASE_DIR}/test.rb:165:in `__send__'"] +
+ BT_MIDDLE +
+ ["#{MINITEST_BASE_DIR}/test.rb:29",
+ "test/test_autotest.rb:422"])
+ bt = util_expand_bt bt
+
+ ex = ["lib/autotest.rb:571:in `add_exception'",
+ "test/test_autotest.rb:62:in `test_add_exception'"]
+ ex = util_expand_bt ex
+
+ fu = Test::filter_backtrace(bt)
+
+ assert_equal ex, fu
+ end
+
+ def test_filter_backtrace_all_unit
+ bt = (["#{MINITEST_BASE_DIR}/test.rb:165:in `__send__'"] +
+ BT_MIDDLE +
+ ["#{MINITEST_BASE_DIR}/test.rb:29"])
+ ex = bt.clone
+ fu = Test::filter_backtrace(bt)
+ assert_equal ex, fu
+ end
+
+ def test_filter_backtrace_unit_starts
+ bt = (["#{MINITEST_BASE_DIR}/test.rb:165:in `__send__'"] +
+ BT_MIDDLE +
+ ["#{MINITEST_BASE_DIR}/mini/test.rb:29",
+ "-e:1"])
+
+ bt = util_expand_bt bt
+
+ ex = ["-e:1"]
+ fu = Test::filter_backtrace bt
+ assert_equal ex, fu
+ end
+
+ def test_default_runner_is_minitest_unit
+ assert_instance_of Test::Unit::Runner, Test::Unit::Runner.runner
+ end
+
+
+ def test_passed_eh_teardown_good
+ test_class = Class.new Test::Unit::TestCase do
+ def teardown; assert true; end
+ def test_omg; assert true; end
+ end
+
+ test = test_class.new :test_omg
+ test.run @tu
+ assert test.passed?
+ end
+
+ def test_passed_eh_teardown_skipped
+ test_class = Class.new Test::Unit::TestCase do
+ def teardown; assert true; end
+ def test_omg; skip "bork"; end
+ end
+
+ test = test_class.new :test_omg
+ test.run @tu
+ assert test.passed?
+ end
+
+ def test_passed_eh_teardown_flunked
+ test_class = Class.new Test::Unit::TestCase do
+ def teardown; flunk; end
+ def test_omg; assert true; end
+ end
+
+ test = test_class.new :test_omg
+ test.run @tu
+ refute test.passed?
+ end
+
+ def util_expand_bt bt
+ bt.map { |f| (f =~ /^\./) ? File.expand_path(f) : f }
+ end
+end
+
+class TestMiniTestUnitInherited < MetaMetaMetaTestCase
+ def with_overridden_include
+ Class.class_eval do
+ def inherited_with_hacks klass
+ throw :inherited_hook
+ end
+
+ alias inherited_without_hacks inherited
+ alias inherited inherited_with_hacks
+ alias IGNORE_ME! inherited # 1.8 bug. god I love venture bros
+ end
+
+ yield
+ ensure
+ Class.class_eval do
+ alias inherited inherited_without_hacks
+
+ undef_method :inherited_with_hacks
+ undef_method :inherited_without_hacks
+ end
+
+ refute_respond_to Class, :inherited_with_hacks
+ refute_respond_to Class, :inherited_without_hacks
+ end
+
+ def test_inherited_hook_plays_nice_with_others
+ with_overridden_include do
+ assert_throws :inherited_hook do
+ Class.new Test::Unit::TestCase
+ end
+ end
+ end
+end
+
+class TestMiniTestRunner < MetaMetaMetaTestCase
+ # do not parallelize this suite... it just can't handle it.
+
+ def test_class_test_suites
+ @assertion_count = 0
+
+ tc = Class.new(Test::Unit::TestCase)
+
+ assert_equal 2, Test::Unit::TestCase.test_suites.size
+ assert_equal [tc, Test::Unit::TestCase], Test::Unit::TestCase.test_suites.sort_by {|ts| ts.name.to_s}
+ end
+
+ def assert_filtering name, expected, a = false
+ args = %W[--name #{name} --seed 42]
+
+ alpha = Class.new Test::Unit::TestCase do
+ define_method :test_something do
+ assert a
+ end
+ end
+ Object.const_set(:Alpha, alpha)
+
+ beta = Class.new Test::Unit::TestCase do
+ define_method :test_something do
+ assert true
+ end
+ end
+ Object.const_set(:Beta, beta)
+
+ assert_report expected, args
+ ensure
+ Object.send :remove_const, :Alpha
+ Object.send :remove_const, :Beta
+ end
+
+ def test_run_with_other_runner
+ pend "We don't imagine to replace the default runner with ruby/ruby test suite."
+ Test::Unit::Runner.runner = Class.new Test::Unit::Runner do
+ def _run_suite suite, type
+ suite.before_suite # Run once before each suite
+ super suite, type
+ end
+ end.new
+
+ Class.new Test::Unit::TestCase do
+ def self.name; "wacky!" end
+
+ def self.before_suite
+ Test::Unit::Runner.output.puts "Running #{self.name} tests"
+ @@foo = 1
+ end
+
+ def test_something
+ assert_equal 1, @@foo
+ end
+
+ def test_something_else
+ assert_equal 1, @@foo
+ end
+ end
+
+ expected = clean <<-EOM
+ Running wacky! tests
+ ..
+
+ Finished tests in 0.00
+
+ 2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
+ EOM
+
+ assert_report expected
+ end
+
+ require 'monitor'
+
+ class Latch
+ def initialize count = 1
+ @count = count
+ @lock = Monitor.new
+ @cv = @lock.new_cond
+ end
+
+ def release
+ @lock.synchronize do
+ @count -= 1 if @count > 0
+ @cv.broadcast if @count == 0
+ end
+ end
+
+ def await
+ @lock.synchronize { @cv.wait_while { @count > 0 } }
+ end
+ end
+end
+
+class TestMiniTestUnitOrder < MetaMetaMetaTestCase
+ # do not parallelize this suite... it just can't handle it.
+
+ def test_before_setup
+ pend "Surpressing the raise message when running with tests"
+
+ call_order = []
+ Class.new Test::Unit::TestCase do
+ define_method :setup do
+ super()
+ call_order << :setup
+ end
+
+ define_method :before_setup do
+ call_order << :before_setup
+ end
+
+ def test_omg; assert true; end
+ end
+
+ with_output do
+ @tu.run %w[--seed 42]
+ end
+
+ expected = [:before_setup, :setup]
+ assert_equal expected, call_order
+ end
+
+ def test_after_teardown
+ pend "Surpressing the result message of this tests"
+
+ call_order = []
+ Class.new Test::Unit::TestCase do
+ define_method :teardown do
+ super()
+ call_order << :teardown
+ end
+
+ define_method :after_teardown do
+ call_order << :after_teardown
+ end
+
+ def test_omg; assert true; end
+ end
+
+ with_output do
+ @tu.run %w[--seed 42]
+ end
+
+ expected = [:teardown, :after_teardown]
+ assert_equal expected, call_order
+ end
+
+ def test_all_teardowns_are_guaranteed_to_run
+ pend "Surpressing the raise message when running with tests"
+
+ call_order = []
+ Class.new Test::Unit::TestCase do
+ define_method :after_teardown do
+ super()
+ call_order << :after_teardown
+ raise
+ end
+
+ define_method :teardown do
+ super()
+ call_order << :teardown
+ raise
+ end
+
+ define_method :before_teardown do
+ super()
+ call_order << :before_teardown
+ raise
+ end
+
+ def test_omg; assert true; end
+ end
+
+ with_output do
+ @tu.run %w[--seed 42]
+ end
+
+ expected = [:before_teardown, :teardown, :after_teardown]
+ assert_equal expected, call_order
+ end
+
+ def test_setup_and_teardown_survive_inheritance
+ pend "Surpressing the result message of this tests"
+
+ call_order = []
+
+ parent = Class.new Test::Unit::TestCase do
+ define_method :setup do
+ call_order << :setup_method
+ end
+
+ define_method :teardown do
+ call_order << :teardown_method
+ end
+
+ define_method :test_something do
+ call_order << :test
+ end
+ end
+
+ _ = Class.new parent
+
+ with_output do
+ @tu.run %w[--seed 42]
+ end
+
+ # Once for the parent class, once for the child
+ expected = [:setup_method, :test, :teardown_method] * 2
+
+ assert_equal expected, call_order
+ end
+end
+
+class TestMiniTestUnitTestCase < Test::Unit::TestCase
+ # do not call parallelize_me! - teardown accesses @tc._assertions
+ # which is not threadsafe. Nearly every method in here is an
+ # assertion test so it isn't worth splitting it out further.
+
+ def setup
+ super
+
+ Test::Unit::TestCase.reset
+
+ @tc = Test::Unit::TestCase.new 'fake tc'
+ @zomg = "zomg ponies!"
+ @assertion_count = 1
+ end
+
+ def teardown
+ assert_equal(@assertion_count, @tc._assertions,
+ "expected #{@assertion_count} assertions to be fired during the test, not #{@tc._assertions}") if @tc.passed?
+ end
+
+ def non_verbose
+ orig_verbose = $VERBOSE
+ $VERBOSE = false
+
+ yield
+ ensure
+ $VERBOSE = orig_verbose
+ end
+
+ def test_assert
+ @assertion_count = 2
+
+ @tc.assert_equal true, @tc.assert(true), "returns true on success"
+ end
+
+ def test_assert__triggered
+ util_assert_triggered "Failed assertion, no message given." do
+ @tc.assert false
+ end
+ end
+
+ def test_assert__triggered_message
+ util_assert_triggered @zomg do
+ @tc.assert false, @zomg
+ end
+ end
+
+ def test_assert_empty
+ @assertion_count = 2
+
+ @tc.assert_empty []
+ end
+
+ def test_assert_empty_triggered
+ @assertion_count = 2
+
+ util_assert_triggered "Expected [1] to be empty." do
+ @tc.assert_empty [1]
+ end
+ end
+
+ def test_assert_equal
+ @tc.assert_equal 1, 1
+ end
+
+ def test_assert_equal_different_collection_array_hex_invisible
+ object1 = Object.new
+ object2 = Object.new
+ msg = "<[#{object1.inspect}]> expected but was
+ <[#{object2.inspect}]>.".gsub(/^ +/, "")
+ util_assert_triggered msg do
+ @tc.assert_equal [object1], [object2]
+ end
+ end
+
+ def test_assert_equal_different_collection_hash_hex_invisible
+ h1, h2 = {}, {}
+ h1[1] = Object.new
+ h2[1] = Object.new
+ msg = "<#{h1.inspect}> expected but was
+ <#{h2.inspect}>.".gsub(/^ +/, "")
+
+ util_assert_triggered msg do
+ @tc.assert_equal h1, h2
+ end
+ end
+
+ def test_assert_equal_different_diff_deactivated
+ without_diff do
+ util_assert_triggered util_msg("haha" * 10, "blah" * 10) do
+ o1 = "haha" * 10
+ o2 = "blah" * 10
+
+ @tc.assert_equal o1, o2
+ end
+ end
+ end
+
+ def test_assert_equal_different_hex
+ c = Class.new do
+ def initialize s; @name = s; end
+ end
+
+ o1 = c.new "a"
+ o2 = c.new "b"
+ msg = "<#{o1.inspect}> expected but was
+ <#{o2.inspect}>.".gsub(/^ +/, "")
+
+ util_assert_triggered msg do
+ @tc.assert_equal o1, o2
+ end
+ end
+
+ def test_assert_equal_different_hex_invisible
+ o1 = Object.new
+ o2 = Object.new
+
+ msg = "<#{o1.inspect}> expected but was
+ <#{o2.inspect}>.".gsub(/^ +/, "")
+
+ util_assert_triggered msg do
+ @tc.assert_equal o1, o2
+ end
+ end
+
+ def test_assert_equal_different_long
+ msg = "<\"hahahahahahahahahahahahahahahahahahahaha\"> expected but was
+ <\"blahblahblahblahblahblahblahblahblahblah\">.".gsub(/^ +/, "")
+
+ util_assert_triggered msg do
+ o1 = "haha" * 10
+ o2 = "blah" * 10
+
+ @tc.assert_equal o1, o2
+ end
+ end
+
+ def test_assert_equal_different_long_invisible
+ msg = "<\"blahblahblahblahblahblahblahblahblahblah\"> (UTF-8) expected but was
+ <\"blahblahblahblahblahblahblahblahblahblah\"> (UTF-8).".gsub(/^ +/, "")
+
+ util_assert_triggered msg do
+ o1 = "blah" * 10
+ o2 = "blah" * 10
+ def o1.== o
+ false
+ end
+ @tc.assert_equal o1, o2
+ end
+ end
+
+ def test_assert_equal_different_long_msg
+ msg = "message.
+ <\"hahahahahahahahahahahahahahahahahahahaha\"> expected but was
+ <\"blahblahblahblahblahblahblahblahblahblah\">.".gsub(/^ +/, "")
+
+ util_assert_triggered msg do
+ o1 = "haha" * 10
+ o2 = "blah" * 10
+ @tc.assert_equal o1, o2, "message"
+ end
+ end
+
+ def test_assert_equal_different_short
+ util_assert_triggered util_msg(1, 2) do
+ @tc.assert_equal 1, 2
+ end
+ end
+
+ def test_assert_equal_different_short_msg
+ util_assert_triggered util_msg(1, 2, "message") do
+ @tc.assert_equal 1, 2, "message"
+ end
+ end
+
+ def test_assert_equal_different_short_multiline
+ msg = "<\"a\\n\" + \"b\"> expected but was\n<\"a\\n\" + \"c\">."
+ util_assert_triggered msg do
+ @tc.assert_equal "a\nb", "a\nc"
+ end
+ end
+
+ def test_assert_equal_different_escaped_newline
+ msg = "<\"xxx\\n\" + \"a\\\\nb\"> expected but was\n<\"xxx\\n\" + \"a\\\\nc\">."
+ util_assert_triggered msg do
+ @tc.assert_equal "xxx\na\\nb", "xxx\na\\nc"
+ end
+ end
+
+ def test_assert_in_delta
+ @tc.assert_in_delta 0.0, 1.0 / 1000, 0.1
+ end
+
+ def test_delta_consistency
+ @tc.assert_in_delta 0, 1, 1
+
+ util_assert_triggered "Expected |0 - 1| (1) to not be <= 1." do
+ @tc.refute_in_delta 0, 1, 1
+ end
+ end
+
+ def test_assert_in_delta_triggered
+ x = "1.0e-06"
+ util_assert_triggered "Expected |0.0 - 0.001| (0.001) to be <= #{x}." do
+ @tc.assert_in_delta 0.0, 1.0 / 1000, 0.000001
+ end
+ end
+
+ def test_assert_in_epsilon
+ @assertion_count = 10
+
+ @tc.assert_in_epsilon 10000, 9991
+ @tc.assert_in_epsilon 9991, 10000
+ @tc.assert_in_epsilon 1.0, 1.001
+ @tc.assert_in_epsilon 1.001, 1.0
+
+ @tc.assert_in_epsilon 10000, 9999.1, 0.0001
+ @tc.assert_in_epsilon 9999.1, 10000, 0.0001
+ @tc.assert_in_epsilon 1.0, 1.0001, 0.0001
+ @tc.assert_in_epsilon 1.0001, 1.0, 0.0001
+
+ @tc.assert_in_epsilon(-1, -1)
+ @tc.assert_in_epsilon(-10000, -9991)
+ end
+
+ def test_epsilon_consistency
+ @tc.assert_in_epsilon 1.0, 1.001
+
+ msg = "Expected |1.0 - 1.001| (0.000999xxx) to not be <= 0.001."
+ util_assert_triggered msg do
+ @tc.refute_in_epsilon 1.0, 1.001
+ end
+ end
+
+ def test_assert_in_epsilon_triggered
+ util_assert_triggered 'Expected |10000 - 9990| (10) to be <= 9.99.' do
+ @tc.assert_in_epsilon 10000, 9990
+ end
+ end
+
+ def test_assert_in_epsilon_triggered_negative_case
+ x = "0.100000xxx"
+ y = "0.1"
+ util_assert_triggered "Expected |-1.1 - -1| (#{x}) to be <= #{y}." do
+ @tc.assert_in_epsilon(-1.1, -1, 0.1)
+ end
+ end
+
+ def test_assert_includes
+ @assertion_count = 2
+
+ @tc.assert_includes [true], true
+ end
+
+ def test_assert_includes_triggered
+ @assertion_count = 3
+
+ e = @tc.assert_raise Test::Unit::AssertionFailedError do
+ @tc.assert_includes [true], false
+ end
+
+ expected = "Expected [true] to include false."
+ assert_equal expected, e.message
+ end
+
+ def test_assert_instance_of
+ @tc.assert_instance_of String, "blah"
+ end
+
+ def test_assert_instance_of_triggered
+ util_assert_triggered 'Expected "blah" to be an instance of Array, not String.' do
+ @tc.assert_instance_of Array, "blah"
+ end
+ end
+
+ def test_assert_kind_of
+ @tc.assert_kind_of String, "blah"
+ end
+
+ def test_assert_kind_of_triggered
+ util_assert_triggered 'Expected "blah" to be a kind of Array, not String.' do
+ @tc.assert_kind_of Array, "blah"
+ end
+ end
+
+ def test_assert_match
+ @assertion_count = 2
+ @tc.assert_match(/\w+/, "blah blah blah")
+ end
+
+ def test_assert_match_matcher_object
+ @assertion_count = 2
+
+ pattern = Object.new
+ def pattern.=~(other) true end
+
+ @tc.assert_match pattern, 5
+ end
+
+ def test_assert_match_matchee_to_str
+ @assertion_count = 2
+
+ obj = Object.new
+ def obj.to_str; "blah" end
+
+ @tc.assert_match "blah", obj
+ end
+
+ def test_assert_match_object_triggered
+ @assertion_count = 2
+
+ pattern = Object.new
+ def pattern.=~(other) false end
+ def pattern.inspect; "[Object]" end
+
+ util_assert_triggered 'Expected [Object] to match 5.' do
+ @tc.assert_match pattern, 5
+ end
+ end
+
+ def test_assert_match_triggered
+ @assertion_count = 2
+ util_assert_triggered 'Expected /\d+/ to match "blah blah blah".' do
+ @tc.assert_match(/\d+/, "blah blah blah")
+ end
+ end
+
+ def test_assert_nil
+ @tc.assert_nil nil
+ end
+
+ def test_assert_nil_triggered
+ util_assert_triggered 'Expected 42 to be nil.' do
+ @tc.assert_nil 42
+ end
+ end
+
+ def test_assert_operator
+ @tc.assert_operator 2, :>, 1
+ end
+
+ def test_assert_operator_bad_object
+ bad = Object.new
+ def bad.==(other) true end
+
+ @tc.assert_operator bad, :equal?, bad
+ end
+
+ def test_assert_operator_triggered
+ util_assert_triggered "Expected 2 to be < 1." do
+ @tc.assert_operator 2, :<, 1
+ end
+ end
+
+ def test_assert_output_both
+ @assertion_count = 2
+
+ @tc.assert_output "yay", "blah" do
+ print "yay"
+ $stderr.print "blah"
+ end
+ end
+
+ def test_assert_output_both_regexps
+ @assertion_count = 4
+
+ @tc.assert_output(/y.y/, /bl.h/) do
+ print "yay"
+ $stderr.print "blah"
+ end
+ end
+
+ def test_assert_output_err
+ @tc.assert_output nil, "blah" do
+ $stderr.print "blah"
+ end
+ end
+
+ def test_assert_output_neither
+ @assertion_count = 0
+
+ @tc.assert_output do
+ # do nothing
+ end
+ end
+
+ def test_assert_output_out
+ @tc.assert_output "blah" do
+ print "blah"
+ end
+ end
+
+ def test_assert_output_triggered_both
+ util_assert_triggered util_msg("blah", "blah blah", "In stderr") do
+ @tc.assert_output "yay", "blah" do
+ print "boo"
+ $stderr.print "blah blah"
+ end
+ end
+ end
+
+ def test_assert_output_triggered_err
+ util_assert_triggered util_msg("blah", "blah blah", "In stderr") do
+ @tc.assert_output nil, "blah" do
+ $stderr.print "blah blah"
+ end
+ end
+ end
+
+ def test_assert_output_triggered_out
+ util_assert_triggered util_msg("blah", "blah blah", "In stdout") do
+ @tc.assert_output "blah" do
+ print "blah blah"
+ end
+ end
+ end
+
+ def test_assert_predicate
+ @tc.assert_predicate "", :empty?
+ end
+
+ def test_assert_predicate_triggered
+ util_assert_triggered 'Expected "blah" to be empty?.' do
+ @tc.assert_predicate "blah", :empty?
+ end
+ end
+
+ def test_assert_raise
+ @tc.assert_raise RuntimeError do
+ raise "blah"
+ end
+ end
+
+ def test_assert_raise_module
+ @tc.assert_raise MyModule do
+ raise AnError
+ end
+ end
+
+ ##
+ # *sigh* This is quite an odd scenario, but it is from real (albeit
+ # ugly) test code in ruby-core:
+ #
+ # https://github.com/ruby/ruby/commit/6bab4ea9917dc05cd2c94aead2e96eb7df7d4be1
+
+ def test_assert_raise_skip
+ @assertion_count = 0
+
+ util_assert_triggered "skipped", Test::Unit::PendedError do
+ @tc.assert_raise ArgumentError do
+ begin
+ raise "blah"
+ rescue
+ skip "skipped"
+ end
+ end
+ end
+ end
+
+ def test_assert_raise_triggered_different
+ e = assert_raise Test::Unit::AssertionFailedError do
+ @tc.assert_raise RuntimeError do
+ raise SyntaxError, "icky"
+ end
+ end
+
+ expected = clean <<-EOM.chomp
+ [RuntimeError] exception expected, not #<SyntaxError: icky>.
+ EOM
+
+ actual = e.message.gsub(/^.+:\d+/, 'FILE:LINE')
+ actual.gsub!(/block \(\d+ levels\) in /, '') if RUBY_VERSION >= '1.9.0'
+
+ assert_equal expected, actual
+ end
+
+ def test_assert_raise_triggered_different_msg
+ e = assert_raise Test::Unit::AssertionFailedError do
+ @tc.assert_raise RuntimeError, "XXX" do
+ raise SyntaxError, "icky"
+ end
+ end
+
+ expected = clean <<-EOM
+ XXX.
+ [RuntimeError] exception expected, not #<SyntaxError: icky>.
+ EOM
+
+ actual = e.message.gsub(/^.+:\d+/, 'FILE:LINE')
+ actual.gsub!(/block \(\d+ levels\) in /, '') if RUBY_VERSION >= '1.9.0'
+
+ assert_equal expected.chomp, actual
+ end
+
+ def test_assert_raise_triggered_none
+ e = assert_raise Test::Unit::AssertionFailedError do
+ @tc.assert_raise Test::Unit::AssertionFailedError do
+ # do nothing
+ end
+ end
+
+ expected = "Test::Unit::AssertionFailedError expected but nothing was raised."
+
+ assert_equal expected, e.message
+ end
+
+ def test_assert_raise_triggered_none_msg
+ e = assert_raise Test::Unit::AssertionFailedError do
+ @tc.assert_raise Test::Unit::AssertionFailedError, "XXX" do
+ # do nothing
+ end
+ end
+
+ expected = "XXX.\nTest::Unit::AssertionFailedError expected but nothing was raised."
+
+ assert_equal expected, e.message
+ end
+
+ def test_assert_raise_triggered_subclass
+ e = assert_raise Test::Unit::AssertionFailedError do
+ @tc.assert_raise StandardError do
+ raise AnError
+ end
+ end
+
+ expected = clean <<-EOM.chomp
+ [StandardError] exception expected, not #<AnError: AnError>.
+ EOM
+
+ actual = e.message.gsub(/^.+:\d+/, 'FILE:LINE')
+ actual.gsub!(/block \(\d+ levels\) in /, '') if RUBY_VERSION >= '1.9.0'
+
+ assert_equal expected, actual
+ end
+
+ def test_assert_respond_to
+ @tc.assert_respond_to "blah", :empty?
+ end
+
+ def test_assert_respond_to_triggered
+ util_assert_triggered 'Expected "blah" (String) to respond to #rawr!.' do
+ @tc.assert_respond_to "blah", :rawr!
+ end
+ end
+
+ def test_assert_same
+ @assertion_count = 3
+
+ o = "blah"
+ @tc.assert_same 1, 1
+ @tc.assert_same :blah, :blah
+ @tc.assert_same o, o
+ end
+
+ def test_assert_same_triggered
+ @assertion_count = 2
+
+ util_assert_triggered 'Expected 2 (oid=N) to be the same as 1 (oid=N).' do
+ @tc.assert_same 1, 2
+ end
+
+ s1 = "blah"
+ s2 = "blah"
+
+ util_assert_triggered 'Expected "blah" (oid=N) to be the same as "blah" (oid=N).' do
+ @tc.assert_same s1, s2
+ end
+ end
+
+ def test_assert_send
+ @tc.assert_send [1, :<, 2]
+ end
+
+ def test_assert_send_bad
+ util_assert_triggered "Expected 1.>(2) to return true." do
+ @tc.assert_send [1, :>, 2]
+ end
+ end
+
+ def test_assert_silent
+ @assertion_count = 2
+
+ @tc.assert_silent do
+ # do nothing
+ end
+ end
+
+ def test_assert_silent_triggered_err
+ util_assert_triggered util_msg("", "blah blah", "In stderr") do
+ @tc.assert_silent do
+ $stderr.print "blah blah"
+ end
+ end
+ end
+
+ def test_assert_silent_triggered_out
+ @assertion_count = 2
+
+ util_assert_triggered util_msg("", "blah blah", "In stdout") do
+ @tc.assert_silent do
+ print "blah blah"
+ end
+ end
+ end
+
+ def test_assert_throws
+ @tc.assert_throws :blah do
+ throw :blah
+ end
+ end
+
+ def test_assert_throws_different
+ util_assert_triggered 'Expected :blah to have been thrown, not :not_blah.' do
+ @tc.assert_throws :blah do
+ throw :not_blah
+ end
+ end
+ end
+
+ def test_assert_throws_unthrown
+ util_assert_triggered 'Expected :blah to have been thrown.' do
+ @tc.assert_throws :blah do
+ # do nothing
+ end
+ end
+ end
+
+ def test_capture_output
+ @assertion_count = 0
+
+ non_verbose do
+ out, err = capture_output do
+ puts 'hi'
+ $stderr.puts 'bye!'
+ end
+
+ assert_equal "hi\n", out
+ assert_equal "bye!\n", err
+ end
+ end
+
+ def test_flunk
+ util_assert_triggered 'Epic Fail!' do
+ @tc.flunk
+ end
+ end
+
+ def test_flunk_message
+ util_assert_triggered @zomg do
+ @tc.flunk @zomg
+ end
+ end
+
+ def test_message
+ @assertion_count = 0
+
+ assert_equal "blah2.", @tc.message { "blah2" }.call
+ assert_equal "blah2.", @tc.message("") { "blah2" }.call
+ assert_equal "blah1.\nblah2.", @tc.message(:blah1) { "blah2" }.call
+ assert_equal "blah1.\nblah2.", @tc.message("blah1") { "blah2" }.call
+
+ message = proc { "blah1" }
+ assert_equal "blah1.\nblah2.", @tc.message(message) { "blah2" }.call
+
+ message = @tc.message { "blah1" }
+ assert_equal "blah1.\nblah2.", @tc.message(message) { "blah2" }.call
+ end
+
+ def test_message_message
+ util_assert_triggered "whoops.\n<1> expected but was\n<2>." do
+ @tc.assert_equal 1, 2, message { "whoops" }
+ end
+ end
+
+ def test_message_lambda
+ util_assert_triggered "whoops.\n<1> expected but was\n<2>." do
+ @tc.assert_equal 1, 2, lambda { "whoops" }
+ end
+ end
+
+ def test_message_deferred
+ @assertion_count, var = 0, nil
+
+ msg = message { var = "blah" }
+
+ assert_nil var
+
+ msg.call
+
+ assert_equal "blah", var
+ end
+
+ def test_pass
+ @tc.pass
+ end
+
+ def test_prints
+ printer = Class.new { extend Test::Unit::CoreAssertions }
+ @tc.assert_equal '"test"', printer.mu_pp(ImmutableString.new 'test')
+ end
+
+ def test_refute
+ @assertion_count = 2
+
+ @tc.assert_equal false, @tc.refute(false), "returns false on success"
+ end
+
+ def test_refute_empty
+ @assertion_count = 2
+
+ @tc.refute_empty [1]
+ end
+
+ def test_refute_empty_triggered
+ @assertion_count = 2
+
+ util_assert_triggered "Expected [] to not be empty." do
+ @tc.refute_empty []
+ end
+ end
+
+ def test_refute_equal
+ @tc.refute_equal "blah", "yay"
+ end
+
+ def test_refute_equal_triggered
+ util_assert_triggered 'Expected "blah" to not be equal to "blah".' do
+ @tc.refute_equal "blah", "blah"
+ end
+ end
+
+ def test_refute_in_delta
+ @tc.refute_in_delta 0.0, 1.0 / 1000, 0.000001
+ end
+
+ def test_refute_in_delta_triggered
+ x = "0.1"
+ util_assert_triggered "Expected |0.0 - 0.001| (0.001) to not be <= #{x}." do
+ @tc.refute_in_delta 0.0, 1.0 / 1000, 0.1
+ end
+ end
+
+ def test_refute_in_epsilon
+ @tc.refute_in_epsilon 10000, 9990-1
+ end
+
+ def test_refute_in_epsilon_triggered
+ util_assert_triggered 'Expected |10000 - 9990| (10) to not be <= 10.0.' do
+ @tc.refute_in_epsilon 10000, 9990
+ fail
+ end
+ end
+
+ def test_refute_includes
+ @assertion_count = 2
+
+ @tc.refute_includes [true], false
+ end
+
+ def test_refute_includes_triggered
+ @assertion_count = 3
+
+ e = @tc.assert_raise Test::Unit::AssertionFailedError do
+ @tc.refute_includes [true], true
+ end
+
+ expected = "Expected [true] to not include true."
+ assert_equal expected, e.message
+ end
+
+ def test_refute_instance_of
+ @tc.refute_instance_of Array, "blah"
+ end
+
+ def test_refute_instance_of_triggered
+ util_assert_triggered 'Expected "blah" to not be an instance of String.' do
+ @tc.refute_instance_of String, "blah"
+ end
+ end
+
+ def test_refute_kind_of
+ @tc.refute_kind_of Array, "blah"
+ end
+
+ def test_refute_kind_of_triggered
+ util_assert_triggered 'Expected "blah" to not be a kind of String.' do
+ @tc.refute_kind_of String, "blah"
+ end
+ end
+
+ def test_refute_match
+ @assertion_count = 2
+ @tc.refute_match(/\d+/, "blah blah blah")
+ end
+
+ def test_refute_match_matcher_object
+ @assertion_count = 2
+ non_verbose do
+ obj = Object.new
+ def obj.=~(other); false; end
+ @tc.refute_match obj, 5
+ end
+ end
+
+ def test_refute_match_object_triggered
+ @assertion_count = 2
+
+ pattern = Object.new
+ def pattern.=~(other) true end
+ def pattern.inspect; "[Object]" end
+
+ util_assert_triggered 'Expected [Object] to not match 5.' do
+ @tc.refute_match pattern, 5
+ end
+ end
+
+ def test_refute_match_triggered
+ @assertion_count = 2
+ util_assert_triggered 'Expected /\w+/ to not match "blah blah blah".' do
+ @tc.refute_match(/\w+/, "blah blah blah")
+ end
+ end
+
+ def test_refute_nil
+ @tc.refute_nil 42
+ end
+
+ def test_refute_nil_triggered
+ util_assert_triggered 'Expected nil to not be nil.' do
+ @tc.refute_nil nil
+ end
+ end
+
+ def test_refute_predicate
+ @tc.refute_predicate "42", :empty?
+ end
+
+ def test_refute_predicate_triggered
+ util_assert_triggered 'Expected "" to not be empty?.' do
+ @tc.refute_predicate "", :empty?
+ end
+ end
+
+ def test_refute_operator
+ @tc.refute_operator 2, :<, 1
+ end
+
+ def test_refute_operator_bad_object
+ bad = Object.new
+ def bad.==(other) true end
+
+ @tc.refute_operator true, :equal?, bad
+ end
+
+ def test_refute_operator_triggered
+ util_assert_triggered "Expected 2 to not be > 1." do
+ @tc.refute_operator 2, :>, 1
+ end
+ end
+
+ def test_refute_respond_to
+ @tc.refute_respond_to "blah", :rawr!
+ end
+
+ def test_refute_respond_to_triggered
+ util_assert_triggered 'Expected "blah" to not respond to empty?.' do
+ @tc.refute_respond_to "blah", :empty?
+ end
+ end
+
+ def test_refute_same
+ @tc.refute_same 1, 2
+ end
+
+ def test_refute_same_triggered
+ util_assert_triggered 'Expected 1 (oid=N) to not be the same as 1 (oid=N).' do
+ @tc.refute_same 1, 1
+ end
+ end
+
+ def test_skip
+ @assertion_count = 0
+
+ util_assert_triggered "haha!", Test::Unit::PendedError do
+ @tc.skip "haha!"
+ end
+ end
+
+ def test_test_methods
+ @assertion_count = 0
+
+ sample_test_case = Class.new Test::Unit::TestCase do
+ def test_test1; assert "does not matter" end
+ def test_test2; assert "does not matter" end
+ def test_test3; assert "does not matter" end
+ end
+
+ expected = %i(test_test1 test_test2 test_test3)
+ assert_equal expected, sample_test_case.test_methods.sort
+ end
+
+ def assert_triggered expected, klass = Test::Unit::AssertionFailedError
+ e = assert_raise klass do
+ yield
+ end
+
+ msg = e.message.sub(/(---Backtrace---).*/m, '\1')
+ msg.gsub!(/\(oid=[-0-9]+\)/, '(oid=N)')
+ msg.gsub!(/(\d\.\d{6})\d+/, '\1xxx') # normalize: ruby version, impl, platform
+
+ assert_equal expected, msg
+ end
+ alias util_assert_triggered assert_triggered
+
+ def util_msg exp, act, msg = nil
+ s = "<#{exp.inspect}> expected but was\n<#{act.inspect}>."
+ s = "#{msg}.\n#{s}" if msg
+ s
+ end
+
+ def without_diff
+ old_diff = Test::Unit::Assertions.diff
+ Test::Unit::Assertions.diff = nil
+
+ yield
+ ensure
+ Test::Unit::Assertions.diff = old_diff
+ end
+end
+
+class TestMiniTestGuard < Test::Unit::TestCase
+ def test_mri_eh
+ assert self.class.mri? "ruby blah"
+ assert self.mri? "ruby blah"
+ end
+
+ def test_jruby_eh
+ assert self.class.jruby? "java"
+ assert self.jruby? "java"
+ end
+
+ def test_windows_eh
+ assert self.class.windows? "mswin"
+ assert self.windows? "mswin"
+ end
+end
+
+class TestMiniTestUnitRecording < MetaMetaMetaTestCase
+ # do not parallelize this suite... it just can't handle it.
+
+ def assert_run_record(*expected, &block)
+ pend "Test::Unit::Runner#run was changed about recoding feature. We should fix it."
+ def @tu.record suite, method, assertions, time, error
+ recording[method] << error
+ end
+
+ def @tu.recording
+ @recording ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ Test::Unit::Runner.runner = @tu
+
+ Class.new Test::Unit::TestCase, &block
+
+ with_output do
+ @tu.run
+ end
+
+ recorded = @tu.recording.fetch("test_method").map(&:class)
+
+ assert_equal expected, recorded
+ end
+
+ def test_record_passing
+ assert_run_record NilClass do
+ def test_method
+ assert true
+ end
+ end
+ end
+
+ def test_record_failing
+ assert_run_record Test::Unit::AssertionFailedError do
+ def test_method
+ assert false
+ end
+ end
+ end
+
+ def test_record_error
+ assert_run_record RuntimeError do
+ def test_method
+ raise "unhandled exception"
+ end
+ end
+ end
+
+ def test_record_error_teardown
+ assert_run_record NilClass, RuntimeError do
+ def test_method
+ assert true
+ end
+
+ def teardown
+ raise "unhandled exception"
+ end
+ end
+ end
+
+ def test_record_error_in_test_and_teardown
+ assert_run_record AnError, RuntimeError do
+ def test_method
+ raise AnError
+ end
+
+ def teardown
+ raise "unhandled exception"
+ end
+ end
+ end
+
+ def test_record_skip
+ assert_run_record Test::Unit::PendedError do
+ def test_method
+ skip "not yet"
+ end
+ end
+ end
+end
diff --git a/tool/test/testunit/test_parallel.rb b/tool/test/testunit/test_parallel.rb
new file mode 100644
index 0000000000..006354aee2
--- /dev/null
+++ b/tool/test/testunit/test_parallel.rb
@@ -0,0 +1,219 @@
+# frozen_string_literal: false
+require 'test/unit'
+require 'timeout'
+
+module TestParallel
+ PARALLEL_RB = "#{__dir__}/../../lib/test/unit/parallel.rb"
+ TESTS = "#{__dir__}/tests_for_parallel"
+ # use large timeout for --jit-wait
+ TIMEOUT = EnvUtil.apply_timeout_scale(defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? ? 100 : 30)
+
+ class TestParallelWorker < Test::Unit::TestCase
+ def setup
+ i, @worker_in = IO.pipe
+ @worker_out, o = IO.pipe
+ @worker_pid = spawn(*@options[:ruby], PARALLEL_RB,
+ "--ruby", @options[:ruby].join(" "),
+ "-j", "t1", "-v", out: o, in: i)
+ [i,o].each(&:close)
+ end
+
+ def teardown
+ if @worker_pid && @worker_in
+ begin
+ begin
+ @worker_in.puts "quit"
+ rescue IOError, Errno::EPIPE
+ end
+ Timeout.timeout(2) do
+ Process.waitpid(@worker_pid)
+ end
+ rescue Timeout::Error
+ begin
+ Process.kill(:KILL, @worker_pid)
+ rescue Errno::ESRCH
+ end
+ end
+ end
+ ensure
+ begin
+ @worker_in.close
+ @worker_out.close
+ rescue Errno::EPIPE
+ # may already broken and rescue'ed in above code
+ end
+ end
+
+ def test_run
+ Timeout.timeout(TIMEOUT) do
+ assert_match(/^ready/,@worker_out.gets)
+ @worker_in.puts "run #{TESTS}/ptest_first.rb test"
+ assert_match(/^okay/,@worker_out.gets)
+ assert_match(/^start/,@worker_out.gets)
+ assert_match(/^record/,@worker_out.gets)
+ assert_match(/^p/,@worker_out.gets)
+ assert_match(/^done/,@worker_out.gets)
+ assert_match(/^ready/,@worker_out.gets)
+ end
+ end
+
+ def test_run_multiple_testcase_in_one_file
+ Timeout.timeout(TIMEOUT) do
+ assert_match(/^ready/,@worker_out.gets)
+ @worker_in.puts "run #{TESTS}/ptest_second.rb test"
+ assert_match(/^okay/,@worker_out.gets)
+ assert_match(/^start/,@worker_out.gets)
+ assert_match(/^record/,@worker_out.gets)
+ assert_match(/^p/,@worker_out.gets)
+ assert_match(/^done/,@worker_out.gets)
+ assert_match(/^start/,@worker_out.gets)
+ assert_match(/^record/,@worker_out.gets)
+ assert_match(/^p/,@worker_out.gets)
+ assert_match(/^done/,@worker_out.gets)
+ assert_match(/^ready/,@worker_out.gets)
+ end
+ end
+
+ def test_accept_run_command_multiple_times
+ Timeout.timeout(TIMEOUT) do
+ assert_match(/^ready/,@worker_out.gets)
+ @worker_in.puts "run #{TESTS}/ptest_first.rb test"
+ assert_match(/^okay/,@worker_out.gets)
+ assert_match(/^start/,@worker_out.gets)
+ assert_match(/^record/,@worker_out.gets)
+ assert_match(/^p/,@worker_out.gets)
+ assert_match(/^done/,@worker_out.gets)
+ assert_match(/^ready/,@worker_out.gets)
+ @worker_in.puts "run #{TESTS}/ptest_second.rb test"
+ assert_match(/^okay/,@worker_out.gets)
+ assert_match(/^start/,@worker_out.gets)
+ assert_match(/^record/,@worker_out.gets)
+ assert_match(/^p/,@worker_out.gets)
+ assert_match(/^done/,@worker_out.gets)
+ assert_match(/^start/,@worker_out.gets)
+ assert_match(/^record/,@worker_out.gets)
+ assert_match(/^p/,@worker_out.gets)
+ assert_match(/^done/,@worker_out.gets)
+ assert_match(/^ready/,@worker_out.gets)
+ end
+ end
+
+ def test_p
+ Timeout.timeout(TIMEOUT) do
+ @worker_in.puts "run #{TESTS}/ptest_first.rb test"
+ while buf = @worker_out.gets
+ break if /^p (.+?)$/ =~ buf
+ end
+ assert_not_nil($1, "'p' was not found")
+ assert_match(/TestA#test_nothing_test = \d+\.\d+ s = \.\n/, $1.chomp.unpack1("m"))
+ end
+ end
+
+ def test_done
+ Timeout.timeout(TIMEOUT) do
+ @worker_in.puts "run #{TESTS}/ptest_forth.rb test"
+ while buf = @worker_out.gets
+ break if /^done (.+?)$/ =~ buf
+ end
+ assert_not_nil($1, "'done' was not found")
+
+ result = Marshal.load($1.chomp.unpack1("m"))
+ assert_equal(5, result[0])
+ pend "TODO: result[1] returns 17. We should investigate it" do
+ assert_equal(12, result[1])
+ end
+ assert_kind_of(Array,result[2])
+ assert_kind_of(Array,result[3])
+ assert_kind_of(Array,result[4])
+ assert_kind_of(Array,result[2][1])
+ assert_kind_of(Test::Unit::AssertionFailedError,result[2][0][2])
+ assert_kind_of(Test::Unit::PendedError,result[2][1][2])
+ assert_kind_of(Test::Unit::PendedError,result[2][2][2])
+ assert_kind_of(Exception, result[2][3][2])
+ assert_equal(result[5], "TestE")
+ end
+ end
+
+ def test_quit
+ Timeout.timeout(TIMEOUT) do
+ @worker_in.puts "quit"
+ assert_match(/^bye$/m,@worker_out.read)
+ end
+ end
+ end
+
+ class TestParallel < Test::Unit::TestCase
+ def spawn_runner(*opt_args)
+ @test_out, o = IO.pipe
+ @test_pid = spawn(*@options[:ruby], TESTS+"/runner.rb",
+ "--ruby", @options[:ruby].join(" "),
+ "-j","t1",*opt_args, out: o, err: o)
+ o.close
+ end
+
+ def teardown
+ begin
+ if @test_pid
+ Timeout.timeout(2) do
+ Process.waitpid(@test_pid)
+ end
+ end
+ rescue Timeout::Error
+ Process.kill(:KILL, @test_pid) if @test_pid
+ ensure
+ @test_out&.close
+ end
+ end
+
+ def test_ignore_jzero
+ @test_out, o = IO.pipe
+ @test_pid = spawn(*@options[:ruby], TESTS+"/runner.rb",
+ "--ruby", @options[:ruby].join(" "),
+ "-j","0", out: File::NULL, err: o)
+ o.close
+ Timeout.timeout(TIMEOUT) {
+ assert_match(/Error: parameter of -j option should be greater than 0/,@test_out.read)
+ }
+ end
+
+ def test_should_run_all_without_any_leaks
+ spawn_runner
+ buf = Timeout.timeout(TIMEOUT) {@test_out.read}
+ assert_match(/^9 tests/,buf)
+ end
+
+ def test_should_retry_failed_on_workers
+ spawn_runner
+ buf = Timeout.timeout(TIMEOUT) {@test_out.read}
+ assert_match(/^Retrying\.+$/,buf)
+ end
+
+ def test_no_retry_option
+ spawn_runner "--no-retry"
+ buf = Timeout.timeout(TIMEOUT) {@test_out.read}
+ refute_match(/^Retrying\.+$/,buf)
+ assert_match(/^ +\d+\) Failure:\nTestD#test_fail_at_worker/,buf)
+ end
+
+ def test_jobs_status
+ spawn_runner "--jobs-status"
+ buf = Timeout.timeout(TIMEOUT) {@test_out.read}
+ assert_match(/\d+=ptest_(first|second|third|forth) */,buf)
+ end
+
+ def test_separate
+ # this test depends to --jobs-status
+ spawn_runner "--jobs-status", "--separate"
+ buf = Timeout.timeout(TIMEOUT) {@test_out.read}
+ assert(buf.scan(/^\[\s*\d+\/\d+\]\s*(\d+?)=/).flatten.uniq.size > 1,
+ message("retried tests should run in different processes") {buf})
+ end
+
+ def test_hungup
+ spawn_runner "--worker-timeout=1", "test4test_hungup.rb"
+ buf = Timeout.timeout(TIMEOUT) {@test_out.read}
+ assert_match(/^Retrying hung up testcases\.+$/, buf)
+ assert_match(/^2 tests,.* 0 failures,/, buf)
+ end
+ end
+end
diff --git a/tool/test/testunit/test_redefinition.rb b/tool/test/testunit/test_redefinition.rb
new file mode 100644
index 0000000000..b4f5cabd4f
--- /dev/null
+++ b/tool/test/testunit/test_redefinition.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: false
+require 'test/unit'
+
+class TestRedefinition < Test::Unit::TestCase
+ def test_redefinition
+ message = %r[test/unit: method TestForTestRedefinition#test_redefinition is redefined$]
+ assert_raise_with_message(Test::Unit::AssertionFailedError, message) do
+ require_relative("test4test_redefinition.rb")
+ end
+ end
+end
diff --git a/tool/test/testunit/test_sorting.rb b/tool/test/testunit/test_sorting.rb
new file mode 100644
index 0000000000..7678249ec2
--- /dev/null
+++ b/tool/test/testunit/test_sorting.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: false
+require 'test/unit'
+
+class TestTestUnitSorting < Test::Unit::TestCase
+ def test_sorting
+ result = sorting("--show-skip")
+ assert_match(/^ 1\) Skipped:/, result)
+ assert_match(/^ 2\) Failure:/, result)
+ assert_match(/^ 3\) Error:/, result)
+ end
+
+ def sorting(*args)
+ IO.popen([*@options[:ruby], "#{File.dirname(__FILE__)}/test4test_sorting.rb",
+ "--verbose", *args], err: [:child, :out]) {|f|
+ f.read
+ }
+ end
+
+ Item = Struct.new(:name)
+ SEED = 0x50975eed
+
+ def make_test_list
+ (1..16).map {"test_%.3x" % rand(0x1000)}.freeze
+ end
+
+ def test_sort_alpha
+ sorter = Test::Unit::Order::Types[:alpha].new(SEED)
+ assert_kind_of(Test::Unit::Order::Types[:sorted], sorter)
+
+ list = make_test_list
+ sorted = list.sort
+ 16.times do
+ assert_equal(sorted, sorter.sort_by_string(list))
+ end
+
+ list = list.map {|s| Item.new(s)}.freeze
+ sorted = list.sort_by(&:name)
+ 16.times do
+ assert_equal(sorted, sorter.sort_by_name(list))
+ end
+ end
+
+ def test_sort_nosort
+ sorter = Test::Unit::Order::Types[:nosort].new(SEED)
+
+ list = make_test_list
+ 16.times do
+ assert_equal(list, sorter.sort_by_string(list))
+ end
+
+ list = list.map {|s| Item.new(s)}.freeze
+ 16.times do
+ assert_equal(list, sorter.sort_by_name(list))
+ end
+ end
+
+ def test_sort_random
+ type = Test::Unit::Order::Types[:random]
+ sorter = type.new(SEED)
+
+ list = make_test_list
+ sorted = type.new(SEED).sort_by_string(list).freeze
+ 16.times do
+ assert_equal(sorted, sorter.sort_by_string(list))
+ end
+ assert_not_equal(sorted, type.new(SEED+1).sort_by_string(list))
+
+ list = list.map {|s| Item.new(s)}.freeze
+ sorted = sorted.map {|s| Item.new(s)}.freeze
+ 16.times do
+ assert_equal(sorted, sorter.sort_by_name(list))
+ end
+ assert_not_equal(sorted, type.new(SEED+1).sort_by_name(list))
+ end
+end
diff --git a/tool/test/testunit/tests_for_parallel/ptest_first.rb b/tool/test/testunit/tests_for_parallel/ptest_first.rb
new file mode 100644
index 0000000000..f8687335b5
--- /dev/null
+++ b/tool/test/testunit/tests_for_parallel/ptest_first.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: false
+require 'test/unit'
+
+class TestA < Test::Unit::TestCase
+ def test_nothing_test
+ end
+end
+
diff --git a/tool/test/testunit/tests_for_parallel/ptest_forth.rb b/tool/test/testunit/tests_for_parallel/ptest_forth.rb
new file mode 100644
index 0000000000..8831676e19
--- /dev/null
+++ b/tool/test/testunit/tests_for_parallel/ptest_forth.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: false
+require 'test/unit'
+
+class TestE < Test::Unit::TestCase
+ class UnknownError < RuntimeError; end
+
+ def test_not_fail
+ assert_equal(1,1)
+ end
+
+ def test_always_skip
+ skip "always"
+ end
+
+ def test_always_fail
+ assert_equal(0,1)
+ end
+
+ def test_skip_after_unknown_error
+ begin
+ raise UnknownError, "unknown error"
+ rescue
+ skip "after raise"
+ end
+ end
+
+ def test_unknown_error
+ raise UnknownError, "unknown error"
+ end
+end
diff --git a/tool/test/testunit/tests_for_parallel/ptest_second.rb b/tool/test/testunit/tests_for_parallel/ptest_second.rb
new file mode 100644
index 0000000000..a793c17eb3
--- /dev/null
+++ b/tool/test/testunit/tests_for_parallel/ptest_second.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: false
+require 'test/unit'
+
+class TestB < Test::Unit::TestCase
+ def test_nothing
+ end
+end
+
+class TestC < Test::Unit::TestCase
+ def test_nothing
+ end
+end
diff --git a/tool/test/testunit/tests_for_parallel/ptest_third.rb b/tool/test/testunit/tests_for_parallel/ptest_third.rb
new file mode 100644
index 0000000000..3f448ecfc1
--- /dev/null
+++ b/tool/test/testunit/tests_for_parallel/ptest_third.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: false
+require 'test/unit'
+
+class TestD < Test::Unit::TestCase
+ def test_fail_at_worker
+ #if /test\/unit\/parallel\.rb/ =~ $0
+ if on_parallel_worker?
+ assert_equal(0,1)
+ end
+ end
+end
diff --git a/tool/test/testunit/tests_for_parallel/runner.rb b/tool/test/testunit/tests_for_parallel/runner.rb
new file mode 100644
index 0000000000..02699e271e
--- /dev/null
+++ b/tool/test/testunit/tests_for_parallel/runner.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: false
+require 'rbconfig'
+
+$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../../../lib"
+
+require 'test/unit'
+
+src_testdir = File.dirname(File.expand_path(__FILE__))
+
+class Test::Unit::Runner
+ @@testfile_prefix = "ptest"
+end
+
+exit Test::Unit::AutoRunner.run(true, src_testdir)
diff --git a/tool/test/testunit/tests_for_parallel/test4test_hungup.rb b/tool/test/testunit/tests_for_parallel/test4test_hungup.rb
new file mode 100644
index 0000000000..65a75f7c4d
--- /dev/null
+++ b/tool/test/testunit/tests_for_parallel/test4test_hungup.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+require_relative '../../../lib/test/unit'
+
+class TestHung < Test::Unit::TestCase
+ def test_success_at_worker
+ assert true
+ end
+
+ def test_hungup_at_worker
+ if on_parallel_worker?
+ sleep 10
+ end
+ assert true
+ end
+end
diff --git a/tool/test/webrick/.htaccess b/tool/test/webrick/.htaccess
new file mode 100644
index 0000000000..69d4659b9f
--- /dev/null
+++ b/tool/test/webrick/.htaccess
@@ -0,0 +1 @@
+this file should not be published.
diff --git a/tool/test/webrick/test_cgi.rb b/tool/test/webrick/test_cgi.rb
new file mode 100644
index 0000000000..7a75cf565e
--- /dev/null
+++ b/tool/test/webrick/test_cgi.rb
@@ -0,0 +1,170 @@
+# coding: US-ASCII
+# frozen_string_literal: false
+require_relative "utils"
+require "webrick"
+require "test/unit"
+
+class TestWEBrickCGI < Test::Unit::TestCase
+ CRLF = "\r\n"
+
+ def teardown
+ WEBrick::Utils::TimeoutHandler.terminate
+ super
+ end
+
+ def start_cgi_server(log_tester=TestWEBrick::DefaultLogTester, &block)
+ config = {
+ :CGIInterpreter => TestWEBrick::RubyBin,
+ :DocumentRoot => File.dirname(__FILE__),
+ :DirectoryIndex => ["webrick.cgi"],
+ :RequestCallback => Proc.new{|req, res|
+ def req.meta_vars
+ meta = super
+ meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR)
+ meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV']
+ return meta
+ end
+ },
+ }
+ if RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32/
+ config[:CGIPathEnv] = ENV['PATH'] # runtime dll may not be in system dir.
+ end
+ TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log|
+ block.call(server, addr, port, log)
+ }
+ end
+
+ def test_cgi
+ start_cgi_server{|server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/webrick.cgi")
+ http.request(req){|res| assert_equal("/webrick.cgi", res.body, log.call)}
+ req = Net::HTTP::Get.new("/webrick.cgi/path/info")
+ http.request(req){|res| assert_equal("/path/info", res.body, log.call)}
+ req = Net::HTTP::Get.new("/webrick.cgi/%3F%3F%3F?foo=bar")
+ http.request(req){|res| assert_equal("/???", res.body, log.call)}
+ unless RUBY_PLATFORM =~ /mswin|mingw|cygwin|bccwin32|java/
+ # Path info of res.body is passed via ENV.
+ # ENV[] returns different value on Windows depending on locale.
+ req = Net::HTTP::Get.new("/webrick.cgi/%A4%DB%A4%B2/%A4%DB%A4%B2")
+ http.request(req){|res|
+ assert_equal("/\xA4\xDB\xA4\xB2/\xA4\xDB\xA4\xB2", res.body, log.call)}
+ end
+ req = Net::HTTP::Get.new("/webrick.cgi?a=1;a=2;b=x")
+ http.request(req){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)}
+ req = Net::HTTP::Get.new("/webrick.cgi?a=1&a=2&b=x")
+ http.request(req){|res| assert_equal("a=1, a=2, b=x", res.body, log.call)}
+
+ req = Net::HTTP::Post.new("/webrick.cgi?a=x;a=y;b=1")
+ req["Content-Type"] = "application/x-www-form-urlencoded"
+ http.request(req, "a=1;a=2;b=x"){|res|
+ assert_equal("a=1, a=2, b=x", res.body, log.call)}
+ req = Net::HTTP::Post.new("/webrick.cgi?a=x&a=y&b=1")
+ req["Content-Type"] = "application/x-www-form-urlencoded"
+ http.request(req, "a=1&a=2&b=x"){|res|
+ assert_equal("a=1, a=2, b=x", res.body, log.call)}
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ ary = res.body.lines.to_a
+ assert_match(%r{/$}, ary[0], log.call)
+ assert_match(%r{/webrick.cgi$}, ary[1], log.call)
+ }
+
+ req = Net::HTTP::Get.new("/webrick.cgi")
+ req["Cookie"] = "CUSTOMER=WILE_E_COYOTE; PART_NUMBER=ROCKET_LAUNCHER_0001"
+ http.request(req){|res|
+ assert_equal(
+ "CUSTOMER=WILE_E_COYOTE\nPART_NUMBER=ROCKET_LAUNCHER_0001\n",
+ res.body, log.call)
+ }
+
+ req = Net::HTTP::Get.new("/webrick.cgi")
+ cookie = %{$Version="1"; }
+ cookie << %{Customer="WILE_E_COYOTE"; $Path="/acme"; }
+ cookie << %{Part_Number="Rocket_Launcher_0001"; $Path="/acme"; }
+ cookie << %{Shipping="FedEx"; $Path="/acme"}
+ req["Cookie"] = cookie
+ http.request(req){|res|
+ assert_equal("Customer=WILE_E_COYOTE, Shipping=FedEx",
+ res["Set-Cookie"], log.call)
+ assert_equal("Customer=WILE_E_COYOTE\n" +
+ "Part_Number=Rocket_Launcher_0001\n" +
+ "Shipping=FedEx\n", res.body, log.call)
+ }
+ }
+ end
+
+ def test_bad_request
+ log_tester = lambda {|log, access_log|
+ assert_match(/BadRequest/, log.join)
+ }
+ start_cgi_server(log_tester) {|server, addr, port, log|
+ sock = TCPSocket.new(addr, port)
+ begin
+ sock << "POST /webrick.cgi HTTP/1.0" << CRLF
+ sock << "Content-Type: application/x-www-form-urlencoded" << CRLF
+ sock << "Content-Length: 1024" << CRLF
+ sock << CRLF
+ sock << "a=1&a=2&b=x"
+ sock.close_write
+ assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, sock.read, log.call)
+ ensure
+ sock.close
+ end
+ }
+ end
+
+ def test_cgi_env
+ start_cgi_server do |server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/webrick.cgi/dumpenv")
+ req['proxy'] = 'http://example.com/'
+ req['hello'] = 'world'
+ http.request(req) do |res|
+ env = Marshal.load(res.body)
+ assert_equal 'world', env['HTTP_HELLO']
+ assert_not_operator env, :include?, 'HTTP_PROXY'
+ end
+ end
+ end
+
+ CtrlSeq = [0x7f, *(1..31)].pack("C*").gsub(/\s+/, '')
+ CtrlPat = /#{Regexp.quote(CtrlSeq)}/o
+ DumpPat = /#{Regexp.quote(CtrlSeq.dump[1...-1])}/o
+
+ def test_bad_uri
+ log_tester = lambda {|log, access_log|
+ assert_equal(1, log.length)
+ assert_match(/ERROR bad URI/, log[0])
+ }
+ start_cgi_server(log_tester) {|server, addr, port, log|
+ res = TCPSocket.open(addr, port) {|sock|
+ sock << "GET /#{CtrlSeq}#{CRLF}#{CRLF}"
+ sock.close_write
+ sock.read
+ }
+ assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, res)
+ s = log.call.each_line.grep(/ERROR bad URI/)[0]
+ assert_match(DumpPat, s)
+ assert_not_match(CtrlPat, s)
+ }
+ end
+
+ def test_bad_header
+ log_tester = lambda {|log, access_log|
+ assert_equal(1, log.length)
+ assert_match(/ERROR bad header/, log[0])
+ }
+ start_cgi_server(log_tester) {|server, addr, port, log|
+ res = TCPSocket.open(addr, port) {|sock|
+ sock << "GET / HTTP/1.0#{CRLF}#{CtrlSeq}#{CRLF}#{CRLF}"
+ sock.close_write
+ sock.read
+ }
+ assert_match(%r{\AHTTP/\d.\d 400 Bad Request}, res)
+ s = log.call.each_line.grep(/ERROR bad header/)[0]
+ assert_match(DumpPat, s)
+ assert_not_match(CtrlPat, s)
+ }
+ end
+end
diff --git a/tool/test/webrick/test_config.rb b/tool/test/webrick/test_config.rb
new file mode 100644
index 0000000000..a54a667452
--- /dev/null
+++ b/tool/test/webrick/test_config.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: false
+require "test/unit"
+require "webrick/config"
+
+class TestWEBrickConfig < Test::Unit::TestCase
+ def test_server_name_default
+ config = WEBrick::Config::General.dup
+ assert_equal(false, config.key?(:ServerName))
+ assert_equal(WEBrick::Utils.getservername, config[:ServerName])
+ assert_equal(true, config.key?(:ServerName))
+ end
+
+ def test_server_name_set_nil
+ config = WEBrick::Config::General.dup.update(ServerName: nil)
+ assert_equal(nil, config[:ServerName])
+ end
+end
diff --git a/tool/test/webrick/test_cookie.rb b/tool/test/webrick/test_cookie.rb
new file mode 100644
index 0000000000..e46185f127
--- /dev/null
+++ b/tool/test/webrick/test_cookie.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: false
+require "test/unit"
+require "webrick/cookie"
+
+class TestWEBrickCookie < Test::Unit::TestCase
+ def test_new
+ cookie = WEBrick::Cookie.new("foo","bar")
+ assert_equal("foo", cookie.name)
+ assert_equal("bar", cookie.value)
+ assert_equal("foo=bar", cookie.to_s)
+ end
+
+ def test_time
+ cookie = WEBrick::Cookie.new("foo","bar")
+ t = 1000000000
+ cookie.max_age = t
+ assert_match(t.to_s, cookie.to_s)
+
+ cookie = WEBrick::Cookie.new("foo","bar")
+ t = Time.at(1000000000)
+ cookie.expires = t
+ assert_equal(Time, cookie.expires.class)
+ assert_equal(t, cookie.expires)
+ ts = t.httpdate
+ cookie.expires = ts
+ assert_equal(Time, cookie.expires.class)
+ assert_equal(t, cookie.expires)
+ assert_match(ts, cookie.to_s)
+ end
+
+ def test_parse
+ data = ""
+ data << '$Version="1"; '
+ data << 'Customer="WILE_E_COYOTE"; $Path="/acme"; '
+ data << 'Part_Number="Rocket_Launcher_0001"; $Path="/acme"; '
+ data << 'Shipping="FedEx"; $Path="/acme"'
+ cookies = WEBrick::Cookie.parse(data)
+ assert_equal(3, cookies.size)
+ assert_equal(1, cookies[0].version)
+ assert_equal("Customer", cookies[0].name)
+ assert_equal("WILE_E_COYOTE", cookies[0].value)
+ assert_equal("/acme", cookies[0].path)
+ assert_equal(1, cookies[1].version)
+ assert_equal("Part_Number", cookies[1].name)
+ assert_equal("Rocket_Launcher_0001", cookies[1].value)
+ assert_equal(1, cookies[2].version)
+ assert_equal("Shipping", cookies[2].name)
+ assert_equal("FedEx", cookies[2].value)
+
+ data = "hoge=moge; __div__session=9865ecfd514be7f7"
+ cookies = WEBrick::Cookie.parse(data)
+ assert_equal(2, cookies.size)
+ assert_equal(0, cookies[0].version)
+ assert_equal("hoge", cookies[0].name)
+ assert_equal("moge", cookies[0].value)
+ assert_equal("__div__session", cookies[1].name)
+ assert_equal("9865ecfd514be7f7", cookies[1].value)
+
+ # don't allow ,-separator
+ data = "hoge=moge, __div__session=9865ecfd514be7f7"
+ cookies = WEBrick::Cookie.parse(data)
+ assert_equal(1, cookies.size)
+ assert_equal(0, cookies[0].version)
+ assert_equal("hoge", cookies[0].name)
+ assert_equal("moge, __div__session=9865ecfd514be7f7", cookies[0].value)
+ end
+
+ def test_parse_no_whitespace
+ data = [
+ '$Version="1"; ',
+ 'Customer="WILE_E_COYOTE";$Path="/acme";', # no SP between cookie-string
+ 'Part_Number="Rocket_Launcher_0001";$Path="/acme";', # no SP between cookie-string
+ 'Shipping="FedEx";$Path="/acme"'
+ ].join
+ cookies = WEBrick::Cookie.parse(data)
+ assert_equal(1, cookies.size)
+ end
+
+ def test_parse_too_much_whitespaces
+ # According to RFC6265,
+ # cookie-string = cookie-pair *( ";" SP cookie-pair )
+ # So single 0x20 is needed after ';'. We allow multiple spaces here for
+ # compatibility with older WEBrick versions.
+ data = [
+ '$Version="1"; ',
+ 'Customer="WILE_E_COYOTE";$Path="/acme"; ', # no SP between cookie-string
+ 'Part_Number="Rocket_Launcher_0001";$Path="/acme"; ', # no SP between cookie-string
+ 'Shipping="FedEx";$Path="/acme"'
+ ].join
+ cookies = WEBrick::Cookie.parse(data)
+ assert_equal(3, cookies.size)
+ end
+
+ def test_parse_set_cookie
+ data = %(Customer="WILE_E_COYOTE"; Version="1"; Path="/acme")
+ cookie = WEBrick::Cookie.parse_set_cookie(data)
+ assert_equal("Customer", cookie.name)
+ assert_equal("WILE_E_COYOTE", cookie.value)
+ assert_equal(1, cookie.version)
+ assert_equal("/acme", cookie.path)
+
+ data = %(Shipping="FedEx"; Version="1"; Path="/acme"; Secure)
+ cookie = WEBrick::Cookie.parse_set_cookie(data)
+ assert_equal("Shipping", cookie.name)
+ assert_equal("FedEx", cookie.value)
+ assert_equal(1, cookie.version)
+ assert_equal("/acme", cookie.path)
+ assert_equal(true, cookie.secure)
+ end
+
+ def test_parse_set_cookies
+ data = %(Shipping="FedEx"; Version="1"; Path="/acme"; Secure)
+ data << %(, CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT; path=/; Secure)
+ data << %(, name="Aaron"; Version="1"; path="/acme")
+ cookies = WEBrick::Cookie.parse_set_cookies(data)
+ assert_equal(3, cookies.length)
+
+ fed_ex = cookies.find { |c| c.name == 'Shipping' }
+ assert_not_nil(fed_ex)
+ assert_equal("Shipping", fed_ex.name)
+ assert_equal("FedEx", fed_ex.value)
+ assert_equal(1, fed_ex.version)
+ assert_equal("/acme", fed_ex.path)
+ assert_equal(true, fed_ex.secure)
+
+ name = cookies.find { |c| c.name == 'name' }
+ assert_not_nil(name)
+ assert_equal("name", name.name)
+ assert_equal("Aaron", name.value)
+ assert_equal(1, name.version)
+ assert_equal("/acme", name.path)
+
+ customer = cookies.find { |c| c.name == 'CUSTOMER' }
+ assert_not_nil(customer)
+ assert_equal("CUSTOMER", customer.name)
+ assert_equal("WILE_E_COYOTE", customer.value)
+ assert_equal(0, customer.version)
+ assert_equal("/", customer.path)
+ assert_equal(Time.utc(1999, 11, 9, 23, 12, 40), customer.expires)
+ end
+end
diff --git a/tool/test/webrick/test_do_not_reverse_lookup.rb b/tool/test/webrick/test_do_not_reverse_lookup.rb
new file mode 100644
index 0000000000..efcb5a9299
--- /dev/null
+++ b/tool/test/webrick/test_do_not_reverse_lookup.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: false
+require "test/unit"
+require "webrick"
+require_relative "utils"
+
+class TestDoNotReverseLookup < Test::Unit::TestCase
+ class DNRL < WEBrick::GenericServer
+ def run(sock)
+ sock << sock.do_not_reverse_lookup.to_s
+ end
+ end
+
+ @@original_do_not_reverse_lookup_value = Socket.do_not_reverse_lookup
+
+ def teardown
+ Socket.do_not_reverse_lookup = @@original_do_not_reverse_lookup_value
+ end
+
+ def do_not_reverse_lookup?(config)
+ result = nil
+ TestWEBrick.start_server(DNRL, config) do |server, addr, port, log|
+ TCPSocket.open(addr, port) do |sock|
+ result = {'true' => true, 'false' => false}[sock.gets]
+ end
+ end
+ result
+ end
+
+ # +--------------------------------------------------------------------------+
+ # | Expected interaction between Socket.do_not_reverse_lookup |
+ # | and WEBrick::Config::General[:DoNotReverseLookup] |
+ # +----------------------------+---------------------------------------------+
+ # | |WEBrick::Config::General[:DoNotReverseLookup]|
+ # +----------------------------+--------------+---------------+--------------+
+ # |Socket.do_not_reverse_lookup| TRUE | FALSE | NIL |
+ # +----------------------------+--------------+---------------+--------------+
+ # | TRUE | true | false | true |
+ # +----------------------------+--------------+---------------+--------------+
+ # | FALSE | true | false | false |
+ # +----------------------------+--------------+---------------+--------------+
+
+ def test_socket_dnrl_true_server_dnrl_true
+ Socket.do_not_reverse_lookup = true
+ assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => true))
+ end
+
+ def test_socket_dnrl_true_server_dnrl_false
+ Socket.do_not_reverse_lookup = true
+ assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => false))
+ end
+
+ def test_socket_dnrl_true_server_dnrl_nil
+ Socket.do_not_reverse_lookup = true
+ assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => nil))
+ end
+
+ def test_socket_dnrl_false_server_dnrl_true
+ Socket.do_not_reverse_lookup = false
+ assert_equal(true, do_not_reverse_lookup?(:DoNotReverseLookup => true))
+ end
+
+ def test_socket_dnrl_false_server_dnrl_false
+ Socket.do_not_reverse_lookup = false
+ assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => false))
+ end
+
+ def test_socket_dnrl_false_server_dnrl_nil
+ Socket.do_not_reverse_lookup = false
+ assert_equal(false, do_not_reverse_lookup?(:DoNotReverseLookup => nil))
+ end
+end
diff --git a/tool/test/webrick/test_filehandler.rb b/tool/test/webrick/test_filehandler.rb
new file mode 100644
index 0000000000..146d8ce792
--- /dev/null
+++ b/tool/test/webrick/test_filehandler.rb
@@ -0,0 +1,403 @@
+# frozen_string_literal: false
+require "test/unit"
+require_relative "utils.rb"
+require "webrick"
+require "stringio"
+require "tmpdir"
+
+class WEBrick::TestFileHandler < Test::Unit::TestCase
+ def teardown
+ WEBrick::Utils::TimeoutHandler.terminate
+ super
+ end
+
+ def default_file_handler(filename)
+ klass = WEBrick::HTTPServlet::DefaultFileHandler
+ klass.new(WEBrick::Config::HTTP, filename)
+ end
+
+ def windows?
+ File.directory?("\\")
+ end
+
+ def get_res_body(res)
+ sio = StringIO.new
+ sio.binmode
+ res.send_body(sio)
+ sio.string
+ end
+
+ def make_range_request(range_spec)
+ msg = <<-END_OF_REQUEST
+ GET / HTTP/1.0
+ Range: #{range_spec}
+
+ END_OF_REQUEST
+ return StringIO.new(msg.gsub(/^ {6}/, ""))
+ end
+
+ def make_range_response(file, range_spec)
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(make_range_request(range_spec))
+ res = WEBrick::HTTPResponse.new(WEBrick::Config::HTTP)
+ size = File.size(file)
+ handler = default_file_handler(file)
+ handler.make_partial_content(req, res, file, size)
+ return res
+ end
+
+ def test_make_partial_content
+ filename = __FILE__
+ filesize = File.size(filename)
+
+ res = make_range_response(filename, "bytes=#{filesize-100}-")
+ assert_match(%r{^text/plain}, res["content-type"])
+ assert_equal(100, get_res_body(res).size)
+
+ res = make_range_response(filename, "bytes=-100")
+ assert_match(%r{^text/plain}, res["content-type"])
+ assert_equal(100, get_res_body(res).size)
+
+ res = make_range_response(filename, "bytes=0-99")
+ assert_match(%r{^text/plain}, res["content-type"])
+ assert_equal(100, get_res_body(res).size)
+
+ res = make_range_response(filename, "bytes=100-199")
+ assert_match(%r{^text/plain}, res["content-type"])
+ assert_equal(100, get_res_body(res).size)
+
+ res = make_range_response(filename, "bytes=0-0")
+ assert_match(%r{^text/plain}, res["content-type"])
+ assert_equal(1, get_res_body(res).size)
+
+ res = make_range_response(filename, "bytes=-1")
+ assert_match(%r{^text/plain}, res["content-type"])
+ assert_equal(1, get_res_body(res).size)
+
+ res = make_range_response(filename, "bytes=0-0, -2")
+ assert_match(%r{^multipart/byteranges}, res["content-type"])
+ body = get_res_body(res)
+ boundary = /; boundary=(.+)/.match(res['content-type'])[1]
+ off = filesize - 2
+ last = filesize - 1
+
+ exp = "--#{boundary}\r\n" \
+ "Content-Type: text/plain\r\n" \
+ "Content-Range: bytes 0-0/#{filesize}\r\n" \
+ "\r\n" \
+ "#{IO.read(__FILE__, 1)}\r\n" \
+ "--#{boundary}\r\n" \
+ "Content-Type: text/plain\r\n" \
+ "Content-Range: bytes #{off}-#{last}/#{filesize}\r\n" \
+ "\r\n" \
+ "#{IO.read(__FILE__, 2, off)}\r\n" \
+ "--#{boundary}--\r\n"
+ assert_equal exp, body
+ end
+
+ def test_filehandler
+ config = { :DocumentRoot => File.dirname(__FILE__), }
+ this_file = File.basename(__FILE__)
+ filesize = File.size(__FILE__)
+ this_data = File.binread(__FILE__)
+ range = nil
+ bug2593 = '[ruby-dev:40030]'
+
+ TestWEBrick.start_httpserver(config) do |server, addr, port, log|
+ begin
+ server[:DocumentRootOptions][:NondisclosureName] = []
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_equal("200", res.code, log.call)
+ assert_equal("text/html", res.content_type, log.call)
+ assert_match(/HREF="#{this_file}"/, res.body, log.call)
+ }
+ req = Net::HTTP::Get.new("/#{this_file}")
+ http.request(req){|res|
+ assert_equal("200", res.code, log.call)
+ assert_equal("text/plain", res.content_type, log.call)
+ assert_equal(this_data, res.body, log.call)
+ }
+
+ req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=#{filesize-100}-")
+ http.request(req){|res|
+ assert_equal("206", res.code, log.call)
+ assert_equal("text/plain", res.content_type, log.call)
+ assert_nothing_raised(bug2593) {range = res.content_range}
+ assert_equal((filesize-100)..(filesize-1), range, log.call)
+ assert_equal(this_data[-100..-1], res.body, log.call)
+ }
+
+ req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=-100")
+ http.request(req){|res|
+ assert_equal("206", res.code, log.call)
+ assert_equal("text/plain", res.content_type, log.call)
+ assert_nothing_raised(bug2593) {range = res.content_range}
+ assert_equal((filesize-100)..(filesize-1), range, log.call)
+ assert_equal(this_data[-100..-1], res.body, log.call)
+ }
+
+ req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-99")
+ http.request(req){|res|
+ assert_equal("206", res.code, log.call)
+ assert_equal("text/plain", res.content_type, log.call)
+ assert_nothing_raised(bug2593) {range = res.content_range}
+ assert_equal(0..99, range, log.call)
+ assert_equal(this_data[0..99], res.body, log.call)
+ }
+
+ req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=100-199")
+ http.request(req){|res|
+ assert_equal("206", res.code, log.call)
+ assert_equal("text/plain", res.content_type, log.call)
+ assert_nothing_raised(bug2593) {range = res.content_range}
+ assert_equal(100..199, range, log.call)
+ assert_equal(this_data[100..199], res.body, log.call)
+ }
+
+ req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-0")
+ http.request(req){|res|
+ assert_equal("206", res.code, log.call)
+ assert_equal("text/plain", res.content_type, log.call)
+ assert_nothing_raised(bug2593) {range = res.content_range}
+ assert_equal(0..0, range, log.call)
+ assert_equal(this_data[0..0], res.body, log.call)
+ }
+
+ req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=-1")
+ http.request(req){|res|
+ assert_equal("206", res.code, log.call)
+ assert_equal("text/plain", res.content_type, log.call)
+ assert_nothing_raised(bug2593) {range = res.content_range}
+ assert_equal((filesize-1)..(filesize-1), range, log.call)
+ assert_equal(this_data[-1, 1], res.body, log.call)
+ }
+
+ req = Net::HTTP::Get.new("/#{this_file}", "range"=>"bytes=0-0, -2")
+ http.request(req){|res|
+ assert_equal("206", res.code, log.call)
+ assert_equal("multipart/byteranges", res.content_type, log.call)
+ }
+ ensure
+ server[:DocumentRootOptions].delete :NondisclosureName
+ end
+ end
+ end
+
+ def test_non_disclosure_name
+ config = { :DocumentRoot => File.dirname(__FILE__), }
+ log_tester = lambda {|log, access_log|
+ log = log.reject {|s| /ERROR `.*\' not found\./ =~ s }
+ log = log.reject {|s| /WARN the request refers nondisclosure name/ =~ s }
+ assert_equal([], log)
+ }
+ this_file = File.basename(__FILE__)
+ TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ doc_root_opts = server[:DocumentRootOptions]
+ doc_root_opts[:NondisclosureName] = %w(.ht* *~ test_*)
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_equal("200", res.code, log.call)
+ assert_equal("text/html", res.content_type, log.call)
+ assert_no_match(/HREF="#{File.basename(__FILE__)}"/, res.body)
+ }
+ req = Net::HTTP::Get.new("/#{this_file}")
+ http.request(req){|res|
+ assert_equal("404", res.code, log.call)
+ }
+ doc_root_opts[:NondisclosureName] = %w(.ht* *~ TEST_*)
+ http.request(req){|res|
+ assert_equal("404", res.code, log.call)
+ }
+ end
+ end
+
+ def test_directory_traversal
+ return if File.executable?(__FILE__) # skip on strange file system
+
+ config = { :DocumentRoot => File.dirname(__FILE__), }
+ log_tester = lambda {|log, access_log|
+ log = log.reject {|s| /ERROR bad URI/ =~ s }
+ log = log.reject {|s| /ERROR `.*\' not found\./ =~ s }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/../../")
+ http.request(req){|res| assert_equal("400", res.code, log.call) }
+ req = Net::HTTP::Get.new("/..%5c../#{File.basename(__FILE__)}")
+ http.request(req){|res| assert_equal(windows? ? "200" : "404", res.code, log.call) }
+ req = Net::HTTP::Get.new("/..%5c..%5cruby.c")
+ http.request(req){|res| assert_equal("404", res.code, log.call) }
+ end
+ end
+
+ def test_unwise_in_path
+ if windows?
+ config = { :DocumentRoot => File.dirname(__FILE__), }
+ TestWEBrick.start_httpserver(config) do |server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/..%5c..")
+ http.request(req){|res| assert_equal("301", res.code, log.call) }
+ end
+ end
+ end
+
+ def test_short_filename
+ return if File.executable?(__FILE__) # skip on strange file system
+ return if /mswin/ =~ RUBY_PLATFORM && ENV.key?('GITHUB_ACTIONS') # not working from the beginning
+
+ config = {
+ :CGIInterpreter => TestWEBrick::RubyBin,
+ :DocumentRoot => File.dirname(__FILE__),
+ :CGIPathEnv => ENV['PATH'],
+ }
+ log_tester = lambda {|log, access_log|
+ log = log.reject {|s| /ERROR `.*\' not found\./ =~ s }
+ log = log.reject {|s| /WARN the request refers nondisclosure name/ =~ s }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ if windows?
+ root = config[:DocumentRoot].tr("/", "\\")
+ fname = IO.popen(%W[dir /x #{root}\\webrick_long_filename.cgi], encoding: "binary", &:read)
+ fname.sub!(/\A.*$^$.*$^$/m, '')
+ if fname
+ fname = fname[/\s(w.+?cgi)\s/i, 1]
+ fname.downcase!
+ end
+ else
+ fname = "webric~1.cgi"
+ end
+ req = Net::HTTP::Get.new("/#{fname}/test")
+ http.request(req) do |res|
+ if windows?
+ assert_equal("200", res.code, log.call)
+ assert_equal("/test", res.body, log.call)
+ else
+ assert_equal("404", res.code, log.call)
+ end
+ end
+
+ req = Net::HTTP::Get.new("/.htaccess")
+ http.request(req) {|res| assert_equal("404", res.code, log.call) }
+ req = Net::HTTP::Get.new("/htacce~1")
+ http.request(req) {|res| assert_equal("404", res.code, log.call) }
+ req = Net::HTTP::Get.new("/HTACCE~1")
+ http.request(req) {|res| assert_equal("404", res.code, log.call) }
+ end
+ end
+
+ def test_multibyte_char_in_path
+ if Encoding.default_external == Encoding.find('US-ASCII')
+ reset_encoding = true
+ verb = $VERBOSE
+ $VERBOSE = false
+ Encoding.default_external = Encoding.find('UTF-8')
+ end
+
+ c = "\u00a7"
+ begin
+ c = c.encode('filesystem')
+ rescue EncodingError
+ c = c.b
+ end
+ Dir.mktmpdir(c) do |dir|
+ basename = "#{c}.txt"
+ File.write("#{dir}/#{basename}", "test_multibyte_char_in_path")
+ Dir.mkdir("#{dir}/#{c}")
+ File.write("#{dir}/#{c}/#{basename}", "nested")
+ config = {
+ :DocumentRoot => dir,
+ :DirectoryIndex => [basename],
+ }
+ TestWEBrick.start_httpserver(config) do |server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ path = "/#{basename}"
+ req = Net::HTTP::Get.new(WEBrick::HTTPUtils::escape(path))
+ http.request(req){|res| assert_equal("200", res.code, log.call + "\nFilesystem encoding is #{Encoding.find('filesystem')}") }
+ path = "/#{c}/#{basename}"
+ req = Net::HTTP::Get.new(WEBrick::HTTPUtils::escape(path))
+ http.request(req){|res| assert_equal("200", res.code, log.call) }
+ req = Net::HTTP::Get.new('/')
+ http.request(req){|res|
+ assert_equal("test_multibyte_char_in_path", res.body, log.call)
+ }
+ end
+ end
+ ensure
+ if reset_encoding
+ Encoding.default_external = Encoding.find('US-ASCII')
+ $VERBOSE = verb
+ end
+ end
+
+ def test_script_disclosure
+ return if File.executable?(__FILE__) # skip on strange file system
+
+ config = {
+ :CGIInterpreter => TestWEBrick::RubyBinArray,
+ :DocumentRoot => File.dirname(__FILE__),
+ :CGIPathEnv => ENV['PATH'],
+ :RequestCallback => Proc.new{|req, res|
+ def req.meta_vars
+ meta = super
+ meta["RUBYLIB"] = $:.join(File::PATH_SEPARATOR)
+ meta[RbConfig::CONFIG['LIBPATHENV']] = ENV[RbConfig::CONFIG['LIBPATHENV']] if RbConfig::CONFIG['LIBPATHENV']
+ return meta
+ end
+ },
+ }
+ log_tester = lambda {|log, access_log|
+ log = log.reject {|s| /ERROR `.*\' not found\./ =~ s }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ http.read_timeout = EnvUtil.apply_timeout_scale(60)
+ http.write_timeout = EnvUtil.apply_timeout_scale(60) if http.respond_to?(:write_timeout=)
+
+ req = Net::HTTP::Get.new("/webrick.cgi/test")
+ http.request(req) do |res|
+ assert_equal("200", res.code, log.call)
+ assert_equal("/test", res.body, log.call)
+ end
+
+ resok = windows?
+ response_assertion = Proc.new do |res|
+ if resok
+ assert_equal("200", res.code, log.call)
+ assert_equal("/test", res.body, log.call)
+ else
+ assert_equal("404", res.code, log.call)
+ end
+ end
+ req = Net::HTTP::Get.new("/webrick.cgi%20/test")
+ http.request(req, &response_assertion)
+ req = Net::HTTP::Get.new("/webrick.cgi./test")
+ http.request(req, &response_assertion)
+ resok &&= File.exist?(__FILE__+"::$DATA")
+ req = Net::HTTP::Get.new("/webrick.cgi::$DATA/test")
+ http.request(req, &response_assertion)
+ end
+ end
+
+ def test_erbhandler
+ config = { :DocumentRoot => File.dirname(__FILE__) }
+ log_tester = lambda {|log, access_log|
+ log = log.reject {|s| /ERROR `.*\' not found\./ =~ s }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpserver(config, log_tester) do |server, addr, port, log|
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/webrick.rhtml")
+ http.request(req) do |res|
+ assert_equal("200", res.code, log.call)
+ assert_match %r!\Areq to http://[^/]+/webrick\.rhtml {}\n!, res.body
+ end
+ end
+ end
+end
diff --git a/tool/test/webrick/test_htgroup.rb b/tool/test/webrick/test_htgroup.rb
new file mode 100644
index 0000000000..8749711df5
--- /dev/null
+++ b/tool/test/webrick/test_htgroup.rb
@@ -0,0 +1,19 @@
+require "tempfile"
+require "test/unit"
+require "webrick/httpauth/htgroup"
+
+class TestHtgroup < Test::Unit::TestCase
+ def test_htgroup
+ Tempfile.create('test_htgroup') do |tmpfile|
+ tmpfile.close
+ tmp_group = WEBrick::HTTPAuth::Htgroup.new(tmpfile.path)
+ tmp_group.add 'superheroes', %w[spiderman batman]
+ tmp_group.add 'supervillains', %w[joker]
+ tmp_group.flush
+
+ htgroup = WEBrick::HTTPAuth::Htgroup.new(tmpfile.path)
+ assert_equal(htgroup.members('superheroes'), %w[spiderman batman])
+ assert_equal(htgroup.members('supervillains'), %w[joker])
+ end
+ end
+end
diff --git a/tool/test/webrick/test_htmlutils.rb b/tool/test/webrick/test_htmlutils.rb
new file mode 100644
index 0000000000..ae1b8efa95
--- /dev/null
+++ b/tool/test/webrick/test_htmlutils.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: false
+require "test/unit"
+require "webrick/htmlutils"
+
+class TestWEBrickHTMLUtils < Test::Unit::TestCase
+ include WEBrick::HTMLUtils
+
+ def test_escape
+ assert_equal("foo", escape("foo"))
+ assert_equal("foo bar", escape("foo bar"))
+ assert_equal("foo&amp;bar", escape("foo&bar"))
+ assert_equal("foo&quot;bar", escape("foo\"bar"))
+ assert_equal("foo&gt;bar", escape("foo>bar"))
+ assert_equal("foo&lt;bar", escape("foo<bar"))
+ assert_equal("\u{3053 3093 306B 3061 306F}", escape("\u{3053 3093 306B 3061 306F}"))
+ bug8425 = '[Bug #8425] [ruby-core:55052]'
+ assert_nothing_raised(ArgumentError, Encoding::CompatibilityError, bug8425) {
+ assert_equal("\u{3053 3093 306B}\xff&lt;", escape("\u{3053 3093 306B}\xff<"))
+ }
+ end
+end
diff --git a/tool/test/webrick/test_httpauth.rb b/tool/test/webrick/test_httpauth.rb
new file mode 100644
index 0000000000..9fe8af8be2
--- /dev/null
+++ b/tool/test/webrick/test_httpauth.rb
@@ -0,0 +1,366 @@
+# frozen_string_literal: false
+require "test/unit"
+require "net/http"
+require "tempfile"
+require "webrick"
+require "webrick/httpauth/basicauth"
+require "stringio"
+require_relative "utils"
+
+class TestWEBrickHTTPAuth < Test::Unit::TestCase
+ def teardown
+ WEBrick::Utils::TimeoutHandler.terminate
+ super
+ end
+
+ def test_basic_auth
+ log_tester = lambda {|log, access_log|
+ assert_equal(1, log.length)
+ assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[0])
+ }
+ TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
+ realm = "WEBrick's realm"
+ path = "/basic_auth"
+
+ server.mount_proc(path){|req, res|
+ WEBrick::HTTPAuth.basic_auth(req, res, realm){|user, pass|
+ user == "webrick" && pass == "supersecretpassword"
+ }
+ res.body = "hoge"
+ }
+ http = Net::HTTP.new(addr, port)
+ g = Net::HTTP::Get.new(path)
+ g.basic_auth("webrick", "supersecretpassword")
+ http.request(g){|res| assert_equal("hoge", res.body, log.call)}
+ g.basic_auth("webrick", "not super")
+ http.request(g){|res| assert_not_equal("hoge", res.body, log.call)}
+ }
+ end
+
+ def test_basic_auth_sha
+ Tempfile.create("test_webrick_auth") {|tmpfile|
+ tmpfile.puts("webrick:{SHA}GJYFRpBbdchp595jlh3Bhfmgp8k=")
+ tmpfile.flush
+ assert_raise(NotImplementedError){
+ WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
+ }
+ }
+ end
+
+ def test_basic_auth_md5
+ Tempfile.create("test_webrick_auth") {|tmpfile|
+ tmpfile.puts("webrick:$apr1$IOVMD/..$rmnOSPXr0.wwrLPZHBQZy0")
+ tmpfile.flush
+ assert_raise(NotImplementedError){
+ WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path)
+ }
+ }
+ end
+
+ [nil, :crypt, :bcrypt].each do |hash_algo|
+ # OpenBSD does not support insecure DES-crypt
+ next if /openbsd/ =~ RUBY_PLATFORM && hash_algo != :bcrypt
+
+ begin
+ case hash_algo
+ when :crypt
+ # require 'string/crypt'
+ when :bcrypt
+ require 'bcrypt'
+ end
+ rescue LoadError
+ next
+ end
+
+ define_method(:"test_basic_auth_htpasswd_#{hash_algo}") do
+ log_tester = lambda {|log, access_log|
+ log.reject! {|line| /\A\s*\z/ =~ line }
+ pats = [
+ /ERROR Basic WEBrick's realm: webrick: password unmatch\./,
+ /ERROR WEBrick::HTTPStatus::Unauthorized/
+ ]
+ pats.each {|pat|
+ assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
+ log.reject! {|line| pat =~ line }
+ }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
+ realm = "WEBrick's realm"
+ path = "/basic_auth2"
+
+ Tempfile.create("test_webrick_auth") {|tmpfile|
+ tmpfile.close
+ tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
+ tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
+ tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
+ tmp_pass.flush
+
+ htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
+ users = []
+ htpasswd.each{|user, pass| users << user }
+ assert_equal(2, users.size, log.call)
+ assert(users.member?("webrick"), log.call)
+ assert(users.member?("foo"), log.call)
+
+ server.mount_proc(path){|req, res|
+ auth = WEBrick::HTTPAuth::BasicAuth.new(
+ :Realm => realm, :UserDB => htpasswd,
+ :Logger => server.logger
+ )
+ auth.authenticate(req, res)
+ res.body = "hoge"
+ }
+ http = Net::HTTP.new(addr, port)
+ g = Net::HTTP::Get.new(path)
+ g.basic_auth("webrick", "supersecretpassword")
+ http.request(g){|res| assert_equal("hoge", res.body, log.call)}
+ g.basic_auth("webrick", "not super")
+ http.request(g){|res| assert_not_equal("hoge", res.body, log.call)}
+ }
+ }
+ end
+
+ define_method(:"test_basic_auth_bad_username_htpasswd_#{hash_algo}") do
+ log_tester = lambda {|log, access_log|
+ assert_equal(2, log.length)
+ assert_match(/ERROR Basic WEBrick's realm: foo\\ebar: the user is not allowed\./, log[0])
+ assert_match(/ERROR WEBrick::HTTPStatus::Unauthorized/, log[1])
+ }
+ TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
+ realm = "WEBrick's realm"
+ path = "/basic_auth"
+
+ Tempfile.create("test_webrick_auth") {|tmpfile|
+ tmpfile.close
+ tmp_pass = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
+ tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
+ tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
+ tmp_pass.flush
+
+ htpasswd = WEBrick::HTTPAuth::Htpasswd.new(tmpfile.path, password_hash: hash_algo)
+ users = []
+ htpasswd.each{|user, pass| users << user }
+ server.mount_proc(path){|req, res|
+ auth = WEBrick::HTTPAuth::BasicAuth.new(
+ :Realm => realm, :UserDB => htpasswd,
+ :Logger => server.logger
+ )
+ auth.authenticate(req, res)
+ res.body = "hoge"
+ }
+ http = Net::HTTP.new(addr, port)
+ g = Net::HTTP::Get.new(path)
+ g.basic_auth("foo\ebar", "passwd")
+ http.request(g){|res| assert_not_equal("hoge", res.body, log.call) }
+ }
+ }
+ end
+ end
+
+ DIGESTRES_ = /
+ ([a-zA-Z\-]+)
+ [ \t]*(?:\r\n[ \t]*)*
+ =
+ [ \t]*(?:\r\n[ \t]*)*
+ (?:
+ "((?:[^"]+|\\[\x00-\x7F])*)" |
+ ([!\#$%&'*+\-.0-9A-Z^_`a-z|~]+)
+ )/x
+
+ def test_digest_auth
+ log_tester = lambda {|log, access_log|
+ log.reject! {|line| /\A\s*\z/ =~ line }
+ pats = [
+ /ERROR Digest WEBrick's realm: no credentials in the request\./,
+ /ERROR WEBrick::HTTPStatus::Unauthorized/,
+ /ERROR Digest WEBrick's realm: webrick: digest unmatch\./
+ ]
+ pats.each {|pat|
+ assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
+ log.reject! {|line| pat =~ line }
+ }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
+ realm = "WEBrick's realm"
+ path = "/digest_auth"
+
+ Tempfile.create("test_webrick_auth") {|tmpfile|
+ tmpfile.close
+ tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
+ tmp_pass.set_passwd(realm, "webrick", "supersecretpassword")
+ tmp_pass.set_passwd(realm, "foo", "supersecretpassword")
+ tmp_pass.flush
+
+ htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
+ users = []
+ htdigest.each{|user, pass| users << user }
+ assert_equal(2, users.size, log.call)
+ assert(users.member?("webrick"), log.call)
+ assert(users.member?("foo"), log.call)
+
+ auth = WEBrick::HTTPAuth::DigestAuth.new(
+ :Realm => realm, :UserDB => htdigest,
+ :Algorithm => 'MD5',
+ :Logger => server.logger
+ )
+ server.mount_proc(path){|req, res|
+ auth.authenticate(req, res)
+ res.body = "hoge"
+ }
+
+ Net::HTTP.start(addr, port) do |http|
+ g = Net::HTTP::Get.new(path)
+ params = {}
+ http.request(g) do |res|
+ assert_equal('401', res.code, log.call)
+ res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token|
+ params[key.downcase] = token || quoted.delete('\\')
+ end
+ params['uri'] = "http://#{addr}:#{port}#{path}"
+ end
+
+ g['Authorization'] = credentials_for_request('webrick', "supersecretpassword", params)
+ http.request(g){|res| assert_equal("hoge", res.body, log.call)}
+
+ params['algorithm'].downcase! #4936
+ g['Authorization'] = credentials_for_request('webrick', "supersecretpassword", params)
+ http.request(g){|res| assert_equal("hoge", res.body, log.call)}
+
+ g['Authorization'] = credentials_for_request('webrick', "not super", params)
+ http.request(g){|res| assert_not_equal("hoge", res.body, log.call)}
+ end
+ }
+ }
+ end
+
+ def test_digest_auth_int
+ log_tester = lambda {|log, access_log|
+ log.reject! {|line| /\A\s*\z/ =~ line }
+ pats = [
+ /ERROR Digest wb auth-int realm: no credentials in the request\./,
+ /ERROR WEBrick::HTTPStatus::Unauthorized/,
+ /ERROR Digest wb auth-int realm: foo: digest unmatch\./
+ ]
+ pats.each {|pat|
+ assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
+ log.reject! {|line| pat =~ line }
+ }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
+ realm = "wb auth-int realm"
+ path = "/digest_auth_int"
+
+ Tempfile.create("test_webrick_auth_int") {|tmpfile|
+ tmpfile.close
+ tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
+ tmp_pass.set_passwd(realm, "foo", "Hunter2")
+ tmp_pass.flush
+
+ htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
+ users = []
+ htdigest.each{|user, pass| users << user }
+ assert_equal %w(foo), users
+
+ auth = WEBrick::HTTPAuth::DigestAuth.new(
+ :Realm => realm, :UserDB => htdigest,
+ :Algorithm => 'MD5',
+ :Logger => server.logger,
+ :Qop => %w(auth-int),
+ )
+ server.mount_proc(path){|req, res|
+ auth.authenticate(req, res)
+ res.body = "bbb"
+ }
+ Net::HTTP.start(addr, port) do |http|
+ post = Net::HTTP::Post.new(path)
+ params = {}
+ data = 'hello=world'
+ body = StringIO.new(data)
+ post.content_length = data.bytesize
+ post['Content-Type'] = 'application/x-www-form-urlencoded'
+ post.body_stream = body
+
+ http.request(post) do |res|
+ assert_equal('401', res.code, log.call)
+ res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token|
+ params[key.downcase] = token || quoted.delete('\\')
+ end
+ params['uri'] = "http://#{addr}:#{port}#{path}"
+ end
+
+ body.rewind
+ cred = credentials_for_request('foo', 'Hunter3', params, body)
+ post['Authorization'] = cred
+ post.body_stream = body
+ http.request(post){|res|
+ assert_equal('401', res.code, log.call)
+ assert_not_equal("bbb", res.body, log.call)
+ }
+
+ body.rewind
+ cred = credentials_for_request('foo', 'Hunter2', params, body)
+ post['Authorization'] = cred
+ post.body_stream = body
+ http.request(post){|res| assert_equal("bbb", res.body, log.call)}
+ end
+ }
+ }
+ end
+
+ def test_digest_auth_invalid
+ digest_auth = WEBrick::HTTPAuth::DigestAuth.new(Realm: 'realm', UserDB: '')
+
+ def digest_auth.error(fmt, *)
+ end
+
+ def digest_auth.try_bad_request(len)
+ request = {"Authorization" => %[Digest a="#{'\b'*len}]}
+ authenticate request, nil
+ end
+
+ bad_request = WEBrick::HTTPStatus::BadRequest
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ assert_raise(bad_request) {digest_auth.try_bad_request(10)}
+ limit = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0)
+ [20, 50, 100, 200].each do |len|
+ assert_raise(bad_request) do
+ Timeout.timeout(len*limit) {digest_auth.try_bad_request(len)}
+ end
+ end
+ end
+
+ private
+ def credentials_for_request(user, password, params, body = nil)
+ cnonce = "hoge"
+ nonce_count = 1
+ ha1 = "#{user}:#{params['realm']}:#{password}"
+ if body
+ dig = Digest::MD5.new
+ while buf = body.read(16384)
+ dig.update(buf)
+ end
+ body.rewind
+ ha2 = "POST:#{params['uri']}:#{dig.hexdigest}"
+ else
+ ha2 = "GET:#{params['uri']}"
+ end
+
+ request_digest =
+ "#{Digest::MD5.hexdigest(ha1)}:" \
+ "#{params['nonce']}:#{'%08x' % nonce_count}:#{cnonce}:#{params['qop']}:" \
+ "#{Digest::MD5.hexdigest(ha2)}"
+ "Digest username=\"#{user}\"" \
+ ", realm=\"#{params['realm']}\"" \
+ ", nonce=\"#{params['nonce']}\"" \
+ ", uri=\"#{params['uri']}\"" \
+ ", qop=#{params['qop']}" \
+ ", nc=#{'%08x' % nonce_count}" \
+ ", cnonce=\"#{cnonce}\"" \
+ ", response=\"#{Digest::MD5.hexdigest(request_digest)}\"" \
+ ", opaque=\"#{params['opaque']}\"" \
+ ", algorithm=#{params['algorithm']}"
+ end
+end
diff --git a/tool/test/webrick/test_httpproxy.rb b/tool/test/webrick/test_httpproxy.rb
new file mode 100644
index 0000000000..66dae6f6f6
--- /dev/null
+++ b/tool/test/webrick/test_httpproxy.rb
@@ -0,0 +1,467 @@
+# frozen_string_literal: false
+require "test/unit"
+require "net/http"
+require "webrick"
+require "webrick/httpproxy"
+begin
+ require "webrick/ssl"
+ require "net/https"
+rescue LoadError
+ # test_connect will be skipped
+end
+require File.expand_path("utils.rb", File.dirname(__FILE__))
+
+class TestWEBrickHTTPProxy < Test::Unit::TestCase
+ def teardown
+ WEBrick::Utils::TimeoutHandler.terminate
+ super
+ end
+
+ def test_fake_proxy
+ assert_nil(WEBrick::FakeProxyURI.scheme)
+ assert_nil(WEBrick::FakeProxyURI.host)
+ assert_nil(WEBrick::FakeProxyURI.port)
+ assert_nil(WEBrick::FakeProxyURI.path)
+ assert_nil(WEBrick::FakeProxyURI.userinfo)
+ assert_raise(NoMethodError){ WEBrick::FakeProxyURI.foo }
+ end
+
+ def test_proxy
+ # Testing GET or POST to the proxy server
+ # Note that the proxy server works as the origin server.
+ # +------+
+ # V |
+ # client -------> proxy ---+
+ # GET / POST GET / POST
+ #
+ proxy_handler_called = request_handler_called = 0
+ config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 },
+ :RequestCallback => Proc.new{|req, res| request_handler_called += 1 }
+ }
+ TestWEBrick.start_httpproxy(config){|server, addr, port, log|
+ server.mount_proc("/"){|req, res|
+ res.body = "#{req.request_method} #{req.path} #{req.body}"
+ }
+ http = Net::HTTP.new(addr, port, addr, port)
+
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call)
+ assert_equal("GET / ", res.body, log.call)
+ }
+ assert_equal(1, proxy_handler_called, log.call)
+ assert_equal(2, request_handler_called, log.call)
+
+ req = Net::HTTP::Head.new("/")
+ http.request(req){|res|
+ assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call)
+ assert_nil(res.body, log.call)
+ }
+ assert_equal(2, proxy_handler_called, log.call)
+ assert_equal(4, request_handler_called, log.call)
+
+ req = Net::HTTP::Post.new("/")
+ req.body = "post-data"
+ req.content_type = "application/x-www-form-urlencoded"
+ http.request(req){|res|
+ assert_equal("1.1 localhost.localdomain:#{port}", res["via"], log.call)
+ assert_equal("POST / post-data", res.body, log.call)
+ }
+ assert_equal(3, proxy_handler_called, log.call)
+ assert_equal(6, request_handler_called, log.call)
+ }
+ end
+
+ def test_no_proxy
+ # Testing GET or POST to the proxy server without proxy request.
+ #
+ # client -------> proxy
+ # GET / POST
+ #
+ proxy_handler_called = request_handler_called = 0
+ config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 },
+ :RequestCallback => Proc.new{|req, res| request_handler_called += 1 }
+ }
+ TestWEBrick.start_httpproxy(config){|server, addr, port, log|
+ server.mount_proc("/"){|req, res|
+ res.body = "#{req.request_method} #{req.path} #{req.body}"
+ }
+ http = Net::HTTP.new(addr, port)
+
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_nil(res["via"], log.call)
+ assert_equal("GET / ", res.body, log.call)
+ }
+ assert_equal(0, proxy_handler_called, log.call)
+ assert_equal(1, request_handler_called, log.call)
+
+ req = Net::HTTP::Head.new("/")
+ http.request(req){|res|
+ assert_nil(res["via"], log.call)
+ assert_nil(res.body, log.call)
+ }
+ assert_equal(0, proxy_handler_called, log.call)
+ assert_equal(2, request_handler_called, log.call)
+
+ req = Net::HTTP::Post.new("/")
+ req.content_type = "application/x-www-form-urlencoded"
+ req.body = "post-data"
+ http.request(req){|res|
+ assert_nil(res["via"], log.call)
+ assert_equal("POST / post-data", res.body, log.call)
+ }
+ assert_equal(0, proxy_handler_called, log.call)
+ assert_equal(3, request_handler_called, log.call)
+ }
+ end
+
+ def test_big_bodies
+ require 'digest/md5'
+ rand_str = File.read(__FILE__)
+ rand_str.freeze
+ nr = 1024 ** 2 / rand_str.size # bigger works, too
+ exp = Digest::MD5.new
+ nr.times { exp.update(rand_str) }
+ exp = exp.hexdigest
+ TestWEBrick.start_httpserver do |o_server, o_addr, o_port, o_log|
+ o_server.mount_proc('/') do |req, res|
+ case req.request_method
+ when 'GET'
+ res['content-type'] = 'application/octet-stream'
+ if req.path == '/length'
+ res['content-length'] = (nr * rand_str.size).to_s
+ else
+ res.chunked = true
+ end
+ res.body = ->(socket) { nr.times { socket.write(rand_str) } }
+ when 'POST'
+ dig = Digest::MD5.new
+ req.body { |buf| dig.update(buf); buf.clear }
+ res['content-type'] = 'text/plain'
+ res['content-length'] = '32'
+ res.body = dig.hexdigest
+ end
+ end
+
+ http = Net::HTTP.new(o_addr, o_port)
+ IO.pipe do |rd, wr|
+ headers = {
+ 'Content-Type' => 'application/octet-stream',
+ 'Transfer-Encoding' => 'chunked',
+ }
+ post = Net::HTTP::Post.new('/', headers)
+ th = Thread.new { nr.times { wr.write(rand_str) }; wr.close }
+ post.body_stream = rd
+ http.request(post) do |res|
+ assert_equal 'text/plain', res['content-type']
+ assert_equal 32, res.content_length
+ assert_equal exp, res.body
+ end
+ assert_nil th.value
+ end
+
+ TestWEBrick.start_httpproxy do |p_server, p_addr, p_port, p_log|
+ http = Net::HTTP.new(o_addr, o_port, p_addr, p_port)
+ http.request_get('/length') do |res|
+ assert_equal(nr * rand_str.size, res.content_length)
+ dig = Digest::MD5.new
+ res.read_body { |buf| dig.update(buf); buf.clear }
+ assert_equal exp, dig.hexdigest
+ end
+ http.request_get('/') do |res|
+ assert_predicate res, :chunked?
+ dig = Digest::MD5.new
+ res.read_body { |buf| dig.update(buf); buf.clear }
+ assert_equal exp, dig.hexdigest
+ end
+
+ IO.pipe do |rd, wr|
+ headers = {
+ 'Content-Type' => 'application/octet-stream',
+ 'Content-Length' => (nr * rand_str.size).to_s,
+ }
+ post = Net::HTTP::Post.new('/', headers)
+ th = Thread.new { nr.times { wr.write(rand_str) }; wr.close }
+ post.body_stream = rd
+ http.request(post) do |res|
+ assert_equal 'text/plain', res['content-type']
+ assert_equal 32, res.content_length
+ assert_equal exp, res.body
+ end
+ assert_nil th.value
+ end
+
+ IO.pipe do |rd, wr|
+ headers = {
+ 'Content-Type' => 'application/octet-stream',
+ 'Transfer-Encoding' => 'chunked',
+ }
+ post = Net::HTTP::Post.new('/', headers)
+ th = Thread.new { nr.times { wr.write(rand_str) }; wr.close }
+ post.body_stream = rd
+ http.request(post) do |res|
+ assert_equal 'text/plain', res['content-type']
+ assert_equal 32, res.content_length
+ assert_equal exp, res.body
+ end
+ assert_nil th.value
+ end
+ end
+ end
+ end if RUBY_VERSION >= '2.5'
+
+ def test_http10_proxy_chunked
+ # Testing HTTP/1.0 client request and HTTP/1.1 chunked response
+ # from origin server.
+ # +------+
+ # V |
+ # client -------> proxy ---+
+ # GET GET
+ # HTTP/1.0 HTTP/1.1
+ # non-chunked chunked
+ #
+ proxy_handler_called = request_handler_called = 0
+ config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1 },
+ :RequestCallback => Proc.new{|req, res| request_handler_called += 1 }
+ }
+ log_tester = lambda {|log, access_log|
+ log.reject! {|str|
+ %r{WARN chunked is set for an HTTP/1\.0 request\. \(ignored\)} =~ str
+ }
+ assert_equal([], log)
+ }
+ TestWEBrick.start_httpproxy(config, log_tester){|server, addr, port, log|
+ body = nil
+ server.mount_proc("/"){|req, res|
+ body = "#{req.request_method} #{req.path} #{req.body}"
+ res.chunked = true
+ res.body = -> (socket) { body.each_char {|c| socket.write c } }
+ }
+
+ # Don't use Net::HTTP because it uses HTTP/1.1.
+ TCPSocket.open(addr, port) {|s|
+ s.write "GET / HTTP/1.0\r\nHost: localhost.localdomain\r\n\r\n"
+ response = s.read
+ assert_equal(body, response[/.*\z/])
+ }
+ }
+ end
+
+ def make_certificate(key, cn)
+ subject = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=#{cn}")
+ exts = [
+ ["keyUsage", "keyEncipherment,digitalSignature", true],
+ ]
+ cert = OpenSSL::X509::Certificate.new
+ cert.version = 2
+ cert.serial = 1
+ cert.subject = subject
+ cert.issuer = subject
+ cert.public_key = key
+ cert.not_before = Time.now - 3600
+ cert.not_after = Time.now + 3600
+ ef = OpenSSL::X509::ExtensionFactory.new(cert, cert)
+ exts.each {|args| cert.add_extension(ef.create_extension(*args)) }
+ cert.sign(key, "sha256")
+ return cert
+ end if defined?(OpenSSL::SSL)
+
+ def test_connect
+ # Testing CONNECT to proxy server
+ #
+ # client -----------> proxy -----------> https
+ # 1. CONNECT establish TCP
+ # 2. ---- establish SSL session --->
+ # 3. ------- GET or POST ---------->
+ #
+ key = TEST_KEY_RSA2048
+ cert = make_certificate(key, "127.0.0.1")
+ s_config = {
+ :SSLEnable =>true,
+ :ServerName => "localhost",
+ :SSLCertificate => cert,
+ :SSLPrivateKey => key,
+ }
+ config = {
+ :ServerName => "localhost.localdomain",
+ :RequestCallback => Proc.new{|req, res|
+ assert_equal("CONNECT", req.request_method)
+ },
+ }
+ TestWEBrick.start_httpserver(s_config){|s_server, s_addr, s_port, s_log|
+ s_server.mount_proc("/"){|req, res|
+ res.body = "SSL #{req.request_method} #{req.path} #{req.body}"
+ }
+ TestWEBrick.start_httpproxy(config){|server, addr, port, log|
+ http = Net::HTTP.new("127.0.0.1", s_port, addr, port)
+ http.use_ssl = true
+ http.verify_callback = Proc.new do |preverify_ok, store_ctx|
+ store_ctx.current_cert.to_der == cert.to_der
+ end
+
+ req = Net::HTTP::Get.new("/")
+ req["Content-Type"] = "application/x-www-form-urlencoded"
+ http.request(req){|res|
+ assert_equal("SSL GET / ", res.body, s_log.call + log.call)
+ }
+
+ req = Net::HTTP::Post.new("/")
+ req["Content-Type"] = "application/x-www-form-urlencoded"
+ req.body = "post-data"
+ http.request(req){|res|
+ assert_equal("SSL POST / post-data", res.body, s_log.call + log.call)
+ }
+ }
+ }
+ end if defined?(OpenSSL::SSL)
+
+ def test_upstream_proxy
+ return if /mswin/ =~ RUBY_PLATFORM && ENV.key?('GITHUB_ACTIONS') # not working from the beginning
+ # Testing GET or POST through the upstream proxy server
+ # Note that the upstream proxy server works as the origin server.
+ # +------+
+ # V |
+ # client -------> proxy -------> proxy ---+
+ # GET / POST GET / POST GET / POST
+ #
+ up_proxy_handler_called = up_request_handler_called = 0
+ proxy_handler_called = request_handler_called = 0
+ up_config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyContentHandler => Proc.new{|req, res| up_proxy_handler_called += 1},
+ :RequestCallback => Proc.new{|req, res| up_request_handler_called += 1}
+ }
+ TestWEBrick.start_httpproxy(up_config){|up_server, up_addr, up_port, up_log|
+ up_server.mount_proc("/"){|req, res|
+ res.body = "#{req.request_method} #{req.path} #{req.body}"
+ }
+ config = {
+ :ServerName => "localhost.localdomain",
+ :ProxyURI => URI.parse("http://localhost:#{up_port}"),
+ :ProxyContentHandler => Proc.new{|req, res| proxy_handler_called += 1},
+ :RequestCallback => Proc.new{|req, res| request_handler_called += 1},
+ }
+ TestWEBrick.start_httpproxy(config){|server, addr, port, log|
+ http = Net::HTTP.new(up_addr, up_port, addr, port)
+
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ skip res.message unless res.code == '200'
+ via = res["via"].split(/,\s+/)
+ assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call)
+ assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call)
+ assert_equal("GET / ", res.body)
+ }
+ assert_equal(1, up_proxy_handler_called, up_log.call + log.call)
+ assert_equal(2, up_request_handler_called, up_log.call + log.call)
+ assert_equal(1, proxy_handler_called, up_log.call + log.call)
+ assert_equal(1, request_handler_called, up_log.call + log.call)
+
+ req = Net::HTTP::Head.new("/")
+ http.request(req){|res|
+ via = res["via"].split(/,\s+/)
+ assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call)
+ assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call)
+ assert_nil(res.body, up_log.call + log.call)
+ }
+ assert_equal(2, up_proxy_handler_called, up_log.call + log.call)
+ assert_equal(4, up_request_handler_called, up_log.call + log.call)
+ assert_equal(2, proxy_handler_called, up_log.call + log.call)
+ assert_equal(2, request_handler_called, up_log.call + log.call)
+
+ req = Net::HTTP::Post.new("/")
+ req.body = "post-data"
+ req.content_type = "application/x-www-form-urlencoded"
+ http.request(req){|res|
+ via = res["via"].split(/,\s+/)
+ assert(via.include?("1.1 localhost.localdomain:#{up_port}"), up_log.call + log.call)
+ assert(via.include?("1.1 localhost.localdomain:#{port}"), up_log.call + log.call)
+ assert_equal("POST / post-data", res.body, up_log.call + log.call)
+ }
+ assert_equal(3, up_proxy_handler_called, up_log.call + log.call)
+ assert_equal(6, up_request_handler_called, up_log.call + log.call)
+ assert_equal(3, proxy_handler_called, up_log.call + log.call)
+ assert_equal(3, request_handler_called, up_log.call + log.call)
+
+ if defined?(OpenSSL::SSL)
+ # Testing CONNECT to the upstream proxy server
+ #
+ # client -------> proxy -------> proxy -------> https
+ # 1. CONNECT CONNECT establish TCP
+ # 2. -------- establish SSL session ------>
+ # 3. ---------- GET or POST -------------->
+ #
+ key = TEST_KEY_RSA2048
+ cert = make_certificate(key, "127.0.0.1")
+ s_config = {
+ :SSLEnable =>true,
+ :ServerName => "localhost",
+ :SSLCertificate => cert,
+ :SSLPrivateKey => key,
+ }
+ TestWEBrick.start_httpserver(s_config){|s_server, s_addr, s_port, s_log|
+ s_server.mount_proc("/"){|req2, res|
+ res.body = "SSL #{req2.request_method} #{req2.path} #{req2.body}"
+ }
+ http = Net::HTTP.new("127.0.0.1", s_port, addr, port, up_log.call + log.call + s_log.call)
+ http.use_ssl = true
+ http.verify_callback = Proc.new do |preverify_ok, store_ctx|
+ store_ctx.current_cert.to_der == cert.to_der
+ end
+
+ req2 = Net::HTTP::Get.new("/")
+ http.request(req2){|res|
+ assert_equal("SSL GET / ", res.body, up_log.call + log.call + s_log.call)
+ }
+
+ req2 = Net::HTTP::Post.new("/")
+ req2.body = "post-data"
+ req2.content_type = "application/x-www-form-urlencoded"
+ http.request(req2){|res|
+ assert_equal("SSL POST / post-data", res.body, up_log.call + log.call + s_log.call)
+ }
+ }
+ end
+ }
+ }
+ end
+
+ if defined?(OpenSSL::SSL)
+ TEST_KEY_RSA2048 = OpenSSL::PKey.read <<-_end_of_pem_
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAuV9ht9J7k4NBs38jOXvvTKY9gW8nLICSno5EETR1cuF7i4pN
+s9I1QJGAFAX0BEO4KbzXmuOvfCpD3CU+Slp1enenfzq/t/e/1IRW0wkJUJUFQign
+4CtrkJL+P07yx18UjyPlBXb81ApEmAB5mrJVSrWmqbjs07JbuS4QQGGXLc+Su96D
+kYKmSNVjBiLxVVSpyZfAY3hD37d60uG+X8xdW5v68JkRFIhdGlb6JL8fllf/A/bl
+NwdJOhVr9mESHhwGjwfSeTDPfd8ZLE027E5lyAVX9KZYcU00mOX+fdxOSnGqS/8J
+DRh0EPHDL15RcJjV2J6vZjPb0rOYGDoMcH+94wIDAQABAoIBAAzsamqfYQAqwXTb
+I0CJtGg6msUgU7HVkOM+9d3hM2L791oGHV6xBAdpXW2H8LgvZHJ8eOeSghR8+dgq
+PIqAffo4x1Oma+FOg3A0fb0evyiACyrOk+EcBdbBeLo/LcvahBtqnDfiUMQTpy6V
+seSoFCwuN91TSCeGIsDpRjbG1vxZgtx+uI+oH5+ytqJOmfCksRDCkMglGkzyfcl0
+Xc5CUhIJ0my53xijEUQl19rtWdMnNnnkdbG8PT3LZlOta5Do86BElzUYka0C6dUc
+VsBDQ0Nup0P6rEQgy7tephHoRlUGTYamsajGJaAo1F3IQVIrRSuagi7+YpSpCqsW
+wORqorkCgYEA7RdX6MDVrbw7LePnhyuaqTiMK+055/R1TqhB1JvvxJ1CXk2rDL6G
+0TLHQ7oGofd5LYiemg4ZVtWdJe43BPZlVgT6lvL/iGo8JnrncB9Da6L7nrq/+Rvj
+XGjf1qODCK+LmreZWEsaLPURIoR/Ewwxb9J2zd0CaMjeTwafJo1CZvcCgYEAyCgb
+aqoWvUecX8VvARfuA593Lsi50t4MEArnOXXcd1RnXoZWhbx5rgO8/ATKfXr0BK/n
+h2GF9PfKzHFm/4V6e82OL7gu/kLy2u9bXN74vOvWFL5NOrOKPM7Kg+9I131kNYOw
+Ivnr/VtHE5s0dY7JChYWE1F3vArrOw3T00a4CXUCgYEA0SqY+dS2LvIzW4cHCe9k
+IQqsT0yYm5TFsUEr4sA3xcPfe4cV8sZb9k/QEGYb1+SWWZ+AHPV3UW5fl8kTbSNb
+v4ng8i8rVVQ0ANbJO9e5CUrepein2MPL0AkOATR8M7t7dGGpvYV0cFk8ZrFx0oId
+U0PgYDotF/iueBWlbsOM430CgYEAqYI95dFyPI5/AiSkY5queeb8+mQH62sdcCCr
+vd/w/CZA/K5sbAo4SoTj8dLk4evU6HtIa0DOP63y071eaxvRpTNqLUOgmLh+D6gS
+Cc7TfLuFrD+WDBatBd5jZ+SoHccVrLR/4L8jeodo5FPW05A+9gnKXEXsTxY4LOUC
+9bS4e1kCgYAqVXZh63JsMwoaxCYmQ66eJojKa47VNrOeIZDZvd2BPVf30glBOT41
+gBoDG3WMPZoQj9pb7uMcrnvs4APj2FIhMU8U15LcPAj59cD6S6rWnAxO8NFK7HQG
+4Jxg3JNNf8ErQoCHb1B3oVdXJkmbJkARoDpBKmTCgKtP8ADYLmVPQw==
+-----END RSA PRIVATE KEY-----
+ _end_of_pem_
+ end
+end
diff --git a/tool/test/webrick/test_httprequest.rb b/tool/test/webrick/test_httprequest.rb
new file mode 100644
index 0000000000..759ccbdada
--- /dev/null
+++ b/tool/test/webrick/test_httprequest.rb
@@ -0,0 +1,488 @@
+# frozen_string_literal: false
+require "webrick"
+require "stringio"
+require "test/unit"
+
+class TestWEBrickHTTPRequest < Test::Unit::TestCase
+ def teardown
+ WEBrick::Utils::TimeoutHandler.terminate
+ super
+ end
+
+ def test_simple_request
+ msg = <<-_end_of_message_
+GET /
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert(req.meta_vars) # fails if @header was not initialized and iteration is attempted on the nil reference
+ end
+
+ def test_parse_09
+ msg = <<-_end_of_message_
+ GET /
+ foobar # HTTP/0.9 request don't have header nor entity body.
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal("GET", req.request_method)
+ assert_equal("/", req.unparsed_uri)
+ assert_equal(WEBrick::HTTPVersion.new("0.9"), req.http_version)
+ assert_equal(WEBrick::Config::HTTP[:ServerName], req.host)
+ assert_equal(80, req.port)
+ assert_equal(false, req.keep_alive?)
+ assert_equal(nil, req.body)
+ assert(req.query.empty?)
+ end
+
+ def test_parse_10
+ msg = <<-_end_of_message_
+ GET / HTTP/1.0
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal("GET", req.request_method)
+ assert_equal("/", req.unparsed_uri)
+ assert_equal(WEBrick::HTTPVersion.new("1.0"), req.http_version)
+ assert_equal(WEBrick::Config::HTTP[:ServerName], req.host)
+ assert_equal(80, req.port)
+ assert_equal(false, req.keep_alive?)
+ assert_equal(nil, req.body)
+ assert(req.query.empty?)
+ end
+
+ def test_parse_11
+ msg = <<-_end_of_message_
+ GET /path HTTP/1.1
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal("GET", req.request_method)
+ assert_equal("/path", req.unparsed_uri)
+ assert_equal("", req.script_name)
+ assert_equal("/path", req.path_info)
+ assert_equal(WEBrick::HTTPVersion.new("1.1"), req.http_version)
+ assert_equal(WEBrick::Config::HTTP[:ServerName], req.host)
+ assert_equal(80, req.port)
+ assert_equal(true, req.keep_alive?)
+ assert_equal(nil, req.body)
+ assert(req.query.empty?)
+ end
+
+ def test_request_uri_too_large
+ msg = <<-_end_of_message_
+ GET /#{"a"*2084} HTTP/1.1
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ assert_raise(WEBrick::HTTPStatus::RequestURITooLarge){
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ }
+ end
+
+ def test_parse_headers
+ msg = <<-_end_of_message_
+ GET /path HTTP/1.1
+ Host: test.ruby-lang.org:8080
+ Connection: close
+ Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1,
+ text/html;level=2;q=0.4, */*;q=0.5
+ Accept-Encoding: compress;q=0.5
+ Accept-Encoding: gzip;q=1.0, identity; q=0.4, *;q=0
+ Accept-Language: en;q=0.5, *; q=0
+ Accept-Language: ja
+ Content-Type: text/plain
+ Content-Length: 7
+ X-Empty-Header:
+
+ foobar
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal(
+ URI.parse("http://test.ruby-lang.org:8080/path"), req.request_uri)
+ assert_equal("test.ruby-lang.org", req.host)
+ assert_equal(8080, req.port)
+ assert_equal(false, req.keep_alive?)
+ assert_equal(
+ %w(text/html;level=1 text/html */* text/html;level=2 text/*),
+ req.accept)
+ assert_equal(%w(gzip compress identity *), req.accept_encoding)
+ assert_equal(%w(ja en *), req.accept_language)
+ assert_equal(7, req.content_length)
+ assert_equal("text/plain", req.content_type)
+ assert_equal("foobar\n", req.body)
+ assert_equal("", req["x-empty-header"])
+ assert_equal(nil, req["x-no-header"])
+ assert(req.query.empty?)
+ end
+
+ def test_parse_header2()
+ msg = <<-_end_of_message_
+ POST /foo/bar/../baz?q=a HTTP/1.0
+ Content-Length: 9
+ User-Agent:
+ FOO BAR
+ BAZ
+
+ hogehoge
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal("POST", req.request_method)
+ assert_equal("/foo/baz", req.path)
+ assert_equal("", req.script_name)
+ assert_equal("/foo/baz", req.path_info)
+ assert_equal("9", req['content-length'])
+ assert_equal("FOO BAR BAZ", req['user-agent'])
+ assert_equal("hogehoge\n", req.body)
+ end
+
+ def test_parse_headers3
+ msg = <<-_end_of_message_
+ GET /path HTTP/1.1
+ Host: test.ruby-lang.org
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal(URI.parse("http://test.ruby-lang.org/path"), req.request_uri)
+ assert_equal("test.ruby-lang.org", req.host)
+ assert_equal(80, req.port)
+
+ msg = <<-_end_of_message_
+ GET /path HTTP/1.1
+ Host: 192.168.1.1
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal(URI.parse("http://192.168.1.1/path"), req.request_uri)
+ assert_equal("192.168.1.1", req.host)
+ assert_equal(80, req.port)
+
+ msg = <<-_end_of_message_
+ GET /path HTTP/1.1
+ Host: [fe80::208:dff:feef:98c7]
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]/path"),
+ req.request_uri)
+ assert_equal("[fe80::208:dff:feef:98c7]", req.host)
+ assert_equal(80, req.port)
+
+ msg = <<-_end_of_message_
+ GET /path HTTP/1.1
+ Host: 192.168.1.1:8080
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal(URI.parse("http://192.168.1.1:8080/path"), req.request_uri)
+ assert_equal("192.168.1.1", req.host)
+ assert_equal(8080, req.port)
+
+ msg = <<-_end_of_message_
+ GET /path HTTP/1.1
+ Host: [fe80::208:dff:feef:98c7]:8080
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ assert_equal(URI.parse("http://[fe80::208:dff:feef:98c7]:8080/path"),
+ req.request_uri)
+ assert_equal("[fe80::208:dff:feef:98c7]", req.host)
+ assert_equal(8080, req.port)
+ end
+
+ def test_parse_get_params
+ param = "foo=1;foo=2;foo=3;bar=x"
+ msg = <<-_end_of_message_
+ GET /path?#{param} HTTP/1.1
+ Host: test.ruby-lang.org:8080
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ query = req.query
+ assert_equal("1", query["foo"])
+ assert_equal(["1", "2", "3"], query["foo"].to_ary)
+ assert_equal(["1", "2", "3"], query["foo"].list)
+ assert_equal("x", query["bar"])
+ assert_equal(["x"], query["bar"].list)
+ end
+
+ def test_parse_post_params
+ param = "foo=1;foo=2;foo=3;bar=x"
+ msg = <<-_end_of_message_
+ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1
+ Host: test.ruby-lang.org:8080
+ Content-Length: #{param.size}
+ Content-Type: application/x-www-form-urlencoded
+
+ #{param}
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ query = req.query
+ assert_equal("1", query["foo"])
+ assert_equal(["1", "2", "3"], query["foo"].to_ary)
+ assert_equal(["1", "2", "3"], query["foo"].list)
+ assert_equal("x", query["bar"])
+ assert_equal(["x"], query["bar"].list)
+ end
+
+ def test_chunked
+ crlf = "\x0d\x0a"
+ expect = File.binread(__FILE__).freeze
+ msg = <<-_end_of_message_
+ POST /path HTTP/1.1
+ Host: test.ruby-lang.org:8080
+ Transfer-Encoding: chunked
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ open(__FILE__){|io|
+ while chunk = io.read(100)
+ msg << chunk.size.to_s(16) << crlf
+ msg << chunk << crlf
+ end
+ }
+ msg << "0" << crlf
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert_equal(expect, req.body)
+
+ # chunked req.body_reader
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ dst = StringIO.new
+ IO.copy_stream(req.body_reader, dst)
+ assert_equal(expect, dst.string)
+ end
+
+ def test_forwarded
+ msg = <<-_end_of_message_
+ GET /foo HTTP/1.1
+ Host: localhost:10080
+ User-Agent: w3m/0.5.2
+ X-Forwarded-For: 123.123.123.123
+ X-Forwarded-Host: forward.example.com
+ X-Forwarded-Server: server.example.com
+ Connection: Keep-Alive
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert_equal("server.example.com", req.server_name)
+ assert_equal("http://forward.example.com/foo", req.request_uri.to_s)
+ assert_equal("forward.example.com", req.host)
+ assert_equal(80, req.port)
+ assert_equal("123.123.123.123", req.remote_ip)
+ assert(!req.ssl?)
+
+ msg = <<-_end_of_message_
+ GET /foo HTTP/1.1
+ Host: localhost:10080
+ User-Agent: w3m/0.5.2
+ X-Forwarded-For: 192.168.1.10, 172.16.1.1, 123.123.123.123
+ X-Forwarded-Host: forward.example.com:8080
+ X-Forwarded-Server: server.example.com
+ Connection: Keep-Alive
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert_equal("server.example.com", req.server_name)
+ assert_equal("http://forward.example.com:8080/foo", req.request_uri.to_s)
+ assert_equal("forward.example.com", req.host)
+ assert_equal(8080, req.port)
+ assert_equal("123.123.123.123", req.remote_ip)
+ assert(!req.ssl?)
+
+ msg = <<-_end_of_message_
+ GET /foo HTTP/1.1
+ Host: localhost:10080
+ Client-IP: 234.234.234.234
+ X-Forwarded-Proto: https, http
+ X-Forwarded-For: 192.168.1.10, 10.0.0.1, 123.123.123.123
+ X-Forwarded-Host: forward.example.com
+ X-Forwarded-Server: server.example.com
+ X-Requested-With: XMLHttpRequest
+ Connection: Keep-Alive
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert_equal("server.example.com", req.server_name)
+ assert_equal("https://forward.example.com/foo", req.request_uri.to_s)
+ assert_equal("forward.example.com", req.host)
+ assert_equal(443, req.port)
+ assert_equal("234.234.234.234", req.remote_ip)
+ assert(req.ssl?)
+
+ msg = <<-_end_of_message_
+ GET /foo HTTP/1.1
+ Host: localhost:10080
+ Client-IP: 234.234.234.234
+ X-Forwarded-Proto: https
+ X-Forwarded-For: 192.168.1.10
+ X-Forwarded-Host: forward1.example.com:1234, forward2.example.com:5678
+ X-Forwarded-Server: server1.example.com, server2.example.com
+ X-Requested-With: XMLHttpRequest
+ Connection: Keep-Alive
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert_equal("server1.example.com", req.server_name)
+ assert_equal("https://forward1.example.com:1234/foo", req.request_uri.to_s)
+ assert_equal("forward1.example.com", req.host)
+ assert_equal(1234, req.port)
+ assert_equal("234.234.234.234", req.remote_ip)
+ assert(req.ssl?)
+
+ msg = <<-_end_of_message_
+ GET /foo HTTP/1.1
+ Host: localhost:10080
+ Client-IP: 234.234.234.234
+ X-Forwarded-Proto: https
+ X-Forwarded-For: 192.168.1.10
+ X-Forwarded-Host: [fd20:8b1e:b255:8154:250:56ff:fea8:4d84], forward2.example.com:5678
+ X-Forwarded-Server: server1.example.com, server2.example.com
+ X-Requested-With: XMLHttpRequest
+ Connection: Keep-Alive
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert_equal("server1.example.com", req.server_name)
+ assert_equal("https://[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]/foo", req.request_uri.to_s)
+ assert_equal("[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]", req.host)
+ assert_equal(443, req.port)
+ assert_equal("234.234.234.234", req.remote_ip)
+ assert(req.ssl?)
+
+ msg = <<-_end_of_message_
+ GET /foo HTTP/1.1
+ Host: localhost:10080
+ Client-IP: 234.234.234.234
+ X-Forwarded-Proto: https
+ X-Forwarded-For: 192.168.1.10
+ X-Forwarded-Host: [fd20:8b1e:b255:8154:250:56ff:fea8:4d84]:1234, forward2.example.com:5678
+ X-Forwarded-Server: server1.example.com, server2.example.com
+ X-Requested-With: XMLHttpRequest
+ Connection: Keep-Alive
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert_equal("server1.example.com", req.server_name)
+ assert_equal("https://[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]:1234/foo", req.request_uri.to_s)
+ assert_equal("[fd20:8b1e:b255:8154:250:56ff:fea8:4d84]", req.host)
+ assert_equal(1234, req.port)
+ assert_equal("234.234.234.234", req.remote_ip)
+ assert(req.ssl?)
+ end
+
+ def test_continue_sent
+ msg = <<-_end_of_message_
+ POST /path HTTP/1.1
+ Expect: 100-continue
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert req['expect']
+ l = msg.size
+ req.continue
+ assert_not_equal l, msg.size
+ assert_match(/HTTP\/1.1 100 continue\r\n\r\n\z/, msg)
+ assert !req['expect']
+ end
+
+ def test_continue_not_sent
+ msg = <<-_end_of_message_
+ POST /path HTTP/1.1
+
+ _end_of_message_
+ msg.gsub!(/^ {6}/, "")
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg))
+ assert !req['expect']
+ l = msg.size
+ req.continue
+ assert_equal l, msg.size
+ end
+
+ def test_empty_post
+ msg = <<-_end_of_message_
+ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1
+ Host: test.ruby-lang.org:8080
+ Content-Type: application/x-www-form-urlencoded
+
+ _end_of_message_
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ req.body
+ end
+
+ def test_bad_messages
+ param = "foo=1;foo=2;foo=3;bar=x"
+ msg = <<-_end_of_message_
+ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1
+ Host: test.ruby-lang.org:8080
+ Content-Type: application/x-www-form-urlencoded
+
+ #{param}
+ _end_of_message_
+ assert_raise(WEBrick::HTTPStatus::LengthRequired){
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ req.body
+ }
+
+ msg = <<-_end_of_message_
+ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1
+ Host: test.ruby-lang.org:8080
+ Content-Length: 100000
+
+ body is too short.
+ _end_of_message_
+ assert_raise(WEBrick::HTTPStatus::BadRequest){
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ req.body
+ }
+
+ msg = <<-_end_of_message_
+ POST /path?foo=x;foo=y;foo=z;bar=1 HTTP/1.1
+ Host: test.ruby-lang.org:8080
+ Transfer-Encoding: foobar
+
+ body is too short.
+ _end_of_message_
+ assert_raise(WEBrick::HTTPStatus::NotImplemented){
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(msg.gsub(/^ {6}/, "")))
+ req.body
+ }
+ end
+
+ def test_eof_raised_when_line_is_nil
+ assert_raise(WEBrick::HTTPStatus::EOFError) {
+ req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
+ req.parse(StringIO.new(""))
+ }
+ end
+end
diff --git a/tool/test/webrick/test_httpresponse.rb b/tool/test/webrick/test_httpresponse.rb
new file mode 100644
index 0000000000..4410f63e89
--- /dev/null
+++ b/tool/test/webrick/test_httpresponse.rb
@@ -0,0 +1,282 @@
+# frozen_string_literal: false
+require "webrick"
+require "test/unit"
+require "stringio"
+require "net/http"
+
+module WEBrick
+ class TestHTTPResponse < Test::Unit::TestCase
+ class FakeLogger
+ attr_reader :messages
+
+ def initialize
+ @messages = []
+ end
+
+ def warn msg
+ @messages << msg
+ end
+ end
+
+ attr_reader :config, :logger, :res
+
+ def setup
+ super
+ @logger = FakeLogger.new
+ @config = Config::HTTP
+ @config[:Logger] = logger
+ @res = HTTPResponse.new config
+ @res.keep_alive = true
+ end
+
+ def test_prevent_response_splitting_headers_crlf
+ res['X-header'] = "malicious\r\nCookie: cracked_indicator_for_test"
+ io = StringIO.new
+ res.send_response io
+ io.rewind
+ res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io))
+ assert_equal '500', res.code
+ refute_match 'cracked_indicator_for_test', io.string
+ end
+
+ def test_prevent_response_splitting_cookie_headers_crlf
+ user_input = "malicious\r\nCookie: cracked_indicator_for_test"
+ res.cookies << WEBrick::Cookie.new('author', user_input)
+ io = StringIO.new
+ res.send_response io
+ io.rewind
+ res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io))
+ assert_equal '500', res.code
+ refute_match 'cracked_indicator_for_test', io.string
+ end
+
+ def test_prevent_response_splitting_headers_cr
+ res['X-header'] = "malicious\rCookie: cracked_indicator_for_test"
+ io = StringIO.new
+ res.send_response io
+ io.rewind
+ res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io))
+ assert_equal '500', res.code
+ refute_match 'cracked_indicator_for_test', io.string
+ end
+
+ def test_prevent_response_splitting_cookie_headers_cr
+ user_input = "malicious\rCookie: cracked_indicator_for_test"
+ res.cookies << WEBrick::Cookie.new('author', user_input)
+ io = StringIO.new
+ res.send_response io
+ io.rewind
+ res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io))
+ assert_equal '500', res.code
+ refute_match 'cracked_indicator_for_test', io.string
+ end
+
+ def test_prevent_response_splitting_headers_lf
+ res['X-header'] = "malicious\nCookie: cracked_indicator_for_test"
+ io = StringIO.new
+ res.send_response io
+ io.rewind
+ res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io))
+ assert_equal '500', res.code
+ refute_match 'cracked_indicator_for_test', io.string
+ end
+
+ def test_prevent_response_splitting_cookie_headers_lf
+ user_input = "malicious\nCookie: cracked_indicator_for_test"
+ res.cookies << WEBrick::Cookie.new('author', user_input)
+ io = StringIO.new
+ res.send_response io
+ io.rewind
+ res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io))
+ assert_equal '500', res.code
+ refute_match 'cracked_indicator_for_test', io.string
+ end
+
+ def test_set_redirect_response_splitting
+ url = "malicious\r\nCookie: cracked_indicator_for_test"
+ assert_raise(URI::InvalidURIError) do
+ res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url)
+ end
+ end
+
+ def test_set_redirect_html_injection
+ url = 'http://example.com////?a</a><head></head><body><img src=1></body>'
+ assert_raise(WEBrick::HTTPStatus::MultipleChoices) do
+ res.set_redirect(WEBrick::HTTPStatus::MultipleChoices, url)
+ end
+ res.status = 300
+ io = StringIO.new
+ res.send_response(io)
+ io.rewind
+ res = Net::HTTPResponse.read_new(Net::BufferedIO.new(io))
+ assert_equal '300', res.code
+ refute_match(/<img/, io.string)
+ end
+
+ def test_304_does_not_log_warning
+ res.status = 304
+ res.setup_header
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_204_does_not_log_warning
+ res.status = 204
+ res.setup_header
+
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_1xx_does_not_log_warnings
+ res.status = 105
+ res.setup_header
+
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_200_chunked_does_not_set_content_length
+ res.chunked = false
+ res["Transfer-Encoding"] = 'chunked'
+ res.setup_header
+ assert_nil res.header.fetch('content-length', nil)
+ end
+
+ def test_send_body_io
+ IO.pipe {|body_r, body_w|
+ body_w.write 'hello'
+ body_w.close
+
+ @res.body = body_r
+
+ IO.pipe {|r, w|
+
+ @res.send_body w
+
+ w.close
+
+ assert_equal 'hello', r.read
+ }
+ }
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_send_body_string
+ @res.body = 'hello'
+
+ IO.pipe {|r, w|
+ @res.send_body w
+
+ w.close
+
+ assert_equal 'hello', r.read
+ }
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_send_body_string_io
+ @res.body = StringIO.new 'hello'
+
+ IO.pipe {|r, w|
+ @res.send_body w
+
+ w.close
+
+ assert_equal 'hello', r.read
+ }
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_send_body_io_chunked
+ @res.chunked = true
+
+ IO.pipe {|body_r, body_w|
+
+ body_w.write 'hello'
+ body_w.close
+
+ @res.body = body_r
+
+ IO.pipe {|r, w|
+ @res.send_body w
+
+ w.close
+
+ r.binmode
+ assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read
+ }
+ }
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_send_body_string_chunked
+ @res.chunked = true
+
+ @res.body = 'hello'
+
+ IO.pipe {|r, w|
+ @res.send_body w
+
+ w.close
+
+ r.binmode
+ assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read
+ }
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_send_body_string_io_chunked
+ @res.chunked = true
+
+ @res.body = StringIO.new 'hello'
+
+ IO.pipe {|r, w|
+ @res.send_body w
+
+ w.close
+
+ r.binmode
+ assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read
+ }
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_send_body_proc
+ @res.body = Proc.new { |out| out.write('hello') }
+ IO.pipe do |r, w|
+ @res.send_body(w)
+ w.close
+ r.binmode
+ assert_equal 'hello', r.read
+ end
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_send_body_proc_chunked
+ @res.body = Proc.new { |out| out.write('hello') }
+ @res.chunked = true
+ IO.pipe do |r, w|
+ @res.send_body(w)
+ w.close
+ r.binmode
+ assert_equal "5\r\nhello\r\n0\r\n\r\n", r.read
+ end
+ assert_equal 0, logger.messages.length
+ end
+
+ def test_set_error
+ status = 400
+ message = 'missing attribute'
+ @res.status = status
+ error = WEBrick::HTTPStatus[status].new(message)
+ body = @res.set_error(error)
+ assert_match(/#{@res.reason_phrase}/, body)
+ assert_match(/#{message}/, body)
+ end
+
+ def test_no_extraneous_space
+ [200, 300, 400, 500].each do |status|
+ @res.status = status
+ assert_match(/\S\r\n/, @res.status_line)
+ end
+ end
+ end
+end
diff --git a/tool/test/webrick/test_https.rb b/tool/test/webrick/test_https.rb
new file mode 100644
index 0000000000..ec0aac354a
--- /dev/null
+++ b/tool/test/webrick/test_https.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: false
+require "test/unit"
+require "net/http"
+require "webrick"
+require "webrick/https"
+require "webrick/utils"
+require_relative "utils"
+
+class TestWEBrickHTTPS < Test::Unit::TestCase
+ empty_log = Object.new
+ def empty_log.<<(str)
+ assert_equal('', str)
+ self
+ end
+ NoLog = WEBrick::Log.new(empty_log, WEBrick::BasicLog::WARN)
+
+ class HTTPSNITest < ::Net::HTTP
+ attr_accessor :sni_hostname
+
+ def ssl_socket_connect(s, timeout)
+ s.hostname = sni_hostname
+ super
+ end
+ end
+
+ def teardown
+ WEBrick::Utils::TimeoutHandler.terminate
+ super
+ end
+
+ def https_get(addr, port, hostname, path, verifyname = nil)
+ subject = nil
+ http = HTTPSNITest.new(addr, port)
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ http.verify_callback = proc { |x, store| subject = store.chain[0].subject.to_s; x }
+ http.sni_hostname = hostname
+ req = Net::HTTP::Get.new(path)
+ req["Host"] = "#{hostname}:#{port}"
+ response = http.start { http.request(req).body }
+ assert_equal("/CN=#{verifyname || hostname}", subject)
+ response
+ end
+
+ def test_sni
+ config = {
+ :ServerName => "localhost",
+ :SSLEnable => true,
+ :SSLCertName => "/CN=localhost",
+ }
+ TestWEBrick.start_httpserver(config){|server, addr, port, log|
+ server.mount_proc("/") {|req, res| res.body = "master" }
+
+ # catch stderr in create_self_signed_cert
+ stderr_buffer = StringIO.new
+ old_stderr, $stderr = $stderr, stderr_buffer
+
+ begin
+ vhost_config1 = {
+ :ServerName => "vhost1",
+ :Port => port,
+ :DoNotListen => true,
+ :Logger => NoLog,
+ :AccessLog => [],
+ :SSLEnable => true,
+ :SSLCertName => "/CN=vhost1",
+ }
+ vhost1 = WEBrick::HTTPServer.new(vhost_config1)
+ vhost1.mount_proc("/") {|req, res| res.body = "vhost1" }
+ server.virtual_host(vhost1)
+
+ vhost_config2 = {
+ :ServerName => "vhost2",
+ :ServerAlias => ["vhost2alias"],
+ :Port => port,
+ :DoNotListen => true,
+ :Logger => NoLog,
+ :AccessLog => [],
+ :SSLEnable => true,
+ :SSLCertName => "/CN=vhost2",
+ }
+ vhost2 = WEBrick::HTTPServer.new(vhost_config2)
+ vhost2.mount_proc("/") {|req, res| res.body = "vhost2" }
+ server.virtual_host(vhost2)
+ ensure
+ # restore stderr
+ $stderr = old_stderr
+ end
+
+ assert_match(/\A([.+*]+\n)+\z/, stderr_buffer.string)
+ assert_equal("master", https_get(addr, port, "localhost", "/localhost"))
+ assert_equal("master", https_get(addr, port, "unknown", "/unknown", "localhost"))
+ assert_equal("vhost1", https_get(addr, port, "vhost1", "/vhost1"))
+ assert_equal("vhost2", https_get(addr, port, "vhost2", "/vhost2"))
+ assert_equal("vhost2", https_get(addr, port, "vhost2alias", "/vhost2alias", "vhost2"))
+ }
+ end
+
+ def test_check_ssl_virtual
+ config = {
+ :ServerName => "localhost",
+ :SSLEnable => true,
+ :SSLCertName => "/CN=localhost",
+ }
+ TestWEBrick.start_httpserver(config){|server, addr, port, log|
+ assert_raise ArgumentError do
+ vhost = WEBrick::HTTPServer.new({:DoNotListen => true, :Logger => NoLog})
+ server.virtual_host(vhost)
+ end
+ }
+ end
+end
diff --git a/tool/test/webrick/test_httpserver.rb b/tool/test/webrick/test_httpserver.rb
new file mode 100644
index 0000000000..4133be85ad
--- /dev/null
+++ b/tool/test/webrick/test_httpserver.rb
@@ -0,0 +1,543 @@
+# frozen_string_literal: false
+require "test/unit"
+require "net/http"
+require "webrick"
+require_relative "utils"
+
+class TestWEBrickHTTPServer < Test::Unit::TestCase
+ empty_log = Object.new
+ def empty_log.<<(str)
+ assert_equal('', str)
+ self
+ end
+ NoLog = WEBrick::Log.new(empty_log, WEBrick::BasicLog::WARN)
+
+ def teardown
+ WEBrick::Utils::TimeoutHandler.terminate
+ super
+ end
+
+ def test_mount
+ httpd = WEBrick::HTTPServer.new(
+ :Logger => NoLog,
+ :DoNotListen=>true
+ )
+ httpd.mount("/", :Root)
+ httpd.mount("/foo", :Foo)
+ httpd.mount("/foo/bar", :Bar, :bar1)
+ httpd.mount("/foo/bar/baz", :Baz, :baz1, :baz2)
+
+ serv, opts, script_name, path_info = httpd.search_servlet("/")
+ assert_equal(:Root, serv)
+ assert_equal([], opts)
+ assert_equal("", script_name)
+ assert_equal("/", path_info)
+
+ serv, opts, script_name, path_info = httpd.search_servlet("/sub")
+ assert_equal(:Root, serv)
+ assert_equal([], opts)
+ assert_equal("", script_name)
+ assert_equal("/sub", path_info)
+
+ serv, opts, script_name, path_info = httpd.search_servlet("/sub/")
+ assert_equal(:Root, serv)
+ assert_equal([], opts)
+ assert_equal("", script_name)
+ assert_equal("/sub/", path_info)
+
+ serv, opts, script_name, path_info = httpd.search_servlet("/foo")
+ assert_equal(:Foo, serv)
+ assert_equal([], opts)
+ assert_equal("/foo", script_name)
+ assert_equal("", path_info)
+
+ serv, opts, script_name, path_info = httpd.search_servlet("/foo/")
+ assert_equal(:Foo, serv)
+ assert_equal([], opts)
+ assert_equal("/foo", script_name)
+ assert_equal("/", path_info)
+
+ serv, opts, script_name, path_info = httpd.search_servlet("/foo/sub")
+ assert_equal(:Foo, serv)
+ assert_equal([], opts)
+ assert_equal("/foo", script_name)
+ assert_equal("/sub", path_info)
+
+ serv, opts, script_name, path_info = httpd.search_servlet("/foo/bar")
+ assert_equal(:Bar, serv)
+ assert_equal([:bar1], opts)
+ assert_equal("/foo/bar", script_name)
+ assert_equal("", path_info)
+
+ serv, opts, script_name, path_info = httpd.search_servlet("/foo/bar/baz")
+ assert_equal(:Baz, serv)
+ assert_equal([:baz1, :baz2], opts)
+ assert_equal("/foo/bar/baz", script_name)
+ assert_equal("", path_info)
+ end
+
+ class Req
+ attr_reader :port, :host
+ def initialize(addr, port, host)
+ @addr, @port, @host = addr, port, host
+ end
+ def addr
+ [0,0,0,@addr]
+ end
+ end
+
+ def httpd(addr, port, host, ali)
+ config ={
+ :Logger => NoLog,
+ :DoNotListen => true,
+ :BindAddress => addr,
+ :Port => port,
+ :ServerName => host,
+ :ServerAlias => ali,
+ }
+ return WEBrick::HTTPServer.new(config)
+ end
+
+ def assert_eql?(v1, v2)
+ assert_equal(v1.object_id, v2.object_id)
+ end
+
+ def test_lookup_server
+ addr1 = "192.168.100.1"
+ addr2 = "192.168.100.2"
+ addrz = "192.168.100.254"
+ local = "127.0.0.1"
+ port1 = 80
+ port2 = 8080
+ port3 = 10080
+ portz = 32767
+ name1 = "www.example.com"
+ name2 = "www2.example.com"
+ name3 = "www3.example.com"
+ namea = "www.example.co.jp"
+ nameb = "www.example.jp"
+ namec = "www2.example.co.jp"
+ named = "www2.example.jp"
+ namez = "foobar.example.com"
+ alias1 = [namea, nameb]
+ alias2 = [namec, named]
+
+ host1 = httpd(nil, port1, name1, nil)
+ hosts = [
+ host2 = httpd(addr1, port1, name1, nil),
+ host3 = httpd(addr1, port1, name2, alias1),
+ host4 = httpd(addr1, port2, name1, nil),
+ host5 = httpd(addr1, port2, name2, alias1),
+ httpd(addr1, port2, name3, alias2),
+ host7 = httpd(addr2, nil, name1, nil),
+ host8 = httpd(addr2, nil, name2, alias1),
+ httpd(addr2, nil, name3, alias2),
+ host10 = httpd(local, nil, nil, nil),
+ host11 = httpd(nil, port3, nil, nil),
+ ].sort_by{ rand }
+ hosts.each{|h| host1.virtual_host(h) }
+
+ # connect to addr1
+ assert_eql?(host2, host1.lookup_server(Req.new(addr1, port1, name1)))
+ assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, name2)))
+ assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, namea)))
+ assert_eql?(host3, host1.lookup_server(Req.new(addr1, port1, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr1, port1, namez)))
+ assert_eql?(host4, host1.lookup_server(Req.new(addr1, port2, name1)))
+ assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, name2)))
+ assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, namea)))
+ assert_eql?(host5, host1.lookup_server(Req.new(addr1, port2, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr1, port2, namez)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, name1)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, name2)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, namea)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, nameb)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addr1, port3, namez)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, name1)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, name2)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, namea)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr1, portz, namez)))
+
+ # connect to addr2
+ assert_eql?(host7, host1.lookup_server(Req.new(addr2, port1, name1)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, name2)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, namea)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port1, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr2, port1, namez)))
+ assert_eql?(host7, host1.lookup_server(Req.new(addr2, port2, name1)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, name2)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, namea)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port2, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr2, port2, namez)))
+ assert_eql?(host7, host1.lookup_server(Req.new(addr2, port3, name1)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, name2)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, namea)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, port3, nameb)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addr2, port3, namez)))
+ assert_eql?(host7, host1.lookup_server(Req.new(addr2, portz, name1)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, name2)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, namea)))
+ assert_eql?(host8, host1.lookup_server(Req.new(addr2, portz, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addr2, portz, namez)))
+
+ # connect to addrz
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, name1)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, name2)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, namea)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port1, namez)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, name1)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, name2)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, namea)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, port2, namez)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, name1)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, name2)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, namea)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, nameb)))
+ assert_eql?(host11, host1.lookup_server(Req.new(addrz, port3, namez)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, name1)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, name2)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, namea)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, nameb)))
+ assert_eql?(nil, host1.lookup_server(Req.new(addrz, portz, namez)))
+
+ # connect to localhost
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port1, name1)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port1, name2)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port1, namea)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port1, nameb)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port1, namez)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port2, name1)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port2, name2)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port2, namea)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port2, nameb)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port2, namez)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port3, name1)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port3, name2)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port3, namea)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port3, nameb)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, port3, namez)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, portz, name1)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, portz, name2)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, portz, namea)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, portz, nameb)))
+ assert_eql?(host10, host1.lookup_server(Req.new(local, portz, namez)))
+ end
+
+ def test_callbacks
+ accepted = started = stopped = 0
+ requested0 = requested1 = 0
+ config = {
+ :ServerName => "localhost",
+ :AcceptCallback => Proc.new{ accepted += 1 },
+ :StartCallback => Proc.new{ started += 1 },
+ :StopCallback => Proc.new{ stopped += 1 },
+ :RequestCallback => Proc.new{|req, res| requested0 += 1 },
+ }
+ log_tester = lambda {|log, access_log|
+ assert(log.find {|s| %r{ERROR `/' not found\.} =~ s })
+ assert_equal([], log.reject {|s| %r{ERROR `/' not found\.} =~ s })
+ }
+ TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log|
+ vhost_config = {
+ :ServerName => "myhostname",
+ :BindAddress => addr,
+ :Port => port,
+ :DoNotListen => true,
+ :Logger => NoLog,
+ :AccessLog => [],
+ :RequestCallback => Proc.new{|req, res| requested1 += 1 },
+ }
+ server.virtual_host(WEBrick::HTTPServer.new(vhost_config))
+
+ Thread.pass while server.status != :Running
+ sleep 1 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # server.status behaves unexpectedly with --jit-wait
+ assert_equal(1, started, log.call)
+ assert_equal(0, stopped, log.call)
+ assert_equal(0, accepted, log.call)
+
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/")
+ req["Host"] = "myhostname:#{port}"
+ http.request(req){|res| assert_equal("404", res.code, log.call)}
+ http.request(req){|res| assert_equal("404", res.code, log.call)}
+ http.request(req){|res| assert_equal("404", res.code, log.call)}
+ req["Host"] = "localhost:#{port}"
+ http.request(req){|res| assert_equal("404", res.code, log.call)}
+ http.request(req){|res| assert_equal("404", res.code, log.call)}
+ http.request(req){|res| assert_equal("404", res.code, log.call)}
+ assert_equal(6, accepted, log.call)
+ assert_equal(3, requested0, log.call)
+ assert_equal(3, requested1, log.call)
+ }
+ assert_equal(started, 1)
+ assert_equal(stopped, 1)
+ end
+
+ class CustomRequest < ::WEBrick::HTTPRequest; end
+ class CustomResponse < ::WEBrick::HTTPResponse; end
+ class CustomServer < ::WEBrick::HTTPServer
+ def create_request(config)
+ CustomRequest.new(config)
+ end
+
+ def create_response(config)
+ CustomResponse.new(config)
+ end
+ end
+
+ def test_custom_server_request_and_response
+ config = { :ServerName => "localhost" }
+ TestWEBrick.start_server(CustomServer, config){|server, addr, port, log|
+ server.mount_proc("/", lambda {|req, res|
+ assert_kind_of(CustomRequest, req)
+ assert_kind_of(CustomResponse, res)
+ res.body = "via custom response"
+ })
+ Thread.pass while server.status != :Running
+
+ Net::HTTP.start(addr, port) do |http|
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res|
+ assert_equal("via custom response", res.body)
+ }
+ server.shutdown
+ end
+ }
+ end
+
+ # This class is needed by test_response_io_with_chunked_set method
+ class EventManagerForChunkedResponseTest
+ def initialize
+ @listeners = []
+ end
+ def add_listener( &block )
+ @listeners << block
+ end
+ def raise_str_event( str )
+ @listeners.each{ |e| e.call( :str, str ) }
+ end
+ def raise_close_event()
+ @listeners.each{ |e| e.call( :cls ) }
+ end
+ end
+ def test_response_io_with_chunked_set
+ evt_man = EventManagerForChunkedResponseTest.new
+ t = Thread.new do
+ begin
+ config = {
+ :ServerName => "localhost"
+ }
+ TestWEBrick.start_httpserver(config) do |server, addr, port, log|
+ body_strs = [ 'aaaaaa', 'bb', 'cccc' ]
+ server.mount_proc( "/", ->( req, res ){
+ # Test for setting chunked...
+ res.chunked = true
+ r,w = IO.pipe
+ evt_man.add_listener do |type,str|
+ type == :cls ? ( w.close ) : ( w << str )
+ end
+ res.body = r
+ } )
+ Thread.pass while server.status != :Running
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/")
+ http.request(req) do |res|
+ i = 0
+ evt_man.raise_str_event( body_strs[i] )
+ res.read_body do |s|
+ assert_equal( body_strs[i], s )
+ i += 1
+ if i < body_strs.length
+ evt_man.raise_str_event( body_strs[i] )
+ else
+ evt_man.raise_close_event()
+ end
+ end
+ assert_equal( body_strs.length, i )
+ end
+ end
+ rescue => err
+ flunk( 'exception raised in thread: ' + err.to_s )
+ end
+ end
+ if t.join( 3 ).nil?
+ evt_man.raise_close_event()
+ flunk( 'timeout' )
+ if t.join( 1 ).nil?
+ Thread.kill t
+ end
+ end
+ end
+
+ def test_response_io_without_chunked_set
+ config = {
+ :ServerName => "localhost"
+ }
+ log_tester = lambda {|log, access_log|
+ assert_equal(1, log.length)
+ assert_match(/WARN Could not determine content-length of response body./, log[0])
+ }
+ TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log|
+ server.mount_proc("/", lambda { |req, res|
+ r,w = IO.pipe
+ # Test for not setting chunked...
+ # res.chunked = true
+ res.body = r
+ w << "foo"
+ w.close
+ })
+ Thread.pass while server.status != :Running
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/")
+ req['Connection'] = 'Keep-Alive'
+ begin
+ Timeout.timeout(2) do
+ http.request(req){|res| assert_equal("foo", res.body) }
+ end
+ rescue Timeout::Error
+ flunk('corrupted response')
+ end
+ }
+ end
+
+ def test_request_handler_callback_is_deprecated
+ requested = 0
+ config = {
+ :ServerName => "localhost",
+ :RequestHandler => Proc.new{|req, res| requested += 1 },
+ }
+ log_tester = lambda {|log, access_log|
+ assert_equal(2, log.length)
+ assert_match(/WARN :RequestHandler is deprecated, please use :RequestCallback/, log[0])
+ assert_match(%r{ERROR `/' not found\.}, log[1])
+ }
+ TestWEBrick.start_httpserver(config, log_tester){|server, addr, port, log|
+ Thread.pass while server.status != :Running
+
+ http = Net::HTTP.new(addr, port)
+ req = Net::HTTP::Get.new("/")
+ req["Host"] = "localhost:#{port}"
+ http.request(req){|res| assert_equal("404", res.code, log.call)}
+ assert_match(%r{:RequestHandler is deprecated, please use :RequestCallback$}, log.call, log.call)
+ }
+ assert_equal(1, requested)
+ end
+
+ def test_shutdown_with_busy_keepalive_connection
+ requested = 0
+ config = {
+ :ServerName => "localhost",
+ }
+ TestWEBrick.start_httpserver(config){|server, addr, port, log|
+ server.mount_proc("/", lambda {|req, res| res.body = "heffalump" })
+ Thread.pass while server.status != :Running
+
+ Net::HTTP.start(addr, port) do |http|
+ req = Net::HTTP::Get.new("/")
+ http.request(req){|res| assert_equal('Keep-Alive', res['Connection'], log.call) }
+ server.shutdown
+ begin
+ 10.times {|n| http.request(req); requested += 1 }
+ rescue
+ # Errno::ECONNREFUSED or similar
+ end
+ end
+ }
+ assert_equal(0, requested, "Server responded to #{requested} requests after shutdown")
+ end
+
+ def test_cntrl_in_path
+ log_ary = []
+ access_log_ary = []
+ config = {
+ :Port => 0,
+ :BindAddress => '127.0.0.1',
+ :Logger => WEBrick::Log.new(log_ary, WEBrick::BasicLog::WARN),
+ :AccessLog => [[access_log_ary, '']],
+ }
+ s = WEBrick::HTTPServer.new(config)
+ s.mount('/foo', WEBrick::HTTPServlet::FileHandler, __FILE__)
+ th = Thread.new { s.start }
+ addr = s.listeners[0].addr
+
+ http = Net::HTTP.new(addr[3], addr[1])
+ req = Net::HTTP::Get.new('/notexist%0a/foo')
+ http.request(req) { |res| assert_equal('404', res.code) }
+ exp = %Q(ERROR `/notexist\\n/foo' not found.\n)
+ assert_equal 1, log_ary.size
+ assert_include log_ary[0], exp
+ ensure
+ s&.shutdown
+ th&.join
+ end
+
+ def test_gigantic_request_header
+ log_tester = lambda {|log, access_log|
+ assert_equal 1, log.size
+ assert_include log[0], 'ERROR headers too large'
+ }
+ TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log|
+ server.mount('/', WEBrick::HTTPServlet::FileHandler, __FILE__)
+ TCPSocket.open(addr, port) do |c|
+ c.write("GET / HTTP/1.0\r\n")
+ junk = -"X-Junk: #{' ' * 1024}\r\n"
+ assert_raise(Errno::ECONNRESET, Errno::EPIPE, Errno::EPROTOTYPE) do
+ loop { c.write(junk) }
+ end
+ end
+ }
+ end
+
+ def test_eof_in_chunk
+ log_tester = lambda do |log, access_log|
+ assert_equal 1, log.size
+ assert_include log[0], 'ERROR bad chunk data size'
+ end
+ TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log|
+ server.mount_proc('/', ->(req, res) { res.body = req.body })
+ TCPSocket.open(addr, port) do |c|
+ c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \
+ "Transfer-Encoding: chunked\r\n\r\n5\r\na")
+ c.shutdown(Socket::SHUT_WR) # trigger EOF in server
+ res = c.read
+ assert_match %r{\AHTTP/1\.1 400 }, res
+ end
+ }
+ end
+
+ def test_big_chunks
+ nr_out = 3
+ buf = 'big' # 3 bytes is bigger than 2!
+ config = { :InputBufferSize => 2 }.freeze
+ total = 0
+ all = ''
+ TestWEBrick.start_httpserver(config){|server, addr, port, log|
+ server.mount_proc('/', ->(req, res) {
+ err = []
+ ret = req.body do |chunk|
+ n = chunk.bytesize
+ n > config[:InputBufferSize] and err << "#{n} > :InputBufferSize"
+ total += n
+ all << chunk
+ end
+ ret.nil? or err << 'req.body should return nil'
+ (buf * nr_out) == all or err << 'input body does not match expected'
+ res.header['connection'] = 'close'
+ res.body = err.join("\n")
+ })
+ TCPSocket.open(addr, port) do |c|
+ c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \
+ "Transfer-Encoding: chunked\r\n\r\n")
+ chunk = "#{buf.bytesize.to_s(16)}\r\n#{buf}\r\n"
+ nr_out.times { c.write(chunk) }
+ c.write("0\r\n\r\n")
+ head, body = c.read.split("\r\n\r\n")
+ assert_match %r{\AHTTP/1\.1 200 OK}, head
+ assert_nil body
+ end
+ }
+ end
+end
diff --git a/tool/test/webrick/test_httpstatus.rb b/tool/test/webrick/test_httpstatus.rb
new file mode 100644
index 0000000000..fd0570d5c6
--- /dev/null
+++ b/tool/test/webrick/test_httpstatus.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: false
+require "test/unit"
+require "webrick"
+
+class TestWEBrickHTTPStatus < Test::Unit::TestCase
+ def test_info?
+ assert WEBrick::HTTPStatus.info?(100)
+ refute WEBrick::HTTPStatus.info?(200)
+ end
+
+ def test_success?
+ assert WEBrick::HTTPStatus.success?(200)
+ refute WEBrick::HTTPStatus.success?(300)
+ end
+
+ def test_redirect?
+ assert WEBrick::HTTPStatus.redirect?(300)
+ refute WEBrick::HTTPStatus.redirect?(400)
+ end
+
+ def test_error?
+ assert WEBrick::HTTPStatus.error?(400)
+ refute WEBrick::HTTPStatus.error?(600)
+ end
+
+ def test_client_error?
+ assert WEBrick::HTTPStatus.client_error?(400)
+ refute WEBrick::HTTPStatus.client_error?(500)
+ end
+
+ def test_server_error?
+ assert WEBrick::HTTPStatus.server_error?(500)
+ refute WEBrick::HTTPStatus.server_error?(600)
+ end
+end
diff --git a/tool/test/webrick/test_httputils.rb b/tool/test/webrick/test_httputils.rb
new file mode 100644
index 0000000000..00f297bd09
--- /dev/null
+++ b/tool/test/webrick/test_httputils.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: false
+require "test/unit"
+require "webrick/httputils"
+
+class TestWEBrickHTTPUtils < Test::Unit::TestCase
+ include WEBrick::HTTPUtils
+
+ def test_normilize_path
+ assert_equal("/foo", normalize_path("/foo"))
+ assert_equal("/foo/bar/", normalize_path("/foo/bar/"))
+
+ assert_equal("/", normalize_path("/foo/../"))
+ assert_equal("/", normalize_path("/foo/.."))
+ assert_equal("/", normalize_path("/foo/bar/../../"))
+ assert_equal("/", normalize_path("/foo/bar/../.."))
+ assert_equal("/", normalize_path("/foo/bar/../.."))
+ assert_equal("/baz", normalize_path("/foo/bar/../../baz"))
+ assert_equal("/baz", normalize_path("/foo/../bar/../baz"))
+ assert_equal("/baz/", normalize_path("/foo/../bar/../baz/"))
+ assert_equal("/...", normalize_path("/bar/../..."))
+ assert_equal("/.../", normalize_path("/bar/../.../"))
+
+ assert_equal("/foo/", normalize_path("/foo/./"))
+ assert_equal("/foo/", normalize_path("/foo/."))
+ assert_equal("/foo/", normalize_path("/foo/././"))
+ assert_equal("/foo/", normalize_path("/foo/./."))
+ assert_equal("/foo/bar", normalize_path("/foo/./bar"))
+ assert_equal("/foo/bar/", normalize_path("/foo/./bar/."))
+ assert_equal("/foo/bar/", normalize_path("/./././foo/./bar/."))
+
+ assert_equal("/foo/bar/", normalize_path("//foo///.//bar/.///.//"))
+ assert_equal("/", normalize_path("//foo///..///bar/.///..//.//"))
+
+ assert_raise(RuntimeError){ normalize_path("foo/bar") }
+ assert_raise(RuntimeError){ normalize_path("..") }
+ assert_raise(RuntimeError){ normalize_path("/..") }
+ assert_raise(RuntimeError){ normalize_path("/./..") }
+ assert_raise(RuntimeError){ normalize_path("/./../") }
+ assert_raise(RuntimeError){ normalize_path("/./../..") }
+ assert_raise(RuntimeError){ normalize_path("/./../../") }
+ assert_raise(RuntimeError){ normalize_path("/./../") }
+ assert_raise(RuntimeError){ normalize_path("/../..") }
+ assert_raise(RuntimeError){ normalize_path("/../../") }
+ assert_raise(RuntimeError){ normalize_path("/../../..") }
+ assert_raise(RuntimeError){ normalize_path("/../../../") }
+ assert_raise(RuntimeError){ normalize_path("/../foo/../") }
+ assert_raise(RuntimeError){ normalize_path("/../foo/../../") }
+ assert_raise(RuntimeError){ normalize_path("/foo/bar/../../../../") }
+ assert_raise(RuntimeError){ normalize_path("/foo/../bar/../../") }
+ assert_raise(RuntimeError){ normalize_path("/./../bar/") }
+ assert_raise(RuntimeError){ normalize_path("/./../") }
+ end
+
+ def test_split_header_value
+ assert_equal(['foo', 'bar'], split_header_value('foo, bar'))
+ assert_equal(['"foo"', 'bar'], split_header_value('"foo", bar'))
+ assert_equal(['foo', '"bar"'], split_header_value('foo, "bar"'))
+ assert_equal(['*'], split_header_value('*'))
+ assert_equal(['W/"xyzzy"', 'W/"r2d2xxxx"', 'W/"c3piozzzz"'],
+ split_header_value('W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"'))
+ end
+
+ def test_escape
+ assert_equal("/foo/bar", escape("/foo/bar"))
+ assert_equal("/~foo/bar", escape("/~foo/bar"))
+ assert_equal("/~foo%20bar", escape("/~foo bar"))
+ assert_equal("/~foo%20bar", escape("/~foo bar"))
+ assert_equal("/~foo%09bar", escape("/~foo\tbar"))
+ assert_equal("/~foo+bar", escape("/~foo+bar"))
+ bug8425 = '[Bug #8425] [ruby-core:55052]'
+ assert_nothing_raised(ArgumentError, Encoding::CompatibilityError, bug8425) {
+ assert_equal("%E3%83%AB%E3%83%93%E3%83%BC%E3%81%95%E3%82%93", escape("\u{30EB 30D3 30FC 3055 3093}"))
+ }
+ end
+
+ def test_escape_form
+ assert_equal("%2Ffoo%2Fbar", escape_form("/foo/bar"))
+ assert_equal("%2F~foo%2Fbar", escape_form("/~foo/bar"))
+ assert_equal("%2F~foo+bar", escape_form("/~foo bar"))
+ assert_equal("%2F~foo+%2B+bar", escape_form("/~foo + bar"))
+ end
+
+ def test_unescape
+ assert_equal("/foo/bar", unescape("%2ffoo%2fbar"))
+ assert_equal("/~foo/bar", unescape("/%7efoo/bar"))
+ assert_equal("/~foo/bar", unescape("%2f%7efoo%2fbar"))
+ assert_equal("/~foo+bar", unescape("/%7efoo+bar"))
+ end
+
+ def test_unescape_form
+ assert_equal("//foo/bar", unescape_form("/%2Ffoo/bar"))
+ assert_equal("//foo/bar baz", unescape_form("/%2Ffoo/bar+baz"))
+ assert_equal("/~foo/bar baz", unescape_form("/%7Efoo/bar+baz"))
+ end
+
+ def test_escape_path
+ assert_equal("/foo/bar", escape_path("/foo/bar"))
+ assert_equal("/foo/bar/", escape_path("/foo/bar/"))
+ assert_equal("/%25foo/bar/", escape_path("/%foo/bar/"))
+ end
+end
diff --git a/tool/test/webrick/test_httpversion.rb b/tool/test/webrick/test_httpversion.rb
new file mode 100644
index 0000000000..e50ee17971
--- /dev/null
+++ b/tool/test/webrick/test_httpversion.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: false
+require "test/unit"
+require "webrick/httpversion"
+
+class TestWEBrickHTTPVersion < Test::Unit::TestCase
+ def setup
+ @v09 = WEBrick::HTTPVersion.new("0.9")
+ @v10 = WEBrick::HTTPVersion.new("1.0")
+ @v11 = WEBrick::HTTPVersion.new("1.001")
+ end
+
+ def test_to_s()
+ assert_equal("0.9", @v09.to_s)
+ assert_equal("1.0", @v10.to_s)
+ assert_equal("1.1", @v11.to_s)
+ end
+
+ def test_major()
+ assert_equal(0, @v09.major)
+ assert_equal(1, @v10.major)
+ assert_equal(1, @v11.major)
+ end
+
+ def test_minor()
+ assert_equal(9, @v09.minor)
+ assert_equal(0, @v10.minor)
+ assert_equal(1, @v11.minor)
+ end
+
+ def test_compar()
+ assert_equal(0, @v09 <=> "0.9")
+ assert_equal(0, @v09 <=> "0.09")
+
+ assert_equal(-1, @v09 <=> @v10)
+ assert_equal(-1, @v09 <=> "1.00")
+
+ assert_equal(1, @v11 <=> @v09)
+ assert_equal(1, @v11 <=> "1.0")
+ assert_equal(1, @v11 <=> "0.9")
+ end
+end
diff --git a/tool/test/webrick/test_server.rb b/tool/test/webrick/test_server.rb
new file mode 100644
index 0000000000..815cc3ce39
--- /dev/null
+++ b/tool/test/webrick/test_server.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: false
+require "test/unit"
+require "tempfile"
+require "webrick"
+require_relative "utils"
+
+class TestWEBrickServer < Test::Unit::TestCase
+ class Echo < WEBrick::GenericServer
+ def run(sock)
+ while line = sock.gets
+ sock << line
+ end
+ end
+ end
+
+ def test_server
+ TestWEBrick.start_server(Echo){|server, addr, port, log|
+ TCPSocket.open(addr, port){|sock|
+ sock.puts("foo"); assert_equal("foo\n", sock.gets, log.call)
+ sock.puts("bar"); assert_equal("bar\n", sock.gets, log.call)
+ sock.puts("baz"); assert_equal("baz\n", sock.gets, log.call)
+ sock.puts("qux"); assert_equal("qux\n", sock.gets, log.call)
+ }
+ }
+ end
+
+ def test_start_exception
+ stopped = 0
+
+ log = []
+ logger = WEBrick::Log.new(log, WEBrick::BasicLog::WARN)
+
+ assert_raise(SignalException) do
+ listener = Object.new
+ def listener.to_io # IO.select invokes #to_io.
+ raise SignalException, 'SIGTERM' # simulate signal in main thread
+ end
+ def listener.shutdown
+ end
+ def listener.close
+ end
+
+ server = WEBrick::HTTPServer.new({
+ :BindAddress => "127.0.0.1", :Port => 0,
+ :StopCallback => Proc.new{ stopped += 1 },
+ :Logger => logger,
+ })
+ server.listeners[0].close
+ server.listeners[0] = listener
+
+ server.start
+ end
+
+ assert_equal(1, stopped)
+ assert_equal(1, log.length)
+ assert_match(/FATAL SignalException: SIGTERM/, log[0])
+ end
+
+ def test_callbacks
+ accepted = started = stopped = 0
+ config = {
+ :AcceptCallback => Proc.new{ accepted += 1 },
+ :StartCallback => Proc.new{ started += 1 },
+ :StopCallback => Proc.new{ stopped += 1 },
+ }
+ TestWEBrick.start_server(Echo, config){|server, addr, port, log|
+ true while server.status != :Running
+ sleep 1 if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # server.status behaves unexpectedly with --jit-wait
+ assert_equal(1, started, log.call)
+ assert_equal(0, stopped, log.call)
+ assert_equal(0, accepted, log.call)
+ TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets }
+ TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets }
+ TCPSocket.open(addr, port){|sock| (sock << "foo\n").gets }
+ assert_equal(3, accepted, log.call)
+ }
+ assert_equal(1, started)
+ assert_equal(1, stopped)
+ end
+
+ def test_daemon
+ begin
+ r, w = IO.pipe
+ pid1 = Process.fork{
+ r.close
+ WEBrick::Daemon.start
+ w.puts(Process.pid)
+ sleep 10
+ }
+ pid2 = r.gets.to_i
+ assert(Process.kill(:KILL, pid2))
+ assert_not_equal(pid1, pid2)
+ rescue NotImplementedError
+ # snip this test
+ ensure
+ Process.wait(pid1) if pid1
+ r.close
+ w.close
+ end
+ end
+
+ def test_restart_after_shutdown
+ address = '127.0.0.1'
+ port = 0
+ log = []
+ config = {
+ :BindAddress => address,
+ :Port => port,
+ :Logger => WEBrick::Log.new(log, WEBrick::BasicLog::WARN),
+ }
+ server = Echo.new(config)
+ client_proc = lambda {|str|
+ begin
+ ret = server.listeners.first.connect_address.connect {|s|
+ s.write(str)
+ s.close_write
+ s.read
+ }
+ assert_equal(str, ret)
+ ensure
+ server.shutdown
+ end
+ }
+ server_thread = Thread.new { server.start }
+ client_thread = Thread.new { client_proc.call("a") }
+ assert_join_threads([client_thread, server_thread])
+ server.listen(address, port)
+ server_thread = Thread.new { server.start }
+ client_thread = Thread.new { client_proc.call("b") }
+ assert_join_threads([client_thread, server_thread])
+ assert_equal([], log)
+ end
+
+ def test_restart_after_stop
+ log = Object.new
+ class << log
+ include Test::Unit::Assertions
+ def <<(msg)
+ flunk "unexpected log: #{msg.inspect}"
+ end
+ end
+ client_thread = nil
+ wakeup = -> {client_thread.wakeup}
+ warn_flunk = WEBrick::Log.new(log, WEBrick::BasicLog::WARN)
+ server = WEBrick::HTTPServer.new(
+ :StartCallback => wakeup,
+ :StopCallback => wakeup,
+ :BindAddress => '0.0.0.0',
+ :Port => 0,
+ :Logger => warn_flunk)
+ 2.times {
+ server_thread = Thread.start {
+ server.start
+ }
+ client_thread = Thread.start {
+ sleep 0.1 until server.status == :Running || !server_thread.status
+ server.stop
+ sleep 0.1 until server.status == :Stop || !server_thread.status
+ }
+ assert_join_threads([client_thread, server_thread])
+ }
+ end
+
+ def test_port_numbers
+ config = {
+ :BindAddress => '0.0.0.0',
+ :Logger => WEBrick::Log.new([], WEBrick::BasicLog::WARN),
+ }
+
+ ports = [0, "0"]
+
+ ports.each do |port|
+ config[:Port]= port
+ server = WEBrick::GenericServer.new(config)
+ server_thread = Thread.start { server.start }
+ client_thread = Thread.start {
+ sleep 0.1 until server.status == :Running || !server_thread.status
+ server_port = server.listeners[0].addr[1]
+ server.stop
+ assert_equal server.config[:Port], server_port
+ sleep 0.1 until server.status == :Stop || !server_thread.status
+ }
+ assert_join_threads([client_thread, server_thread])
+ end
+
+ assert_raise(ArgumentError) do
+ config[:Port]= "FOO"
+ WEBrick::GenericServer.new(config)
+ end
+ end
+end
diff --git a/tool/test/webrick/test_ssl_server.rb b/tool/test/webrick/test_ssl_server.rb
new file mode 100644
index 0000000000..4e52598bf5
--- /dev/null
+++ b/tool/test/webrick/test_ssl_server.rb
@@ -0,0 +1,67 @@
+require "test/unit"
+require "webrick"
+require "webrick/ssl"
+require_relative "utils"
+require 'timeout'
+
+class TestWEBrickSSLServer < Test::Unit::TestCase
+ class Echo < WEBrick::GenericServer
+ def run(sock)
+ while line = sock.gets
+ sock << line
+ end
+ end
+ end
+
+ def test_self_signed_cert_server
+ assert_self_signed_cert(
+ :SSLEnable => true,
+ :SSLCertName => [["C", "JP"], ["O", "www.ruby-lang.org"], ["CN", "Ruby"]],
+ )
+ end
+
+ def test_self_signed_cert_server_with_string
+ assert_self_signed_cert(
+ :SSLEnable => true,
+ :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby",
+ )
+ end
+
+ def assert_self_signed_cert(config)
+ TestWEBrick.start_server(Echo, config){|server, addr, port, log|
+ io = TCPSocket.new(addr, port)
+ sock = OpenSSL::SSL::SSLSocket.new(io)
+ sock.connect
+ sock.puts(server.ssl_context.cert.subject.to_s)
+ assert_equal("/C=JP/O=www.ruby-lang.org/CN=Ruby\n", sock.gets, log.call)
+ sock.close
+ io.close
+ }
+ end
+
+ def test_slow_connect
+ poke = lambda do |io, msg|
+ begin
+ sock = OpenSSL::SSL::SSLSocket.new(io)
+ sock.connect
+ sock.puts(msg)
+ assert_equal "#{msg}\n", sock.gets, msg
+ ensure
+ sock&.close
+ io.close
+ end
+ end
+ config = {
+ :SSLEnable => true,
+ :SSLCertName => "/C=JP/O=www.ruby-lang.org/CN=Ruby",
+ }
+ EnvUtil.timeout(10) do
+ TestWEBrick.start_server(Echo, config) do |server, addr, port, log|
+ outer = TCPSocket.new(addr, port)
+ inner = TCPSocket.new(addr, port)
+ poke.call(inner, 'fast TLS negotiation')
+ poke.call(outer, 'slow TLS negotiation')
+ end
+ end
+ end
+end
diff --git a/tool/test/webrick/test_utils.rb b/tool/test/webrick/test_utils.rb
new file mode 100644
index 0000000000..c2b7a36e8a
--- /dev/null
+++ b/tool/test/webrick/test_utils.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: false
+require "test/unit"
+require "webrick/utils"
+
+class TestWEBrickUtils < Test::Unit::TestCase
+ def teardown
+ WEBrick::Utils::TimeoutHandler.terminate
+ super
+ end
+
+ def assert_expired(m)
+ Thread.handle_interrupt(Timeout::Error => :never, EX => :never) do
+ assert_empty(m::TimeoutHandler.instance.instance_variable_get(:@timeout_info))
+ end
+ end
+
+ def assert_not_expired(m)
+ Thread.handle_interrupt(Timeout::Error => :never, EX => :never) do
+ assert_not_empty(m::TimeoutHandler.instance.instance_variable_get(:@timeout_info))
+ end
+ end
+
+ EX = Class.new(StandardError)
+
+ def test_no_timeout
+ m = WEBrick::Utils
+ assert_equal(:foo, m.timeout(10){ :foo })
+ assert_expired(m)
+ end
+
+ def test_nested_timeout_outer
+ m = WEBrick::Utils
+ i = 0
+ assert_raise(Timeout::Error){
+ m.timeout(1){
+ assert_raise(Timeout::Error){ m.timeout(0.1){ i += 1; sleep(1) } }
+ assert_not_expired(m)
+ i += 1
+ sleep(2)
+ }
+ }
+ assert_equal(2, i)
+ assert_expired(m)
+ end
+
+ def test_timeout_default_exception
+ m = WEBrick::Utils
+ assert_raise(Timeout::Error){ m.timeout(0.01){ sleep } }
+ assert_expired(m)
+ end
+
+ def test_timeout_custom_exception
+ m = WEBrick::Utils
+ ex = EX
+ assert_raise(ex){ m.timeout(0.01, ex){ sleep } }
+ assert_expired(m)
+ end
+
+ def test_nested_timeout_inner_custom_exception
+ m = WEBrick::Utils
+ ex = EX
+ i = 0
+ assert_raise(ex){
+ m.timeout(10){
+ m.timeout(0.01, ex){ i += 1; sleep }
+ }
+ sleep
+ }
+ assert_equal(1, i)
+ assert_expired(m)
+ end
+
+ def test_nested_timeout_outer_custom_exception
+ m = WEBrick::Utils
+ ex = EX
+ i = 0
+ assert_raise(Timeout::Error){
+ m.timeout(0.01){
+ m.timeout(1.0, ex){ i += 1; sleep }
+ }
+ sleep
+ }
+ assert_equal(1, i)
+ assert_expired(m)
+ end
+
+ def test_create_listeners
+ addr = listener_address(0)
+ port = addr.slice!(1)
+ assert_kind_of(Integer, port, "dynamically chosen port number")
+ assert_equal(["AF_INET", "127.0.0.1", "127.0.0.1"], addr)
+
+ assert_equal(["AF_INET", port, "127.0.0.1", "127.0.0.1"],
+ listener_address(port),
+ "specific port number")
+
+ assert_equal(["AF_INET", port, "127.0.0.1", "127.0.0.1"],
+ listener_address(port.to_s),
+ "specific port number string")
+ end
+
+ def listener_address(port)
+ listeners = WEBrick::Utils.create_listeners("127.0.0.1", port)
+ srv = listeners.first
+ assert_kind_of TCPServer, srv
+ srv.addr
+ ensure
+ listeners.each(&:close) if listeners
+ end
+end
diff --git a/tool/test/webrick/utils.rb b/tool/test/webrick/utils.rb
new file mode 100644
index 0000000000..a8568d0a43
--- /dev/null
+++ b/tool/test/webrick/utils.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: false
+require "webrick"
+begin
+ require "webrick/https"
+rescue LoadError
+end
+require "webrick/httpproxy"
+
+module TestWEBrick
+ NullWriter = Object.new
+ def NullWriter.<<(msg)
+ puts msg if $DEBUG
+ return self
+ end
+
+ class WEBrick::HTTPServlet::CGIHandler
+ remove_const :Ruby
+ require "envutil" unless defined?(EnvUtil)
+ Ruby = EnvUtil.rubybin
+ remove_const :CGIRunner
+ CGIRunner = "\"#{Ruby}\" \"#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb\"" # :nodoc:
+ remove_const :CGIRunnerArray
+ CGIRunnerArray = [Ruby, "#{WEBrick::Config::LIBDIR}/httpservlet/cgi_runner.rb"] # :nodoc:
+ end
+
+ RubyBin = "\"#{EnvUtil.rubybin}\""
+ RubyBin << " --disable-gems"
+ RubyBin << " \"-I#{File.expand_path("../..", File.dirname(__FILE__))}/lib\""
+ RubyBin << " \"-I#{File.dirname(EnvUtil.rubybin)}/.ext/common\""
+ RubyBin << " \"-I#{File.dirname(EnvUtil.rubybin)}/.ext/#{RUBY_PLATFORM}\""
+
+ RubyBinArray = [EnvUtil.rubybin]
+ RubyBinArray << "--disable-gems"
+ RubyBinArray << "-I" << "#{File.expand_path("../..", File.dirname(__FILE__))}/lib"
+ RubyBinArray << "-I" << "#{File.dirname(EnvUtil.rubybin)}/.ext/common"
+ RubyBinArray << "-I" << "#{File.dirname(EnvUtil.rubybin)}/.ext/#{RUBY_PLATFORM}"
+
+ require "test/unit" unless defined?(Test::Unit)
+ include Test::Unit::Assertions
+ extend Test::Unit::Assertions
+ include Test::Unit::CoreAssertions
+ extend Test::Unit::CoreAssertions
+
+ module_function
+
+ DefaultLogTester = lambda {|log, access_log| assert_equal([], log) }
+
+ def start_server(klass, config={}, log_tester=DefaultLogTester, &block)
+ log_ary = []
+ access_log_ary = []
+ log = proc { "webrick log start:\n" + (log_ary+access_log_ary).join.gsub(/^/, " ").chomp + "\nwebrick log end" }
+ config = ({
+ :BindAddress => "127.0.0.1", :Port => 0,
+ :ServerType => Thread,
+ :Logger => WEBrick::Log.new(log_ary, WEBrick::BasicLog::WARN),
+ :AccessLog => [[access_log_ary, ""]]
+ }.update(config))
+ server = capture_output {break klass.new(config)}
+ server_thread = server.start
+ server_thread2 = Thread.new {
+ server_thread.join
+ if log_tester
+ log_tester.call(log_ary, access_log_ary)
+ end
+ }
+ addr = server.listeners[0].addr
+ client_thread = Thread.new {
+ begin
+ block.yield([server, addr[3], addr[1], log])
+ ensure
+ server.shutdown
+ end
+ }
+ assert_join_threads([client_thread, server_thread2])
+ end
+
+ def start_httpserver(config={}, log_tester=DefaultLogTester, &block)
+ start_server(WEBrick::HTTPServer, config, log_tester, &block)
+ end
+
+ def start_httpproxy(config={}, log_tester=DefaultLogTester, &block)
+ start_server(WEBrick::HTTPProxyServer, config, log_tester, &block)
+ end
+end
diff --git a/tool/test/webrick/webrick.cgi b/tool/test/webrick/webrick.cgi
new file mode 100644
index 0000000000..a294fa72f9
--- /dev/null
+++ b/tool/test/webrick/webrick.cgi
@@ -0,0 +1,38 @@
+#!ruby
+require "webrick/cgi"
+
+class TestApp < WEBrick::CGI
+ def do_GET(req, res)
+ res["content-type"] = "text/plain"
+ if req.path_info == "/dumpenv"
+ res.body = Marshal.dump(ENV.to_hash)
+ elsif (p = req.path_info) && p.length > 0
+ res.body = p
+ elsif (q = req.query).size > 0
+ res.body = q.keys.sort.collect{|key|
+ q[key].list.sort.collect{|v|
+ "#{key}=#{v}"
+ }.join(", ")
+ }.join(", ")
+ elsif %r{/$} =~ req.request_uri.to_s
+ res.body = ""
+ res.body << req.request_uri.to_s << "\n"
+ res.body << req.script_name
+ elsif !req.cookies.empty?
+ res.body = req.cookies.inject(""){|result, cookie|
+ result << "%s=%s\n" % [cookie.name, cookie.value]
+ }
+ res.cookies << WEBrick::Cookie.new("Customer", "WILE_E_COYOTE")
+ res.cookies << WEBrick::Cookie.new("Shipping", "FedEx")
+ else
+ res.body = req.script_name
+ end
+ end
+
+ def do_POST(req, res)
+ do_GET(req, res)
+ end
+end
+
+cgi = TestApp.new
+cgi.start
diff --git a/tool/test/webrick/webrick.rhtml b/tool/test/webrick/webrick.rhtml
new file mode 100644
index 0000000000..a7bbe43fb5
--- /dev/null
+++ b/tool/test/webrick/webrick.rhtml
@@ -0,0 +1,4 @@
+req to <%=
+servlet_request.request_uri
+%> <%=
+servlet_request.query.inspect %>
diff --git a/tool/test/webrick/webrick_long_filename.cgi b/tool/test/webrick/webrick_long_filename.cgi
new file mode 100644
index 0000000000..43c1af825c
--- /dev/null
+++ b/tool/test/webrick/webrick_long_filename.cgi
@@ -0,0 +1,36 @@
+#!ruby
+require "webrick/cgi"
+
+class TestApp < WEBrick::CGI
+ def do_GET(req, res)
+ res["content-type"] = "text/plain"
+ if (p = req.path_info) && p.length > 0
+ res.body = p
+ elsif (q = req.query).size > 0
+ res.body = q.keys.sort.collect{|key|
+ q[key].list.sort.collect{|v|
+ "#{key}=#{v}"
+ }.join(", ")
+ }.join(", ")
+ elsif %r{/$} =~ req.request_uri.to_s
+ res.body = ""
+ res.body << req.request_uri.to_s << "\n"
+ res.body << req.script_name
+ elsif !req.cookies.empty?
+ res.body = req.cookies.inject(""){|result, cookie|
+ result << "%s=%s\n" % [cookie.name, cookie.value]
+ }
+ res.cookies << WEBrick::Cookie.new("Customer", "WILE_E_COYOTE")
+ res.cookies << WEBrick::Cookie.new("Shipping", "FedEx")
+ else
+ res.body = req.script_name
+ end
+ end
+
+ def do_POST(req, res)
+ do_GET(req, res)
+ end
+end
+
+cgi = TestApp.new
+cgi.start
diff --git a/tool/transcode-tblgen.rb b/tool/transcode-tblgen.rb
new file mode 100644
index 0000000000..dba6f33ff9
--- /dev/null
+++ b/tool/transcode-tblgen.rb
@@ -0,0 +1,1118 @@
+# frozen_string_literal: true
+
+require 'optparse'
+require 'erb'
+require 'fileutils'
+require 'pp'
+
+class Array
+ unless [].respond_to? :product
+ def product(*args)
+ if args.empty?
+ self.map {|e| [e] }
+ else
+ result = []
+ self.each {|e0|
+ result.concat args.first.product(*args[1..-1]).map {|es| [e0, *es] }
+ }
+ result
+ end
+ end
+ end
+end
+
+class String
+ unless "".respond_to? :start_with?
+ def start_with?(*prefixes)
+ prefixes.each {|prefix|
+ return true if prefix.length <= self.length && prefix == self[0, prefix.length]
+ }
+ false
+ end
+ end
+end
+
+NUM_ELEM_BYTELOOKUP = 2
+
+C_ESC = {
+ "\\" => "\\\\",
+ '"' => '\"',
+ "\n" => '\n',
+}
+
+0x00.upto(0x1f) {|ch| C_ESC[[ch].pack("C")] ||= "\\%03o" % ch }
+0x7f.upto(0xff) {|ch| C_ESC[[ch].pack("C")] = "\\%03o" % ch }
+C_ESC_PAT = Regexp.union(*C_ESC.keys)
+
+def c_esc(str)
+ '"' + str.gsub(C_ESC_PAT) { C_ESC[$&] } + '"'
+end
+
+HEX2 = /(?:[0-9A-Fa-f]{2})/
+
+class ArrayCode
+ def initialize(type, name)
+ @type = type
+ @name = name
+ @len = 0;
+ @content = ''.dup
+ end
+
+ def length
+ @len
+ end
+
+ def insert_at_last(num, str)
+ # newnum = self.length + num
+ @content << str
+ @len += num
+ end
+
+ def to_s
+ <<"End"
+static const #{@type}
+#{@name}[#{@len}] = {
+#{@content}};
+End
+ end
+end
+
+class Action
+ def initialize(value)
+ @value = value
+ end
+ attr_reader :value
+
+ def hash
+ @value.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ @value == other.value
+ end
+ alias == eql?
+end
+
+class Branch
+ def initialize(byte_min, byte_max, child_tree)
+ @byte_min = byte_min
+ @byte_max = byte_max
+ @child_tree = child_tree
+ @hash = byte_min.hash ^ byte_max.hash ^ child_tree.hash
+ end
+ attr_reader :byte_min, :byte_max, :child_tree, :hash
+
+ def eql?(other)
+ self.class == other.class &&
+ @hash == other.hash &&
+ @byte_min == other.byte_min &&
+ @byte_max == other.byte_max &&
+ @child_tree == other.child_tree
+ end
+ alias == eql?
+end
+
+class ActionMap
+ def self.parse_to_rects(mapping)
+ rects = []
+ n = 0
+ mapping.each {|pat, action|
+ pat = pat.to_s
+ if /\A\s*\(empset\)\s*\z/ =~ pat
+ next
+ elsif /\A\s*\(empstr\)\s*\z/ =~ pat
+ rects << ['', '', action]
+ n += 1
+ elsif /\A\s*(#{HEX2}+)\s*\z/o =~ pat
+ hex = $1.upcase
+ rects << [hex, hex, action]
+ elsif /\A\s*((#{HEX2}|\{#{HEX2}(?:-#{HEX2})?(,#{HEX2}(?:-#{HEX2})?)*\})+(\s+|\z))*\z/o =~ pat
+ pat = pat.upcase
+ pat.scan(/\S+/) {
+ pat1 = $&
+ ranges_list = []
+ pat1.scan(/#{HEX2}|\{([^\}]*)\}/o) {
+ ranges_list << []
+ if !$1
+ ranges_list.last << [$&,$&]
+ else
+ set = {}
+ $1.scan(/(#{HEX2})(?:-(#{HEX2}))?/o) {
+ if !$2
+ c = $1.to_i(16)
+ set[c] = true
+ else
+ b = $1.to_i(16)
+ e = $2.to_i(16)
+ b.upto(e) {|_| set[_] = true }
+ end
+ }
+ i = nil
+ 0.upto(256) {|j|
+ if set[j]
+ if !i
+ i = j
+ end
+ if !set[j+1]
+ ranges_list.last << ["%02X" % i, "%02X" % j]
+ i = nil
+ end
+ end
+ }
+ end
+ }
+ first_ranges = ranges_list.shift
+ first_ranges.product(*ranges_list).each {|range_list|
+ min = range_list.map {|x, y| x }.join
+ max = range_list.map {|x, y| y }.join
+ rects << [min, max, action]
+ }
+ }
+ else
+ raise ArgumentError, "invalid pattern: #{pat.inspect}"
+ end
+ }
+ rects
+ end
+
+ def self.unambiguous_action(actions0)
+ actions = actions0.uniq
+ if actions.length == 1
+ actions[0]
+ else
+ actions.delete(:nomap0)
+ if actions.length == 1
+ actions[0]
+ else
+ raise ArgumentError, "ambiguous actions: #{actions0.inspect}"
+ end
+ end
+ end
+
+ def self.build_tree(rects)
+ expand(rects) {|prefix, actions|
+ unambiguous_action(actions)
+ }
+ end
+
+ def self.parse(mapping)
+ rects = parse_to_rects(mapping)
+ tree = build_tree(rects)
+ self.new(tree)
+ end
+
+ def self.merge_rects(*rects_list)
+ if rects_list.length < 2
+ raise ArgumentError, "not enough arguments"
+ end
+
+ all_rects = []
+ rects_list.each_with_index {|rects, i|
+ all_rects.concat rects.map {|min, max, action| [min, max, [i, action]] }
+ }
+
+ tree = expand(all_rects) {|prefix, actions|
+ args = Array.new(rects_list.length) { [] }
+ actions.each {|i, action|
+ args[i] << action
+ }
+ yield(prefix, *args)
+ }
+
+ self.new(tree)
+ end
+
+ def self.merge(*mappings, &block)
+ merge_rects(*mappings.map {|m| parse_to_rects(m) }, &block)
+ end
+
+ def self.merge2(map1, map2, &block)
+ rects1 = parse_to_rects(map1)
+ rects2 = parse_to_rects(map2)
+
+ actions = []
+ all_rects = []
+
+ rects1.each {|rect|
+ _, _, action = rect
+ rect[2] = actions.length
+ actions << action
+ all_rects << rect
+ }
+
+ boundary = actions.length
+
+ rects2.each {|rect|
+ _, _, action = rect
+ rect[2] = actions.length
+ actions << action
+ all_rects << rect
+ }
+
+ tree = expand(all_rects) {|prefix, as0|
+ as1 = []
+ as2 = []
+ as0.each {|i|
+ if i < boundary
+ as1 << actions[i]
+ else
+ as2 << actions[i]
+ end
+ }
+ yield(prefix, as1, as2)
+ }
+
+ self.new(tree)
+ end
+
+ def self.expand(rects, &block)
+ #numsing = numreg = 0
+ #rects.each {|min, max, action| if min == max then numsing += 1 else numreg += 1 end }
+ #puts "#{numsing} singleton mappings and #{numreg} region mappings."
+ singleton_rects = []
+ region_rects = []
+ rects.each {|rect|
+ min, max, = rect
+ if min == max
+ singleton_rects << rect
+ else
+ region_rects << rect
+ end
+ }
+ @singleton_rects = singleton_rects.sort_by {|min, max, action| min }
+ @singleton_rects.reverse!
+ ret = expand_rec("", region_rects, &block)
+ @singleton_rects = nil
+ ret
+ end
+
+ TMPHASH = {}
+ def self.expand_rec(prefix, region_rects, &block)
+ return region_rects if region_rects.empty? && !((s_rect = @singleton_rects.last) && s_rect[0].start_with?(prefix))
+ if region_rects.empty? ? s_rect[0].length == prefix.length : region_rects[0][0].empty?
+ h = TMPHASH
+ while (s_rect = @singleton_rects.last) && s_rect[0].start_with?(prefix)
+ min, _, action = @singleton_rects.pop
+ raise ArgumentError, "ambiguous pattern: #{prefix}" if min.length != prefix.length
+ h[action] = true
+ end
+ for min, _, action in region_rects
+ raise ArgumentError, "ambiguous pattern: #{prefix}" if !min.empty?
+ h[action] = true
+ end
+ tree = Action.new(block.call(prefix, h.keys))
+ h.clear
+ else
+ tree = []
+ each_firstbyte_range(prefix, region_rects) {|byte_min, byte_max, r_rects2|
+ if byte_min == byte_max
+ prefix2 = prefix + "%02X" % byte_min
+ else
+ prefix2 = prefix + "{%02X-%02X}" % [byte_min, byte_max]
+ end
+ child_tree = expand_rec(prefix2, r_rects2, &block)
+ tree << Branch.new(byte_min, byte_max, child_tree)
+ }
+ end
+ return tree
+ end
+
+ def self.each_firstbyte_range(prefix, region_rects)
+ index_from = TMPHASH
+
+ region_ary = []
+ region_rects.each {|min, max, action|
+ raise ArgumentError, "ambiguous pattern: #{prefix}" if min.empty?
+ min_firstbyte = min[0,2].to_i(16)
+ min_rest = min[2..-1]
+ max_firstbyte = max[0,2].to_i(16)
+ max_rest = max[2..-1]
+ region_ary << [min_firstbyte, max_firstbyte, [min_rest, max_rest, action]]
+ index_from[min_firstbyte] = true
+ index_from[max_firstbyte+1] = true
+ }
+
+ byte_from = Array.new(index_from.size)
+ bytes = index_from.keys
+ bytes.sort!
+ bytes.reverse!
+ bytes.each_with_index {|byte, i|
+ index_from[byte] = i
+ byte_from[i] = byte
+ }
+
+ region_rects_ary = Array.new(index_from.size) { [] }
+ region_ary.each {|min_firstbyte, max_firstbyte, rest_elt|
+ index_from[min_firstbyte].downto(index_from[max_firstbyte+1]+1) {|i|
+ region_rects_ary[i] << rest_elt
+ }
+ }
+
+ index_from.clear
+
+ r_rects = region_rects_ary.pop
+ region_byte = byte_from.pop
+ prev_r_start = region_byte
+ prev_r_rects = []
+ while r_rects && (s_rect = @singleton_rects.last) && (seq = s_rect[0]).start_with?(prefix)
+ singleton_byte = seq[prefix.length, 2].to_i(16)
+ min_byte = singleton_byte < region_byte ? singleton_byte : region_byte
+ if prev_r_start < min_byte && !prev_r_rects.empty?
+ yield prev_r_start, min_byte-1, prev_r_rects
+ end
+ if region_byte < singleton_byte
+ prev_r_start = region_byte
+ prev_r_rects = r_rects
+ r_rects = region_rects_ary.pop
+ region_byte = byte_from.pop
+ elsif region_byte > singleton_byte
+ yield singleton_byte, singleton_byte, prev_r_rects
+ prev_r_start = singleton_byte+1
+ else # region_byte == singleton_byte
+ prev_r_start = region_byte+1
+ prev_r_rects = r_rects
+ r_rects = region_rects_ary.pop
+ region_byte = byte_from.pop
+ yield singleton_byte, singleton_byte, prev_r_rects
+ end
+ end
+
+ while r_rects
+ if prev_r_start < region_byte && !prev_r_rects.empty?
+ yield prev_r_start, region_byte-1, prev_r_rects
+ end
+ prev_r_start = region_byte
+ prev_r_rects = r_rects
+ r_rects = region_rects_ary.pop
+ region_byte = byte_from.pop
+ end
+
+ while (s_rect = @singleton_rects.last) && (seq = s_rect[0]).start_with?(prefix)
+ singleton_byte = seq[prefix.length, 2].to_i(16)
+ yield singleton_byte, singleton_byte, []
+ end
+ end
+
+ def initialize(tree)
+ @tree = tree
+ end
+
+ def inspect
+ "\#<#{self.class}:" +
+ @tree.inspect +
+ ">"
+ end
+
+ def max_input_length_rec(tree)
+ case tree
+ when Action
+ 0
+ else
+ tree.map {|branch|
+ max_input_length_rec(branch.child_tree)
+ }.max + 1
+ end
+ end
+
+ def max_input_length
+ max_input_length_rec(@tree)
+ end
+
+ def empty_action
+ if @tree.kind_of? Action
+ @tree.value
+ else
+ nil
+ end
+ end
+
+ OffsetsMemo = {}
+ InfosMemo = {}
+
+ def format_offsets(min, max, offsets)
+ offsets = offsets[min..max]
+ code = "%d, %d,\n" % [min, max]
+ 0.step(offsets.length-1,16) {|i|
+ code << " "
+ code << offsets[i,8].map {|off| "%3d," % off.to_s }.join('')
+ if i+8 < offsets.length
+ code << " "
+ code << offsets[i+8,8].map {|off| "%3d," % off.to_s }.join('')
+ end
+ code << "\n"
+ }
+ code
+ end
+
+ UsedName = {}
+
+ StrMemo = {}
+
+ def str_name(bytes)
+ size = @bytes_code.length
+ rawbytes = [bytes].pack("H*")
+
+ n = nil
+ if !n && !(suf = rawbytes.gsub(/[^A-Za-z0-9_]/, '')).empty? && !UsedName[nn = "str1_" + suf] then n = nn end
+ if !n && !UsedName[nn = "str1_" + bytes] then n = nn end
+ n ||= "str1s_#{size}"
+
+ StrMemo[bytes] = n
+ UsedName[n] = true
+ n
+ end
+
+ def gen_str(bytes)
+ if n = StrMemo[bytes]
+ n
+ else
+ len = bytes.length/2
+ size = @bytes_code.length
+ n = str_name(bytes)
+ @bytes_code.insert_at_last(1 + len,
+ "\#define #{n} makeSTR1(#{size})\n" +
+ " makeSTR1LEN(#{len})," + bytes.gsub(/../, ' 0x\&,') + "\n\n")
+ n
+ end
+ end
+
+ def generate_info(info)
+ case info
+ when :nomap, :nomap0
+ # :nomap0 is low priority. it never collides.
+ "NOMAP"
+ when :undef
+ "UNDEF"
+ when :invalid
+ "INVALID"
+ when :func_ii
+ "FUNii"
+ when :func_si
+ "FUNsi"
+ when :func_io
+ "FUNio"
+ when :func_so
+ "FUNso"
+ when /\A(#{HEX2})\z/o
+ "o1(0x#$1)"
+ when /\A(#{HEX2})(#{HEX2})\z/o
+ "o2(0x#$1,0x#$2)"
+ when /\A(#{HEX2})(#{HEX2})(#{HEX2})\z/o
+ "o3(0x#$1,0x#$2,0x#$3)"
+ when /funsio\((\d+)\)/
+ "funsio(#{$1})"
+ when /\A(#{HEX2})(3[0-9])(#{HEX2})(3[0-9])\z/o
+ "g4(0x#$1,0x#$2,0x#$3,0x#$4)"
+ when /\A(f[0-7])(#{HEX2})(#{HEX2})(#{HEX2})\z/o
+ "o4(0x#$1,0x#$2,0x#$3,0x#$4)"
+ when /\A(#{HEX2}){4,259}\z/o
+ gen_str(info.upcase)
+ when /\A\/\*BYTE_LOOKUP\*\// # pointer to BYTE_LOOKUP structure
+ $'.to_s
+ else
+ raise "unexpected action: #{info.inspect}"
+ end
+ end
+
+ def format_infos(infos)
+ infos = infos.map {|info| generate_info(info) }
+ maxlen = infos.map {|info| info.length }.max
+ columns = maxlen <= 16 ? 4 : 2
+ code = "".dup
+ 0.step(infos.length-1, columns) {|i|
+ code << " "
+ is = infos[i,columns]
+ is.each {|info|
+ code << sprintf(" %#{maxlen}s,", info)
+ }
+ code << "\n"
+ }
+ code
+ end
+
+ def generate_lookup_node(name, table)
+ bytes_code = @bytes_code
+ words_code = @words_code
+ offsets = []
+ infos = []
+ infomap = {}
+ min = max = nil
+ table.each_with_index {|action, byte|
+ action ||= :invalid
+ if action != :invalid
+ min = byte if !min
+ max = byte
+ end
+ unless o = infomap[action]
+ infomap[action] = o = infos.length
+ infos[o] = action
+ end
+ offsets[byte] = o
+ }
+ infomap.clear
+ if !min
+ min = max = 0
+ end
+
+ offsets_key = [min, max, offsets[min..max]]
+ if n = OffsetsMemo[offsets_key]
+ offsets_name = n
+ else
+ offsets_name = "#{name}_offsets"
+ OffsetsMemo[offsets_key] = offsets_name
+ size = bytes_code.length
+ bytes_code.insert_at_last(2+max-min+1,
+ "\#define #{offsets_name} #{size}\n" +
+ format_offsets(min,max,offsets) + "\n")
+ end
+
+ if n = InfosMemo[infos]
+ infos_name = n
+ else
+ infos_name = "#{name}_infos"
+ InfosMemo[infos] = infos_name
+
+ size = words_code.length
+ words_code.insert_at_last(infos.length,
+ "\#define #{infos_name} WORDINDEX2INFO(#{size})\n" +
+ format_infos(infos) + "\n")
+ end
+
+ size = words_code.length
+ words_code.insert_at_last(NUM_ELEM_BYTELOOKUP,
+ "\#define #{name} WORDINDEX2INFO(#{size})\n" +
+ <<"End" + "\n")
+ #{offsets_name},
+ #{infos_name},
+End
+ end
+
+ PreMemo = {}
+ NextName = "a"
+
+ def generate_node(name_hint=nil)
+ if n = PreMemo[@tree]
+ return n
+ end
+
+ table = Array.new(0x100, :invalid)
+ @tree.each {|branch|
+ byte_min, byte_max, child_tree = branch.byte_min, branch.byte_max, branch.child_tree
+ rest = ActionMap.new(child_tree)
+ if a = rest.empty_action
+ table.fill(a, byte_min..byte_max)
+ else
+ name_hint2 = nil
+ if name_hint
+ name_hint2 = "#{name_hint}_#{byte_min == byte_max ? '%02X' % byte_min : '%02Xto%02X' % [byte_min, byte_max]}"
+ end
+ v = "/*BYTE_LOOKUP*/" + rest.gennode(@bytes_code, @words_code, name_hint2)
+ table.fill(v, byte_min..byte_max)
+ end
+ }
+
+ if !name_hint
+ name_hint = "fun_" + NextName
+ NextName.succ!
+ end
+
+ PreMemo[@tree] = name_hint
+
+ generate_lookup_node(name_hint, table)
+ name_hint
+ end
+
+ def gennode(bytes_code, words_code, name_hint=nil)
+ @bytes_code = bytes_code
+ @words_code = words_code
+ name = generate_node(name_hint)
+ @bytes_code = nil
+ @words_code = nil
+ return name
+ end
+end
+
+def citrus_mskanji_cstomb(csid, index)
+ case csid
+ when 0
+ index
+ when 1
+ index + 0x80
+ when 2, 3
+ row = index >> 8
+ raise "invalid byte sequence" if row < 0x21
+ if csid == 3
+ if row <= 0x2F
+ offset = (row == 0x22 || row >= 0x26) ? 0xED : 0xF0
+ elsif row >= 0x4D && row <= 0x7E
+ offset = 0xCE
+ else
+ raise "invalid byte sequence"
+ end
+ else
+ raise "invalid byte sequence" if row > 0x97
+ offset = (row < 0x5F) ? 0x81 : 0xC1
+ end
+ col = index & 0xFF
+ raise "invalid byte sequence" if (col < 0x21 || col > 0x7E)
+
+ row -= 0x21
+ col -= 0x21
+ if (row & 1) == 0
+ col += 0x40
+ col += 1 if (col >= 0x7F)
+ else
+ col += 0x9F;
+ end
+ row = row / 2 + offset
+ (row << 8) | col
+ end.to_s(16)
+end
+
+def citrus_euc_cstomb(csid, index)
+ case csid
+ when 0x0000
+ index
+ when 0x8080
+ index | 0x8080
+ when 0x0080
+ index | 0x8E80
+ when 0x8000
+ index | 0x8F8080
+ end.to_s(16)
+end
+
+def citrus_stateless_iso_cstomb(csid, index)
+ (index | 0x8080 | (csid << 16)).to_s(16)
+end
+
+def citrus_cstomb(ces, csid, index)
+ case ces
+ when 'mskanji'
+ citrus_mskanji_cstomb(csid, index)
+ when 'euc'
+ citrus_euc_cstomb(csid, index)
+ when 'stateless_iso'
+ citrus_stateless_iso_cstomb(csid, index)
+ end
+end
+
+SUBDIR = %w/APPLE AST BIG5 CNS CP EBCDIC EMOJI GB GEORGIAN ISO646 ISO-8859 JIS KAZAKH KOI KS MISC TCVN/
+
+
+def citrus_decode_mapsrc(ces, csid, mapsrcs)
+ table = []
+ mapsrcs.split(',').each do |mapsrc|
+ path = [$srcdir]
+ mode = nil
+ if mapsrc.rindex(/UCS(?:@[A-Z]+)?/, 0)
+ mode = :from_ucs
+ from = mapsrc[$&.size+1..-1]
+ path << SUBDIR.find{|x| from.rindex(x, 0) }
+ else
+ mode = :to_ucs
+ path << SUBDIR.find{|x| mapsrc.rindex(x, 0) }
+ end
+ if /\bUCS@(BMP|SMP|SIP|TIP|SSP)\b/ =~ mapsrc
+ plane = {"BMP"=>0, "SMP"=>1, "SIP"=>2, "TIP"=>3, "SSP"=>14}[$1]
+ else
+ plane = 0
+ end
+ plane <<= 16
+ path << mapsrc.gsub(':', '@')
+ path = File.join(*path)
+ path << ".src"
+ path[path.rindex('/')] = '%'
+ STDOUT.puts 'load mapsrc %s' % path if VERBOSE_MODE > 1
+ open(path, 'rb') do |f|
+ f.each_line do |l|
+ break if /^BEGIN_MAP/ =~ l
+ end
+ f.each_line do |l|
+ next if /^\s*(?:#|$)/ =~ l
+ break if /^END_MAP/ =~ l
+ case mode
+ when :from_ucs
+ case l
+ when /0x(\w+)\s*-\s*0x(\w+)\s*=\s*INVALID/
+ # Citrus OOB_MODE
+ when /(0x\w+)\s*=\s*(0x\w+)/
+ table.push << [plane | $1.hex, citrus_cstomb(ces, csid, $2.hex)]
+ else
+ raise "unknown notation '%s'"% l.chomp
+ end
+ when :to_ucs
+ case l
+ when /(0x\w+)\s*=\s*(0x\w+)/
+ table.push << [citrus_cstomb(ces, csid, $1.hex), plane | $2.hex]
+ else
+ raise "unknown notation '%s'"% l.chomp
+ end
+ end
+ end
+ end
+ end
+ return table
+end
+
+def import_ucm(path)
+ to_ucs = []
+ from_ucs = []
+ File.foreach(File.join($srcdir, "ucm", path)) do |line|
+ uc, bs, fb = nil
+ if /^<U([0-9a-fA-F]+)>\s*([\+0-9a-fA-Fx\\]+)\s*\|(\d)/ =~ line
+ uc = $1.hex
+ bs = $2.delete('x\\')
+ fb = $3.to_i
+ next if uc < 128 && uc == bs.hex
+ elsif /^([<U0-9a-fA-F>+]+)\s*([\+0-9a-fA-Fx\\]+)\s*\|(\d)/ =~ line
+ uc = $1.scan(/[0-9a-fA-F]+>/).map(&:hex).pack("U*").unpack("H*")[0]
+ bs = $2.delete('x\\')
+ fb = $3.to_i
+ end
+ to_ucs << [bs, uc] if fb == 0 || fb == 3
+ from_ucs << [uc, bs] if fb == 0 || fb == 1
+ end
+ [to_ucs, from_ucs]
+end
+
+def encode_utf8(map)
+ r = []
+ map.each {|k, v|
+ # integer means UTF-8 encoded sequence.
+ k = [k].pack("U").unpack("H*")[0].upcase if Integer === k
+ v = [v].pack("U").unpack("H*")[0].upcase if Integer === v
+ r << [k,v]
+ }
+ r
+end
+
+UnspecifiedValidEncoding = Object.new
+
+def transcode_compile_tree(name, from, map, valid_encoding)
+ map = encode_utf8(map)
+ h = {}
+ map.each {|k, v|
+ h[k] = v unless h[k] # use first mapping
+ }
+ if valid_encoding.equal? UnspecifiedValidEncoding
+ valid_encoding = ValidEncoding.fetch(from)
+ end
+ if valid_encoding
+ am = ActionMap.merge2(h, {valid_encoding => :undef}) {|prefix, as1, as2|
+ a1 = as1.empty? ? nil : ActionMap.unambiguous_action(as1)
+ a2 = as2.empty? ? nil : ActionMap.unambiguous_action(as2)
+ if !a2
+ raise "invalid mapping: #{prefix}"
+ end
+ a1 || a2
+ }
+ else
+ am = ActionMap.parse(h)
+ end
+ h.clear
+
+ max_input = am.max_input_length
+ defined_name = am.gennode(TRANSCODE_GENERATED_BYTES_CODE, TRANSCODE_GENERATED_WORDS_CODE, name)
+ return defined_name, max_input
+end
+
+TRANSCODERS = []
+TRANSCODE_GENERATED_TRANSCODER_CODE = ''.dup
+
+def transcode_tbl_only(from, to, map, valid_encoding=UnspecifiedValidEncoding)
+ if VERBOSE_MODE > 1
+ if from.empty? || to.empty?
+ STDOUT.puts "converter for #{from.empty? ? to : from}"
+ else
+ STDOUT.puts "converter from #{from} to #{to}"
+ end
+ end
+ id_from = from.tr('^0-9A-Za-z', '_')
+ id_to = to.tr('^0-9A-Za-z', '_')
+ if from == "UTF-8"
+ tree_name = "to_#{id_to}"
+ elsif to == "UTF-8"
+ tree_name = "from_#{id_from}"
+ else
+ tree_name = "from_#{id_from}_to_#{id_to}"
+ end
+ real_tree_name, max_input = transcode_compile_tree(tree_name, from, map, valid_encoding)
+ return map, tree_name, real_tree_name, max_input
+end
+
+#
+# call-seq:
+# transcode_tblgen(from_name, to_name, map [, valid_encoding_check [, ascii_compatibility]]) -> ''
+#
+# Returns an empty string just in case the result is used somewhere.
+# Stores the actual product for later output with transcode_generated_code and
+# transcode_register_code.
+#
+# The first argument is a string that will be used for the source (from) encoding.
+# The second argument is a string that will be used for the target (to) encoding.
+#
+# The third argument is the actual data, a map represented as an array of two-element
+# arrays. Each element of the array stands for one character being converted. The
+# first element of each subarray is the code of the character in the source encoding,
+# the second element of each subarray is the code of the character in the target encoding.
+#
+# Each code (i.e. byte sequence) is represented as a string of hexadecimal characters
+# of even length. Codes can also be represented as integers (usually in the form Ox...),
+# in which case they are interpreted as Unicode codepoints encoded in UTF-8. So as
+# an example, 0x677E is the same as "E69DBE" (but somewhat easier to produce and check).
+#
+# In addition, the following symbols can also be used instead of actual codes in the
+# second element of a subarray:
+# :nomap (no mapping, just copy input to output), :nomap0 (same as :nomap, but low priority),
+# :undef (input code undefined in the destination encoding),
+# :invalid (input code is an invalid byte sequence in the source encoding),
+# :func_ii, :func_si, :func_io, :func_so (conversion by function with specific call
+# convention).
+#
+# The forth argument specifies the overall structure of the encoding. For examples,
+# see ValidEncoding below. This is used to cross-check the data in the third argument
+# and to automatically add :undef and :invalid mappings where necessary.
+#
+# The fifth argument gives the ascii-compatibility of the transcoding. See
+# rb_transcoder_asciicompat_type_t in transcode_data.h for details. In most
+# cases, this argument can be left out.
+#
+def transcode_tblgen(from, to, map, valid_encoding=UnspecifiedValidEncoding,
+ ascii_compatibility='asciicompat_converter')
+ map, tree_name, real_tree_name, max_input = transcode_tbl_only(from, to, map, valid_encoding)
+ transcoder_name = "rb_#{tree_name}"
+ TRANSCODERS << transcoder_name
+ input_unit_length = UnitLength[from]
+ max_output = map.map {|k,v| String === v ? v.length/2 : 1 }.max
+ transcoder_code = <<"End"
+static const rb_transcoder
+#{transcoder_name} = {
+ #{c_esc from}, #{c_esc to}, #{real_tree_name},
+ TRANSCODE_TABLE_INFO,
+ #{input_unit_length}, /* input_unit_length */
+ #{max_input}, /* max_input */
+ #{max_output}, /* max_output */
+ #{ascii_compatibility}, /* asciicompat_type */
+ 0, 0, 0, /* state_size, state_init, state_fini */
+ 0, 0, 0, 0,
+ 0, 0, 0
+};
+End
+ TRANSCODE_GENERATED_TRANSCODER_CODE << transcoder_code
+ ''
+end
+
+def transcode_generate_node(am, name_hint=nil)
+ STDOUT.puts "converter for #{name_hint}" if VERBOSE_MODE > 1
+ am.gennode(TRANSCODE_GENERATED_BYTES_CODE, TRANSCODE_GENERATED_WORDS_CODE, name_hint)
+ ''
+end
+
+def transcode_generated_code
+ TRANSCODE_GENERATED_BYTES_CODE.to_s +
+ TRANSCODE_GENERATED_WORDS_CODE.to_s +
+ "\#define TRANSCODE_TABLE_INFO " +
+ "#{OUTPUT_PREFIX}byte_array, #{TRANSCODE_GENERATED_BYTES_CODE.length}, " +
+ "#{OUTPUT_PREFIX}word_array, #{TRANSCODE_GENERATED_WORDS_CODE.length}, " +
+ "((int)sizeof(unsigned int))\n" +
+ TRANSCODE_GENERATED_TRANSCODER_CODE
+end
+
+def transcode_register_code
+ code = ''.dup
+ TRANSCODERS.each {|transcoder_name|
+ code << " rb_register_transcoder(&#{transcoder_name});\n"
+ }
+ code
+end
+
+UnitLength = {
+ 'UTF-16BE' => 2,
+ 'UTF-16LE' => 2,
+ 'UTF-32BE' => 4,
+ 'UTF-32LE' => 4,
+}
+UnitLength.default = 1
+
+ValidEncoding = {
+ '1byte' => '{00-ff}',
+ '2byte' => '{00-ff}{00-ff}',
+ '4byte' => '{00-ff}{00-ff}{00-ff}{00-ff}',
+ 'US-ASCII' => '{00-7f}',
+ 'UTF-8' => '{00-7f}
+ {c2-df}{80-bf}
+ e0{a0-bf}{80-bf}
+ {e1-ec}{80-bf}{80-bf}
+ ed{80-9f}{80-bf}
+ {ee-ef}{80-bf}{80-bf}
+ f0{90-bf}{80-bf}{80-bf}
+ {f1-f3}{80-bf}{80-bf}{80-bf}
+ f4{80-8f}{80-bf}{80-bf}',
+ 'UTF-16BE' => '{00-d7,e0-ff}{00-ff}
+ {d8-db}{00-ff}{dc-df}{00-ff}',
+ 'UTF-16LE' => '{00-ff}{00-d7,e0-ff}
+ {00-ff}{d8-db}{00-ff}{dc-df}',
+ 'UTF-32BE' => '0000{00-d7,e0-ff}{00-ff}
+ 00{01-10}{00-ff}{00-ff}',
+ 'UTF-32LE' => '{00-ff}{00-d7,e0-ff}0000
+ {00-ff}{00-ff}{01-10}00',
+ 'EUC-JP' => '{00-7f}
+ {a1-fe}{a1-fe}
+ 8e{a1-fe}
+ 8f{a1-fe}{a1-fe}',
+ 'CP51932' => '{00-7f}
+ {a1-fe}{a1-fe}
+ 8e{a1-fe}',
+ 'EUC-JIS-2004' => '{00-7f}
+ {a1-fe}{a1-fe}
+ 8e{a1-fe}
+ 8f{a1-fe}{a1-fe}',
+ 'Shift_JIS' => '{00-7f}
+ {81-9f,e0-fc}{40-7e,80-fc}
+ {a1-df}',
+ 'EUC-KR' => '{00-7f}
+ {a1-fe}{a1-fe}',
+ 'CP949' => '{00-7f}
+ {81-fe}{41-5a,61-7a,81-fe}',
+ 'Big5' => '{00-7f}
+ {81-fe}{40-7e,a1-fe}',
+ 'EUC-TW' => '{00-7f}
+ {a1-fe}{a1-fe}
+ 8e{a1-b0}{a1-fe}{a1-fe}',
+ 'GBK' => '{00-80}
+ {81-fe}{40-7e,80-fe}',
+ 'GB18030' => '{00-7f}
+ {81-fe}{40-7e,80-fe}
+ {81-fe}{30-39}{81-fe}{30-39}',
+}
+
+def ValidEncoding(enc)
+ ValidEncoding.fetch(enc)
+end
+
+def set_valid_byte_pattern(encoding, pattern_or_label)
+ pattern =
+ if ValidEncoding[pattern_or_label]
+ ValidEncoding[pattern_or_label]
+ else
+ pattern_or_label
+ end
+ if ValidEncoding[encoding] and ValidEncoding[encoding]!=pattern
+ raise ArgumentError, "trying to change valid byte pattern for encoding #{encoding} from #{ValidEncoding[encoding]} to #{pattern}"
+ end
+ ValidEncoding[encoding] = pattern
+end
+
+# the following may be used in different places, so keep them here for the moment
+set_valid_byte_pattern 'ASCII-8BIT', '1byte'
+set_valid_byte_pattern 'Windows-31J', 'Shift_JIS'
+set_valid_byte_pattern 'eucJP-ms', 'EUC-JP'
+
+def make_signature(filename, src)
+ "src=#{filename.dump}, len=#{src.length}, checksum=#{src.sum}"
+end
+
+if __FILE__ == $0
+ start_time = Time.now
+
+ output_filename = nil
+ verbose_mode = 0
+ force_mode = false
+
+ op = OptionParser.new
+ op.def_option("--help", "show help message") { puts op; exit 0 }
+ op.def_option("--verbose", "verbose mode, twice for more verbose") { verbose_mode += 1 }
+ op.def_option("--force", "force table generation") { force_mode = true }
+ op.def_option("--output=FILE", "specify output file") {|arg| output_filename = arg }
+ op.parse!
+
+ VERBOSE_MODE = verbose_mode
+
+ OUTPUT_FILENAME = output_filename
+ OUTPUT_PREFIX = output_filename ? File.basename(output_filename)[/\A[A-Za-z0-9_]*/] : "".dup
+ OUTPUT_PREFIX.sub!(/\A_+/, '')
+ OUTPUT_PREFIX.sub!(/_*\z/, '_')
+
+ TRANSCODE_GENERATED_BYTES_CODE = ArrayCode.new("unsigned char", "#{OUTPUT_PREFIX}byte_array")
+ TRANSCODE_GENERATED_WORDS_CODE = ArrayCode.new("unsigned int", "#{OUTPUT_PREFIX}word_array")
+
+ arg = ARGV.shift
+ $srcdir = File.dirname(arg)
+ $:.unshift $srcdir unless $:.include? $srcdir
+ src = File.read(arg)
+ src.force_encoding("ascii-8bit") if src.respond_to? :force_encoding
+ this_script = File.read(__FILE__)
+ this_script.force_encoding("ascii-8bit") if this_script.respond_to? :force_encoding
+
+ base_signature = "/* autogenerated. */\n".dup
+ base_signature << "/* #{make_signature(File.basename(__FILE__), this_script)} */\n"
+ base_signature << "/* #{make_signature(File.basename(arg), src)} */\n"
+
+ if !force_mode && output_filename && File.readable?(output_filename)
+ old_signature = File.open(output_filename) {|f| f.gets("").chomp }
+ chk_signature = base_signature.dup
+ old_signature.each_line {|line|
+ if %r{/\* src="([0-9a-z_.-]+)",} =~ line
+ name = $1
+ next if name == File.basename(arg) || name == File.basename(__FILE__)
+ path = File.join($srcdir, name)
+ if File.readable? path
+ chk_signature << "/* #{make_signature(name, File.read(path))} */\n"
+ end
+ end
+ }
+ if old_signature == chk_signature
+ now = Time.now
+ File.utime(now, now, output_filename)
+ STDOUT.puts "already up-to-date: #{output_filename}" if VERBOSE_MODE > 0
+ exit
+ end
+ end
+
+ if VERBOSE_MODE > 0
+ if output_filename
+ STDOUT.puts "generating #{output_filename} ..."
+ end
+ end
+
+ libs1 = $".dup
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
+ erb = ERB.new(src, trim_mode: '%')
+ else
+ erb = ERB.new(src, nil, '%')
+ end
+ erb.filename = arg
+ erb_result = erb.result(binding)
+ libs2 = $".dup
+
+ libs = libs2 - libs1
+ lib_sigs = ''.dup
+ libs.each {|lib|
+ lib = File.basename(lib)
+ path = File.join($srcdir, lib)
+ if File.readable? path
+ lib_sigs << "/* #{make_signature(lib, File.read(path))} */\n"
+ end
+ }
+
+ result = ''.dup
+ result << base_signature
+ result << lib_sigs
+ result << "\n"
+ result << erb_result
+ result << "\n"
+
+ if output_filename
+ new_filename = output_filename + ".new"
+ FileUtils.mkdir_p(File.dirname(output_filename))
+ File.open(new_filename, "wb") {|f| f << result }
+ File.rename(new_filename, output_filename)
+ tms = Process.times
+ elapsed = Time.now - start_time
+ STDOUT.puts "done. (#{'%.2f' % tms.utime}user #{'%.2f' % tms.stime}system #{'%.2f' % elapsed}elapsed)" if VERBOSE_MODE > 1
+ else
+ print result
+ end
+end
diff --git a/tool/transform_mjit_header.rb b/tool/transform_mjit_header.rb
new file mode 100644
index 0000000000..2359ceab7c
--- /dev/null
+++ b/tool/transform_mjit_header.rb
@@ -0,0 +1,326 @@
+# Copyright (C) 2017 Vladimir Makarov, <vmakarov@redhat.com>
+# This is a script to transform functions to static inline.
+# Usage: transform_mjit_header.rb <c-compiler> <header file> <out>
+
+require 'fileutils'
+require 'tempfile'
+
+PROGRAM = File.basename($0, ".*")
+
+module MJITHeader
+ ATTR_VALUE_REGEXP = /[^()]|\([^()]*\)/
+ ATTR_REGEXP = /__attribute__\s*\(\(#{ATTR_VALUE_REGEXP}*\)\)/
+ # Example:
+ # VALUE foo(int bar)
+ # VALUE __attribute__ ((foo)) bar(int baz)
+ # __attribute__ ((foo)) VALUE bar(int baz)
+ FUNC_HEADER_REGEXP = /\A[^\[{(]*(\s*#{ATTR_REGEXP})*[^\[{(]*\((#{ATTR_REGEXP}|[^()])*\)(\s*#{ATTR_REGEXP})*\s*/
+ TARGET_NAME_REGEXP = /\A(rb|ruby|vm|insn|attr|Init)_/
+
+ # Predefined macros for compilers which are already supported by MJIT.
+ # We're going to support cl.exe too (WIP) but `cl.exe -E` can't produce macro.
+ SUPPORTED_CC_MACROS = [
+ '__GNUC__', # gcc
+ '__clang__', # clang
+ ]
+
+ # These macros are relied on this script's transformation
+ PREFIXED_MACROS = [
+ 'ALWAYS_INLINE',
+ 'COLDFUNC',
+ 'inline',
+ 'RBIMPL_ATTR_COLD',
+ ]
+
+ # For MinGW's ras.h. Those macros have its name in its definition and can't be preprocessed multiple times.
+ RECURSIVE_MACROS = %w[
+ RASCTRYINFO
+ RASIPADDR
+ ]
+
+ IGNORED_FUNCTIONS = [
+ 'rb_vm_search_method_slowpath', # This increases the time to compile when inlined. So we use it as external function.
+ 'rb_equal_opt', # Not used from VM and not compilable
+ ]
+
+ ALWAYS_INLINED_FUNCTIONS = [
+ 'vm_opt_plus',
+ 'vm_opt_minus',
+ 'vm_opt_mult',
+ 'vm_opt_div',
+ 'vm_opt_mod',
+ 'vm_opt_neq',
+ 'vm_opt_lt',
+ 'vm_opt_le',
+ 'vm_opt_gt',
+ 'vm_opt_ge',
+ 'vm_opt_ltlt',
+ 'vm_opt_and',
+ 'vm_opt_or',
+ 'vm_opt_aref',
+ 'vm_opt_aset',
+ 'vm_opt_aref_with',
+ 'vm_opt_aset_with',
+ 'vm_opt_not',
+ ]
+
+ COLD_FUNCTIONS = %w[
+ setup_parameters_complex
+ vm_call_iseq_setup
+ vm_call_iseq_setup_2
+ vm_call_iseq_setup_tailcall
+ vm_call_method_each_type
+ vm_ic_update
+ ]
+
+ # Return start..stop of last decl in CODE ending STOP
+ def self.find_decl(code, stop)
+ level = 0
+ i = stop
+ while i = code.rindex(/[;{}]/, i)
+ if level == 0 && stop != i && decl_found?($&, i)
+ return decl_start($&, i)..stop
+ end
+ case $&
+ when '}'
+ level += 1
+ when '{'
+ level -= 1
+ end
+ i -= 1
+ end
+ nil
+ end
+
+ def self.decl_found?(code, i)
+ i == 0 || code == ';' || code == '}'
+ end
+
+ def self.decl_start(code, i)
+ if i == 0 && code != ';' && code != '}'
+ 0
+ else
+ i + 1
+ end
+ end
+
+ # Given DECL return the name of it, nil if failed
+ def self.decl_name_of(decl)
+ ident_regex = /\w+/
+ decl = decl.gsub(/^#.+$/, '') # remove macros
+ reduced_decl = decl.gsub(ATTR_REGEXP, '') # remove attributes
+ su1_regex = /{[^{}]*}/
+ su2_regex = /{([^{}]|#{su1_regex})*}/
+ su3_regex = /{([^{}]|#{su2_regex})*}/ # 3 nested structs/unions is probably enough
+ reduced_decl.gsub!(su3_regex, '') # remove structs/unions in the header
+ id_seq_regex = /\s*(?:#{ident_regex}(?:\s+|\s*[*]+\s*))*/
+ # Process function header:
+ match = /\A#{id_seq_regex}(?<name>#{ident_regex})\s*\(/.match(reduced_decl)
+ return match[:name] if match
+ # Process non-function declaration:
+ reduced_decl.gsub!(/\s*=[^;]+(?=;)/, '') # remove initialization
+ match = /#{id_seq_regex}(?<name>#{ident_regex})/.match(reduced_decl);
+ return match[:name] if match
+ nil
+ end
+
+ # Return true if CC with CFLAGS compiles successfully the current code.
+ # Use STAGE in the message in case of a compilation failure
+ def self.check_code!(code, cc, cflags, stage)
+ with_code(code) do |path|
+ cmd = "#{cc} #{cflags} #{path}"
+ out = IO.popen(cmd, err: [:child, :out], &:read)
+ unless $?.success?
+ STDERR.puts "error in #{stage} header file:\n#{out}"
+ exit false
+ end
+ end
+ end
+
+ # Remove unpreprocessable macros
+ def self.remove_harmful_macros!(code)
+ code.gsub!(/^#define #{Regexp.union(RECURSIVE_MACROS)} .*$/, '')
+ end
+
+ # -dD outputs those macros, and it produces redefinition warnings or errors
+ # This assumes common.mk passes `-DMJIT_HEADER` first when it creates rb_mjit_header.h.
+ def self.remove_predefined_macros!(code)
+ code.sub!(/\A(#define [^\n]+|\n)*(#define MJIT_HEADER 1\n)/, '\2')
+ end
+
+ # Return [macro, others]. But others include PREFIXED_MACROS to be used in code.
+ def self.separate_macro_and_code(code)
+ code.lines.partition do |l|
+ l.start_with?('#') && PREFIXED_MACROS.all? { |m| !l.start_with?("#define #{m}") }
+ end.map! { |lines| lines.join('') }
+ end
+
+ def self.write(code, out)
+ # create with strict permission, then will install proper
+ # permission
+ FileUtils.mkdir_p(File.dirname(out), mode: 0700)
+ File.binwrite("#{out}.new", code, perm: 0600)
+ FileUtils.mv("#{out}.new", out)
+ end
+
+ # Note that this checks runruby. This conservatively covers platform names.
+ def self.windows?
+ RUBY_PLATFORM =~ /mswin|mingw|msys/
+ end
+
+ def self.cl_exe?(cc)
+ cc =~ /\Acl(\z| |\.exe)/
+ end
+
+ # If code has macro which only supported compilers predefine, return true.
+ def self.supported_header?(code)
+ SUPPORTED_CC_MACROS.any? { |macro| code =~ /^#\s*define\s+#{Regexp.escape(macro)}\b/ }
+ end
+
+ # This checks if syntax check outputs one of the following messages.
+ # "error: conflicting types for 'restrict'"
+ # "error: redefinition of parameter 'restrict'"
+ # If it's true, this script regards platform as AIX or Solaris and adds -std=c99 as workaround.
+ def self.conflicting_types?(code, cc, cflags)
+ with_code(code) do |path|
+ cmd = "#{cc} #{cflags} #{path}"
+ out = IO.popen(cmd, err: [:child, :out], &:read)
+ !$?.success? &&
+ (out.match?(/error: conflicting types for '[^']+'/) ||
+ out.match?(/error: redefinition of parameter '[^']+'/))
+ end
+ end
+
+ def self.with_code(code)
+ # for `system_header` pragma which can't be in the main file.
+ Tempfile.open(['', '.h'], mode: File::BINARY) do |f|
+ f.puts code
+ f.close
+ Tempfile.open(['', '.c'], mode: File::BINARY) do |c|
+ c.puts <<SRC
+#include "#{f.path}"
+SRC
+ c.close
+ return yield(c.path)
+ end
+ end
+ end
+ private_class_method :with_code
+end
+
+if ARGV.size != 3
+ abort "Usage: #{$0} <c-compiler> <header file> <out>"
+end
+
+if STDOUT.tty?
+ require_relative 'lib/colorize'
+ color = Colorize.new
+end
+cc = ARGV[0]
+code = File.binread(ARGV[1]) # Current version of the header file.
+outfile = ARGV[2]
+if MJITHeader.cl_exe?(cc)
+ cflags = '-DMJIT_HEADER -Zs'
+else
+ cflags = '-S -DMJIT_HEADER -fsyntax-only -Werror=implicit-function-declaration -Werror=implicit-int -Wfatal-errors'
+end
+
+if !MJITHeader.cl_exe?(cc) && !MJITHeader.supported_header?(code)
+ puts "This compiler (#{cc}) looks not supported for MJIT. Giving up to generate MJIT header."
+ MJITHeader.write("#error MJIT does not support '#{cc}' yet", outfile)
+ exit
+end
+
+MJITHeader.remove_predefined_macros!(code)
+
+if MJITHeader.windows? # transformation is broken with Windows headers for now
+ MJITHeader.remove_harmful_macros!(code)
+ MJITHeader.check_code!(code, cc, cflags, 'initial')
+ puts "\nSkipped transforming external functions to static on Windows."
+ MJITHeader.write(code, outfile)
+ exit
+end
+
+macro, code = MJITHeader.separate_macro_and_code(code) # note: this does not work on MinGW
+code = <<header + code
+#ifdef __GNUC__
+# pragma GCC system_header
+#endif
+header
+code_to_check = "#{code}#{macro}" # macro should not affect code again
+
+if MJITHeader.conflicting_types?(code_to_check, cc, cflags)
+ cflags = "#{cflags} -std=c99" # For AIX gcc
+end
+
+# Check initial file correctness in the manner of final output.
+MJITHeader.check_code!(code_to_check, cc, cflags, 'initial')
+
+stop_pos = -1
+extern_names = []
+transform_logs = Hash.new { |h, k| h[k] = [] }
+
+# This loop changes function declarations to static inline.
+while (decl_range = MJITHeader.find_decl(code, stop_pos))
+ stop_pos = decl_range.begin - 1
+ decl = code[decl_range]
+ decl_name = MJITHeader.decl_name_of(decl)
+
+ if MJITHeader::IGNORED_FUNCTIONS.include?(decl_name) && /#{MJITHeader::FUNC_HEADER_REGEXP}{/.match(decl)
+ transform_logs[:def_to_decl] << decl_name
+ code[decl_range] = decl.sub(/{.+}/m, ';')
+ elsif MJITHeader::COLD_FUNCTIONS.include?(decl_name) && match = /#{MJITHeader::FUNC_HEADER_REGEXP}{/.match(decl)
+ header = match[0].sub(/{\z/, '').strip
+ header = "static #{header.sub(/\A((static|inline) )+/, '')}"
+ decl[match.begin(0)...match.end(0)] = '{' # remove header
+ code[decl_range] = "\nCOLDFUNC #{header} #{decl}"
+ elsif MJITHeader::ALWAYS_INLINED_FUNCTIONS.include?(decl_name) && match = /#{MJITHeader::FUNC_HEADER_REGEXP}{/.match(decl)
+ header = match[0].sub(/{\z/, '').strip
+ header = "static inline #{header.sub(/\A((static|inline) )+/, '')}"
+ decl[match.begin(0)...match.end(0)] = '{' # remove header
+ code[decl_range] = "\nALWAYS_INLINE(#{header});\n#{header} #{decl}"
+ elsif extern_names.include?(decl_name) && (decl =~ /#{MJITHeader::FUNC_HEADER_REGEXP};/)
+ decl.sub!(/(extern|static|inline) /, ' ')
+ unless decl_name =~ /\Aattr_\w+_\w+\z/ # skip too-many false-positive warnings in insns_info.inc.
+ transform_logs[:static_inline_decl] << decl_name
+ end
+
+ code[decl_range] = "static inline #{decl}"
+ elsif (match = /#{MJITHeader::FUNC_HEADER_REGEXP}{/.match(decl)) && (header = match[0]) !~ /static/
+ unless decl_name.match(MJITHeader::TARGET_NAME_REGEXP)
+ transform_logs[:skipped] << decl_name
+ next
+ end
+
+ extern_names << decl_name
+ decl[match.begin(0)...match.end(0)] = ''
+
+ if decl =~ /\bstatic\b/
+ abort "#{PROGRAM}: a static decl was found inside external definition #{decl_name.dump}"
+ end
+
+ header.sub!(/(extern|inline) /, ' ')
+ unless decl_name =~ /\Aattr_\w+_\w+\z/ # skip too-many false-positive warnings in insns_info.inc.
+ transform_logs[:static_inline_def] << decl_name
+ end
+ code[decl_range] = "static inline #{header}#{decl}"
+ end
+end
+
+code << macro
+
+# Check the final file correctness
+MJITHeader.check_code!(code, cc, cflags, 'final')
+
+MJITHeader.write(code, outfile)
+
+messages = {
+ def_to_decl: 'changing definition to declaration',
+ static_inline_def: 'making external definition static inline',
+ static_inline_decl: 'making declaration static inline',
+ skipped: 'SKIPPED to transform',
+}
+transform_logs.each do |key, decl_names|
+ decl_names = decl_names.map { |s| color.bold(s) } if color
+ puts("#{PROGRAM}: #{messages.fetch(key)}: #{decl_names.join(', ')}")
+end
diff --git a/tool/travis_retry.sh b/tool/travis_retry.sh
new file mode 100755
index 0000000000..9b79c56550
--- /dev/null
+++ b/tool/travis_retry.sh
@@ -0,0 +1,13 @@
+#!/bin/sh -eu
+# The modified version of `travis_retry` to support custom backoffs, which is used by .travis.yml.
+# https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/bash/travis_retry.bash
+
+for sleep in 0 ${WAITS:- 1 25 100}; do
+ sleep "$sleep"
+
+ echo "+ $@"
+ if "$@"; then
+ exit 0
+ fi
+done
+exit 1
diff --git a/tool/travis_wait.sh b/tool/travis_wait.sh
new file mode 100755
index 0000000000..471b765df8
--- /dev/null
+++ b/tool/travis_wait.sh
@@ -0,0 +1,18 @@
+#!/bin/bash -eu
+# The modified version of `travis_wait` to output a log as the command goes.
+# https://github.com/travis-ci/travis-ci/issues/4190#issuecomment-353342526
+
+# Produce an output log every 9 minutes as the timeout without output is 10
+# minutes. A job finishes with a timeout if it takes longer than 50 minutes.
+# https://docs.travis-ci.com/user/customizing-the-build#build-timeouts
+while sleep 9m; do
+ # Print message with bash variable SECONDS.
+ echo "====[ $SECONDS seconds still running ]===="
+done &
+
+echo "+ $@"
+"$@"
+
+jobs
+kill %1
+exit 0
diff --git a/tool/update-bundled_gems.rb b/tool/update-bundled_gems.rb
new file mode 100755
index 0000000000..5b9c6b6974
--- /dev/null
+++ b/tool/update-bundled_gems.rb
@@ -0,0 +1,20 @@
+#!ruby -pla
+BEGIN {
+ require 'rubygems'
+}
+unless /^[^#]/ !~ (gem = $F[0])
+ (gem, src), = Gem::SpecFetcher.fetcher.detect(:latest) {|s|
+ s.platform == "ruby" && s.name == gem
+ }
+ gem = src.fetch_spec(gem)
+ uri = gem.metadata["source_code_uri"] || gem.homepage
+ uri = uri.sub(%r[\Ahttps://github\.com/[^/]+/[^/]+\K/tree/.*], "").chomp(".git")
+ if $F[3]
+ if $F[3].include?($F[1])
+ $F[3][$F[1]] = gem.version.to_s
+ elsif Gem::Version.new($F[1]) != gem.version and /\A\h+\z/ =~ $F[3]
+ $F[3..-1] = []
+ end
+ end
+ $_ = [gem.name, gem.version, uri, *$F[3..-1]].join(" ")
+end
diff --git a/tool/update-deps b/tool/update-deps
new file mode 100755
index 0000000000..2348b36e33
--- /dev/null
+++ b/tool/update-deps
@@ -0,0 +1,650 @@
+#!/usr/bin/ruby
+
+# tool/update-deps verify makefile dependencies.
+
+# Requirements:
+# gcc 4.5 (for -save-temps=obj option)
+# GNU make (for -p option)
+#
+# Warning: ccache (and similar tools) must be disabled for
+# -save-temps=obj to work properly.
+#
+# Usage:
+# 1. Compile ruby with -save-temps=obj option.
+# Ex. ./configure debugflags='-save-temps=obj -g' && make all golf
+# 2. Run tool/update-deps to show dependency problems.
+# Ex. ./ruby tool/update-deps
+# 3. Use --fix to fix makefiles.
+# Ex. ./ruby tool/update-deps --fix
+#
+# Other usages:
+# * Fix makefiles using previously detected dependency problems
+# Ex. ruby tool/update-deps --actual-fix [file]
+# "ruby tool/update-deps --fix" is the same as "ruby tool/update-deps | ruby tool/update-deps --actual-fix".
+
+require 'optparse'
+require 'stringio'
+require 'pathname'
+require 'open3'
+require 'pp'
+
+# When out-of-place build, files may be built in source directory or
+# build directory.
+# Some files are always built in the source directory.
+# Some files are always built in the build directory.
+# Some files are built in the source directory for tarball but build directory for repository (svn).
+
+=begin
+How to build test directories.
+
+VER=2.2.0
+REV=48577
+tar xf ruby-$VER-r$REV.tar.xz
+cp -a ruby-$VER-r$REV tarball_source_dir_original
+mv ruby-$VER-r$REV tarball_source_dir_after_build
+svn co -q -r$REV https://svn.ruby-lang.org/repos/ruby/trunk ruby
+(cd ruby; autoconf)
+cp -a ruby repo_source_dir_original
+mv ruby repo_source_dir_after_build
+mkdir tarball_build_dir repo_build_dir tarball_install_dir repo_install_dir
+(cd tarball_build_dir; ../tarball_source_dir_after_build/configure --prefix=$(cd ../tarball_install_dir; pwd) && make all golf install) > tarball.log 2>&1
+(cd repo_build_dir; ../repo_source_dir_after_build/configure --prefix=$(cd ../repo_install_dir; pwd) && make all golf install) > repo.log 2>&1
+ruby -rpp -rfind -e '
+ds = %w[
+ repo_source_dir_original
+ repo_source_dir_after_build
+ repo_build_dir
+ tarball_source_dir_original
+ tarball_source_dir_after_build
+ tarball_build_dir
+]
+files = {}
+ds.each {|d|
+ files[d] = {}
+ Dir.chdir(d) { Find.find(".") {|f| files[d][f] = true if %r{\.(c|h|inc|dmyh)\z} =~ f } }
+}
+result = {}
+files_union = files.values.map {|h| h.keys }.flatten.uniq.sort
+files_union.each {|f|
+ k = files.map {|d,h| h[f] ? d : nil }.compact.sort
+ next if k == %w[repo_source_dir_after_build repo_source_dir_original tarball_source_dir_after_build tarball_source_dir_original]
+ next if k == %w[repo_build_dir tarball_build_dir] && File.basename(f) == "extconf.h"
+ result[k] ||= []
+ result[k] << f
+}
+result.each {|k,v|
+ k.each {|d|
+ puts d
+ }
+ v.each {|f|
+ puts " " + f.sub(%r{\A\./}, "")
+ }
+ puts
+}
+' | tee compare.log
+=end
+
+# Files built in the source directory.
+# They can be referenced as $(top_srcdir)/filename.
+# % ruby -e 'def g(d) Dir.chdir(d) { Dir["**/*.{c,h,inc,dmyh}"] } end; puts((g("repo_source_dir_after_build") - g("repo_source_dir_original")).sort)'
+FILES_IN_SOURCE_DIRECTORY = %w[
+ revision.h
+]
+
+# Files built in the build directory (except extconf.h).
+# They can be referenced as $(topdir)/filename.
+# % ruby -e 'def g(d) Dir.chdir(d) { Dir["**/*.{c,h,inc,dmyh}"] } end; puts(g("tarball_build_dir").reject {|f| %r{/extconf.h\z} =~ f }.sort)'
+FILES_IN_BUILD_DIRECTORY = %w[
+ encdb.h
+ ext/etc/constdefs.h
+ ext/socket/constdefs.c
+ ext/socket/constdefs.h
+ probes.h
+ transdb.h
+ verconf.h
+]
+
+# They are built in the build directory if the source is obtained from the repository.
+# However they are pre-built for tarball and they exist in the source directory extracted from the tarball.
+# % ruby -e 'def g(d) Dir.chdir(d) { Dir["**/*.{c,h,inc,dmyh}"] } end; puts((g("repo_build_dir") & g("tarball_source_dir_original")).sort)'
+FILES_NEED_VPATH = %w[
+ ext/rbconfig/sizeof/sizes.c
+ ext/ripper/eventids1.c
+ ext/ripper/eventids2table.c
+ ext/ripper/ripper.c
+ golf_prelude.c
+ id.c
+ id.h
+ insns.inc
+ insns_info.inc
+ known_errors.inc
+ lex.c
+ miniprelude.c
+ mjit_compile.inc
+ newline.c
+ node_name.inc
+ opt_sc.inc
+ optinsn.inc
+ optunifs.inc
+ parse.c
+ parse.h
+ probes.dmyh
+ vm.inc
+ vmtc.inc
+
+ enc/trans/big5.c
+ enc/trans/chinese.c
+ enc/trans/emoji.c
+ enc/trans/emoji_iso2022_kddi.c
+ enc/trans/emoji_sjis_docomo.c
+ enc/trans/emoji_sjis_kddi.c
+ enc/trans/emoji_sjis_softbank.c
+ enc/trans/escape.c
+ enc/trans/gb18030.c
+ enc/trans/gbk.c
+ enc/trans/iso2022.c
+ enc/trans/japanese.c
+ enc/trans/japanese_euc.c
+ enc/trans/japanese_sjis.c
+ enc/trans/korean.c
+ enc/trans/single_byte.c
+ enc/trans/utf8_mac.c
+ enc/trans/utf_16_32.c
+]
+
+# Multiple files with same filename.
+# It is not good idea to refer them using VPATH.
+# Files in FILES_SAME_NAME_INC is referenced using $(hdrdir).
+# Files in FILES_SAME_NAME_TOP is referenced using $(top_srcdir).
+
+FILES_SAME_NAME_INC = %w[
+ include/ruby.h
+ include/ruby/ruby.h
+ include/ruby/version.h
+]
+
+FILES_SAME_NAME_TOP = %w[
+ version.h
+]
+
+# Other source files exist in the source directory.
+
+def in_makefile(target, source)
+ target = target.to_s
+ source = source.to_s
+ case target
+ when %r{\A[^/]*\z}, %r{\Acoroutine/}
+ target2 = "#{target.sub(/\.o\z/, '.$(OBJEXT)')}"
+ case source
+ when *FILES_IN_SOURCE_DIRECTORY then source2 = "$(top_srcdir)/#{source}"
+ when *FILES_IN_BUILD_DIRECTORY then source2 = "{$(VPATH)}#{source}" # VPATH is not used now but it may changed in future.
+ when *FILES_NEED_VPATH then source2 = "{$(VPATH)}#{source}"
+ when *FILES_SAME_NAME_INC then source2 = "$(hdrdir)/#{source.sub(%r{\Ainclude/},'')}"
+ when *FILES_SAME_NAME_TOP then source2 = "$(top_srcdir)/#{source}"
+ when 'thread_pthread.c' then source2 = '{$(VPATH)}thread_$(THREAD_MODEL).c'
+ when 'thread_pthread.h' then source2 = '{$(VPATH)}thread_$(THREAD_MODEL).h'
+ when %r{\A[^/]*\z} then source2 = "{$(VPATH)}#{File.basename source}"
+ when %r{\A\.ext/include/[^/]+/ruby/} then source2 = "{$(VPATH)}#{$'}"
+ when %r{\Ainclude/ruby/} then source2 = "{$(VPATH)}#{$'}"
+ when %r{\Aenc/} then source2 = "{$(VPATH)}#{$'}"
+ when %r{\Amissing/} then source2 = "{$(VPATH)}#{$'}"
+ when %r{\Accan/} then source2 = "$(CCAN_DIR)/#{$'}"
+ when %r{\Adefs/} then source2 = "{$(VPATH)}#{source}"
+ when %r{\Acoroutine/} then source2 = "{$(VPATH)}$(COROUTINE_H)"
+ else source2 = "$(top_srcdir)/#{source}"
+ end
+ ["common.mk", target2, source2]
+ when %r{\Aenc/}
+ target2 = "#{target.sub(/\.o\z/, '.$(OBJEXT)')}"
+ case source
+ when *FILES_IN_SOURCE_DIRECTORY then source2 = "$(top_srcdir)/#{source}"
+ when *FILES_IN_BUILD_DIRECTORY then source2 = source
+ when *FILES_NEED_VPATH then source2 = source
+ when *FILES_SAME_NAME_INC then source2 = "$(hdrdir)/#{source.sub(%r{\Ainclude/},'')}"
+ when *FILES_SAME_NAME_TOP then source2 = "$(top_srcdir)/#{source}"
+ when %r{\A\.ext/include/[^/]+/ruby/} then source2 = $'
+ when %r{\Ainclude/ruby/} then source2 = $'
+ when %r{\Aenc/unicode/[\d.]+/} then source2 = '$(UNICODE_HDR_DIR)/' + $'
+ when %r{\Aenc/} then source2 = source
+ else source2 = "$(top_srcdir)/#{source}"
+ end
+ ["enc/depend", target2, source2]
+ when %r{\Aext/}
+ targetdir = File.dirname(target)
+ unless File.exist?("#{targetdir}/extconf.rb")
+ warn "warning: not found: #{targetdir}/extconf.rb"
+ end
+ target2 = File.basename(target)
+ relpath = Pathname(source).relative_path_from(Pathname(target).dirname).to_s
+ case source
+ when *FILES_IN_SOURCE_DIRECTORY then source2 = "$(top_srcdir)/#{source}"
+ when *FILES_IN_BUILD_DIRECTORY then source2 = relpath
+ when *FILES_NEED_VPATH then source2 = "{$(VPATH)}#{File.basename source}"
+ when *FILES_SAME_NAME_INC then source2 = "$(hdrdir)/#{source.sub(%r{\Ainclude/},'')}"
+ when *FILES_SAME_NAME_TOP then source2 = "$(top_srcdir)/#{source}"
+ when %r{\A\.ext/include/[^/]+/ruby/} then source2 = "$(arch_hdrdir)/ruby/#{$'}"
+ when %r{\Ainclude/} then source2 = "$(hdrdir)/#{$'}"
+ when %r{\A#{Regexp.escape targetdir}/extconf\.h\z} then source2 = "$(RUBY_EXTCONF_H)"
+ when %r{\A#{Regexp.escape targetdir}/} then source2 = $'
+ when %r{\A#{Regexp.escape File.dirname(targetdir)}/} then source2 = "$(srcdir)/../#{$'}"
+ else source2 = "$(top_srcdir)/#{source}"
+ end
+ ["#{File.dirname(target)}/depend", target2, source2]
+ else
+ raise "unexpected target: #{target}"
+ end
+end
+
+DEPENDENCIES_SECTION_START_MARK = "\# AUTOGENERATED DEPENDENCIES START\n"
+DEPENDENCIES_SECTION_END_MARK = "\# AUTOGENERATED DEPENDENCIES END\n"
+
+def init_global
+ ENV['LC_ALL'] = 'C'
+ if mkflag0 = ENV['GNUMAKEFLAGS'] and (mkflag = mkflag0.sub(/(\A|\s+)-j\d*(?=\s+|\z)/, '')) != mkflag0
+ mkflag.strip!
+ ENV['GNUMAKEFLAGS'] = mkflag
+ end
+
+ $opt_fix = false
+ $opt_a = false
+ $opt_actual_fix = false
+ $i_not_found = false
+end
+
+def optionparser
+ op = OptionParser.new
+ op.banner = 'Usage: ruby tool/update-deps'
+ op.def_option('-a', 'show valid dependencies') { $opt_a = true }
+ op.def_option('--fix') { $opt_fix = true }
+ op.def_option('--actual-fix') { $opt_actual_fix = true }
+ op
+end
+
+def read_make_deps(cwd)
+ dependencies = {}
+ make_p, make_p_stderr, make_p_status = Open3.capture3("make -p all miniruby exe/ruby golf")
+ File.open('update-deps.make.out.log', 'w') {|f| f.print make_p }
+ File.open('update-deps.make.err.log', 'w') {|f| f.print make_p_stderr }
+ if !make_p_status.success?
+ puts make_p_stderr
+ raise "make failed"
+ end
+ dirstack = [cwd]
+ curdir = nil
+ make_p.scan(%r{Entering\ directory\ ['`](.*)'|
+ ^\#\ (GNU\ Make)\ |
+ ^CURDIR\ :=\ (.*)|
+ ^([/0-9a-zA-Z._-]+):(.*)\n((?:\#.*\n)*)|
+ ^\#\ (Finished\ Make\ data\ base\ on)\ |
+ Leaving\ directory\ ['`](.*)'}x) {
+ directory_enter = $1
+ data_base_start = $2
+ data_base_curdir = $3
+ rule_target = $4
+ rule_sources = $5
+ rule_desc = $6
+ data_base_end = $7
+ directory_leave = $8
+ #p $~
+ if directory_enter
+ enter_dir = Pathname(directory_enter)
+ #p [:enter, enter_dir]
+ dirstack.push enter_dir
+ elsif data_base_start
+ curdir = nil
+ elsif data_base_curdir
+ curdir = Pathname(data_base_curdir)
+ elsif rule_target && rule_sources && rule_desc &&
+ /Modification time never checked/ !~ rule_desc # This pattern match eliminates rules which VPATH is not expanded.
+ target = rule_target
+ deps = rule_sources
+ deps = deps.scan(%r{[/0-9a-zA-Z._-]+})
+ deps.delete_if {|dep| /\.time\z/ =~ dep} # skip timestamp
+ next if /\.o\z/ !~ target.to_s
+ next if /\A\./ =~ target.to_s # skip rules such as ".c.o"
+ #p [curdir, target, deps]
+ dir = curdir || dirstack.last
+ dependencies[dir + target] ||= []
+ dependencies[dir + target] |= deps.map {|dep| dir + dep }
+ elsif data_base_end
+ curdir = nil
+ elsif directory_leave
+ leave_dir = Pathname(directory_leave)
+ #p [:leave, leave_dir]
+ if leave_dir != dirstack.last
+ warn "unexpected leave_dir : #{dirstack.last.inspect} != #{leave_dir.inspect}"
+ end
+ dirstack.pop
+ end
+ }
+ dependencies
+end
+
+#def guess_compiler_wd(filename, hint0)
+# hint = hint0
+# begin
+# guess = hint + filename
+# if guess.file?
+# return hint
+# end
+# hint = hint.parent
+# end while hint.to_s != '.'
+# raise ArgumentError, "can not find #{filename} (hint: #{hint0})"
+#end
+
+def read_single_cc_deps(path_i, cwd)
+ files = {}
+ compiler_wd = nil
+ path_i.each_line {|line|
+ next if /\A\# \d+ "(.*)"/ !~ line
+ dep = $1
+ next if %r{\A<.*>\z} =~ dep # omit <command-line>, etc.
+ next if /\.erb\z/ =~ dep
+ compiler_wd ||= dep
+ files[dep] = true
+ }
+ # gcc emits {# 1 "/absolute/directory/of/the/source/file//"} at 2nd line.
+ if %r{\A/.*//\z} =~ compiler_wd
+ files.delete compiler_wd
+ compiler_wd = Pathname(compiler_wd.sub(%r{//\z}, ''))
+ elsif !(compiler_wd = yield)
+ raise "compiler working directory not found: #{path_i}"
+ end
+ deps = []
+ files.each_key {|dep|
+ dep = Pathname(dep)
+ if dep.relative?
+ dep = compiler_wd + dep
+ end
+ if !dep.file?
+ warn "warning: file not found: #{dep}"
+ next
+ end
+ next if !dep.to_s.start_with?(cwd.to_s) # omit system headers.
+ deps << dep
+ }
+ if deps.include?(cwd + "probes.h")
+ deps << (cwd + "probes.dmyh")
+ end
+ deps
+end
+
+def read_cc_deps(cwd)
+ deps = {}
+ Pathname.glob('**/*.o').sort.each {|fn_o|
+ fn_i = fn_o.sub_ext('.i')
+ if !fn_i.exist?
+ next if fn_o.sub_ext('.S').exist?
+ warn "warning: not found: #{fn_i}"
+ $i_not_found = true
+ next
+ end
+ path_o = cwd + fn_o
+ path_i = cwd + fn_i
+ deps[path_o] = read_single_cc_deps(path_i, cwd) do
+ if fn_o.to_s.start_with?("enc/")
+ cwd
+ else
+ path_i.parent
+ end
+ end
+ }
+ deps
+end
+
+def concentrate(dependencies, cwd)
+ deps = {}
+ dependencies.keys.sort.each {|target|
+ sources = dependencies[target]
+ target = target.relative_path_from(cwd)
+ sources = sources.map {|s|
+ rel = s.relative_path_from(cwd)
+ rel
+ }
+ if %r{\A\.\.(/|\z)} =~ target.to_s
+ warn "warning: out of tree target: #{target}"
+ next
+ end
+ sources = sources.reject {|s|
+ if %r{\A\.\.(/|\z)} =~ s.to_s
+ warn "warning: out of tree source: #{s}"
+ true
+ elsif %r{/\.time\z} =~ s.to_s
+ true
+ else
+ false
+ end
+ }
+ deps[target] = sources
+ }
+ deps
+end
+
+def sort_paths(paths)
+ paths.sort_by {|t|
+ ary = t.to_s.split(%r{/})
+ ary.map.with_index {|e, i| i == ary.length-1 ? [0, e] : [1, e] } # regular file first, directories last.
+ }
+end
+
+def show_deps(tag, deps)
+ targets = sort_paths(deps.keys)
+ targets.each {|t|
+ sources = sort_paths(deps[t])
+ sources.each {|s|
+ puts "#{tag} #{t}: #{s}"
+ }
+ }
+end
+
+def detect_dependencies(out=$stdout)
+ cwd = Pathname.pwd
+ make_deps = read_make_deps(cwd)
+ #pp make_deps
+ make_deps = concentrate(make_deps, cwd)
+ #pp make_deps
+ cc_deps = read_cc_deps(cwd)
+ #pp cc_deps
+ cc_deps = concentrate(cc_deps, cwd)
+ #pp cc_deps
+ return make_deps, cc_deps
+end
+
+def compare_deps(make_deps, cc_deps, out=$stdout)
+ targets = make_deps.keys | cc_deps.keys
+
+ makefiles = {}
+
+ make_lines_hash = {}
+ make_deps.each {|t, sources|
+ sources.each {|s|
+ makefile, t2, s2 = in_makefile(t, s)
+ makefiles[makefile] = true
+ make_lines_hash[makefile] ||= Hash.new(false)
+ make_lines_hash[makefile]["#{t2}: #{s2}"] = true
+ }
+ }
+
+ cc_lines_hash = {}
+ cc_deps.each {|t, sources|
+ sources.each {|s|
+ makefile, t2, s2 = in_makefile(t, s)
+ makefiles[makefile] = true
+ cc_lines_hash[makefile] ||= Hash.new(false)
+ cc_lines_hash[makefile]["#{t2}: #{s2}"] = true
+ }
+ }
+
+ makefiles.keys.sort.each {|makefile|
+ cc_lines = cc_lines_hash[makefile] || Hash.new(false)
+ make_lines = make_lines_hash[makefile] || Hash.new(false)
+ content = begin
+ File.read(makefile)
+ rescue Errno::ENOENT
+ ''
+ end
+ if /^#{Regexp.escape DEPENDENCIES_SECTION_START_MARK}
+ ((?:.*\n)*)
+ #{Regexp.escape DEPENDENCIES_SECTION_END_MARK}/x =~ content
+ pre_post_part = [$`, $']
+ current_lines = Hash.new(false)
+ $1.each_line {|line| current_lines[line.chomp] = true }
+ (cc_lines.keys | current_lines.keys | make_lines.keys).sort.each {|line|
+ status = [cc_lines[line], current_lines[line], make_lines[line]]
+ case status
+ when [true, true, true]
+ # no problem
+ when [true, true, false]
+ out.puts "warning #{makefile} : #{line} (make doesn't detect written dependency)"
+ when [true, false, true]
+ out.puts "add_auto #{makefile} : #{line} (harmless)" # This is automatically updatable.
+ when [true, false, false]
+ out.puts "add_auto #{makefile} : #{line} (harmful)" # This is automatically updatable.
+ when [false, true, true]
+ out.puts "del_cc #{makefile} : #{line}" # Not automatically updatable because build on other OS may need the dependency.
+ when [false, true, false]
+ out.puts "del_cc #{makefile} : #{line} (Curious. make doesn't detect this dependency.)" # Not automatically updatable because build on other OS may need the dependency.
+ when [false, false, true]
+ out.puts "del_make #{makefile} : #{line}" # Not automatically updatable because the dependency is written manually.
+ else
+ raise "unexpected status: #{status.inspect}"
+ end
+ }
+ else
+ (cc_lines.keys | make_lines.keys).sort.each {|line|
+ status = [cc_lines[line], make_lines[line]]
+ case status
+ when [true, true]
+ # no problem
+ when [true, false]
+ out.puts "add_manual #{makefile} : #{line}" # Not automatically updatable because makefile has no section to update automatically.
+ when [false, true]
+ out.puts "del_manual #{makefile} : #{line}" # Not automatically updatable because makefile has no section to update automatically.
+ else
+ raise "unexpected status: #{status.inspect}"
+ end
+ }
+ end
+ }
+end
+
+def prepare_build
+ unless File.exist?("Makefile")
+ if File.exist?("autogen.sh")
+ system("./autogen.sh")
+ elsif !File.exist?("configure")
+ system("autoreconf", "-i", "-s")
+ end
+ system("./configure", "-q", "--enable-load-relative", "--prefix=/.",
+ "--disable-install-doc", "debugflags=-save-temps=obj -g")
+ end
+end
+
+def main_show(out=$stdout)
+ prepare_build
+ make_deps, cc_deps = detect_dependencies(out)
+ compare_deps(make_deps, cc_deps, out)
+end
+
+def extract_deplines(problems)
+ adds = {}
+ others = {}
+ problems.each_line {|line|
+ case line
+ when /\Aadd_auto (\S+) : ((\S+): (\S+))/
+ (adds[$1] ||= []) << [line, "#{$2}\n"]
+ when /\A(?:del_cc|del_make|add_manual|del_manual|warning) (\S+) : /
+ (others[$1] ||= []) << line
+ else
+ raise "unexpected line: #{line.inspect}"
+ end
+ }
+ return adds, others
+end
+
+def main_actual_fix(problems)
+ adds, others = extract_deplines(problems)
+ (adds.keys | others.keys).sort.each {|makefile|
+ content = begin
+ File.read(makefile)
+ rescue Errno::ENOENT
+ nil
+ end
+
+ if content &&
+ /^#{Regexp.escape DEPENDENCIES_SECTION_START_MARK}
+ ((?:.*\n)*)
+ #{Regexp.escape DEPENDENCIES_SECTION_END_MARK}/x =~ content
+ pre_dep_post = [$`, $1, $']
+ else
+ pre_dep_post = nil
+ end
+
+ if pre_dep_post && adds[makefile]
+ pre_lines, dep_lines, post_lines = pre_dep_post
+ dep_lines = dep_lines.lines.to_a
+ add_lines = adds[makefile].map(&:last)
+ new_lines = (dep_lines | add_lines).sort.uniq
+ new_content = [
+ pre_lines,
+ DEPENDENCIES_SECTION_START_MARK,
+ *new_lines,
+ DEPENDENCIES_SECTION_END_MARK,
+ post_lines
+ ].join
+ if content != new_content
+ puts "modified: #{makefile}"
+ tmp_makefile = "#{makefile}.new.#{$$}"
+ File.write(tmp_makefile, new_content)
+ File.rename tmp_makefile, makefile
+ (add_lines - dep_lines).each {|line| puts " added #{line}" }
+ else
+ puts "not modified: #{makefile}"
+ end
+ if others[makefile]
+ others[makefile].each {|line| puts " #{line}" }
+ end
+ else
+ if pre_dep_post
+ puts "no additional lines: #{makefile}"
+ elsif content
+ puts "no dependencies section: #{makefile}"
+ else
+ puts "no makefile: #{makefile}"
+ end
+ if adds[makefile]
+ puts " warning: dependencies section was exist at previous phase."
+ end
+ if adds[makefile]
+ adds[makefile].map(&:first).each {|line| puts " #{line}" }
+ end
+ if others[makefile]
+ others[makefile].each {|line| puts " #{line}" }
+ end
+ end
+ }
+end
+
+def main_fix
+ problems = StringIO.new
+ main_show(problems)
+ main_actual_fix(problems.string)
+end
+
+def run
+ op = optionparser
+ op.parse!(ARGV)
+ if $opt_actual_fix
+ main_actual_fix(ARGF.read)
+ elsif $opt_fix
+ main_fix
+ else
+ main_show
+ end
+end
+
+init_global
+run
+if $i_not_found
+ warn "warning: missing *.i files, see help in #$0 and ensure ccache is disabled"
+end
diff --git a/tool/vtlh.rb b/tool/vtlh.rb
new file mode 100644
index 0000000000..2e1faf2ce8
--- /dev/null
+++ b/tool/vtlh.rb
@@ -0,0 +1,17 @@
+# Convert addresses to line numbers for MiniRuby.
+
+# ARGF = open('ha')
+cd = `pwd`.chomp + '/'
+ARGF.each{|line|
+ if /^0x([a-z0-9]+),/ =~ line
+ stat = line.split(',')
+ addr = stat[0].hex + 0x00400000
+ retired = stat[2].to_i
+ ticks = stat[3].to_i
+
+ src = `addr2line -e miniruby.exe #{addr.to_s(16)}`.chomp
+ src.sub!(cd, '')
+ puts '%-40s 0x%08x %8d %8d' % [src, addr, retired, ticks]
+ end
+}
+
diff --git a/tool/ytab.sed b/tool/ytab.sed
new file mode 100755
index 0000000000..95a9b3e1eb
--- /dev/null
+++ b/tool/ytab.sed
@@ -0,0 +1,80 @@
+#!/bin/sed -f
+# This file is used when generating code for the Ruby parser.
+/^int yydebug;/{
+i\
+#ifndef yydebug
+a\
+#endif
+}
+/^extern int yydebug;/{
+i\
+#ifndef yydebug
+a\
+#endif
+}
+/^yydestruct.*yymsg/,/{/{
+ /^yydestruct/{
+ /,$/N
+ /[, *]p)/!{
+ H
+ s/^/ruby_parser_&/
+ s/)$/, p)/
+ /\*/s/p)$/struct parser_params *&/
+ }
+ }
+ /^#endif/{
+ x
+ /yydestruct/{
+ i\
+\ struct parser_params *p;
+ }
+ x
+ }
+ /^{/{
+ x
+ /yydestruct/{
+ i\
+#define yydestruct(m, t, v) ruby_parser_yydestruct(m, t, v, p)
+ }
+ x
+ }
+}
+/^yy_stack_print /,/{/{
+ /^yy_stack_print/{
+ /[, *]p)/!{
+ H
+ s/^/ruby_parser_&/
+ s/)$/, p)/
+ /\*/s/p)$/struct parser_params *&/
+ }
+ }
+ /^#endif/{
+ x
+ /yy_stack_print/{
+ i\
+\ struct parser_params *p;
+ }
+ x
+ }
+ /^{/{
+ x
+ /yy_stack_print/{
+ i\
+#define yy_stack_print(b, t) ruby_parser_yy_stack_print(b, t, p)
+ }
+ x
+ }
+}
+/^yy_reduce_print/,/^}/{
+ s/fprintf *(stderr,/YYFPRINTF (p,/g
+}
+s/^yysyntax_error (/&struct parser_params *p, /
+s/ yysyntax_error (/&p, /
+s/\( YYFPRINTF *(\)yyoutput,/\1p,/
+s/\( YYFPRINTF *(\)yyo,/\1p,/
+s/\( YYFPRINTF *(\)stderr,/\1p,/
+s/\( YYDPRINTF *((\)stderr,/\1p,/
+s/^\([ ]*\)\(yyerror[ ]*([ ]*parser,\)/\1parser_\2/
+s!^ *extern char \*getenv();!/* & */!
+s/^\(#.*\)".*\.tab\.c"/\1"parse.c"/
+/^\(#.*\)".*\.y"/s:\\\\:/:g